diff --git a/app/sovran_systemsos_hub/application.py b/app/sovran_systemsos_hub/application.py index c140335..7e2f4d5 100644 --- a/app/sovran_systemsos_hub/application.py +++ b/app/sovran_systemsos_hub/application.py @@ -1,3 +1,87 @@ +"""Sovran_SystemsOS_Hub — Main GTK4 Application.""" + +from __future__ import annotations + +import os + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +from gi.repository import Adw, Gdk, Gio, GLib, Gtk + +from .config import load_config +from .service_tile import ServiceTile + +APP_ID = "com.sovransystems.hub" + +# Initialize libadwaita BEFORE any widget creation +Adw.init() + + +class SovranHubWindow(Adw.ApplicationWindow): + + def __init__(self, app, config): + super().__init__( + application=app, + title="Sovran_SystemsOS Hub", + default_width=680, + default_height=700, + ) + self._config = config + self._tiles = [] + + css_path = os.environ.get("SOVRAN_HUB_CSS", "") + if css_path and os.path.isfile(css_path): + provider = Gtk.CssProvider() + provider.load_from_path(css_path) + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, + ) + + header = Adw.HeaderBar() + refresh_btn = Gtk.Button( + icon_name="view-refresh-symbolic", + tooltip_text="Refresh now", + ) + refresh_btn.connect("clicked", lambda _b: self._refresh_all()) + header.pack_end(refresh_btn) + + self._flowbox = Gtk.FlowBox( + max_children_per_line=4, + min_children_per_line=2, + selection_mode=Gtk.SelectionMode.NONE, + homogeneous=True, + row_spacing=12, + column_spacing=12, + margin_top=16, + margin_bottom=16, + margin_start=16, + margin_end=16, + halign=Gtk.Align.CENTER, + valign=Gtk.Align.START, + ) + + scrolled = Gtk.ScrolledWindow( + hscrollbar_policy=Gtk.PolicyType.NEVER, + vscrollbar_policy=Gtk.PolicyType.AUTOMATIC, + vexpand=True, + child=self._flowbox, + ) + + toolbar_view = Adw.ToolbarView() + toolbar_view.add_top_bar(header) + toolbar_view.set_content(scrolled) + self.set_content(toolbar_view) + + self._build_tiles() + + interval = config.get("refresh_interval", 5) + if interval and interval > 0: + GLib.timeout_add_seconds(interval, self._auto_refresh) + def _build_tiles(self): method = self._config.get("command_method", "systemctl") for entry in self._config.get("services", []): @@ -12,4 +96,30 @@ self._flowbox.append(tile) self._tiles.append(tile) - GLib.idle_add(self._refresh_all) \ No newline at end of file + # Defer first status poll so the window renders immediately + GLib.idle_add(self._refresh_all) + + def _refresh_all(self): + for t in self._tiles: + t.refresh() + return False + + def _auto_refresh(self): + self._refresh_all() + return True + + +class SovranHubApp(Adw.Application): + + def __init__(self): + super().__init__( + application_id=APP_ID, + flags=Gio.ApplicationFlags.DEFAULT_FLAGS, + ) + self._config = load_config() + + def do_activate(self): + win = self.get_active_window() + if not win: + win = SovranHubWindow(self, self._config) + win.present() \ No newline at end of file diff --git a/app/sovran_systemsos_hub/service_tile.py b/app/sovran_systemsos_hub/service_tile.py index e32eca8..51ce4c0 100644 --- a/app/sovran_systemsos_hub/service_tile.py +++ b/app/sovran_systemsos_hub/service_tile.py @@ -125,7 +125,7 @@ class ServiceTile(Gtk.Box): def _on_toggled(self, switch, state): if not self._enabled: - return True # block the toggle + return True systemctl.run_action("start" if state else "stop", self._unit, self._scope, self._method) GLib.timeout_add(1500, self.refresh) return False diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix index fc222f7..4fc24cc 100644 --- a/modules/core/sovran-hub.nix +++ b/modules/core/sovran-hub.nix @@ -1,3 +1,8 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.sovran_systemsOS; + monitoredServices = # ── Always-on infrastructure ─────────────────────────────── [ @@ -21,7 +26,101 @@ ] # ── Optional features ────────────────────────────────────── ++ [ - { name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; enabled = cfg.features.haven; } - { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; } - { name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; } - ]; \ No newline at end of file + { name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; enabled = cfg.features.haven; } + { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; } + { name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; } + ]; + + generatedConfig = pkgs.writeText "sovran-hub-config.json" + (builtins.toJSON { + refresh_interval = 5; + command_method = "systemctl"; + services = monitoredServices; + }); + + sovran-hub = pkgs.python3Packages.buildPythonApplication { + pname = "sovran-systemsos-hub"; + version = "1.0.0"; + format = "other"; + + src = ../../app; + + nativeBuildInputs = with pkgs; [ + wrapGAppsHook4 + gobject-introspection + ]; + + buildInputs = with pkgs; [ + gtk4 + libadwaita + gdk-pixbuf + librsvg + ]; + + propagatedBuildInputs = with pkgs.python3Packages; [ + pygobject3 + ]; + + dontBuild = true; + + installPhase = '' + runHook preInstall + + # ── Python source ───────────────────────────────���───────── + install -d $out/lib/sovran-hub + cp -r sovran_systemsos_hub $out/lib/sovran-hub/ + + # ── CSS ──────────────────────────────────────────────────── + cp style.css $out/lib/sovran-hub/style.css + + # ── Generated config ─────────────────────────────────────── + cp ${generatedConfig} $out/lib/sovran-hub/config.json + + # ── Icons (SVG + PNG) ────────────────────────────────────── + install -d $out/share/sovran-hub/icons + cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true + + # ── Launcher script ──────────────────────────────────────── + install -d $out/bin + cat > $out/bin/sovran-hub < $out/share/applications/Sovran_SystemsOS_Hub.desktop <