From 4f0bdb790b75603a6506daa6603b3576349373d6 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sat, 23 May 2026 04:29:07 -0400 Subject: [PATCH] Make built-in widgets plugins --- docs/omarchy-shell.md | 5 +- shell/Commons/Util.qml | 26 +----- shell/README.md | 10 ++- shell/Ui/Panel.qml | 9 +- shell/plugins/README.md | 36 ++++---- .../{panels/Audio.qml => audio/BarWidget.qml} | 4 +- shell/plugins/audio/manifest.json | 20 +++++ shell/plugins/bar/FirstPartyWidgets.qml | 85 ------------------- shell/plugins/bar/README.md | 35 ++++---- .../bar/widgets/ActiveWindow.manifest.json | 20 +++++ shell/plugins/bar/widgets/ActiveWindow.qml | 2 +- shell/plugins/bar/widgets/Clock.manifest.json | 20 +++++ shell/plugins/bar/widgets/Clock.qml | 4 +- .../bar/widgets/Indicators.manifest.json | 65 ++++++++++++++ shell/plugins/bar/widgets/Indicators.qml | 4 +- .../bar/widgets/KeyboardLayout.manifest.json | 20 +++++ shell/plugins/bar/widgets/KeyboardLayout.qml | 2 +- .../bar/widgets/LockKeys.manifest.json | 20 +++++ shell/plugins/bar/widgets/LockKeys.qml | 2 +- .../bar/widgets/Microphone.manifest.json | 20 +++++ shell/plugins/bar/widgets/Microphone.qml | 4 +- .../plugins/bar/widgets/Spacer.manifest.json | 21 +++++ shell/plugins/bar/widgets/Spacer.qml | 2 +- .../bar/widgets/SystemStats.manifest.json | 20 +++++ shell/plugins/bar/widgets/SystemStats.qml | 2 +- .../bar/widgets/SystemUpdate.manifest.json | 20 +++++ shell/plugins/bar/widgets/SystemUpdate.qml | 4 +- shell/plugins/bar/widgets/Tray.manifest.json | 20 +++++ shell/plugins/bar/widgets/Tray.qml | 2 +- .../bar/widgets/Workspaces.manifest.json | 20 +++++ shell/plugins/bar/widgets/Workspaces.qml | 2 +- .../Bluetooth.qml => bluetooth/BarWidget.qml} | 4 +- shell/plugins/bluetooth/manifest.json | 20 +++++ shell/plugins/dev-gallery/GalleryPanel.qml | 6 +- .../Omarchy.qml => menu/BarWidget.qml} | 4 +- shell/plugins/menu/manifest.json | 12 ++- .../Monitor.qml => monitor/BarWidget.qml} | 40 ++++----- shell/plugins/monitor/manifest.json | 20 +++++ .../Network.qml => network/BarWidget.qml} | 4 +- shell/plugins/network/manifest.json | 20 +++++ .../BarWidget.qml} | 2 +- shell/plugins/notifications/manifest.json | 12 ++- .../{panels/Power.qml => power/BarWidget.qml} | 4 +- shell/plugins/power/manifest.json | 20 +++++ .../media/BarWidget.qml} | 2 +- shell/plugins/services/media/manifest.json | 12 ++- .../Weather.qml => weather/BarWidget.qml} | 4 +- .../{panels/Weather.qml => weather/Panel.qml} | 10 +-- shell/plugins/weather/manifest.json | 21 +++++ shell/services/PluginRegistry.qml | 15 ++-- shell/shell.qml | 37 ++++++-- 51 files changed, 565 insertions(+), 230 deletions(-) rename shell/plugins/{panels/Audio.qml => audio/BarWidget.qml} (99%) create mode 100644 shell/plugins/audio/manifest.json delete mode 100644 shell/plugins/bar/FirstPartyWidgets.qml create mode 100644 shell/plugins/bar/widgets/ActiveWindow.manifest.json create mode 100644 shell/plugins/bar/widgets/Clock.manifest.json create mode 100644 shell/plugins/bar/widgets/Indicators.manifest.json create mode 100644 shell/plugins/bar/widgets/KeyboardLayout.manifest.json create mode 100644 shell/plugins/bar/widgets/LockKeys.manifest.json create mode 100644 shell/plugins/bar/widgets/Microphone.manifest.json create mode 100644 shell/plugins/bar/widgets/Spacer.manifest.json create mode 100644 shell/plugins/bar/widgets/SystemStats.manifest.json create mode 100644 shell/plugins/bar/widgets/SystemUpdate.manifest.json create mode 100644 shell/plugins/bar/widgets/Tray.manifest.json create mode 100644 shell/plugins/bar/widgets/Workspaces.manifest.json rename shell/plugins/{panels/Bluetooth.qml => bluetooth/BarWidget.qml} (99%) create mode 100644 shell/plugins/bluetooth/manifest.json rename shell/plugins/{bar/widgets/Omarchy.qml => menu/BarWidget.qml} (78%) rename shell/plugins/{panels/Monitor.qml => monitor/BarWidget.qml} (97%) create mode 100644 shell/plugins/monitor/manifest.json rename shell/plugins/{panels/Network.qml => network/BarWidget.qml} (99%) create mode 100644 shell/plugins/network/manifest.json rename shell/plugins/{bar/widgets/NotificationCenter.qml => notifications/BarWidget.qml} (99%) rename shell/plugins/{panels/Power.qml => power/BarWidget.qml} (99%) create mode 100644 shell/plugins/power/manifest.json rename shell/plugins/{bar/widgets/Media.qml => services/media/BarWidget.qml} (99%) rename shell/plugins/{bar/widgets/Weather.qml => weather/BarWidget.qml} (95%) rename shell/plugins/{panels/Weather.qml => weather/Panel.qml} (98%) create mode 100644 shell/plugins/weather/manifest.json diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index 53c42fd5..dbd1fdbe 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -60,6 +60,7 @@ individual plugins (`bar`, `image-selector`, …). | `summon ` | load + open a plugin | | `hide ` | close a previously-summoned | | `toggle ` | summon if closed, hide if open | +| `call ` | call an already-loaded plugin | | `rescanPlugins` | re-walk plugin dirs | | `setPluginEnabled <"true"\|…>` | flip enabled bit | | `listPlugins` | JSON of every discovered plugin | @@ -98,8 +99,8 @@ Rules: bar widgets, `plugins[]` for everything else. 2. Settings are inline on the entry. No `config:` sub-object, no merge layers. -3. Built-in bar widget ids are namespaced (`omarchy.clock`, `omarchy.audio`, …); - legacy ids such as `Clock` and `AudioPanel` are accepted as aliases. +3. Built-in bar widget ids are namespaced (`omarchy.clock`, `omarchy.audio`, …). + The migration rewrites older ids such as `Clock` and `AudioPanel` forward. 4. Third-party enabled ⇔ present; first-party plugins are always enabled. 5. `allowMultiple: true` in the manifest permits multiple instances. 6. `idle.screensaver` and `idle.lock` are seconds since user idle began. diff --git a/shell/Commons/Util.qml b/shell/Commons/Util.qml index a20a9247..9ad90351 100644 --- a/shell/Commons/Util.qml +++ b/shell/Commons/Util.qml @@ -42,32 +42,8 @@ QtObject { return value !== null && typeof value === "object" && !Array.isArray(value) } - readonly property var builtinWidgetAliases: ({ - "Omarchy": "omarchy.menu", - "Workspaces": "omarchy.workspaces", - "Media": "omarchy.media", - "AudioPanel": "omarchy.audio", - "MonitorPanel": "omarchy.monitor", - "NetworkPanel": "omarchy.network", - "PowerPanel": "omarchy.power", - "BluetoothPanel": "omarchy.bluetooth", - "Clock": "omarchy.clock", - "Indicators": "omarchy.indicators", - "NotificationCenter": "omarchy.notifications", - "SystemUpdate": "omarchy.system-update", - "SystemStats": "omarchy.system-stats", - "Tray": "omarchy.tray", - "Weather": "omarchy.weather", - "Microphone": "omarchy.microphone", - "ActiveWindow": "omarchy.active-window", - "KeyboardLayout": "omarchy.keyboard-layout", - "LockKeys": "omarchy.lock-keys", - "Spacer": "omarchy.spacer" - }) - function canonicalWidgetId(id) { - var key = String(id || "") - return builtinWidgetAliases[key] || key + return String(id || "") } // Best-effort base64 decode. Returns "" on parse failure rather than diff --git a/shell/README.md b/shell/README.md index 08bcc17c..34de8a72 100644 --- a/shell/README.md +++ b/shell/README.md @@ -104,8 +104,10 @@ editing the built-in source. Third-party ids must be namespaced and may not use the reserved `omarchy.*` prefix. ```bash -omarchy plugin clone omarchy.clock local.clock --replace --open pi +omarchy plugin clone omarchy.clock local.clock --replace --with ai --prompt "Customize this clock widget" omarchy plugin clone # interactive source/name/tool picker +omarchy plugin edit local.clock --with editor # edit with `omarchy launch editor` +omarchy plugin edit local.clock --with ai # edit with `omarchy launch ai` ``` First-party plugins under `shell/plugins/` @@ -125,6 +127,7 @@ running a separate Quickshell instance. | `summon ` | `ok` / `unknown` | load + open a panel/overlay plugin | | `hide ` | — | close a previously-summoned plugin | | `toggle ` | — | summon if closed, hide if open | +| `call ` | string | call a method on an already-loaded plugin | | `rescanPlugins` | — | re-walk plugin dirs and pick up new/changed manifests | | `setPluginEnabled ` | — | flip the persisted enabled bit (see note) | | `listPlugins` | JSON | every discovered plugin (id, name, kinds, enabled) | @@ -145,6 +148,7 @@ calls to the running shell. It does not start the shell. ``` omarchy-shell shell ping omarchy-shell shell summon omarchy.settings "{}" +omarchy-shell shell toggle omarchy.menu '{"menu":"root"}' omarchy-shell shell listPlugins omarchy-shell shell rescanPlugins ``` @@ -205,8 +209,8 @@ rewrites the `bar` subtree from the current `shell-defaults.json`. separate per-plugin settings file, no merge layers. The fields on each entry are the values the plugin sees. 3. **Built-in widget ids are namespaced.** Use ids such as `omarchy.clock`, - `omarchy.audio`, and `omarchy.network`. Legacy ids like `Clock` and - `AudioPanel` are accepted as aliases and migrated forward. + `omarchy.audio`, and `omarchy.network`. The migration rewrites older ids + like `Clock` and `AudioPanel` forward. 4. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff its id appears somewhere in shell.json. For bar widgets, the bar settings UI adds/removes layout entries; other plugin kinds are enabled diff --git a/shell/Ui/Panel.qml b/shell/Ui/Panel.qml index 3bf5a4e5..957ebe02 100644 --- a/shell/Ui/Panel.qml +++ b/shell/Ui/Panel.qml @@ -1,10 +1,10 @@ import QtQuick import Quickshell.Io -// Base item for shell panels. Panels are not bar widgets, but the bar may host -// or toggle them and injects the same ambient context while doing so. The base -// owns the shared IPC-backed open/close lifecycle; panel implementations own -// their button behavior, keyboard navigation, and content. +// Base item for plugin popup widgets. Many first-party plugins expose a bar +// button plus a popup from one QML entry point; this base owns the shared +// IPC-backed open/close lifecycle while implementations own button behavior, +// keyboard navigation, and content. Item { id: root @@ -51,4 +51,5 @@ Item { function toggle(): void { root.toggle() } } } + } diff --git a/shell/plugins/README.md b/shell/plugins/README.md index 09b3c262..84af5d17 100644 --- a/shell/plugins/README.md +++ b/shell/plugins/README.md @@ -9,21 +9,27 @@ startup; other panels, overlays, and menus are loaded on demand. User-installed plugins live alongside these conceptually but on disk under `~/.config/omarchy/plugins//` rather than in this directory. -| Plugin | id | kinds | entry point | -|---------------|---------------------------|-----------|---------------------------------------| -| Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` | -| Bar settings | `omarchy.settings` | `panel` | `settings/SettingsPanel.qml` | -| Launcher | `omarchy.launcher` | `overlay` | `launcher/Launcher.qml` | -| Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` | -| Emojis | `omarchy.emojis` | `overlay` | `emojis/Emojis.qml` | -| Clipboard mgr | `omarchy.clipboard` | `overlay` | `clipboard/Clipboard.qml` | -| Omarchy menu | `omarchy.menu` | `menu` | `menu/Menu.qml` | -| Notifications | `omarchy.notifications` | `service` | `notifications/Service.qml` | -| Battery | `omarchy.battery` | `service` | `services/battery/Service.qml` | -| Idle | `omarchy.idle` | `service` | `services/idle/Service.qml` | -| Lock screen | `omarchy.lock` | `service` | `lock/Service.qml` | -| OSD | `omarchy.osd` | `panel` | `osd/Osd.qml` | -| Polkit agent | `omarchy.polkit` | `service` | `polkit/PolkitAgent.qml` | +| Plugin | id | kinds | entry point | +|---------------|---------------------------|-------------------------|---------------------------------------| +| Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` | +| Bar settings | `omarchy.settings` | `panel` | `settings/SettingsPanel.qml` | +| Launcher | `omarchy.launcher` | `overlay` | `launcher/Launcher.qml` | +| Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` | +| Emojis | `omarchy.emojis` | `overlay` | `emojis/Emojis.qml` | +| Clipboard mgr | `omarchy.clipboard` | `overlay` | `clipboard/Clipboard.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` | +| 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` | +| Lock screen | `omarchy.lock` | `service` | `lock/Service.qml` | +| OSD | `omarchy.osd` | `panel` | `osd/Osd.qml` | +| 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`. ## Bar diff --git a/shell/plugins/panels/Audio.qml b/shell/plugins/audio/BarWidget.qml similarity index 99% rename from shell/plugins/panels/Audio.qml rename to shell/plugins/audio/BarWidget.qml index 0da3157b..0f98ae40 100644 --- a/shell/plugins/panels/Audio.qml +++ b/shell/plugins/audio/BarWidget.qml @@ -9,8 +9,8 @@ import qs.Commons Panel { id: root - moduleName: "AudioPanel" - ipcTarget: "panels.audio" + moduleName: "omarchy.audio" + ipcTarget: "omarchy.audio" readonly property var sink: Pipewire.defaultAudioSink readonly property var source: Pipewire.defaultAudioSource diff --git a/shell/plugins/audio/manifest.json b/shell/plugins/audio/manifest.json new file mode 100644 index 00000000..28f28aa6 --- /dev/null +++ b/shell/plugins/audio/manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.audio", + "name": "Audio", + "version": "1.0.0", + "author": "Omarchy", + "description": "Volume slider, output picker, per-app mixer", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Audio", + "description": "Volume slider, output picker, per-app mixer", + "category": "Audio", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/FirstPartyWidgets.qml b/shell/plugins/bar/FirstPartyWidgets.qml deleted file mode 100644 index 5d25faf8..00000000 --- a/shell/plugins/bar/FirstPartyWidgets.qml +++ /dev/null @@ -1,85 +0,0 @@ -import QtQuick - -QtObject { - id: root - - required property var barWidgetRegistry - - readonly property var metadata: ({ - "omarchy.menu": { displayName: "Omarchy menu", description: "Launches the Omarchy menu", category: "Compositor", allowMultiple: false, sourceName: "Omarchy", legacyId: "Omarchy" }, - "omarchy.workspaces": { displayName: "Workspaces", description: "Workspace number indicators", category: "Compositor", allowMultiple: false, sourceName: "Workspaces", legacyId: "Workspaces" }, - "omarchy.media": { displayName: "Media", description: "MPRIS now-playing with playback controls", category: "Media", allowMultiple: false, sourceName: "Media", legacyId: "Media" }, - "omarchy.audio": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false, sourceDir: "../panels", sourceName: "Audio", legacyId: "AudioPanel" }, - "omarchy.monitor": { displayName: "Display", description: "Brightness slider and laptop display controls", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Monitor", legacyId: "MonitorPanel" }, - "omarchy.network": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Network", legacyId: "NetworkPanel" }, - "omarchy.power": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Power", legacyId: "PowerPanel" }, - "omarchy.bluetooth": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Bluetooth", legacyId: "BluetoothPanel" }, - "omarchy.clock": { displayName: "Clock", description: "Day/time label; click to toggle alternate format", category: "Time", allowMultiple: false, sourceName: "Clock", settingsForm: "clockSettings", legacyId: "Clock" }, - "omarchy.indicators": { displayName: "Indicators", description: "Manual state indicators", category: "Status", allowMultiple: true, sourceName: "Indicators", legacyId: "Indicators", - schema: [ - { key: "items", type: "multiselect", label: "Indicators", description: "Choose which indicators this widget instance should show. Leave empty to show all indicators.", noSelectionText: "All indicators", placeholderText: "Search indicators...", emptyText: "No indicators", - options: [ - { value: "Dnd", label: "Do not disturb", description: "Notification silencing" }, - { value: "NightLight", label: "Night light", description: "Blue-light filter" }, - { value: "StayAwake", label: "Stay awake", description: "Idle lock and screensaver override" }, - { value: "ScreenRecording", label: "Screen recording", description: "GPU screen recorder status" }, - { value: "Dictation", label: "Dictation", description: "Voice typing status" } - ] }, - { key: "alwaysShow", type: "boolean", label: "Always Show", description: "Show inactive indicators without waiting for hover.", defaultValue: false } - ] }, - "omarchy.notifications": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false, sourceName: "NotificationCenter", legacyId: "NotificationCenter" }, - "omarchy.system-update": { displayName: "System update", description: "Indicates available system updates", category: "System", allowMultiple: false, sourceName: "SystemUpdate", legacyId: "SystemUpdate" }, - "omarchy.system-stats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false, sourceName: "SystemStats", legacyId: "SystemStats" }, - "omarchy.tray": { displayName: "System tray", description: "Status notifier items", category: "Status", allowMultiple: false, sourceName: "Tray", legacyId: "Tray" }, - "omarchy.weather": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false, sourceName: "Weather", settingsForm: "weatherSettings", legacyId: "Weather" }, - "omarchy.microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false, sourceName: "Microphone", legacyId: "Microphone" }, - "omarchy.active-window": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false, sourceName: "ActiveWindow", legacyId: "ActiveWindow" }, - "omarchy.keyboard-layout": { displayName: "Keyboard layout", description: "Current xkb layout, click cycles", category: "Compositor", allowMultiple: false, sourceName: "KeyboardLayout", legacyId: "KeyboardLayout" }, - "omarchy.lock-keys": { displayName: "Lock keys", description: "Caps / Num / Scroll lock indicators", category: "System", allowMultiple: false, sourceName: "LockKeys", legacyId: "LockKeys" }, - "omarchy.spacer": { displayName: "Spacer", description: "Configurable blank space", category: "Layout", allowMultiple: true, sourceName: "Spacer", settingsForm: "spacerSettings", legacyId: "Spacer" } - }) - - property var registeredComponents: ({}) - - Component.onCompleted: registerWidgets() - - function registerWidgets() { - var ids = Object.keys(metadata) - for (var i = 0; i < ids.length; i++) { - var id = ids[i] - if (barWidgetRegistry.has(id)) continue - registerOne(id) - } - } - - function registerOne(id) { - var meta = metadata[id] || {} - var sourceDir = meta.sourceDir || "widgets" - var sourceName = meta.sourceName || id - var url = Qt.resolvedUrl(sourceDir + "/" + sourceName + ".qml") - var enrichedMeta = { - displayName: meta.displayName || id, - description: meta.description || "", - category: meta.category || "Misc", - allowMultiple: meta.allowMultiple === true, - settingsForm: meta.settingsForm || "", - schema: Array.isArray(meta.schema) ? meta.schema : [], - source: "first-party", - legacyId: meta.legacyId || "" - } - var comp = Qt.createComponent(url, Component.Asynchronous) - function finalize() { - if (comp.status === Component.Ready) { - barWidgetRegistry.register(id, comp, enrichedMeta) - var next = ({}) - for (var k in registeredComponents) next[k] = registeredComponents[k] - next[id] = comp - registeredComponents = next - } else if (comp.status === Component.Error) { - console.warn("first-party widget " + id + " failed to load: " + comp.errorString()) - } - } - if (comp.status === Component.Loading) comp.statusChanged.connect(finalize) - else finalize() - } -} diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index 93d9c32c..852aff4f 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -7,8 +7,8 @@ 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 first-party bar widgets. -- `../panels/` holds first-party panels that the bar can toggle by id. +- `widgets/` holds simple first-party bar widgets with sibling manifests. +- Feature plugins such as `../audio/`, `../network/`, and `../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 `shell-defaults.json` when the user has no file). - `omarchy-style-bar-position` updates only the user shell.json file. @@ -66,17 +66,13 @@ Example `shell.json` (bar subtree only shown): | `omarchy.weather` | Weather icon + popup with forecast | left = popup · right = full notification | | `omarchy.microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio panel · scroll = source volume | -### First-party panels (in `../panels/`) +| `omarchy.audio` | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = popup · scroll = volume | +| `omarchy.network` | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect, DNS provider selection | left = popup · right = nmtui | +| `omarchy.power` | Battery/AC icon + popup with battery stats, power profiles, and system info | left = popup | +| `omarchy.bluetooth` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI | +| `omarchy.monitor` | Brightness and laptop display controls | left = popup | -| Name | What it does | Interactions | -|---|---|---| -| `panels.audio` | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = popup · scroll = volume | -| `panels.network` | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect, DNS provider selection | left = popup · right = nmtui | -| `panels.power` | Battery/AC icon + popup with battery stats, power profiles, and system info | left = popup | -| `panels.bluetooth` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI | -| `panels.monitor` | Brightness and laptop display controls | left = popup | - -The `omarchy.indicators` widget loads individual bar indicators from `indicators/`. Omit `items` (or set it to an empty array) to show all indicators in the default order, or set `items` to a subset such as `["Dnd", "NightLight"]`. Set `alwaysShow` to `true` to keep inactive indicators visible instead of revealing them only on hover. Multiple `omarchy.indicators` instances are allowed, so different sections can show different subsets. Rich popup widgets such as `omarchy.power`, `omarchy.network`, and `omarchy.audio` live in `../panels/` above. +The `omarchy.indicators` widget loads individual bar indicators from `indicators/`. Omit `items` (or set it to an empty array) to show all indicators in the default order, or set `items` to a subset such as `["Dnd", "NightLight"]`. Set `alwaysShow` to `true` to keep inactive indicators visible instead of revealing them only on hover. Multiple `omarchy.indicators` instances are allowed, so different sections can show different subsets. ## Orientation @@ -169,12 +165,15 @@ Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (o - `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup - `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator -First-party bar widgets live in `widgets/.qml`; richer popup widgets -live in `../panels/.qml` and expose IPC targets such as -`panels.audio`. Bar layout ids are namespaced, e.g. `omarchy.audio`, -`omarchy.network`, and `omarchy.clock`. Legacy UpperCamelCase ids such as -`AudioPanel` and `Clock` are still accepted as aliases, but new configs should -use the namespaced ids. +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 +`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 +`AudioPanel` and `Clock` are migrated forward; new configs should use the +namespaced ids. Third-party widgets ship as separate plugins under `~/.config/omarchy/plugins//` with their own `manifest.json` diff --git a/shell/plugins/bar/widgets/ActiveWindow.manifest.json b/shell/plugins/bar/widgets/ActiveWindow.manifest.json new file mode 100644 index 00000000..112b0a22 --- /dev/null +++ b/shell/plugins/bar/widgets/ActiveWindow.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.active-window", + "name": "Active window", + "version": "1.0.0", + "author": "Omarchy", + "description": "Title of the focused window", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "ActiveWindow.qml" + }, + "barWidget": { + "displayName": "Active window", + "description": "Title of the focused window", + "category": "Compositor", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/ActiveWindow.qml b/shell/plugins/bar/widgets/ActiveWindow.qml index 580a0bf5..a6dcd469 100644 --- a/shell/plugins/bar/widgets/ActiveWindow.qml +++ b/shell/plugins/bar/widgets/ActiveWindow.qml @@ -6,7 +6,7 @@ import qs.Ui BarWidget { id: root - moduleName: "ActiveWindow" + moduleName: "omarchy.active-window" readonly property var toplevel: ToplevelManager.activeToplevel diff --git a/shell/plugins/bar/widgets/Clock.manifest.json b/shell/plugins/bar/widgets/Clock.manifest.json new file mode 100644 index 00000000..119dbb96 --- /dev/null +++ b/shell/plugins/bar/widgets/Clock.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.clock", + "name": "Clock", + "version": "1.0.0", + "author": "Omarchy", + "description": "Day/time label; click to toggle alternate format", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "Clock.qml" + }, + "barWidget": { + "displayName": "Clock", + "description": "Day/time label; click to toggle alternate format", + "category": "Time", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/Clock.qml b/shell/plugins/bar/widgets/Clock.qml index 8dc74bff..f1010793 100644 --- a/shell/plugins/bar/widgets/Clock.qml +++ b/shell/plugins/bar/widgets/Clock.qml @@ -6,7 +6,7 @@ import qs.Ui BarWidget { id: root - moduleName: "Clock" + moduleName: "omarchy.clock" property bool alt: false property date displayDate: clock.date @@ -46,7 +46,7 @@ BarWidget { } IpcHandler { - target: "Clock" + target: "omarchy.clock" function refresh(): void { root.refresh() } } diff --git a/shell/plugins/bar/widgets/Indicators.manifest.json b/shell/plugins/bar/widgets/Indicators.manifest.json new file mode 100644 index 00000000..c8c49218 --- /dev/null +++ b/shell/plugins/bar/widgets/Indicators.manifest.json @@ -0,0 +1,65 @@ +{ + "schemaVersion": 1, + "id": "omarchy.indicators", + "name": "Indicators", + "version": "1.0.0", + "author": "Omarchy", + "description": "Manual state indicators", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "Indicators.qml" + }, + "barWidget": { + "displayName": "Indicators", + "description": "Manual state indicators", + "category": "Status", + "allowMultiple": true, + "schema": [ + { + "key": "items", + "type": "multiselect", + "label": "Indicators", + "description": "Choose which indicators this widget instance should show. Leave empty to show all indicators.", + "noSelectionText": "All indicators", + "placeholderText": "Search indicators...", + "emptyText": "No indicators", + "options": [ + { + "value": "Dnd", + "label": "Do not disturb", + "description": "Notification silencing" + }, + { + "value": "NightLight", + "label": "Night light", + "description": "Blue-light filter" + }, + { + "value": "StayAwake", + "label": "Stay awake", + "description": "Idle lock and screensaver override" + }, + { + "value": "ScreenRecording", + "label": "Screen recording", + "description": "GPU screen recorder status" + }, + { + "value": "Dictation", + "label": "Dictation", + "description": "Voice typing status" + } + ] + }, + { + "key": "alwaysShow", + "type": "boolean", + "label": "Always Show", + "description": "Show inactive indicators without waiting for hover.", + "defaultValue": false + } + ] + } +} diff --git a/shell/plugins/bar/widgets/Indicators.qml b/shell/plugins/bar/widgets/Indicators.qml index b04e9a2c..a4cd84f2 100644 --- a/shell/plugins/bar/widgets/Indicators.qml +++ b/shell/plugins/bar/widgets/Indicators.qml @@ -6,7 +6,7 @@ import qs.Ui BarWidget { id: root - moduleName: "Indicators" + moduleName: "omarchy.indicators" readonly property int indicatorSlotExtent: Style.space(22) readonly property var defaultIndicatorEntries: [ "Dnd", "NightLight", "StayAwake", "ScreenRecording", "Dictation" ] @@ -160,7 +160,7 @@ BarWidget { implicitHeight: root.vertical ? verticalIndicators.implicitHeight : horizontalIndicators.implicitHeight IpcHandler { - target: "Indicators" + target: "omarchy.indicators" function refresh(): void { root.refreshRequested() diff --git a/shell/plugins/bar/widgets/KeyboardLayout.manifest.json b/shell/plugins/bar/widgets/KeyboardLayout.manifest.json new file mode 100644 index 00000000..35c12691 --- /dev/null +++ b/shell/plugins/bar/widgets/KeyboardLayout.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.keyboard-layout", + "name": "Keyboard layout", + "version": "1.0.0", + "author": "Omarchy", + "description": "Current xkb layout, click cycles", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "KeyboardLayout.qml" + }, + "barWidget": { + "displayName": "Keyboard layout", + "description": "Current xkb layout, click cycles", + "category": "Compositor", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/KeyboardLayout.qml b/shell/plugins/bar/widgets/KeyboardLayout.qml index 1bd8d784..ec9eb0bf 100644 --- a/shell/plugins/bar/widgets/KeyboardLayout.qml +++ b/shell/plugins/bar/widgets/KeyboardLayout.qml @@ -7,7 +7,7 @@ import qs.Commons BarWidget { id: root - moduleName: "KeyboardLayout" + moduleName: "omarchy.keyboard-layout" property string layoutLabel: "" diff --git a/shell/plugins/bar/widgets/LockKeys.manifest.json b/shell/plugins/bar/widgets/LockKeys.manifest.json new file mode 100644 index 00000000..6db4f0fb --- /dev/null +++ b/shell/plugins/bar/widgets/LockKeys.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.lock-keys", + "name": "Lock keys", + "version": "1.0.0", + "author": "Omarchy", + "description": "Caps / Num / Scroll lock indicators", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "LockKeys.qml" + }, + "barWidget": { + "displayName": "Lock keys", + "description": "Caps / Num / Scroll lock indicators", + "category": "System", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/LockKeys.qml b/shell/plugins/bar/widgets/LockKeys.qml index c68d62af..9ba5adfd 100644 --- a/shell/plugins/bar/widgets/LockKeys.qml +++ b/shell/plugins/bar/widgets/LockKeys.qml @@ -6,7 +6,7 @@ import qs.Ui BarWidget { id: root - moduleName: "LockKeys" + moduleName: "omarchy.lock-keys" property bool capsOn: false diff --git a/shell/plugins/bar/widgets/Microphone.manifest.json b/shell/plugins/bar/widgets/Microphone.manifest.json new file mode 100644 index 00000000..7cc837c5 --- /dev/null +++ b/shell/plugins/bar/widgets/Microphone.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.microphone", + "name": "Microphone", + "version": "1.0.0", + "author": "Omarchy", + "description": "Mic input state and mute toggle", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "Microphone.qml" + }, + "barWidget": { + "displayName": "Microphone", + "description": "Mic input state and mute toggle", + "category": "Audio", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/Microphone.qml b/shell/plugins/bar/widgets/Microphone.qml index 7d41843e..e8052964 100644 --- a/shell/plugins/bar/widgets/Microphone.qml +++ b/shell/plugins/bar/widgets/Microphone.qml @@ -5,7 +5,7 @@ import qs.Ui BarWidget { id: root - moduleName: "Microphone" + moduleName: "omarchy.microphone" readonly property var source: Pipewire.defaultAudioSource @@ -42,7 +42,7 @@ BarWidget { active: root.inUse tooltipText: root.muted ? "Microphone muted" : (root.inUse ? "Microphone in use" : "Microphone live") onPressed: function(b) { - if (b === Qt.MiddleButton) root.bar.run("omarchy-shell panels.audio toggle") + if (b === Qt.MiddleButton) root.bar.run("omarchy-shell omarchy.audio toggle") else root.toggleMute() } onWheelMoved: function(delta) { diff --git a/shell/plugins/bar/widgets/Spacer.manifest.json b/shell/plugins/bar/widgets/Spacer.manifest.json new file mode 100644 index 00000000..a258f147 --- /dev/null +++ b/shell/plugins/bar/widgets/Spacer.manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "id": "omarchy.spacer", + "name": "Spacer", + "version": "1.0.0", + "author": "Omarchy", + "description": "Configurable blank space", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "Spacer.qml" + }, + "barWidget": { + "displayName": "Spacer", + "description": "Configurable blank space", + "category": "Layout", + "allowMultiple": true, + "settingsForm": "spacerSettings" + } +} diff --git a/shell/plugins/bar/widgets/Spacer.qml b/shell/plugins/bar/widgets/Spacer.qml index 3f555207..542d7d73 100644 --- a/shell/plugins/bar/widgets/Spacer.qml +++ b/shell/plugins/bar/widgets/Spacer.qml @@ -3,7 +3,7 @@ import qs.Ui BarWidget { id: root - moduleName: "Spacer" + moduleName: "omarchy.spacer" readonly property int span: settings && settings.size !== undefined ? Number(settings.size) : 12 diff --git a/shell/plugins/bar/widgets/SystemStats.manifest.json b/shell/plugins/bar/widgets/SystemStats.manifest.json new file mode 100644 index 00000000..df619e7d --- /dev/null +++ b/shell/plugins/bar/widgets/SystemStats.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.system-stats", + "name": "System stats", + "version": "1.0.0", + "author": "Omarchy", + "description": "CPU icon \u2014 hover for graphs, click to open btop", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "SystemStats.qml" + }, + "barWidget": { + "displayName": "System stats", + "description": "CPU icon \u2014 hover for graphs, click to open btop", + "category": "System", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/SystemStats.qml b/shell/plugins/bar/widgets/SystemStats.qml index a41c214c..6a33630a 100644 --- a/shell/plugins/bar/widgets/SystemStats.qml +++ b/shell/plugins/bar/widgets/SystemStats.qml @@ -6,7 +6,7 @@ import qs.Commons BarWidget { id: root - moduleName: "SystemStats" + moduleName: "omarchy.system-stats" property real cpuPercent: 0 diff --git a/shell/plugins/bar/widgets/SystemUpdate.manifest.json b/shell/plugins/bar/widgets/SystemUpdate.manifest.json new file mode 100644 index 00000000..c5164d52 --- /dev/null +++ b/shell/plugins/bar/widgets/SystemUpdate.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.system-update", + "name": "System update", + "version": "1.0.0", + "author": "Omarchy", + "description": "Indicates available system updates", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "SystemUpdate.qml" + }, + "barWidget": { + "displayName": "System update", + "description": "Indicates available system updates", + "category": "System", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/SystemUpdate.qml b/shell/plugins/bar/widgets/SystemUpdate.qml index f0af7fd4..218f8dd3 100644 --- a/shell/plugins/bar/widgets/SystemUpdate.qml +++ b/shell/plugins/bar/widgets/SystemUpdate.qml @@ -6,7 +6,7 @@ import qs.Ui BarWidget { id: root - moduleName: "SystemUpdate" + moduleName: "omarchy.system-update" property bool updateAvailable: false @@ -18,7 +18,7 @@ BarWidget { implicitHeight: button.implicitHeight IpcHandler { - target: "SystemUpdate" + target: "omarchy.system-update" function refresh(): void { root.refresh() diff --git a/shell/plugins/bar/widgets/Tray.manifest.json b/shell/plugins/bar/widgets/Tray.manifest.json new file mode 100644 index 00000000..050757e2 --- /dev/null +++ b/shell/plugins/bar/widgets/Tray.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.tray", + "name": "System tray", + "version": "1.0.0", + "author": "Omarchy", + "description": "Status notifier items", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "Tray.qml" + }, + "barWidget": { + "displayName": "System tray", + "description": "Status notifier items", + "category": "Status", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/Tray.qml b/shell/plugins/bar/widgets/Tray.qml index 386356c9..4e316553 100644 --- a/shell/plugins/bar/widgets/Tray.qml +++ b/shell/plugins/bar/widgets/Tray.qml @@ -6,7 +6,7 @@ import qs.Ui BarWidget { id: root - moduleName: "Tray" + moduleName: "omarchy.tray" property bool expanded: false property bool managePopupOpen: false diff --git a/shell/plugins/bar/widgets/Workspaces.manifest.json b/shell/plugins/bar/widgets/Workspaces.manifest.json new file mode 100644 index 00000000..e51f7a0a --- /dev/null +++ b/shell/plugins/bar/widgets/Workspaces.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.workspaces", + "name": "Workspaces", + "version": "1.0.0", + "author": "Omarchy", + "description": "Workspace number indicators", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "Workspaces.qml" + }, + "barWidget": { + "displayName": "Workspaces", + "description": "Workspace number indicators", + "category": "Compositor", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/Workspaces.qml b/shell/plugins/bar/widgets/Workspaces.qml index a72af41f..a47e2d53 100644 --- a/shell/plugins/bar/widgets/Workspaces.qml +++ b/shell/plugins/bar/widgets/Workspaces.qml @@ -6,7 +6,7 @@ import qs.Ui BarWidget { id: root - moduleName: "Workspaces" + moduleName: "omarchy.workspaces" function workspaceById(id) { var values = Hyprland.workspaces.values diff --git a/shell/plugins/panels/Bluetooth.qml b/shell/plugins/bluetooth/BarWidget.qml similarity index 99% rename from shell/plugins/panels/Bluetooth.qml rename to shell/plugins/bluetooth/BarWidget.qml index 9201ff16..88a58894 100644 --- a/shell/plugins/panels/Bluetooth.qml +++ b/shell/plugins/bluetooth/BarWidget.qml @@ -8,8 +8,8 @@ import qs.Commons Panel { id: root - moduleName: "BluetoothPanel" - ipcTarget: "panels.bluetooth" + moduleName: "omarchy.bluetooth" + ipcTarget: "omarchy.bluetooth" // Address -> "connecting" | "disconnecting" | "forgetting". // The actual Bluetooth sequencing lives in bin/omarchy-bluetooth-device; diff --git a/shell/plugins/bluetooth/manifest.json b/shell/plugins/bluetooth/manifest.json new file mode 100644 index 00000000..8ee26f95 --- /dev/null +++ b/shell/plugins/bluetooth/manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.bluetooth", + "name": "Bluetooth", + "version": "1.0.0", + "author": "Omarchy", + "description": "Bluetooth device list with connect/disconnect", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Bluetooth", + "description": "Bluetooth device list with connect/disconnect", + "category": "Network", + "allowMultiple": false + } +} diff --git a/shell/plugins/dev-gallery/GalleryPanel.qml b/shell/plugins/dev-gallery/GalleryPanel.qml index 96d343da..da81ff14 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/panels/Audio.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/audio/BarWidget.qml for the canonical recipe." } Text { width: parent.width @@ -1524,7 +1524,7 @@ Item { width: Style.spacing.dropdownWidth label: "Center anchor" fontFamily: root.fontFamily - options: ["Clock", "Weather", "PowerPanel"] + options: ["omarchy.clock", "omarchy.weather", "omarchy.power"] value: root.dropdownDemoValue hasCursor: root.focusSection === "dropdown" && root.selectedIndex === 0 onHovered: function(h) { @@ -1589,7 +1589,7 @@ Item { options: [ { value: "Clock", label: "Clock", description: "Time + date display" }, { value: "Weather", label: "Weather", description: "Local conditions and forecast" }, - { value: "PowerPanel", label: "Power", description: "Charge level + power profile" }, + { value: "omarchy.power", label: "Power", description: "Charge level + power profile" }, { value: "audio", label: "Audio", description: "Output sink + volume" }, { value: "network", label: "Network", description: "Wi-Fi + ethernet status" }, { value: "bluetooth", label: "Bluetooth", description: "Paired and nearby devices" }, diff --git a/shell/plugins/bar/widgets/Omarchy.qml b/shell/plugins/menu/BarWidget.qml similarity index 78% rename from shell/plugins/bar/widgets/Omarchy.qml rename to shell/plugins/menu/BarWidget.qml index 6b1e0cf2..8fa7e1b3 100644 --- a/shell/plugins/bar/widgets/Omarchy.qml +++ b/shell/plugins/menu/BarWidget.qml @@ -3,7 +3,7 @@ import qs.Ui BarWidget { id: root - moduleName: "Omarchy" + moduleName: "omarchy.menu" implicitWidth: button.implicitWidth implicitHeight: button.implicitHeight @@ -18,7 +18,7 @@ BarWidget { onPressed: function(button) { if (!root.bar) return if (button === Qt.RightButton) root.bar.run("xdg-terminal-exec") - else root.bar.run("omarchy-shell menu toggle root") + else root.bar.run("omarchy-shell shell toggle omarchy.menu '{\"menu\":\"root\"}'") } } } diff --git a/shell/plugins/menu/manifest.json b/shell/plugins/menu/manifest.json index 2154ba0d..4227a227 100644 --- a/shell/plugins/menu/manifest.json +++ b/shell/plugins/menu/manifest.json @@ -6,10 +6,18 @@ "author": "Omarchy", "description": "Quickshell-powered Omarchy command menu", "kinds": [ - "menu" + "menu", + "bar-widget" ], "keepLoaded": true, "entryPoints": { - "menu": "Menu.qml" + "menu": "Menu.qml", + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Omarchy menu", + "description": "Launches the Omarchy menu", + "category": "Compositor", + "allowMultiple": false } } diff --git a/shell/plugins/panels/Monitor.qml b/shell/plugins/monitor/BarWidget.qml similarity index 97% rename from shell/plugins/panels/Monitor.qml rename to shell/plugins/monitor/BarWidget.qml index 537b51f5..619749d9 100644 --- a/shell/plugins/panels/Monitor.qml +++ b/shell/plugins/monitor/BarWidget.qml @@ -7,8 +7,8 @@ import qs.Commons Panel { id: root - moduleName: "MonitorPanel" - ipcTarget: "panels.monitor" + moduleName: "omarchy.monitor" + ipcTarget: "omarchy.monitor" manageIpc: false // manageIpc: false so this panel can own the single IpcHandler the target @@ -169,25 +169,27 @@ Panel { flick.contentY = bottom + margin - flick.height } + function brightnessIpc(percent) { + var value = Number(percent) + root.setBrightness(value) + return "got " + root.pendingBrightnessPercent + } + + function stateIpc() { + return JSON.stringify({ + brightness: root.brightnessPercent, + brightnessAvailable: root.brightnessAvailable, + focusedMonitor: root.focusedMonitor, + scale: root.monitorScale, + displays: root.displays + }) + } + IpcHandler { - target: "panels.monitor" - - function brightness(percent: string): string { - var value = Number(percent) - root.setBrightness(value) - return "got " + root.pendingBrightnessPercent - } - - function state(): string { - return JSON.stringify({ - brightness: root.brightnessPercent, - brightnessAvailable: root.brightnessAvailable, - focusedMonitor: root.focusedMonitor, - scale: root.monitorScale, - displays: root.displays - }) - } + target: "omarchy.monitor" + 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() } diff --git a/shell/plugins/monitor/manifest.json b/shell/plugins/monitor/manifest.json new file mode 100644 index 00000000..b8f7388e --- /dev/null +++ b/shell/plugins/monitor/manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.monitor", + "name": "Display", + "version": "1.0.0", + "author": "Omarchy", + "description": "Brightness slider and laptop display controls", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Display", + "description": "Brightness slider and laptop display controls", + "category": "System", + "allowMultiple": false + } +} diff --git a/shell/plugins/panels/Network.qml b/shell/plugins/network/BarWidget.qml similarity index 99% rename from shell/plugins/panels/Network.qml rename to shell/plugins/network/BarWidget.qml index ad91fe23..30cbe509 100644 --- a/shell/plugins/panels/Network.qml +++ b/shell/plugins/network/BarWidget.qml @@ -8,8 +8,8 @@ import qs.Commons Panel { id: root - moduleName: "NetworkPanel" - ipcTarget: "panels.network" + moduleName: "omarchy.network" + ipcTarget: "omarchy.network" // Centralized close so callers can't forget to drop the passphrase prompt. function close() { diff --git a/shell/plugins/network/manifest.json b/shell/plugins/network/manifest.json new file mode 100644 index 00000000..a9914bbd --- /dev/null +++ b/shell/plugins/network/manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.network", + "name": "Network", + "version": "1.0.0", + "author": "Omarchy", + "description": "Wi-Fi list and connection state", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Network", + "description": "Wi-Fi list and connection state", + "category": "Network", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/NotificationCenter.qml b/shell/plugins/notifications/BarWidget.qml similarity index 99% rename from shell/plugins/bar/widgets/NotificationCenter.qml rename to shell/plugins/notifications/BarWidget.qml index 20788cfe..19730ec2 100644 --- a/shell/plugins/bar/widgets/NotificationCenter.qml +++ b/shell/plugins/notifications/BarWidget.qml @@ -6,7 +6,7 @@ import qs.Ui BarWidget { id: root - moduleName: "NotificationCenter" + moduleName: "omarchy.notifications" property bool popupOpen: false diff --git a/shell/plugins/notifications/manifest.json b/shell/plugins/notifications/manifest.json index 61cdb188..404f6864 100644 --- a/shell/plugins/notifications/manifest.json +++ b/shell/plugins/notifications/manifest.json @@ -6,10 +6,18 @@ "author": "Omarchy", "description": "Notification daemon, popups, and history", "kinds": [ - "service" + "service", + "bar-widget" ], "keepLoaded": true, "entryPoints": { - "service": "Service.qml" + "service": "Service.qml", + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Notification center", + "description": "Recent notifications + DND", + "category": "Status", + "allowMultiple": false } } diff --git a/shell/plugins/panels/Power.qml b/shell/plugins/power/BarWidget.qml similarity index 99% rename from shell/plugins/panels/Power.qml rename to shell/plugins/power/BarWidget.qml index 3966f33c..d047ce45 100644 --- a/shell/plugins/panels/Power.qml +++ b/shell/plugins/power/BarWidget.qml @@ -7,8 +7,8 @@ import qs.Ui Panel { id: root - moduleName: "PowerPanel" - ipcTarget: "panels.power" + moduleName: "omarchy.power" + ipcTarget: "omarchy.power" property var batteryInfo: ({}) property var systemInfo: ({}) property var profiles: [] diff --git a/shell/plugins/power/manifest.json b/shell/plugins/power/manifest.json new file mode 100644 index 00000000..ea017f0e --- /dev/null +++ b/shell/plugins/power/manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "id": "omarchy.power", + "name": "Power", + "version": "1.0.0", + "author": "Omarchy", + "description": "Battery, power profile, and system stats", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Power", + "description": "Battery, power profile, and system stats", + "category": "System", + "allowMultiple": false + } +} diff --git a/shell/plugins/bar/widgets/Media.qml b/shell/plugins/services/media/BarWidget.qml similarity index 99% rename from shell/plugins/bar/widgets/Media.qml rename to shell/plugins/services/media/BarWidget.qml index ae268488..3015c7cc 100644 --- a/shell/plugins/bar/widgets/Media.qml +++ b/shell/plugins/services/media/BarWidget.qml @@ -5,7 +5,7 @@ import qs.Commons BarWidget { id: root - moduleName: "Media" + moduleName: "omarchy.media" readonly property var mediaService: bar && bar.shell ? bar.shell.firstPartyServiceFor("omarchy.media") : null readonly property var activePlayer: mediaService ? mediaService.activePlayer : null diff --git a/shell/plugins/services/media/manifest.json b/shell/plugins/services/media/manifest.json index bdf932ac..661c65a5 100644 --- a/shell/plugins/services/media/manifest.json +++ b/shell/plugins/services/media/manifest.json @@ -6,10 +6,18 @@ "author": "Omarchy", "description": "MPRIS media control service", "kinds": [ - "service" + "service", + "bar-widget" ], "keepLoaded": true, "entryPoints": { - "service": "Service.qml" + "service": "Service.qml", + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Media", + "description": "MPRIS now-playing with playback controls", + "category": "Media", + "allowMultiple": false } } diff --git a/shell/plugins/bar/widgets/Weather.qml b/shell/plugins/weather/BarWidget.qml similarity index 95% rename from shell/plugins/bar/widgets/Weather.qml rename to shell/plugins/weather/BarWidget.qml index 61bff736..b05020de 100644 --- a/shell/plugins/bar/widgets/Weather.qml +++ b/shell/plugins/weather/BarWidget.qml @@ -4,7 +4,7 @@ import qs.Ui BarWidget { id: root - moduleName: "Weather" + moduleName: "omarchy.weather" function injectPanel() { var target = panelLoader.item @@ -32,7 +32,7 @@ BarWidget { Loader { id: panelLoader active: true - source: Qt.resolvedUrl("../../panels/Weather.qml") + source: Qt.resolvedUrl("Panel.qml") visible: false onLoaded: { root.injectPanel() diff --git a/shell/plugins/panels/Weather.qml b/shell/plugins/weather/Panel.qml similarity index 98% rename from shell/plugins/panels/Weather.qml rename to shell/plugins/weather/Panel.qml index 88bd305d..dc57d8d0 100644 --- a/shell/plugins/panels/Weather.qml +++ b/shell/plugins/weather/Panel.qml @@ -6,8 +6,8 @@ import qs.Ui Panel { id: root - moduleName: "WeatherPanel" - ipcTarget: "Weather" + moduleName: "omarchy.weather" + ipcTarget: "omarchy.weather" property var anchorItem: null @@ -21,12 +21,6 @@ Panel { else root.open() } - IpcHandler { - target: "Weather" - function show(): void { root.open() } - function toggle(): void { root.toggle() } - } - // Parsed wttr.in j1 response. Kept on failure so stale data stays visible. property var report: null property var dailyForecastReport: null diff --git a/shell/plugins/weather/manifest.json b/shell/plugins/weather/manifest.json new file mode 100644 index 00000000..23fa84f8 --- /dev/null +++ b/shell/plugins/weather/manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "id": "omarchy.weather", + "name": "Weather", + "version": "1.0.0", + "author": "Omarchy", + "description": "Weather pill with detail popup", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Weather", + "description": "Weather pill with detail popup", + "category": "Info", + "allowMultiple": false, + "settingsForm": "weatherSettings" + } +} diff --git a/shell/services/PluginRegistry.qml b/shell/services/PluginRegistry.qml index 18cd4034..41776e10 100644 --- a/shell/services/PluginRegistry.qml +++ b/shell/services/PluginRegistry.qml @@ -119,19 +119,20 @@ QtObject { function findEntryLocation(config, id) { if (!Util.isPlainObject(config)) return { found: false } + var key = Util.canonicalWidgetId(String(id)) if (Util.isPlainObject(config.bar) && Util.isPlainObject(config.bar.layout)) { var sections = ["left", "center", "right"] for (var s = 0; s < sections.length; s++) { var arr = config.bar.layout[sections[s]] if (!Array.isArray(arr)) continue for (var i = 0; i < arr.length; i++) { - if (arr[i] && arr[i].id === id) return { found: true, kind: "bar", section: sections[s], index: i } + if (arr[i] && Util.canonicalWidgetId(arr[i].id) === key) return { found: true, kind: "bar", section: sections[s], index: i } } } } if (Array.isArray(config.plugins)) { for (var j = 0; j < config.plugins.length; j++) { - if (config.plugins[j] && config.plugins[j].id === id) return { found: true, kind: "plugin", index: j } + if (config.plugins[j] && Util.canonicalWidgetId(config.plugins[j].id) === key) return { found: true, kind: "plugin", index: j } } } return { found: false } @@ -141,7 +142,7 @@ QtObject { // kinds. Bar widgets default to the right section; panels/overlays/menus/ // services go into the plugins[] array. function setEnabled(id, value) { - var key = String(id) + var key = Util.canonicalWidgetId(String(id)) if (!shellConfigMutator) { console.warn("PluginRegistry.setEnabled called before shellConfigMutator wired") return @@ -267,16 +268,20 @@ QtObject { 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 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. // Third-party plugins stay at the top level of ~/.config/omarchy/plugins. var script = "" - + "emit_manifest() { local kind=\"$1\"; local manifest=\"$2\"; local sub=\"${manifest%/manifest.json}\"; " + + "emit_manifest() { local kind=\"$1\"; local manifest=\"$2\"; local sub; " + + " if [[ ${manifest##*/} == \"manifest.json\" ]]; then sub=\"${manifest%/manifest.json}\"; else sub=\"$(dirname -- \"$manifest\")\"; fi; " + " printf '===%s::%s===\\n' \"$kind\" \"$sub\"; " + " cat \"$manifest\"; " + " printf '\\n=== EOM ===\\n'; " + "}; " + "scan_firstparty() { local dir=\"$1\"; " + " [[ -d \"$dir\" ]] || return 0; " - + " while IFS= read -r manifest; do emit_manifest firstparty \"$manifest\"; done < <(find \"$dir\" -mindepth 2 -maxdepth 3 -name manifest.json -type f | sort); " + + " while IFS= read -r manifest; do emit_manifest firstparty \"$manifest\"; done < <(find \"$dir\" -mindepth 2 -maxdepth 3 -type f \\( -name manifest.json -o -name '*.manifest.json' \\) | sort); " + "}; " + "scan_thirdparty() { local dir=\"$1\"; " + " [[ -d \"$dir\" ]] || return 0; " diff --git a/shell/shell.qml b/shell/shell.qml index b0cf618f..0019294b 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -17,9 +17,6 @@ ShellRoot { // own empty copies. property PluginRegistry pluginRegistry: PluginRegistry { } property BarWidgetRegistry barWidgetRegistry: BarWidgetRegistry { } - property FirstPartyWidgets firstPartyWidgets: FirstPartyWidgets { - barWidgetRegistry: shell.barWidgetRegistry - } property string home: Quickshell.env("HOME") @@ -362,9 +359,17 @@ ShellRoot { return true } + function isPluginOpen(pluginId) { + var id = String(pluginId || "") + var loader = panelLoaders[id] + if (loader && loader.item && loader.item.opened !== undefined) + return loader.item.opened === true + return openPanelIds[id] === true + } + function toggle(pluginId, payloadJson) { var id = String(pluginId || "") - return openPanelIds[id] ? hide(id) : summon(id, payloadJson) + return isPluginOpen(id) ? hide(id) : summon(id, payloadJson) } // Map of pluginId -> Loader, populated by the Instantiator delegate below. @@ -411,6 +416,19 @@ ShellRoot { } } + function callIfLoaded(pluginId, method, arg) { + var loader = panelLoaders[pluginId] + if (!loader || !loader.item) return "unknown" + if (typeof loader.item[method] !== "function") return "unknown" + try { + var result = loader.item[method](arg) + return result === undefined || result === null ? "ok" : String(result) + } catch (e) { + console.warn("plugin " + pluginId + " " + method + "() threw:", e) + return "error" + } + } + // One Loader per discoverable panel/overlay/menu plugin. Active when the // host marks it open. The Loader holds onto the instance while active so the // plugin's FloatingWindow + state survive between summons within a session. @@ -490,10 +508,8 @@ ShellRoot { // Mirror plugin registry state into BarWidgetRegistry whenever it changes. // Each enabled plugin with kind "bar-widget" gets a Component created from - // its manifest entry point and registered under its plain manifest id. - // First-party widget ids (clock, weather, etc.) are short and don't - // collide with namespaced plugin ids, so we don't need a separate - // "plugin:" namespace anymore. + // its manifest entry point and registered under its manifest id. Built-in + // widgets use the same first-party manifest contract as third-party widgets. Connections { target: shell.pluginRegistry function onPluginsChanged() { shell.syncPluginWidgets() } @@ -527,6 +543,7 @@ ShellRoot { category: meta.category || "Plugin", allowMultiple: meta.allowMultiple === true, defaults: meta.defaults || {}, + settingsForm: meta.settingsForm || "", schema: meta.schema || [], pluginId: manifest.id, sourceDir: manifest.__sourceDir || "", @@ -641,5 +658,9 @@ ShellRoot { function toggle(id: string, payloadJson: string): void { shell.toggle(id, payloadJson) } + + function call(id: string, method: string, arg: string): string { + return shell.callIfLoaded(id, method, arg) + } } }