summaryrefslogtreecommitdiffstatshomepage
path: root/dot_local
diff options
context:
space:
mode:
authorsommerfeld <sommerfeld@sommerfeld.dev>2026-09-17 15:05:37 +0100
committersommerfeld <sommerfeld@sommerfeld.dev>2026-09-17 15:05:37 +0100
commit9b9b7b6824cb9219f93d1a70414b2412ea7d3585 (patch)
tree96aa6c1f0ede43d6d1e63df186017847345f69fe /dot_local
parentf25d094d0652e8dedff206ca56671b3548f755ff (diff)
downloaddotfiles-9b9b7b6824cb9219f93d1a70414b2412ea7d3585.tar.gz
dotfiles-9b9b7b6824cb9219f93d1a70414b2412ea7d3585.tar.bz2
dotfiles-9b9b7b6824cb9219f93d1a70414b2412ea7d3585.zip
Add GNOME panel status and corporate desktop tools
Diffstat (limited to 'dot_local')
-rwxr-xr-xdot_local/bin/executable_wqr8
-rw-r--r--dot_local/lib/dotfiles/canonical_desktop.py40
-rw-r--r--dot_local/lib/dotfiles/canonical_panel.py109
-rw-r--r--dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/extension.js145
-rw-r--r--dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/metadata.json7
-rw-r--r--dot_local/share/gnome-shell/extensions/corporate-panel@dotfiles/stylesheet.css16
6 files changed, 318 insertions, 7 deletions
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 != "<Super>p"
+ ],
+ saved,
+ )
shell = shlex.quote(str(HOME / ".nix-profile/bin/zsh"))
actions = {
"terminal": ("<Super>Return", "/snap/bin/ghostty"),
@@ -138,6 +154,30 @@ def apply_settings(saved: dict) -> None:
write_key(paper, f"move-{direction}", [f"<Super><Shift>{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;
+}