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