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/plain • 6.19 kiB
Python script, ASCII text executable
        
            
1
"""
2
Panorama background: desktop wallpaper using Wayland layer-shell
3
protocol.
4
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
5
6
This program is free software: you can redistribute it and/or modify
7
it under the terms of the GNU General Public Licence as published by
8
the Free Software Foundation, either version 3 of the Licence, or
9
(at your option) any later version.
10
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
GNU General Public Licence for more details.
15
16
You should have received a copy of the GNU General Public Licence
17
along with this program. If not, see <https://www.gnu.org/licenses/>.
18
"""
19
20
import os
21
import faulthandler
22
import pathlib
23
24
import ruamel.yaml as yaml
25
from pywayland.client import Display, EventQueue
26
from pywayland.protocol.wayland import WlOutput
27
from pathlib import Path
28
29
faulthandler.enable()
30
31
os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"
32
33
from ctypes import CDLL
34
CDLL("libgtk4-layer-shell.so")
35
36
import gi
37
38
gi.require_version("Gtk", "4.0")
39
gi.require_version("Gtk4LayerShell", "1.0")
40
41
from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio, GObject
42
43
import ctypes
44
from cffi import FFI
45
ffi = FFI()
46
ffi.cdef("""
47
void * gdk_wayland_display_get_wl_display (void * display);
48
void * gdk_wayland_surface_get_wl_surface (void * surface);
49
void * gdk_wayland_monitor_get_wl_output (void * monitor);
50
""")
51
gtk = ffi.dlopen("libgtk-4.so.1")
52
53
54
def get_config_file():
55
config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
56
57
return config_home / "panorama-bg" / "config.yaml"
58
59
60
FITTING_MODES = {
61
"cover": Gtk.ContentFit.COVER,
62
"contain": Gtk.ContentFit.CONTAIN,
63
"fill": Gtk.ContentFit.FILL,
64
"scale_down": Gtk.ContentFit.SCALE_DOWN
65
}
66
67
68
class PanoramaBG(Gtk.Application):
69
def __init__(self):
70
super().__init__(
71
application_id="com.roundabout_host.roundabout.PanoramaBG"
72
)
73
self.wl_output_ids: dict[int, WlOutput] = {}
74
self.wl_display = None
75
self.display = None
76
77
def make_backgrounds(self):
78
for window in self.get_windows():
79
window.destroy()
80
81
with open(get_config_file(), "r") as config_file:
82
yaml_loader = yaml.YAML(typ="rt")
83
yaml_file = yaml_loader.load(config_file)
84
85
monitors = self.display.get_monitors()
86
87
for monitor in monitors:
88
if monitor.get_connector() in yaml_file["monitors"]:
89
all_sources = []
90
for source in yaml_file["monitors"][monitor.get_connector()]["sources"]:
91
path = pathlib.Path(source)
92
if path.is_dir():
93
all_sources.extend(f for f in path.iterdir() if f.is_file())
94
else:
95
all_sources.append(path)
96
97
background_window = BackgroundWindow(self, monitor)
98
background_window.all_sources = all_sources
99
background_window.time_per_image = yaml_file["monitors"][monitor.get_connector()]["time_per_image"]
100
background_window.change_image()
101
background_window.picture.set_content_fit(FITTING_MODES[yaml_file["monitors"][monitor.get_connector()]["fitting_mode"]])
102
background_window.present()
103
104
def do_startup(self):
105
Gtk.Application.do_startup(self)
106
self.hold()
107
108
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
109
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,)
110
111
self.display = Gdk.Display.get_default()
112
113
self.wl_display = Display()
114
wl_display_ptr = gtk.gdk_wayland_display_get_wl_display(
115
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.display.__gpointer__, None)))
116
self.wl_display._ptr = wl_display_ptr
117
self.registry = self.wl_display.get_registry()
118
self.registry.dispatcher["global"] = self.on_global
119
self.registry.dispatcher["global_remove"] = self.on_global_remove
120
self.wl_display.roundtrip()
121
122
def receive_output_name(self, output: WlOutput, name: str):
123
output.name = name
124
125
def on_global(self, registry, name, interface, version):
126
if interface == "wl_output":
127
output = registry.bind(name, WlOutput, version)
128
output.id = name
129
output.name = None
130
output.dispatcher["name"] = self.receive_output_name
131
while not output.name:
132
self.wl_display.dispatch(block=True)
133
if not output.name.startswith("live-preview"):
134
self.make_backgrounds()
135
self.wl_output_ids[name] = output
136
137
def on_global_remove(self, registry, name):
138
if name in self.wl_output_ids:
139
self.wl_output_ids[name].destroy()
140
del self.wl_output_ids[name]
141
142
143
class BackgroundWindow(Gtk.Window):
144
def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor):
145
super().__init__(application=application)
146
self.set_decorated(False)
147
148
self.all_sources = []
149
self.time_per_image = 0
150
151
Gtk4LayerShell.init_for_window(self)
152
Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.background")
153
Gtk4LayerShell.set_monitor(self, monitor)
154
Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.BACKGROUND)
155
156
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
157
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
158
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
159
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
160
161
self.picture = Gtk.Picture()
162
self.set_child(self.picture)
163
164
def change_image(self):
165
now = GLib.DateTime.new_now_local()
166
self.picture.set_filename(str(self.all_sources[now.to_unix() // self.time_per_image % len(self.all_sources)]))
167
time_until_next = self.time_per_image - now.to_unix() % self.time_per_image
168
GLib.timeout_add_seconds(time_until_next, self.change_image)
169
170
171
if __name__ == "__main__":
172
app = PanoramaBG()
173
app.run()
174
175