From e145ba949b621460593e92e5dc20210ccca817cd Mon Sep 17 00:00:00 2001 From: naturallaw77 Date: Tue, 31 Mar 2026 13:58:19 -0500 Subject: [PATCH] refiled directories --- app/sovran_systemsos_hub/__init__.py | 10 -- app/sovran_systemsos_hub/application.py | 74 ++++-------- app/sovran_systemsos_hub/service_tile.py | 128 ++++++--------------- app/sovran_systemsos_hub/systemctl.py | 10 -- app/style.css | 19 +--- modules/core/sovran-hub.nix | 136 +++++++++++------------ 6 files changed, 119 insertions(+), 258 deletions(-) diff --git a/app/sovran_systemsos_hub/__init__.py b/app/sovran_systemsos_hub/__init__.py index 88c4dee..651da84 100644 --- a/app/sovran_systemsos_hub/__init__.py +++ b/app/sovran_systemsos_hub/__init__.py @@ -7,7 +7,6 @@ from typing import Literal def _run(cmd: list[str]) -> str: - """Run a command and return stripped stdout (empty string on failure).""" try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) return result.stdout.strip() @@ -16,12 +15,10 @@ def _run(cmd: list[str]) -> str: def is_active(unit: str, scope: Literal["system", "user"] = "system") -> str: - """Return the ActiveState string (active / inactive / failed / …).""" return _run(["systemctl", f"--{scope}", "is-active", unit]) or "unknown" def is_enabled(unit: str, scope: Literal["system", "user"] = "system") -> str: - """Return the UnitFileState string (enabled / disabled / masked / …).""" return _run(["systemctl", f"--{scope}", "is-enabled", unit]) or "unknown" @@ -31,18 +28,11 @@ def run_action( scope: Literal["system", "user"] = "system", method: str = "systemctl", ) -> bool: - """Start / stop / restart a unit. - - For *system* units the command is elevated via *method* (pkexec or sudo). - Returns True on apparent success. - """ base_cmd = ["systemctl", f"--{scope}", action, unit] - if scope == "system" and method == "pkexec": cmd = ["pkexec", "--user", "root"] + base_cmd else: cmd = base_cmd - try: subprocess.Popen(cmd) return True diff --git a/app/sovran_systemsos_hub/application.py b/app/sovran_systemsos_hub/application.py index 9241dff..8d43dd1 100644 --- a/app/sovran_systemsos_hub/application.py +++ b/app/sovran_systemsos_hub/application.py @@ -9,84 +9,63 @@ import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") -from gi.repository import Adw, Gdk, Gio, GLib, Gtk # noqa: E402 +from gi.repository import Adw, Gdk, Gio, GLib, Gtk -from .config import load_config # noqa: E402 -from .service_tile import ServiceTile # noqa: E402 +from .config import load_config +from .service_tile import ServiceTile APP_ID = "com.sovransystems.hub" class SovranHubWindow(Adw.ApplicationWindow): - """Primary window: a 4-across grid of service tiles with auto-refresh.""" - def __init__(self, app: Adw.Application, config: dict): + def __init__(self, app, config): super().__init__( - application=app, - title="Sovran_SystemsOS Hub", - default_width=680, - default_height=700, + application=app, title="Sovran_SystemsOS Hub", + default_width=680, default_height=700, ) self._config = config - self._tiles: list[ServiceTile] = [] + self._tiles = [] - # ── Load custom CSS ── 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, + Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) - # ── Header bar ── header = Adw.HeaderBar() - - refresh_btn = Gtk.Button( - icon_name="view-refresh-symbolic", - tooltip_text="Refresh now", - ) + 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) - # ── FlowBox: 4 tiles across ── 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, + 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, ) - # ── Scrollable content ── scrolled = Gtk.ScrolledWindow( hscrollbar_policy=Gtk.PolicyType.NEVER, vscrollbar_policy=Gtk.PolicyType.AUTOMATIC, - vexpand=True, - child=self._flowbox, + 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) - # ── Populate ── self._build_tiles() - self._start_auto_refresh() + interval = config.get("refresh_interval", 5) + if interval and interval > 0: + GLib.timeout_add_seconds(interval, self._auto_refresh) def _build_tiles(self): - """Create ServiceTile widgets from config.""" for entry in self._config.get("services", []): tile = ServiceTile( name=entry.get("name", entry["unit"]), @@ -99,27 +78,18 @@ class SovranHubWindow(Adw.ApplicationWindow): self._tiles.append(tile) def _refresh_all(self): - for tile in self._tiles: - tile.refresh() + for t in self._tiles: + t.refresh() - def _start_auto_refresh(self): - interval = self._config.get("refresh_interval", 5) - if interval and interval > 0: - GLib.timeout_add_seconds(interval, self._auto_refresh_cb) - - def _auto_refresh_cb(self) -> bool: + def _auto_refresh(self): self._refresh_all() return True class SovranHubApp(Adw.Application): - """Sovran_SystemsOS_Hub Adw.Application.""" def __init__(self): - super().__init__( - application_id=APP_ID, - flags=Gio.ApplicationFlags.DEFAULT_FLAGS, - ) + super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.DEFAULT_FLAGS) self._config = load_config() def do_activate(self): diff --git a/app/sovran_systemsos_hub/service_tile.py b/app/sovran_systemsos_hub/service_tile.py index e59d841..33376f7 100644 --- a/app/sovran_systemsos_hub/service_tile.py +++ b/app/sovran_systemsos_hub/service_tile.py @@ -10,31 +10,19 @@ gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Gdk", "4.0") -from gi.repository import Adw, Gdk, GdkPixbuf, GLib, Gtk # noqa: E402 +from gi.repository import Adw, Gdk, GdkPixbuf, GLib, Gtk -from . import systemctl # noqa: E402 +from . import systemctl LOADING_STATES = {"reloading", "activating", "deactivating", "maintenance"} -# Icon directory injected by the Nix derivation via environment variable ICON_DIR = os.environ.get("SOVRAN_HUB_ICONS", "") - -# Supported icon extensions in priority order ICON_EXTENSIONS = [".svg", ".png"] class ServiceTile(Gtk.Box): - """A square tile showing a service logo, name, status, toggle, and restart.""" - def __init__( - self, - name: str, - unit: str, - scope: str = "system", - method: str = "systemctl", - icon_name: str = "", - **kwargs, - ): + def __init__(self, name, unit, scope="system", method="systemctl", icon_name="", **kw): super().__init__( orientation=Gtk.Orientation.VERTICAL, spacing=6, @@ -43,136 +31,84 @@ class ServiceTile(Gtk.Box): width_request=140, height_request=160, css_classes=["card", "sovran-tile"], - margin_top=6, - margin_bottom=6, - margin_start=6, - margin_end=6, - **kwargs, + margin_top=6, margin_bottom=6, margin_start=6, margin_end=6, + **kw, ) - self._unit = unit self._scope = scope self._method = method - self._name = name - # ── Logo ── - self._logo = Gtk.Image( - pixel_size=48, - margin_top=12, - halign=Gtk.Align.CENTER, - css_classes=["sovran-tile-icon"], - ) + self._logo = Gtk.Image(pixel_size=48, margin_top=12, halign=Gtk.Align.CENTER) self._set_logo(icon_name) self.append(self._logo) - # ── Service name ── - label = Gtk.Label( - label=name, - css_classes=["heading"], - halign=Gtk.Align.CENTER, - ellipsize=3, # PANGO_ELLIPSIZE_END - max_width_chars=14, - ) - self.append(label) + self.append(Gtk.Label( + label=name, css_classes=["heading"], + halign=Gtk.Align.CENTER, ellipsize=3, max_width_chars=14, + )) - # ── Status label ── - self._status_label = Gtk.Label( - css_classes=["caption"], - halign=Gtk.Align.CENTER, - ) + self._status_label = Gtk.Label(css_classes=["caption"], halign=Gtk.Align.CENTER) self.append(self._status_label) - # ── Controls row (switch + restart) ── controls = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL, - spacing=8, - halign=Gtk.Align.CENTER, - margin_bottom=8, + spacing=8, halign=Gtk.Align.CENTER, margin_bottom=8, ) - self._switch = Gtk.Switch(valign=Gtk.Align.CENTER) self._switch.connect("state-set", self._on_toggled) controls.append(self._switch) restart_btn = Gtk.Button( - icon_name="view-refresh-symbolic", - valign=Gtk.Align.CENTER, - tooltip_text="Restart", - css_classes=["flat", "circular"], + icon_name="view-refresh-symbolic", valign=Gtk.Align.CENTER, + tooltip_text="Restart", css_classes=["flat", "circular"], ) restart_btn.connect("clicked", self._on_restart) controls.append(restart_btn) - self.append(controls) - # Initial state self.refresh() - def _set_logo(self, icon_name: str): - """Set the tile logo from an SVG or PNG in the icons dir.""" + def _set_logo(self, icon_name): if icon_name and ICON_DIR: for ext in ICON_EXTENSIONS: - icon_path = os.path.join(ICON_DIR, f"{icon_name}{ext}") - if os.path.isfile(icon_path): - if ext == ".svg": - # GTK4 handles SVGs natively via GdkPixbuf - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - icon_path, 48, 48, True - ) - texture = Gdk.Texture.new_for_pixbuf(pixbuf) - self._logo.set_from_paintable(texture) - else: - # PNG path - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - icon_path, 48, 48, True - ) - texture = Gdk.Texture.new_for_pixbuf(pixbuf) - self._logo.set_from_paintable(texture) + path = os.path.join(ICON_DIR, f"{icon_name}{ext}") + if os.path.isfile(path): + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, 48, 48, True) + texture = Gdk.Texture.new_for_pixbuf(pixbuf) + self._logo.set_from_paintable(texture) return - - # Fallback: themed symbolic icon self._logo.set_from_icon_name("system-run-symbolic") - # ── public API ── - def refresh(self): - """Poll systemctl and update the tile.""" - active_state = systemctl.is_active(self._unit, self._scope) - enabled_state = systemctl.is_enabled(self._unit, self._scope) + active = systemctl.is_active(self._unit, self._scope) + enabled = systemctl.is_enabled(self._unit, self._scope) + is_on = active == "active" + is_loading = active in LOADING_STATES + is_failed = active == "failed" - is_active = active_state == "active" - is_loading = active_state in LOADING_STATES - is_failed = active_state == "failed" - - # Block the handler so we don't trigger a start/stop self._switch.handler_block_by_func(self._on_toggled) - self._switch.set_active(is_active) + self._switch.set_active(is_on) self._switch.handler_unblock_by_func(self._on_toggled) - self._switch.set_sensitive(not is_loading) - # Status text + color if is_failed: self._status_label.set_label("● failed") self._status_label.set_css_classes(["caption", "error"]) - elif is_active: + elif is_on: self._status_label.set_label("● running") self._status_label.set_css_classes(["caption", "success"]) elif is_loading: - self._status_label.set_label(f"● {active_state}") + self._status_label.set_label(f"● {active}") self._status_label.set_css_classes(["caption", "warning"]) else: - self._status_label.set_label(f"● {active_state}") + self._status_label.set_label(f"● {active}") self._status_label.set_css_classes(["caption", "dim-label"]) - # ── signal handlers ── - - def _on_toggled(self, switch: Gtk.Switch, state: bool) -> bool: - action = "start" if state else "stop" - systemctl.run_action(action, self._unit, self._scope, self._method) + def _on_toggled(self, switch, state): + systemctl.run_action("start" if state else "stop", self._unit, self._scope, self._method) GLib.timeout_add(1500, self.refresh) return False - def _on_restart(self, _btn: Gtk.Button): + def _on_restart(self, _btn): systemctl.run_action("restart", self._unit, self._scope, self._method) GLib.timeout_add(1500, self.refresh) \ No newline at end of file diff --git a/app/sovran_systemsos_hub/systemctl.py b/app/sovran_systemsos_hub/systemctl.py index 88c4dee..651da84 100644 --- a/app/sovran_systemsos_hub/systemctl.py +++ b/app/sovran_systemsos_hub/systemctl.py @@ -7,7 +7,6 @@ from typing import Literal def _run(cmd: list[str]) -> str: - """Run a command and return stripped stdout (empty string on failure).""" try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) return result.stdout.strip() @@ -16,12 +15,10 @@ def _run(cmd: list[str]) -> str: def is_active(unit: str, scope: Literal["system", "user"] = "system") -> str: - """Return the ActiveState string (active / inactive / failed / …).""" return _run(["systemctl", f"--{scope}", "is-active", unit]) or "unknown" def is_enabled(unit: str, scope: Literal["system", "user"] = "system") -> str: - """Return the UnitFileState string (enabled / disabled / masked / …).""" return _run(["systemctl", f"--{scope}", "is-enabled", unit]) or "unknown" @@ -31,18 +28,11 @@ def run_action( scope: Literal["system", "user"] = "system", method: str = "systemctl", ) -> bool: - """Start / stop / restart a unit. - - For *system* units the command is elevated via *method* (pkexec or sudo). - Returns True on apparent success. - """ base_cmd = ["systemctl", f"--{scope}", action, unit] - if scope == "system" and method == "pkexec": cmd = ["pkexec", "--user", "root"] + base_cmd else: cmd = base_cmd - try: subprocess.Popen(cmd) return True diff --git a/app/style.css b/app/style.css index 0934be3..1daa90c 100644 --- a/app/style.css +++ b/app/style.css @@ -1,5 +1,3 @@ -/* Sovran_SystemsOS Hub — tile styling */ - .sovran-tile { border-radius: 16px; padding: 8px; @@ -7,21 +5,8 @@ min-height: 160px; transition: box-shadow 200ms ease-in-out; } - .sovran-tile:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); } - -.sovran-tile-icon { - margin-top: 8px; - margin-bottom: 2px; -} - -/* Status dot colors */ -.success { - color: #2ec27e; -} - -.warning { - color: #e5a50a; -} \ No newline at end of file +.success { color: #2ec27e; } +.warning { color: #e5a50a; } \ No newline at end of file diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix index 449c663..0292cc9 100644 --- a/modules/core/sovran-hub.nix +++ b/modules/core/sovran-hub.nix @@ -1,50 +1,42 @@ -# modules/core/sovran-hub.nix -# -# Declarative NixOS module that: -# 1. Builds the Sovran_SystemsOS_Hub GTK4 app as a Nix derivation -# 2. Generates its config.json from existing sovran_systemsOS options -# 3. Uses logos committed directly in the repo (no fetchurl hashes) -# 4. Installs a .desktop file so it appears in GNOME Activities - -{ config, pkgs, lib, ... }: +{ config, lib, pkgs, ... }: let cfg = config.sovran_systemsOS; - # ── Build the list of monitored units from NixOS option state ── - monitoredServices = - (lib.optional cfg.services.bitcoin - { name = "Bitcoind"; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; }) - ++ (lib.optional cfg.services.bitcoin - { name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; }) - ++ (lib.optional cfg.services.bitcoin - { name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; }) - ++ (lib.optional cfg.services.bitcoin - { name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; }) - ++ (lib.optional cfg.services.bitcoin - { name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; }) - ++ (lib.optional cfg.services.synapse - { name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; }) - ++ (lib.optional cfg.services.vaultwarden - { name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; }) - ++ (lib.optional cfg.services.nextcloud - { name = "Nextcloud"; unit = "phpfpm-nextcloud.service"; type = "system"; icon = "nextcloud"; }) - ++ (lib.optional cfg.services.wordpress - { name = "WordPress"; unit = "phpfpm-wordpress.service"; type = "system"; icon = "wordpress"; }) - ++ (lib.optional cfg.features.haven - { name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; }) - ++ (lib.optional cfg.features.mempool - { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; }) - ++ (lib.optional cfg.features.element-calling - { name = "LiveKit"; unit = "livekit.service"; type = "system"; icon = "livekit"; }) - # Always-on infrastructure - ++ [ + [ { name = "Caddy"; unit = "caddy.service"; type = "system"; icon = "caddy"; } { name = "Tor"; unit = "tor.service"; type = "system"; icon = "tor"; } + ] + ++ lib.optionals cfg.services.bitcoin [ + { name = "Bitcoind"; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; } + { name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; } + { name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; } + { name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; } + { name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; } + ] + ++ lib.optionals cfg.services.synapse [ + { name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; } + ] + ++ lib.optionals cfg.services.vaultwarden [ + { name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; } + ] + ++ lib.optionals cfg.services.nextcloud [ + { name = "Nextcloud"; unit = "phpfpm-nextcloud.service"; type = "system"; icon = "nextcloud"; } + ] + ++ lib.optionals cfg.services.wordpress [ + { name = "WordPress"; unit = "phpfpm-wordpress.service"; type = "system"; icon = "wordpress"; } + ] + ++ lib.optionals cfg.features.haven [ + { name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; } + ] + ++ lib.optionals cfg.features.mempool [ + { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; } + ] + ++ lib.optionals cfg.features.element-calling [ + { name = "LiveKit"; unit = "livekit.service"; type = "system"; icon = "livekit"; } ]; - # ── Generate the config.json at build time ── generatedConfig = pkgs.writeText "sovran-hub-config.json" (builtins.toJSON { refresh_interval = 5; @@ -52,7 +44,6 @@ let services = monitoredServices; }); - # ── Package the Python GTK4 app ── sovran-hub = pkgs.python3Packages.buildPythonApplication { pname = "sovran-systemsos-hub"; version = "1.0.0"; @@ -60,54 +51,51 @@ let src = ../../app; - nativeBuildInputs = [ pkgs.wrapGAppsHook4 ]; - - buildInputs = [ - pkgs.gtk4 - pkgs.libadwaita - pkgs.gobject-introspection - pkgs.gdk-pixbuf - pkgs.librsvg + nativeBuildInputs = with pkgs; [ + wrapGAppsHook4 + gobject-introspection ]; - propagatedBuildInputs = [ - pkgs.python3Packages.pygobject3 + buildInputs = with pkgs; [ + gtk4 + libadwaita + gdk-pixbuf + librsvg + ]; + + propagatedBuildInputs = with pkgs.python3Packages; [ + pygobject3 ]; dontBuild = true; installPhase = '' - mkdir -p $out/bin $out/lib/sovran-hub $out/share/applications $out/share/sovran-hub/icons + runHook preInstall - # Copy Python source + install -d $out/lib/sovran-hub cp -r sovran_systemsos_hub $out/lib/sovran-hub/ - - # Copy CSS cp style.css $out/lib/sovran-hub/style.css - - # Copy logos from the repo (no fetchurl needed) - cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true - - # Install the generated config cp ${generatedConfig} $out/lib/sovran-hub/config.json - # Create the launcher script - cat > $out/bin/sovran-hub <<'LAUNCHER' -#!/usr/bin/env python3 -import sys, os -sys.path.insert(0, os.path.join("@out@", "lib", "sovran-hub")) -os.environ["SOVRAN_HUB_CONFIG"] = os.path.join("@out@", "lib", "sovran-hub", "config.json") -os.environ["SOVRAN_HUB_ICONS"] = os.path.join("@out@", "share", "sovran-hub", "icons") -os.environ["SOVRAN_HUB_CSS"] = os.path.join("@out@", "lib", "sovran-hub", "style.css") + install -d $out/share/sovran-hub/icons + cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true + + install -d $out/bin + cat > $out/bin/sovran-hub < $out/share/applications/Sovran_SystemsOS_Hub.desktop < $out/share/applications/Sovran_SystemsOS_Hub.desktop <