roundabout,
created on Friday, 12 September 2025, 21:20:07 (1757712007),
received on Friday, 12 September 2025, 21:20:09 (1757712009)
Author identity: Vlad <vlad.muntoiu@gmail.com>
fb692b8cb031de8ece845df7bfe8ab12f3998d0b
main.py
@@ -20,12 +20,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os import faulthandler import pathlib import sys import localeimport ruamel.yaml as yaml from pywayland.client import Display, EventQueue from pywayland.protocol.wayland import WlOutput from pathlib import Path locale.bindtextdomain("panorama-bg", "locale") locale.textdomain("panorama-bg") locale.setlocale(locale.LC_ALL) _ = locale.gettext faulthandler.enable() os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"
@@ -61,18 +68,35 @@ FITTING_MODES = {
"cover": Gtk.ContentFit.COVER, "contain": Gtk.ContentFit.CONTAIN, "fill": Gtk.ContentFit.FILL, "scale_down": Gtk.ContentFit.SCALE_DOWN"scale_down": Gtk.ContentFit.SCALE_DOWN, } FITTING_MODES_REVERSE = { Gtk.ContentFit.COVER: "cover", Gtk.ContentFit.CONTAIN: "contain", Gtk.ContentFit.FILL: "fill", Gtk.ContentFit.SCALE_DOWN: "scale_down",} class PanoramaBG(Gtk.Application): def __init__(self): super().__init__( application_id="com.roundabout_host.roundabout.PanoramaBG"application_id="com.roundabout_host.roundabout.PanoramaBG", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) self.wl_output_ids: dict[int, WlOutput] = {} self.wl_display = None self.display = None self.settings_window = None self.add_main_option( "open-settings", ord("s"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, _("Open the settings window") )def make_backgrounds(self): for window in self.get_windows():
@@ -85,21 +109,16 @@ class PanoramaBG(Gtk.Application):
monitors = self.display.get_monitors() for monitor in monitors: background_window = BackgroundWindow(self, monitor) monitor.background_window = background_windowif monitor.get_connector() in yaml_file["monitors"]: all_sources = []for source in yaml_file["monitors"][monitor.get_connector()]["sources"]:path = pathlib.Path(source)if path.is_dir():all_sources.extend(f for f in path.iterdir() if f.is_file())else:all_sources.append(path)background_window = BackgroundWindow(self, monitor)background_window.all_sources = all_sourcesbackground_window.put_sources(yaml_file["monitors"][monitor.get_connector()]["sources"])background_window.time_per_image = yaml_file["monitors"][monitor.get_connector()]["time_per_image"] background_window.change_image() background_window.picture.set_content_fit(FITTING_MODES[yaml_file["monitors"][monitor.get_connector()]["fitting_mode"]]) background_window.present()background_window.present()def do_startup(self): Gtk.Application.do_startup(self)
@@ -139,6 +158,46 @@ class PanoramaBG(Gtk.Application):
self.wl_output_ids[name].destroy() del self.wl_output_ids[name] def do_activate(self): Gio.Application.do_activate(self) def open_settings(self): if self.settings_window: self.settings_window.present() return self.settings_window = SettingsWindow(self) self.settings_window.connect("close-request", self.reset_settings_window) self.settings_window.present() def reset_settings_window(self, *args): self.settings_window = None def do_command_line(self, command_line: Gio.ApplicationCommandLine): options = command_line.get_options_dict() args = command_line.get_arguments()[1:] open_settings = options.contains("open-settings") if open_settings: self.open_settings() return 0 def write_config(self): with open(get_config_file(), "w") as config_file: yaml_writer = yaml.YAML(typ="rt") data = {"monitors": {}} for window in self.get_windows(): if isinstance(window, BackgroundWindow): monitor = Gtk4LayerShell.get_monitor(window) data["monitors"][monitor.get_connector()] = { "sources": window.sources, "time_per_image": window.time_per_image, "fitting_mode": FITTING_MODES_REVERSE[window.picture.get_content_fit()], } yaml_writer.dump(data, config_file) class BackgroundWindow(Gtk.Window): def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor):
@@ -146,7 +205,9 @@ class BackgroundWindow(Gtk.Window):
self.set_decorated(False) self.all_sources = [] self.sources = []self.time_per_image = 0 self.current_timeout = NoneGtk4LayerShell.init_for_window(self) Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.background")
@@ -161,14 +222,72 @@ class BackgroundWindow(Gtk.Window):
self.picture = Gtk.Picture() self.set_child(self.picture) def put_sources(self, sources): self.all_sources = [] self.sources = sources for source in sources: path = pathlib.Path(source) if path.is_dir(): self.all_sources.extend(f for f in path.iterdir() if f.is_file()) else: self.all_sources.append(path) self.all_sources = self.all_sources def change_image(self): now = GLib.DateTime.new_now_local() self.picture.set_filename(str(self.all_sources[now.to_unix() // self.time_per_image % len(self.all_sources)])) time_until_next = self.time_per_image - now.to_unix() % self.time_per_image GLib.timeout_add_seconds(time_until_next, self.change_image)self.current_timeout = GLib.timeout_add_seconds(time_until_next, self.change_image) return False @Gtk.Template(filename="settings-window.ui") class SettingsWindow(Gtk.Window): __gtype_name__ = "SettingsWindow" monitor_list: Gtk.ListBox = Gtk.Template.Child() def __init__(self, application: Gtk.Application, **kwargs): super().__init__(application=application, **kwargs) monitors = application.display.get_monitors() for monitor in monitors: monitor_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) monitor_box.append(Gtk.Label.new(monitor.get_description())) monitor_row = Gtk.ListBoxRow(activatable=False, child=monitor_box) formats = Gdk.ContentFormats.new_for_gtype(Gdk.FileList) drop_target = Gtk.DropTarget(formats=formats, actions=Gdk.DragAction.COPY) drop_target.connect("drop", self.on_drop, monitor) monitor_row.add_controller(drop_target) self.monitor_list.append(monitor_row) self.connect("close-request", lambda *args: self.destroy()) def on_drop(self, drop_target: Gtk.DropTarget, value: Gdk.FileList, x, y, monitor: Gdk.Monitor): if hasattr(monitor, "background_window"): files = value.get_files() if not files: return False paths = [] for file in files: paths.append(file.get_path()) if monitor.background_window.current_timeout is not None: GLib.source_remove(monitor.background_window.current_timeout) monitor.background_window.put_sources(paths) monitor.background_window.change_image() self.get_application().write_config() return Falseif __name__ == "__main__": app = PanoramaBG() app.run()app.run(sys.argv)
settings-window.cmb
@@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?> <!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd"> <!-- Created with Cambalache 0.96.1 --> <cambalache-project version="0.96.0" target_tk="gtk-4.0"> <ui template-class="SettingsWindow" filename="settings-window.ui" sha256="8043af49c1f0bc8ad0aaebe5d420bc02f0749a93b19f49df1802a13f12c106ec"/> </cambalache-project>
settings-window.ui
@@ -0,0 +1,40 @@
<?xml version='1.0' encoding='UTF-8'?> <!-- Created with Cambalache 0.96.1 --> <interface> <!-- interface-name settings-window.ui --> <requires lib="gtk" version="4.18"/> <template class="SettingsWindow" parent="GtkWindow"> <property name="title" translatable="yes">Wallpapers</property> <child> <object class="GtkScrolledWindow"> <child> <object class="GtkViewport"> <child> <object class="GtkBox"> <property name="margin-bottom">32</property> <property name="margin-end">32</property> <property name="margin-start">32</property> <property name="margin-top">32</property> <property name="orientation">vertical</property> <property name="spacing">4</property> <child> <object class="GtkListBox" id="monitor_list"> <property name="selection-mode">none</property> </object> </child> <child> <object class="GtkLabel"> <property name="label" translatable="yes"><i>Drop image files or directories with images on a monitor to set</i></property> <property name="use-markup">True</property> <property name="wrap">True</property> <property name="xalign">0.0</property> </object> </child> </object> </child> </object> </child> </object> </child> </template> </interface>