#!/usr/bin/env python3 import subprocess from pathlib import Path import gi gi.require_version("Gtk", "3.0") gi.require_version("GtkLayerShell", "0.1") from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, GtkLayerShell CONFIG_FILE = Path("/etc/ekklesia-classroom/device.conf") CSS_FILE = Path("/etc/ekklesia-classroom/dock/style.css") ICON_DIR = Path("/etc/ekklesia-classroom/icons") def read_device_config(): config = {} if not CONFIG_FILE.exists(): return config for raw_line in CONFIG_FILE.read_text().splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) config[key.strip()] = value.strip() return config def enabled(config, key, default=False): if key not in config: return default return config[key].lower() in { "1", "true", "yes", "on", "enabled", } def launch(command): subprocess.Popen( command, start_new_session=True, ) def notify(title, message): launch([ "notify-send", "--app-name=Ekklesia Classroom", title, message, ]) def themed_icon(*names): theme = Gtk.IconTheme.get_default() for name in names: if theme.has_icon(name): return Gtk.Image.new_from_icon_name(name, Gtk.IconSize.DIALOG) return Gtk.Image.new_from_icon_name( "application-x-executable", Gtk.IconSize.DIALOG, ) def custom_icon(filename, size): path = ICON_DIR / filename if path.exists(): pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( str(path), size, size, True, ) return Gtk.Image.new_from_pixbuf(pixbuf) return None class ClassroomDock(Gtk.Window): def __init__(self): super().__init__() screen = self.get_screen() visual = screen.get_rgba_visual() if visual is not None: self.set_visual(visual) self.set_app_paintable(True) self.config = read_device_config() self.set_name("classroom-dock") self.set_decorated(False) self.set_resizable(False) GtkLayerShell.init_for_window(self) GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP) GtkLayerShell.set_anchor( self, GtkLayerShell.Edge.BOTTOM, True, ) GtkLayerShell.set_margin( self, GtkLayerShell.Edge.BOTTOM, 12, ) GtkLayerShell.set_keyboard_interactivity(self, False) dock = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL, spacing=10, ) dock.get_style_context().add_class("dock") dock.pack_start( self.make_button( self.browser_icon(), "Browser", lambda *_: launch( ["/usr/local/bin/classroom-browser"] ), ), False, False, 0, ) if enabled(self.config, "DVD_ENABLED", False): dock.pack_start( self.make_button( self.dvd_icon(), "DVD", lambda *_: notify( "DVD", "DVD support is being prepared.", ), ), False, False, 0, ) dock.pack_start( self.make_button( self.sanctuary_icon(), "Sanctuary", lambda *_: notify( "Sanctuary", "Sanctuary live view is being prepared.", ), ), False, False, 0, ) self.add(dock) def browser_icon(self): image = custom_icon("browser.svg", 28) if image is not None: return image image = themed_icon("web-browser-symbolic", "internet-web-browser-symbolic") image.set_pixel_size(28) return image def dvd_icon(self): image = custom_icon("dvd.svg", 40) if image is not None: return image image = themed_icon( "media-optical-symbolic", "media-optical", ) image.set_pixel_size(28) return image def sanctuary_icon(self): is_live = enabled( self.config, "SANCTUARY_LIVE", False, ) filename = "live.svg" if is_live else "live-off.svg" image = custom_icon(filename, 38) if image is not None: return image image = themed_icon( "video-display-symbolic", "video-display", ) image.set_pixel_size(28) return image def make_button(self, image, tooltip, callback): button = Gtk.Button() button.set_relief(Gtk.ReliefStyle.NONE) button.get_style_context().add_class("dock-button") button.add(image) button.connect("clicked", callback) return button class ClassroomClock(Gtk.Window): def __init__(self): super().__init__() screen = self.get_screen() visual = screen.get_rgba_visual() if visual is not None: self.set_visual(visual) self.set_app_paintable(True) self.set_name("classroom-clock") self.set_decorated(False) self.set_resizable(False) GtkLayerShell.init_for_window(self) GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP) GtkLayerShell.set_anchor( self, GtkLayerShell.Edge.BOTTOM, True, ) GtkLayerShell.set_anchor( self, GtkLayerShell.Edge.RIGHT, True, ) GtkLayerShell.set_margin( self, GtkLayerShell.Edge.BOTTOM, 16, ) GtkLayerShell.set_margin( self, GtkLayerShell.Edge.RIGHT, 20, ) GtkLayerShell.set_keyboard_interactivity(self, False) self.label = Gtk.Label() self.label.get_style_context().add_class("clock") self.add(self.label) self.update_time() GLib.timeout_add_seconds(15, self.update_time) def update_time(self): from datetime import datetime self.label.set_text( datetime.now().strftime("%-I:%M %p") ) return True def load_css(): provider = Gtk.CssProvider() provider.load_from_path(str(CSS_FILE)) screen = Gdk.Screen.get_default() Gtk.StyleContext.add_provider_for_screen( screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) def main(): load_css() dock = ClassroomDock() dock.connect("destroy", Gtk.main_quit) dock.show_all() clock = ClassroomClock() clock.show_all() Gtk.main() if __name__ == "__main__": main()