From 10898aea168256613e35a511753bf3f9415d91d5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 25 May 2026 14:47:08 +0200 Subject: [PATCH] Move panel plugins under panels --- shell/README.md | 7 +++++++ shell/plugins/README.md | 11 +++++++--- shell/plugins/bar/README.md | 6 +++--- shell/plugins/dev-gallery/GalleryPanel.qml | 2 +- shell/plugins/{ => panels}/audio/Model.js | 0 shell/plugins/{ => panels}/audio/Panel.qml | 0 .../plugins/{ => panels}/audio/manifest.json | 0 shell/plugins/{ => panels}/bluetooth/Model.js | 0 .../plugins/{ => panels}/bluetooth/Panel.qml | 0 .../{ => panels}/bluetooth/manifest.json | 0 shell/plugins/{ => panels}/monitor/Model.js | 0 shell/plugins/{ => panels}/monitor/Panel.qml | 10 ++++----- .../{ => panels}/monitor/manifest.json | 0 shell/plugins/{ => panels}/network/Model.js | 0 shell/plugins/{ => panels}/network/Panel.qml | 0 .../{ => panels}/network/manifest.json | 0 shell/plugins/{ => panels}/power/Model.js | 0 shell/plugins/{ => panels}/power/Panel.qml | 0 .../plugins/{ => panels}/power/manifest.json | 0 .../{ => panels}/weather/BarWidget.qml | 0 shell/plugins/{ => panels}/weather/Model.js | 0 shell/plugins/{ => panels}/weather/Panel.qml | 0 .../{ => panels}/weather/manifest.json | 0 shell/services/PluginRegistry.qml | 3 ++- test/shell.d/audio-test.sh | 2 +- test/shell.d/bluetooth-test.sh | 2 +- test/shell.d/monitor-test.sh | 2 +- test/shell.d/network-test.sh | 2 +- test/shell.d/plugins-test.sh | 21 ++++++++++++------- test/shell.d/power-test.sh | 2 +- test/shell.d/weather-test.sh | 2 +- 31 files changed, 45 insertions(+), 27 deletions(-) rename shell/plugins/{ => panels}/audio/Model.js (100%) rename shell/plugins/{ => panels}/audio/Panel.qml (100%) rename shell/plugins/{ => panels}/audio/manifest.json (100%) rename shell/plugins/{ => panels}/bluetooth/Model.js (100%) rename shell/plugins/{ => panels}/bluetooth/Panel.qml (100%) rename shell/plugins/{ => panels}/bluetooth/manifest.json (100%) rename shell/plugins/{ => panels}/monitor/Model.js (100%) rename shell/plugins/{ => panels}/monitor/Panel.qml (99%) rename shell/plugins/{ => panels}/monitor/manifest.json (100%) rename shell/plugins/{ => panels}/network/Model.js (100%) rename shell/plugins/{ => panels}/network/Panel.qml (100%) rename shell/plugins/{ => panels}/network/manifest.json (100%) rename shell/plugins/{ => panels}/power/Model.js (100%) rename shell/plugins/{ => panels}/power/Panel.qml (100%) rename shell/plugins/{ => panels}/power/manifest.json (100%) rename shell/plugins/{ => panels}/weather/BarWidget.qml (100%) rename shell/plugins/{ => panels}/weather/Model.js (100%) rename shell/plugins/{ => panels}/weather/Panel.qml (100%) rename shell/plugins/{ => panels}/weather/manifest.json (100%) diff --git a/shell/README.md b/shell/README.md index c7c73cdb..03e2490a 100644 --- a/shell/README.md +++ b/shell/README.md @@ -29,6 +29,13 @@ shell/ image-picker/ menu/ notifications/ + panels/ + audio/ + bluetooth/ + monitor/ + network/ + power/ + weather/ services/ battery/ idle/ diff --git a/shell/plugins/README.md b/shell/plugins/README.md index d98b8fd2..fa65c49d 100644 --- a/shell/plugins/README.md +++ b/shell/plugins/README.md @@ -20,6 +20,12 @@ User-installed plugins live alongside these conceptually but on disk under | Reminders | `omarchy.reminders` | `overlay` | `reminders/ReminderFlow.qml` | | Omarchy menu | `omarchy.menu` | `menu`, `bar-widget` | `menu/Menu.qml`, `menu/BarWidget.qml` | | Notifications | `omarchy.notifications` | `service`, `bar-widget` | `notifications/Service.qml`, `notifications/BarWidget.qml` | +| Audio | `omarchy.audio` | `bar-widget` | `panels/audio/Panel.qml` | +| Bluetooth | `omarchy.bluetooth` | `bar-widget` | `panels/bluetooth/Panel.qml` | +| Monitor | `omarchy.monitor` | `bar-widget` | `panels/monitor/Panel.qml` | +| Network | `omarchy.network` | `bar-widget` | `panels/network/Panel.qml` | +| Power | `omarchy.power` | `bar-widget` | `panels/power/Panel.qml` | +| Weather | `omarchy.weather` | `bar-widget` | `panels/weather/BarWidget.qml` | | Media | `omarchy.media` | `service`, `bar-widget` | `services/media/Service.qml`, `services/media/BarWidget.qml` | | Battery | `omarchy.battery` | `service` | `services/battery/Service.qml` | | Idle | `omarchy.idle` | `service` | `services/idle/Service.qml` | @@ -28,9 +34,8 @@ User-installed plugins live alongside these conceptually but on disk under | Polkit agent | `omarchy.polkit` | `service` | `polkit/PolkitAgent.qml` | First-party bar-only widgets also carry manifests next to their QML files, -e.g. `bar/widgets/Clock.manifest.json`. Rich popup widgets live in feature -plugin directories such as `audio/`, `network/`, and `power/`, each with its -own `manifest.json`. +e.g. `bar/widgets/Clock.manifest.json`. Rich popup widgets live under +`panels/`, each with its own `manifest.json`. ## Bar diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index cb84854a..2363b740 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -8,7 +8,7 @@ the shell for its whole session. - `manifest.json` declares the plugin (`id: omarchy.bar`, `kind: bar`) and points at `Bar.qml` as the entry point. - `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly. - `widgets/` holds simple first-party bar widgets with sibling manifests. -- Feature plugins such as `../audio/`, `../network/`, and `../power/` provide richer popup bar widgets. +- Feature plugins such as `../panels/audio/`, `../panels/network/`, and `../panels/power/` provide richer popup bar widgets. - The bar receives its config from the host shell as a `barConfig` property; the host loads it from `~/.config/omarchy/shell.json` (or `config/omarchy/shell.json` when the user has no file). - `omarchy-style-bar-position` updates only the user shell.json file. @@ -167,8 +167,8 @@ Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (o First-party bar widgets are manifest-backed just like third-party widgets. Simple widgets carry sibling manifests such as `widgets/Clock.manifest.json`; -richer popup widgets live in feature directories such as `../audio/` and -`../network/`; and feature plugins such as `omarchy.menu`, `omarchy.media`, and +richer popup widgets live in feature directories such as `../panels/audio/` and +`../panels/network/`; and feature plugins such as `omarchy.menu`, `omarchy.media`, and `omarchy.notifications` declare their bar-widget entry points in their own `manifest.json`. Bar layout ids are namespaced, e.g. `omarchy.audio`, `omarchy.network`, and `omarchy.clock`. Older UpperCamelCase ids such as diff --git a/shell/plugins/dev-gallery/GalleryPanel.qml b/shell/plugins/dev-gallery/GalleryPanel.qml index ac31b3c4..31ded41a 100644 --- a/shell/plugins/dev-gallery/GalleryPanel.qml +++ b/shell/plugins/dev-gallery/GalleryPanel.qml @@ -407,7 +407,7 @@ Item { color: Qt.darker(root.foreground, 1.4) font.family: root.fontFamily font.pixelSize: Style.font.bodySmall - text: "Single cursor. Most reusable panel primitives expose hasCursor: bool and emit hovered(bool); composed rows (including sliders) wrap their content in CursorSurface. The panel root owns cursorActive + focusSection + selectedIndex; each element binds hasCursor: root.cursorActive && root.focusSection === 'X' && root.selectedIndex === N, and onHovered flips cursorActive on while updating the same state. No initial highlight, then one highlight on screen once the keyboard or mouse enters. See plugins/audio/Panel.qml for the canonical recipe." + text: "Single cursor. Most reusable panel primitives expose hasCursor: bool and emit hovered(bool); composed rows (including sliders) wrap their content in CursorSurface. The panel root owns cursorActive + focusSection + selectedIndex; each element binds hasCursor: root.cursorActive && root.focusSection === 'X' && root.selectedIndex === N, and onHovered flips cursorActive on while updating the same state. No initial highlight, then one highlight on screen once the keyboard or mouse enters. See plugins/panels/audio/Panel.qml for the canonical recipe." } Text { width: parent.width diff --git a/shell/plugins/audio/Model.js b/shell/plugins/panels/audio/Model.js similarity index 100% rename from shell/plugins/audio/Model.js rename to shell/plugins/panels/audio/Model.js diff --git a/shell/plugins/audio/Panel.qml b/shell/plugins/panels/audio/Panel.qml similarity index 100% rename from shell/plugins/audio/Panel.qml rename to shell/plugins/panels/audio/Panel.qml diff --git a/shell/plugins/audio/manifest.json b/shell/plugins/panels/audio/manifest.json similarity index 100% rename from shell/plugins/audio/manifest.json rename to shell/plugins/panels/audio/manifest.json diff --git a/shell/plugins/bluetooth/Model.js b/shell/plugins/panels/bluetooth/Model.js similarity index 100% rename from shell/plugins/bluetooth/Model.js rename to shell/plugins/panels/bluetooth/Model.js diff --git a/shell/plugins/bluetooth/Panel.qml b/shell/plugins/panels/bluetooth/Panel.qml similarity index 100% rename from shell/plugins/bluetooth/Panel.qml rename to shell/plugins/panels/bluetooth/Panel.qml diff --git a/shell/plugins/bluetooth/manifest.json b/shell/plugins/panels/bluetooth/manifest.json similarity index 100% rename from shell/plugins/bluetooth/manifest.json rename to shell/plugins/panels/bluetooth/manifest.json diff --git a/shell/plugins/monitor/Model.js b/shell/plugins/panels/monitor/Model.js similarity index 100% rename from shell/plugins/monitor/Model.js rename to shell/plugins/panels/monitor/Model.js diff --git a/shell/plugins/monitor/Panel.qml b/shell/plugins/panels/monitor/Panel.qml similarity index 99% rename from shell/plugins/monitor/Panel.qml rename to shell/plugins/panels/monitor/Panel.qml index dc89c953..e6cc62ba 100644 --- a/shell/plugins/monitor/Panel.qml +++ b/shell/plugins/panels/monitor/Panel.qml @@ -191,11 +191,11 @@ Panel { function brightness(percent: string): string { return root.brightnessIpc(percent) } function state(): string { return root.stateIpc() } - function open(): void { root.open() } - function close(): void { root.close() } - function toggle(): void { root.toggle() } - function show(): void { root.open() } - function hide(): void { root.close() } + function open() { root.open() } + function close() { root.close() } + function toggle() { root.toggle() } + function show() { root.open() } + function hide() { root.close() } } function refresh() { diff --git a/shell/plugins/monitor/manifest.json b/shell/plugins/panels/monitor/manifest.json similarity index 100% rename from shell/plugins/monitor/manifest.json rename to shell/plugins/panels/monitor/manifest.json diff --git a/shell/plugins/network/Model.js b/shell/plugins/panels/network/Model.js similarity index 100% rename from shell/plugins/network/Model.js rename to shell/plugins/panels/network/Model.js diff --git a/shell/plugins/network/Panel.qml b/shell/plugins/panels/network/Panel.qml similarity index 100% rename from shell/plugins/network/Panel.qml rename to shell/plugins/panels/network/Panel.qml diff --git a/shell/plugins/network/manifest.json b/shell/plugins/panels/network/manifest.json similarity index 100% rename from shell/plugins/network/manifest.json rename to shell/plugins/panels/network/manifest.json diff --git a/shell/plugins/power/Model.js b/shell/plugins/panels/power/Model.js similarity index 100% rename from shell/plugins/power/Model.js rename to shell/plugins/panels/power/Model.js diff --git a/shell/plugins/power/Panel.qml b/shell/plugins/panels/power/Panel.qml similarity index 100% rename from shell/plugins/power/Panel.qml rename to shell/plugins/panels/power/Panel.qml diff --git a/shell/plugins/power/manifest.json b/shell/plugins/panels/power/manifest.json similarity index 100% rename from shell/plugins/power/manifest.json rename to shell/plugins/panels/power/manifest.json diff --git a/shell/plugins/weather/BarWidget.qml b/shell/plugins/panels/weather/BarWidget.qml similarity index 100% rename from shell/plugins/weather/BarWidget.qml rename to shell/plugins/panels/weather/BarWidget.qml diff --git a/shell/plugins/weather/Model.js b/shell/plugins/panels/weather/Model.js similarity index 100% rename from shell/plugins/weather/Model.js rename to shell/plugins/panels/weather/Model.js diff --git a/shell/plugins/weather/Panel.qml b/shell/plugins/panels/weather/Panel.qml similarity index 100% rename from shell/plugins/weather/Panel.qml rename to shell/plugins/panels/weather/Panel.qml diff --git a/shell/plugins/weather/manifest.json b/shell/plugins/panels/weather/manifest.json similarity index 100% rename from shell/plugins/weather/manifest.json rename to shell/plugins/panels/weather/manifest.json diff --git a/shell/services/PluginRegistry.qml b/shell/services/PluginRegistry.qml index 41776e10..7b9ec536 100644 --- a/shell/services/PluginRegistry.qml +++ b/shell/services/PluginRegistry.qml @@ -267,7 +267,8 @@ QtObject { if (scanning) return scanning = true // $0 = first-party dir, $1 = third-party dir. Some bash versions need the explicit -- separator. - // First-party plugins may be grouped one level deeper, e.g. services/battery. + // First-party plugins may be grouped one level deeper, e.g. panels/audio + // or services/battery. // First-party bar widgets can also carry sibling manifests such as // widgets/Clock.manifest.json so multiple widgets can live in one source // directory without wrapper folders. diff --git a/test/shell.d/audio-test.sh b/test/shell.d/audio-test.sh index 6e804ea4..710835d8 100644 --- a/test/shell.d/audio-test.sh +++ b/test/shell.d/audio-test.sh @@ -5,7 +5,7 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' -const audio = requireFromRoot('shell/plugins/audio/Model.js') +const audio = requireFromRoot('shell/plugins/panels/audio/Model.js') assert(audio.isPlaybackStream({ isStream: true, isSink: true }), 'audio detects sink-backed playback streams') assert(audio.isPlaybackStream({ isStream: true, type: 'Stream/Output/Audio' }), 'audio detects typed playback streams') diff --git a/test/shell.d/bluetooth-test.sh b/test/shell.d/bluetooth-test.sh index feb1ffdb..f0123141 100644 --- a/test/shell.d/bluetooth-test.sh +++ b/test/shell.d/bluetooth-test.sh @@ -5,7 +5,7 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' -const bluetooth = requireFromRoot('shell/plugins/bluetooth/Model.js') +const bluetooth = requireFromRoot('shell/plugins/panels/bluetooth/Model.js') 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/monitor-test.sh b/test/shell.d/monitor-test.sh index 0a571aa4..aa4ddac7 100644 --- a/test/shell.d/monitor-test.sh +++ b/test/shell.d/monitor-test.sh @@ -5,7 +5,7 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' -const monitor = requireFromRoot('shell/plugins/monitor/Model.js') +const monitor = requireFromRoot('shell/plugins/panels/monitor/Model.js') assertEqual(monitor.clampBrightness(0), 1, 'monitor clamps minimum brightness') assertEqual(monitor.clampBrightness(101), 100, 'monitor clamps maximum brightness') diff --git a/test/shell.d/network-test.sh b/test/shell.d/network-test.sh index b5c84fe0..10a77703 100644 --- a/test/shell.d/network-test.sh +++ b/test/shell.d/network-test.sh @@ -5,7 +5,7 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' -const network = requireFromRoot('shell/plugins/network/Model.js') +const network = requireFromRoot('shell/plugins/panels/network/Model.js') assertDeepEqual( network.parseNetworkStatus('wifi\tCafe WiFi\t78\t5200\n'), diff --git a/test/shell.d/plugins-test.sh b/test/shell.d/plugins-test.sh index 0372917d..2da3bcd0 100644 --- a/test/shell.d/plugins-test.sh +++ b/test/shell.d/plugins-test.sh @@ -59,24 +59,29 @@ function assertSafeEntryPoint(manifest, manifestPath, key, value) { const manifests = walk(pluginsDir) const manifestPaths = manifests.map(relativeFromPlugins) const manifestSet = new Set(manifestPaths) +const groupedPluginRoots = new Set(['panels', 'services']) assert(manifests.length > 0, 'plugin manifests are present') for (const entry of fs.readdirSync(pluginsDir, { withFileTypes: true })) { - if (!entry.isDirectory() || entry.name === 'services') continue + if (!entry.isDirectory() || groupedPluginRoots.has(entry.name)) continue check( manifestSet.has(`${entry.name}/manifest.json`), `top-level plugin ${entry.name} must have a manifest` ) } -const serviceRoot = path.join(pluginsDir, 'services') -for (const entry of fs.readdirSync(serviceRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) continue - check( - manifestSet.has(`services/${entry.name}/manifest.json`), - `service plugin ${entry.name} must have a manifest` - ) +for (const groupName of groupedPluginRoots) { + const groupRoot = path.join(pluginsDir, groupName) + if (!fs.existsSync(groupRoot)) continue + + for (const entry of fs.readdirSync(groupRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + check( + manifestSet.has(`${groupName}/${entry.name}/manifest.json`), + `${groupName} plugin ${entry.name} must have a manifest` + ) + } } for (const manifestPath of manifestPaths) { diff --git a/test/shell.d/power-test.sh b/test/shell.d/power-test.sh index 03b2b731..3b26fe2f 100644 --- a/test/shell.d/power-test.sh +++ b/test/shell.d/power-test.sh @@ -5,7 +5,7 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' -const power = requireFromRoot('shell/plugins/power/Model.js') +const power = requireFromRoot('shell/plugins/panels/power/Model.js') const states = { Charging: 1, Discharging: 2, FullyCharged: 3, PendingCharge: 4 } assertEqual(power.selectProfileIndex(0, 1, ['balanced', 'performance']), 1, 'power advances profile selection') diff --git a/test/shell.d/weather-test.sh b/test/shell.d/weather-test.sh index 9127765d..889afa58 100644 --- a/test/shell.d/weather-test.sh +++ b/test/shell.d/weather-test.sh @@ -5,7 +5,7 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' -const weather = requireFromRoot('shell/plugins/weather/Model.js') +const weather = requireFromRoot('shell/plugins/panels/weather/Model.js') assertDeepEqual( weather.parseWeatherStatus('{"text":"☀","class":"sunny"}'),