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