From 9b9b7b6824cb9219f93d1a70414b2412ea7d3585 Mon Sep 17 00:00:00 2001 From: sommerfeld Date: Thu, 17 Sep 2026 15:05:37 +0100 Subject: Add GNOME panel status and corporate desktop tools --- dot_local/bin/executable_wqr | 8 +- dot_local/lib/dotfiles/canonical_desktop.py | 40 ++++++ dot_local/lib/dotfiles/canonical_panel.py | 109 ++++++++++++++++ .../corporate-panel@dotfiles/extension.js | 145 +++++++++++++++++++++ .../corporate-panel@dotfiles/metadata.json | 7 + .../corporate-panel@dotfiles/stylesheet.css | 16 +++ 6 files changed, 318 insertions(+), 7 deletions(-) create mode 100644 dot_local/lib/dotfiles/canonical_panel.py create mode 100644 dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/extension.js create mode 100644 dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/metadata.json create mode 100644 dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/stylesheet.css (limited to 'dot_local') diff --git a/dot_local/bin/executable_wqr b/dot_local/bin/executable_wqr index b4cc123..5f9d36d 100755 --- a/dot_local/bin/executable_wqr +++ b/dot_local/bin/executable_wqr @@ -8,10 +8,4 @@ else text="$1" fi -if [ "$(cat "$HOME/.config/dotfiles/role" 2>/dev/null)" = canonical ]; then - image=$(mktemp "${XDG_RUNTIME_DIR:?}/dotfiles-qr-XXXXXX.png") - printf '%s' "$text" | qrencode -t PNG -o "$image" - /usr/bin/xdg-open "$image" -else - printf '%s' "$text" | qrencode -t PNG -o - | imv - -fi +printf '%s' "$text" | qrencode -t PNG -o - | imv - diff --git a/dot_local/lib/dotfiles/canonical_desktop.py b/dot_local/lib/dotfiles/canonical_desktop.py index fc9e946..cbda77f 100644 --- a/dot_local/lib/dotfiles/canonical_desktop.py +++ b/dot_local/lib/dotfiles/canonical_desktop.py @@ -13,6 +13,9 @@ EXTENSIONS = [ "paperwm@paperwm.github.com", "copyous@boerdereinar.dev", "emoji-copy@felipeftn", + "Vitals@CoreCoding.com", + "corporate-panel@dotfiles", + "ubuntu-appindicators@ubuntu.com", ] @@ -75,6 +78,19 @@ def merge_key(schema: str, key: str, values: list[str], saved: dict) -> None: def shortcuts(saved: dict) -> None: + display_schema = "org.gnome.mutter.keybindings" + display_keys = settings_object(display_schema) + if display_keys is not None: + write_key( + display_schema, + "switch-monitor", + [ + key + for key in display_keys.get_strv("switch-monitor") + if key != "p" + ], + saved, + ) shell = shlex.quote(str(HOME / ".nix-profile/bin/zsh")) actions = { "terminal": ("Return", "/snap/bin/ghostty"), @@ -138,6 +154,30 @@ def apply_settings(saved: dict) -> None: write_key(paper, f"move-{direction}", [f"{letter}"], saved) shortcuts(saved) workspaces(saved) + panel(saved) + + +def panel(saved: dict) -> None: + schema = "org.gnome.shell.extensions.vitals" + monitor = shlex.join( + ["/snap/bin/ghostty", "-e", str(HOME / ".nix-profile/bin/htop")] + ) + for key, value in { + "hot-sensors": [ + "_processor_usage_", + "__temperature_max__", + "_memory_usage_", + "__network-rx_max__", + "__network-tx_max__", + ], + "position-in-panel": 2, + "update-time": 5, + "include-public-ip": False, + "monitor-cmd": monitor, + }.items(): + write_key(schema, key, value, saved) + write_key("org.gnome.desktop.interface", "show-battery-percentage", True, saved) + write_key("org.gnome.desktop.interface", "clock-show-weekday", True, saved) def workspaces(saved: dict) -> None: diff --git a/dot_local/lib/dotfiles/canonical_panel.py b/dot_local/lib/dotfiles/canonical_panel.py new file mode 100644 index 0000000..befc3b6 --- /dev/null +++ b/dot_local/lib/dotfiles/canonical_panel.py @@ -0,0 +1,109 @@ +"""Read corporate panel status or open a requested desktop action.""" + +import json +import os +import shlex +import subprocess +import sys +from pathlib import Path + + +def output(*command: str) -> str: + return subprocess.check_output( + command, + text=True, + timeout=10, + stderr=subprocess.PIPE, + env={**os.environ, "LC_ALL": "C"}, + ) + + +def failed_units() -> str: + count = 0 + for scope in ([], ["--user"]): + units = json.loads( + output( + "/usr/bin/systemctl", *scope, "--failed", "--output=json", "--no-pager" + ) + ) + count += len(units) + return f"FAIL {count}" + + +def apt_updates() -> str: + lines = output("/usr/bin/apt", "list", "--upgradable").splitlines() + count = sum("/" in line.split()[0] for line in lines if line.strip()) + return f"APT {count}" + + +def displays(root: Path = Path("/sys/class/drm")) -> str: + connected = [ + path + for path in root.glob("*/status") + if "-eDP-" not in path.parent.name + and "-LVDS-" not in path.parent.name + and path.read_text().strip() == "connected" + ] + return f"EXT {len(connected)}" + + +def collect() -> dict: + status = { + "errors": "", + "reboot": "REBOOT" if Path("/run/reboot-required").exists() else "", + } + for name, label, read in [ + ("displays", "EXT", displays), + ("updates", "APT", apt_updates), + ("failed", "FAIL", failed_units), + ]: + try: + status[name] = read() + except (OSError, ValueError, subprocess.SubprocessError, RuntimeError) as error: + status[name] = f"{label} ?" + status["errors"] += f"{label}: {error}\n" + return status + + +def terminal(command: str) -> list[str]: + shell = str(Path.home() / ".nix-profile/bin/zsh") + pause = "; printf '\\nPress Enter to close'; read -r reply" + return ["/snap/bin/ghostty", "-e", shell, "-lc", command + pause] + + +def action_command(action: str) -> list[str]: + if action == "update": + source = output( + str(Path.home() / ".nix-profile/bin/chezmoi"), "source-path" + ).strip() + return terminal(f"cd -- {shlex.quote(source)} && just update") + if action == "failed": + return terminal("systemctl --failed; systemctl --user --failed") + if action == "updates": + return terminal("apt list --upgradable; snap refresh --list") + if action == "reboot": + return terminal("cat /run/reboot-required /run/reboot-required.pkgs") + if action == "monitor": + return terminal("htop") + if action == "audio": + return terminal("pulsemixer") + if action == "mail": + return ["/snap/bin/thunderbird"] + if action == "displays": + return ["/usr/bin/gnome-control-center", "display"] + raise ValueError(f"Unknown panel action: {action}") + + +def main() -> None: + if (Path.home() / ".config/dotfiles/role").read_text().strip() != "canonical": + raise SystemExit("The panel requires the canonical role.") + if sys.argv[1:] == ["status"]: + print(json.dumps(collect())) + elif len(sys.argv) == 3 and sys.argv[1] == "action": + subprocess.Popen(action_command(sys.argv[2]), start_new_session=True) + else: + raise SystemExit("Use status or action NAME.") + + +if __name__ == "__main__": + main() diff --git a/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/extension.js b/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/extension.js new file mode 100644 index 0000000..d959669 --- /dev/null +++ b/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/extension.js @@ -0,0 +1,145 @@ +import Clutter from "gi://Clutter"; +import Gio from "gi://Gio"; +import GLib from "gi://GLib"; +import St from "gi://St"; +import { Extension } from "resource:///org/gnome/shell/extensions/extension.js"; +import * as Main from "resource:///org/gnome/shell/ui/main.js"; +import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js"; +import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js"; + +export default class CorporatePanel extends Extension { + enable() { + this._cancellable = new Gio.Cancellable(); + this._button = new PanelMenu.Button(0.0, "Corporate status"); + const box = new St.BoxLayout({ style_class: "corporate-panel" }); + this._labels = {}; + for (const name of ["displays", "updates", "failed", "reboot"]) { + const label = new St.Label({ + text: "", + y_align: Clutter.ActorAlign.CENTER, + }); + this._labels[name] = label; + box.add_child(label); + } + this._button.add_child(box); + this._addActions(); + Main.panel.addToStatusArea(this.uuid, this._button, 1, "right"); + this._refresh(); + this._timer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 60, () => { + this._refresh(); + return GLib.SOURCE_CONTINUE; + }); + } + + _command(...args) { + return [ + "/usr/bin/python3", + GLib.build_filenamev([ + GLib.get_home_dir(), + ".local/lib/dotfiles/canonical_panel.py", + ]), + ...args, + ]; + } + + _addActions() { + this._status = new PopupMenu.PopupMenuItem("Reading status...", { + reactive: false, + }); + this._button.menu.addMenuItem(this._status); + for (const [name, title] of [ + ["monitor", "System monitor"], + ["audio", "Audio mixer"], + ["displays", "Display settings"], + ["failed", "Failed services"], + ["updates", "Available updates (apt and Snap)"], + ["update", "Run dotfiles update"], + ["reboot", "Reboot requirement details"], + ["mail", "Thunderbird"], + ]) { + this._button.menu.addAction(title, () => { + try { + Gio.Subprocess.new( + this._command("action", name), + Gio.SubprocessFlags.NONE, + ); + } catch (error) { + Main.notifyError("Corporate panel", error.message); + } + }); + } + this._button.menu.addAction("Refresh status", () => this._refresh()); + } + + _refresh() { + if (this._process) return; + const token = this._cancellable; + try { + const process = Gio.Subprocess.new( + this._command("status"), + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE, + ); + this._process = process; + process.communicate_utf8_async(null, token, (source, result) => { + if (token.is_cancelled()) return; + this._process = null; + try { + const [, stdout, stderr] = source.communicate_utf8_finish(result); + if (!source.get_successful()) + throw new Error(stderr || "Status command failed"); + this._render(JSON.parse(stdout)); + } catch (error) { + this._showError(error); + } + }); + } catch (error) { + this._showError(error); + } + } + + _render(status) { + for (const [name, label] of Object.entries(this._labels)) { + const value = status[name]; + label.set_text(value); + label.visible = Boolean(value); + const warning = + value.includes("?") || (name === "updates" && value !== "APT 0"); + const critical = + name === "reboot" || (name === "failed" && value !== "FAIL 0"); + label.set_style_class_name( + critical + ? "corporate-critical" + : warning + ? "corporate-warning" + : "corporate-ok", + ); + } + this._status.label.set_text( + status.errors + ? "Some checks failed; see journal" + : "APT uses the local package cache", + ); + if (status.errors) console.warn(`Corporate panel: ${status.errors}`); + } + + _showError(error) { + this._render({ + displays: "EXT ?", + updates: "APT ?", + failed: "FAIL ?", + reboot: "", + errors: error.message, + }); + } + + disable() { + this._cancellable?.cancel(); + if (this._timer) GLib.Source.remove(this._timer); + this._timer = null; + this._process?.force_exit(); + this._process = null; + this._button?.destroy(); + this._button = null; + this._labels = null; + } +} diff --git a/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/metadata.json b/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/metadata.json new file mode 100644 index 0000000..a21b618 --- /dev/null +++ b/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/metadata.json @@ -0,0 +1,7 @@ +{ + "uuid": "corporate-panel@dotfiles", + "name": "Corporate Panel", + "description": "Display status, package updates, and failed services.", + "shell-version": ["50"], + "version": 1 +} diff --git a/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/stylesheet.css b/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/stylesheet.css new file mode 100644 index 0000000..a198ccc --- /dev/null +++ b/dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/stylesheet.css @@ -0,0 +1,16 @@ +.corporate-panel { + spacing: 8px; + font-size: 12px; +} + +.corporate-ok { + color: #b8bb26; +} + +.corporate-warning { + color: #fabd2f; +} + +.corporate-critical { + color: #fb4934; +} -- cgit v1.3.1