By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 main.py

View raw Download
text/x-script.python • 5.45 kiB
Python script, ASCII text executable
        
            
1
import os
2
import faulthandler
3
import pathlib
4
5
import ruamel.yaml as yaml
6
from pywayland.client import Display, EventQueue
7
from pywayland.protocol.wayland import WlOutput
8
from pathlib import Path
9
10
faulthandler.enable()
11
12
os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"
13
14
from ctypes import CDLL
15
CDLL("libgtk4-layer-shell.so")
16
17
import gi
18
19
gi.require_version("Gtk", "4.0")
20
gi.require_version("Gtk4LayerShell", "1.0")
21
22
from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio, GObject
23
24
import ctypes
25
from cffi import FFI
26
ffi = FFI()
27
ffi.cdef("""
28
void * gdk_wayland_display_get_wl_display (void * display);
29
void * gdk_wayland_surface_get_wl_surface (void * surface);
30
void * gdk_wayland_monitor_get_wl_output (void * monitor);
31
""")
32
gtk = ffi.dlopen("libgtk-4.so.1")
33
34
35
def get_config_file():
36
config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
37
38
return config_home / "panorama-bg" / "config.yaml"
39
40
41
FITTING_MODES = {
42
"cover": Gtk.ContentFit.COVER,
43
"contain": Gtk.ContentFit.CONTAIN,
44
"fill": Gtk.ContentFit.FILL,
45
"scale_down": Gtk.ContentFit.SCALE_DOWN
46
}
47
48
49
class PanoramaBG(Gtk.Application):
50
def __init__(self):
51
super().__init__(
52
application_id="com.roundabout_host.roundabout.PanoramaBG"
53
)
54
self.wl_output_ids: dict[int, WlOutput] = {}
55
self.wl_display = None
56
self.display = None
57
58
def make_backgrounds(self):
59
for window in self.get_windows():
60
window.destroy()
61
62
with open(get_config_file(), "r") as config_file:
63
yaml_loader = yaml.YAML(typ="rt")
64
yaml_file = yaml_loader.load(config_file)
65
66
monitors = self.display.get_monitors()
67
68
for monitor in monitors:
69
if monitor.get_connector() in yaml_file["monitors"]:
70
all_sources = []
71
for source in yaml_file["monitors"][monitor.get_connector()]["sources"]:
72
path = pathlib.Path(source)
73
if path.is_dir():
74
all_sources.extend(f for f in path.iterdir() if f.is_file())
75
else:
76
all_sources.append(path)
77
78
background_window = BackgroundWindow(self, monitor)
79
background_window.all_sources = all_sources
80
background_window.time_per_image = yaml_file["monitors"][monitor.get_connector()]["time_per_image"]
81
background_window.change_image()
82
background_window.picture.set_content_fit(FITTING_MODES[yaml_file["monitors"][monitor.get_connector()]["fitting_mode"]])
83
background_window.present()
84
85
def do_startup(self):
86
Gtk.Application.do_startup(self)
87
self.hold()
88
89
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
90
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,)
91
92
self.display = Gdk.Display.get_default()
93
94
self.wl_display = Display()
95
wl_display_ptr = gtk.gdk_wayland_display_get_wl_display(
96
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.display.__gpointer__, None)))
97
self.wl_display._ptr = wl_display_ptr
98
self.registry = self.wl_display.get_registry()
99
self.registry.dispatcher["global"] = self.on_global
100
self.registry.dispatcher["global_remove"] = self.on_global_remove
101
self.wl_display.roundtrip()
102
103
def receive_output_name(self, output: WlOutput, name: str):
104
output.name = name
105
106
def on_global(self, registry, name, interface, version):
107
if interface == "wl_output":
108
output = registry.bind(name, WlOutput, version)
109
output.id = name
110
output.name = None
111
output.dispatcher["name"] = self.receive_output_name
112
while not output.name:
113
self.wl_display.dispatch(block=True)
114
if not output.name.startswith("live-preview"):
115
self.make_backgrounds()
116
self.wl_output_ids[name] = output
117
118
def on_global_remove(self, registry, name):
119
if name in self.wl_output_ids:
120
self.wl_output_ids[name].destroy()
121
del self.wl_output_ids[name]
122
123
124
class BackgroundWindow(Gtk.Window):
125
def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor):
126
super().__init__(application=application)
127
self.set_decorated(False)
128
129
self.all_sources = []
130
self.time_per_image = 0
131
132
Gtk4LayerShell.init_for_window(self)
133
Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.background")
134
Gtk4LayerShell.set_monitor(self, monitor)
135
Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.BACKGROUND)
136
137
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
138
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
139
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
140
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
141
142
self.picture = Gtk.Picture()
143
self.set_child(self.picture)
144
145
def change_image(self):
146
now = GLib.DateTime.new_now_local()
147
self.picture.set_filename(str(self.all_sources[now.to_unix() // self.time_per_image % len(self.all_sources)]))
148
time_until_next = self.time_per_image - now.to_unix() % self.time_per_image
149
GLib.timeout_add_seconds(time_until_next, self.change_image)
150
151
152
if __name__ == "__main__":
153
app = PanoramaBG()
154
app.run()
155
156