From f54edbeba820cecae58596bb009800786ddae5f6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 1 Aug 2026 15:43:13 -0500 Subject: [PATCH] Expose the shell's remaining UI-only toggles over IPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every toggle you could only reach by mouse or panel hotkey is now scriptable through omarchy-shell: - omarchy.bluetooth toggleBluetooth — the radio (was right-click / B) - omarchy.network toggleNetwork — Wi-Fi on/off (was W in the panel) - omarchy.tailscale toggleTailscale — the connection as one call, matching the widget's right-click, instead of separate up/down - omarchy.clock toggleWeekStart — calendar week start (was W) - shell toggleBarTransparency — the bar background (was manage UI only) Bluetooth and network follow the power/monitor pattern: manageIpc: false so the panel owns the target's single IpcHandler and can extend it past the inherited lifecycle methods. Co-Authored-By: Claude Fable 5 --- docs/omarchy-shell.md | 1 + shell/plugins/panels/bluetooth/Panel.qml | 14 ++++++++++++++ shell/plugins/panels/clock/BarWidget.qml | 5 +++++ shell/plugins/panels/network/Panel.qml | 14 ++++++++++++++ shell/plugins/panels/tailscale/Panel.qml | 1 + shell/shell.qml | 8 ++++++++ test/shell.d/bar-test.sh | 3 +++ test/shell.d/bluetooth-test.sh | 5 +++++ test/shell.d/clock-test.sh | 1 + test/shell.d/network-test.sh | 5 +++++ test/shell.d/tailscale-test.sh | 4 ++++ 11 files changed, 61 insertions(+) diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index 7895224c..a1349ae4 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -100,6 +100,7 @@ individual plugins (`bar`, `image-selector`, …). | `call ` | call an already-loaded plugin | | `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code | | `reloadConfig` | reload shell.json | +| `toggleBarTransparency` | flip the bar background between solid and transparent | | `setPluginEnabled <"true"\|…>` | flip enabled bit (`ok` / `unknown`) | | `enablePlugin ` | enable and place in one mutation | | `moveBarWidget ` | move a configured widget | diff --git a/shell/plugins/panels/bluetooth/Panel.qml b/shell/plugins/panels/bluetooth/Panel.qml index a11d2bf0..845b05b3 100644 --- a/shell/plugins/panels/bluetooth/Panel.qml +++ b/shell/plugins/panels/bluetooth/Panel.qml @@ -12,6 +12,9 @@ Panel { id: root moduleName: "omarchy.bluetooth" ipcTarget: "omarchy.bluetooth" + // manageIpc: false so this panel can own the single IpcHandler the target + // permits — needed for the toggleBluetooth method below. + manageIpc: false // Address -> "connecting" | "disconnecting" | "forgetting". // The actual Bluetooth sequencing lives in bin/omarchy-bluetooth-device; @@ -515,6 +518,17 @@ Panel { adapter.enabled = !adapter.enabled } + IpcHandler { + target: "omarchy.bluetooth" + + function open() { root.open() } + function close() { root.close() } + function show() { root.open() } + function hide() { root.close() } + function toggle() { root.toggle() } + function toggleBluetooth() { root.toggleBluetooth() } + } + BarIconButton { id: button anchors.fill: parent diff --git a/shell/plugins/panels/clock/BarWidget.qml b/shell/plugins/panels/clock/BarWidget.qml index 92f2d425..92f0ae56 100644 --- a/shell/plugins/panels/clock/BarWidget.qml +++ b/shell/plugins/panels/clock/BarWidget.qml @@ -73,6 +73,10 @@ BarWidget { if (panelLoader.item) panelLoader.item.toggle() } + function toggleWeekStart() { + if (panelLoader.item) panelLoader.item.toggleWeekStart() + } + // The clock fills more slot than it paints a mark for, at both // orientations: horizontally it is a text label in a padded slot, so the // dot takes the label width; vertically it is a stack of icon-sized lines, @@ -127,6 +131,7 @@ BarWidget { function refresh(): void { root.broadcast("refresh") } function cycleFormat(): void { root.cycleFormat() } + function toggleWeekStart(): void { root.toggleWeekStart() } function open(): void { root.open() } function close(): void { root.close() } function show(): void { root.open() } diff --git a/shell/plugins/panels/network/Panel.qml b/shell/plugins/panels/network/Panel.qml index 089b5797..eb8a5f1f 100644 --- a/shell/plugins/panels/network/Panel.qml +++ b/shell/plugins/panels/network/Panel.qml @@ -12,6 +12,9 @@ Panel { id: root moduleName: "omarchy.network" ipcTarget: "omarchy.network" + // manageIpc: false so this panel can own the single IpcHandler the target + // permits — needed for the toggleNetwork method below. + manageIpc: false // Centralized close so callers can't forget to drop the passphrase prompt. function close() { @@ -214,6 +217,17 @@ Panel { Qt.callLater(function() { root.refresh(true) }) } + IpcHandler { + target: "omarchy.network" + + function open() { root.open() } + function close() { root.close() } + function show() { root.open() } + function hide() { root.close() } + function toggle() { root.toggle() } + function toggleNetwork() { root.toggleNetwork() } + } + function activateHeader() { if (headerIndex === qrHeaderIndex) showWifiQr() else if (headerIndex === toggleHeaderIndex) toggleNetwork() diff --git a/shell/plugins/panels/tailscale/Panel.qml b/shell/plugins/panels/tailscale/Panel.qml index 5c495706..34278eda 100644 --- a/shell/plugins/panels/tailscale/Panel.qml +++ b/shell/plugins/panels/tailscale/Panel.qml @@ -373,6 +373,7 @@ Panel { function refresh(): string { tailscale.refresh(); return "ok" } function up(): string { tailscale.loginOrUp(); return "ok" } function down(): string { tailscale.down(); return "ok" } + function toggleTailscale(): string { tailscale.toggleTailscale(); return "ok" } function status(): string { return tailscale.statusText } } diff --git a/shell/shell.qml b/shell/shell.qml index 08d408ec..f6062a25 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -896,6 +896,14 @@ ShellRoot { return "ok" } + function toggleBarTransparency(): string { + if (shell.bar && typeof shell.bar.toggleTransparency === "function") { + shell.bar.toggleTransparency() + return "ok" + } + return "no-bar" + } + function setPluginEnabled(id: string, enabled: string): string { return shell.pluginRegistry.setEnabled(id, enabled === "true") ? "ok" : "unknown" } diff --git a/test/shell.d/bar-test.sh b/test/shell.d/bar-test.sh index 6a7da3a1..8d9c44a1 100644 --- a/test/shell.d/bar-test.sh +++ b/test/shell.d/bar-test.sh @@ -18,6 +18,9 @@ run_node_test <<'JS' const fs = require('fs') const bar = requireFromRoot('shell/plugins/bar/BarModel.js') const barSource = fs.readFileSync(root + '/shell/plugins/bar/Bar.qml', 'utf8') +const shellSource = fs.readFileSync(root + '/shell/shell.qml', 'utf8') + +assert(/function toggleBarTransparency\(\): string \{[\s\S]*?shell\.bar\.toggleTransparency\(\)/.test(shellSource), 'shell exposes the bar transparency toggle over IPC') // The center section declares two arrangements and shows one; the hidden one // must not build its modules or every center widget exists twice. diff --git a/test/shell.d/bluetooth-test.sh b/test/shell.d/bluetooth-test.sh index 8e71f24b..9f46f628 100644 --- a/test/shell.d/bluetooth-test.sh +++ b/test/shell.d/bluetooth-test.sh @@ -9,7 +9,12 @@ grep -q '^ConditionPathIsDirectory=/sys/class/bluetooth$' "$ROOT/default/systemd pass "bt-agent is skipped on machines without Bluetooth hardware" run_node_test <<'JS' +const fs = require('fs') const bluetooth = requireFromRoot('shell/plugins/panels/bluetooth/Model.js') +const panelSource = fs.readFileSync(root + '/shell/plugins/panels/bluetooth/Panel.qml', 'utf8') + +assert(/IpcHandler[\s\S]*?function toggleBluetooth\(\) \{ root\.toggleBluetooth\(\) \}/.test(panelSource), 'bluetooth exposes the radio toggle over IPC') +assert(/manageIpc: false/.test(panelSource), 'bluetooth owns its IPC handler so it can extend the target methods') assert(bluetooth.isUuidLike('0000110b-0000-1000-8000-00805f9b34fb'), 'bluetooth detects UUID-like names') assert(bluetooth.isAddressLike('AA:BB:CC:DD:EE:FF'), 'bluetooth detects address-like names') diff --git a/test/shell.d/clock-test.sh b/test/shell.d/clock-test.sh index ac9ea572..6ed9cd6f 100755 --- a/test/shell.d/clock-test.sh +++ b/test/shell.d/clock-test.sh @@ -147,6 +147,7 @@ assert(/ipcTarget: "omarchy\.clock"/.test(panelSource), 'calendar panel register assert(/manageIpc: false/.test(panelSource), 'calendar panel leaves the IPC target to the bar widget') assert(/anchorItem: root\.anchorItem/.test(panelSource), 'calendar panel anchors to the host widget button') assert(/function toggleWeekStart\(\)/.test(panelSource), 'calendar panel exposes a week start toggle') +assert(/function toggleWeekStart\(\): void \{ root\.toggleWeekStart\(\) \}/.test(widgetSource), 'clock exposes the week start toggle over IPC') assert(/setting\("weekStartDay", null\)/.test(panelSource) && /persistSettings\(\{ weekStartDay:/.test(panelSource), 'calendar reads and writes the week start as weekStartDay') assert(/updateEntryInline/.test(panelSource), 'calendar panel persists the week start to shell.json') assert(/function moveMonth\(delta\)/.test(panelSource), 'calendar panel steps between months') diff --git a/test/shell.d/network-test.sh b/test/shell.d/network-test.sh index a6c98e18..c9fdbfd4 100644 --- a/test/shell.d/network-test.sh +++ b/test/shell.d/network-test.sh @@ -5,7 +5,12 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' +const fs = require('fs') const network = requireFromRoot('shell/plugins/panels/network/Model.js') +const panelSource = fs.readFileSync(root + '/shell/plugins/panels/network/Panel.qml', 'utf8') + +assert(/IpcHandler[\s\S]*?function toggleNetwork\(\) \{ root\.toggleNetwork\(\) \}/.test(panelSource), 'network exposes the Wi-Fi radio toggle over IPC') +assert(/manageIpc: false/.test(panelSource), 'network owns its IPC handler so it can extend the target methods') assertDeepEqual( network.parseNetworkStatus('wifi\tCafe WiFi\t78\t5200\n'), diff --git a/test/shell.d/tailscale-test.sh b/test/shell.d/tailscale-test.sh index d46925a0..9c04cca9 100644 --- a/test/shell.d/tailscale-test.sh +++ b/test/shell.d/tailscale-test.sh @@ -5,7 +5,11 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' +const fs = require('fs') const tailscale = requireFromRoot('shell/plugins/panels/tailscale/Model.js') +const panelSource = fs.readFileSync(root + '/shell/plugins/panels/tailscale/Panel.qml', 'utf8') + +assert(/function toggleTailscale\(\): string \{ tailscale\.toggleTailscale\(\); return "ok" \}/.test(panelSource), 'tailscale exposes the connection toggle over IPC') assertDeepEqual( tailscale.filterIPv4(['100.64.0.1', 'fd7a:115c:a1e0::1', '192.168.1.2']),