roundabout,
created on Friday, 12 September 2025, 21:20:07 (1757712007),
received on Saturday, 13 September 2025, 00:41:41 (1757724101)
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 locale
import 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_window
if 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_sources
background_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 = None
Gtk4LayerShell.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 False
if __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>