From d23d46915ff7bb500af94ef8da49dd7df80377b7 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Fri, 22 May 2026 23:35:27 -0400 Subject: [PATCH] Namespace omarchy plugins --- docs/omarchy-shell.md | 26 ++++++---- migrations/1779481017.sh | 41 +++++++++++++++ shell/Commons/Util.qml | 36 +++++++++++++- shell/README.md | 42 +++++++++++----- shell/plugins/bar/Bar.qml | 8 +-- shell/plugins/bar/FirstPartyWidgets.qml | 43 ++++++++-------- shell/plugins/bar/README.md | 63 ++++++++++++------------ shell/plugins/bar/widgets/Tray.qml | 3 +- shell/plugins/settings/SettingsPanel.qml | 20 ++++---- shell/services/PluginRegistry.qml | 8 +-- shell/shell-defaults.json | 26 +++++----- shell/shell.qml | 12 ++--- 12 files changed, 215 insertions(+), 113 deletions(-) create mode 100644 migrations/1779481017.sh diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index afcf2da6..53c42fd5 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -41,9 +41,13 @@ Full schema: [`shell/services/PluginRegistry.qml`](../shell/services/PluginRegis 1. Drop into `~/.config/omarchy/plugins//` with a `manifest.json` plus the QML referenced from `entryPoints`. -2. `omarchy-shell-ipc shell rescanPlugins` -3. `omarchy-shell-ipc shell setPluginEnabled true` -4. Bar widgets also need adding to a section via bar settings. +2. `omarchy plugin rescan` +3. `omarchy plugin enable ` +4. Bar widgets also need adding to a section with `omarchy plugin bar add ` + or the visual editor (`omarchy plugin bar edit`). + +The lower-level IPC methods remain available through `omarchy-shell shell ...` +for callers that need to talk directly to the running shell. ## IPC @@ -77,9 +81,9 @@ individual plugins (`bar`, `image-selector`, …). "centerAnchor": "calendar", "fontFamily": "JetBrainsMono Nerd Font", "layout": { - "left": [ { "id": "Omarchy" } ], - "center": [ { "id": "calendar", "format": "HH:mm" } ], - "right": [ { "id": "AudioPanel" } ] + "left": [ { "id": "omarchy.menu" } ], + "center": [ { "id": "omarchy.clock", "format": "HH:mm" } ], + "right": [ { "id": "omarchy.audio" } ] } }, "plugins": [ @@ -94,10 +98,12 @@ Rules: bar widgets, `plugins[]` for everything else. 2. Settings are inline on the entry. No `config:` sub-object, no merge layers. -3. Third-party enabled ⇔ present; first-party plugins are always enabled. -4. `allowMultiple: true` in the manifest permits multiple instances. -5. `idle.screensaver` and `idle.lock` are seconds since user idle began. -6. `version: 1` is required. +3. Built-in bar widget ids are namespaced (`omarchy.clock`, `omarchy.audio`, …); + legacy ids such as `Clock` and `AudioPanel` are accepted as aliases. +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. +7. `version: 1` is required. `shell-defaults.json` describes the fresh-install state. When no user `shell.json` exists, defaults are used verbatim. Once the user diff --git a/migrations/1779481017.sh b/migrations/1779481017.sh new file mode 100644 index 00000000..2fc9b01a --- /dev/null +++ b/migrations/1779481017.sh @@ -0,0 +1,41 @@ +echo "Namespace Omarchy built-in bar widget ids in shell.json" + +config_file="$HOME/.config/omarchy/shell.json" + +if [[ -s $config_file ]] && jq -e 'type == "object" and .version == 1' "$config_file" >/dev/null 2>&1; then + tmp=$(mktemp) + jq ' + def canonical_widget_id: + if . == "Omarchy" then "omarchy.menu" + elif . == "Workspaces" then "omarchy.workspaces" + elif . == "Media" then "omarchy.media" + elif . == "AudioPanel" then "omarchy.audio" + elif . == "MonitorPanel" then "omarchy.monitor" + elif . == "NetworkPanel" then "omarchy.network" + elif . == "PowerPanel" then "omarchy.power" + elif . == "BluetoothPanel" then "omarchy.bluetooth" + elif . == "Clock" then "omarchy.clock" + elif . == "Indicators" then "omarchy.indicators" + elif . == "NotificationCenter" then "omarchy.notifications" + elif . == "SystemUpdate" then "omarchy.system-update" + elif . == "SystemStats" then "omarchy.system-stats" + elif . == "Tray" then "omarchy.tray" + elif . == "Weather" then "omarchy.weather" + elif . == "Microphone" then "omarchy.microphone" + elif . == "ActiveWindow" then "omarchy.active-window" + elif . == "KeyboardLayout" then "omarchy.keyboard-layout" + elif . == "LockKeys" then "omarchy.lock-keys" + elif . == "Spacer" then "omarchy.spacer" + else . end; + + def canonical_entry: + if type == "string" then canonical_widget_id + elif type == "object" and has("id") then .id = (.id | canonical_widget_id) + else . end; + + .bar.centerAnchor = ((.bar.centerAnchor // "") | canonical_widget_id) | + .bar.layout.left = ((.bar.layout.left // []) | map(canonical_entry)) | + .bar.layout.center = ((.bar.layout.center // []) | map(canonical_entry)) | + .bar.layout.right = ((.bar.layout.right // []) | map(canonical_entry)) + ' "$config_file" >"$tmp" && mv "$tmp" "$config_file" +fi diff --git a/shell/Commons/Util.qml b/shell/Commons/Util.qml index b5c0cf93..a20a9247 100644 --- a/shell/Commons/Util.qml +++ b/shell/Commons/Util.qml @@ -42,6 +42,34 @@ 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 + } + // Best-effort base64 decode. Returns "" on parse failure rather than // surfacing garbage downstream. function decodeBase64(value) { @@ -72,8 +100,12 @@ QtObject { // so the two never drift. Entries are deep-cloned to decouple from the // input config; consumers can mutate without leaking back to shell.json. function normalizeLayoutEntry(entry) { - if (typeof entry === "string") return { id: entry } - if (isPlainObject(entry) && entry.id) return cloneJson(entry) + if (typeof entry === "string") return { id: canonicalWidgetId(entry) } + if (isPlainObject(entry) && entry.id) { + var copy = cloneJson(entry) + copy.id = canonicalWidgetId(copy.id) + return copy + } return null } diff --git a/shell/README.md b/shell/README.md index e1f2191f..08bcc17c 100644 --- a/shell/README.md +++ b/shell/README.md @@ -89,9 +89,24 @@ The full schema lives in `services/PluginRegistry.qml`. 1. Drop the plugin into `~/.config/omarchy/plugins//`. The directory must contain a `manifest.json` plus the QML files referenced from its `entryPoints`. -2. `omarchy-shell shell rescanPlugins`. -3. Enable the plugin with `omarchy-shell shell setPluginEnabled true`. -4. If it's a `bar-widget`, add it to a layout section from the bar editor. +2. `omarchy plugin rescan`. +3. Enable the plugin with `omarchy plugin enable `. +4. If it's a `bar-widget`, place it with `omarchy plugin bar add ` or + open the visual editor with `omarchy plugin bar edit`. + +The lower-level IPC equivalents are still available via `omarchy-shell shell rescanPlugins`, +`omarchy-shell shell setPluginEnabled true`, and `omarchy-shell shell listPlugins`. +The `omarchy plugin` command wraps those calls and can also edit the persisted +bar layout in `shell.json`. + +To hack on an existing widget safely, clone it into a user plugin instead of +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 # interactive source/name/tool picker +``` First-party plugins under `shell/plugins/` are discovered the same way and cannot be disabled. @@ -168,12 +183,12 @@ rewrites the `bar` subtree from the current `shell-defaults.json`. "bar": { "position": "top", "transparent": false, - "centerAnchor": "Clock", + "centerAnchor": "omarchy.clock", "layout": { - "left": [ { "id": "Omarchy" }, { "id": "Workspaces" } ], - "center": [ { "id": "Clock", "format": "HH:mm" } ], + "left": [ { "id": "omarchy.menu" }, { "id": "omarchy.workspaces" } ], + "center": [ { "id": "omarchy.clock", "format": "HH:mm" } ], "right": [ - { "id": "AudioPanel" } + { "id": "omarchy.audio" } ] } }, @@ -189,18 +204,21 @@ rewrites the `bar` subtree from the current `shell-defaults.json`. 2. **Settings are inline on the entry.** No `config:` sub-object, no separate per-plugin settings file, no merge layers. The fields on each entry are the values the plugin sees. -3. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff +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. +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 with the shell IPC. First-party plugins are always enabled. -4. **Multiple instances** are allowed when a manifest sets +5. **Multiple instances** are allowed when a manifest sets `allowMultiple: true`. Each instance is independent — e.g. two clock - widgets in different timezones are just two `{"id":"Clock", "timezone": ...}` + widgets in different timezones are just two `{"id":"omarchy.clock", "timezone": ...}` entries with their own values. -5. **Idle timings are top-level.** `idle.screensaver` and `idle.lock` +6. **Idle timings are top-level.** `idle.screensaver` and `idle.lock` are seconds since user idle began, so the default lock fires at 300s even if the 150s screensaver starts first. -6. **`version: 1` is required** at the top level. The shell will fall back +7. **`version: 1` is required** at the top level. The shell will fall back to defaults rather than load an unknown version. ## Implementation history diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index bae9b89b..09c9fbea 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -31,7 +31,7 @@ Item { property var fallbackBarConfig: ({ position: "top", transparent: false, - centerAnchor: "Clock", + centerAnchor: "omarchy.clock", layout: { left: [], center: [], right: [] } }) property var layoutConfig: fallbackBarConfig.layout @@ -133,7 +133,7 @@ Item { var trayEntry = null var result = [] for (var i = 0; i < entries.length; i++) { - if (entryId(entries[i]) === "Tray") trayEntry = entries[i] + if (entryId(entries[i]) === "omarchy.tray") trayEntry = entries[i] else result.push(entries[i]) } if (trayEntry) { @@ -148,7 +148,7 @@ Item { position = normalizePosition(config.position) transparent = config.transparent === true - centerAnchor = String(config.centerAnchor || "") + centerAnchor = Util.canonicalWidgetId(config.centerAnchor || "") layoutConfig = normalizeLayout(config.layout) barConfigSerial++ } @@ -208,7 +208,7 @@ Item { } function canonicalWidgetId(name) { - return String(name) + return Util.canonicalWidgetId(name) } function expandPath(path) { diff --git a/shell/plugins/bar/FirstPartyWidgets.qml b/shell/plugins/bar/FirstPartyWidgets.qml index e035aa5e..5d25faf8 100644 --- a/shell/plugins/bar/FirstPartyWidgets.qml +++ b/shell/plugins/bar/FirstPartyWidgets.qml @@ -6,16 +6,16 @@ QtObject { required property var barWidgetRegistry readonly property var metadata: ({ - "Omarchy": { displayName: "Omarchy menu", description: "Launches the Omarchy menu", category: "Compositor", allowMultiple: false }, - "Workspaces": { displayName: "Workspaces", description: "Workspace number indicators", category: "Compositor", allowMultiple: false }, - "Media": { displayName: "Media", description: "MPRIS now-playing with playback controls", category: "Media", allowMultiple: false }, - "AudioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false, sourceDir: "../panels", sourceName: "Audio" }, - "MonitorPanel": { displayName: "Display", description: "Brightness slider and laptop display controls", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Monitor" }, - "NetworkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Network" }, - "PowerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Power" }, - "BluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Bluetooth" }, - "Clock": { displayName: "Clock", description: "Day/time label; click to toggle alternate format", category: "Time", allowMultiple: false, settingsForm: "clockSettings" }, - "Indicators": { displayName: "Indicators", description: "Manual state indicators", category: "Status", allowMultiple: true, + "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: [ @@ -27,16 +27,16 @@ QtObject { ] }, { key: "alwaysShow", type: "boolean", label: "Always Show", description: "Show inactive indicators without waiting for hover.", defaultValue: false } ] }, - "NotificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false }, - "SystemUpdate": { displayName: "System update", description: "Indicates available system updates", category: "System", allowMultiple: false }, - "SystemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false }, - "Tray": { displayName: "System tray", description: "Status notifier items", category: "Status", allowMultiple: false }, - "Weather": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false, settingsForm: "weatherSettings" }, - "Microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false }, - "ActiveWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false }, - "KeyboardLayout": { displayName: "Keyboard layout", description: "Current xkb layout, click cycles", category: "Compositor", allowMultiple: false }, - "LockKeys": { displayName: "Lock keys", description: "Caps / Num / Scroll lock indicators", category: "System", allowMultiple: false }, - "Spacer": { displayName: "Spacer", description: "Configurable blank space", category: "Layout", allowMultiple: true, settingsForm: "spacerSettings" } + "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: ({}) @@ -64,7 +64,8 @@ QtObject { allowMultiple: meta.allowMultiple === true, settingsForm: meta.settingsForm || "", schema: Array.isArray(meta.schema) ? meta.schema : [], - source: "first-party" + source: "first-party", + legacyId: meta.legacyId || "" } var comp = Qt.createComponent(url, Component.Asynchronous) function finalize() { diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index da47cbfd..93d9c32c 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -16,7 +16,7 @@ the shell for its whole session. The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`shell-defaults.json`](../../shell-defaults.json). Once you customize anything via `omarchy launch bar settings` or by editing shell.json directly, your file is canonical — there is no deep-merge. -Launch the visual editor with `omarchy launch bar settings` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand. You can also right-click empty space to the left or right of the centered clock module to open it; double-left-click the same empty space to toggle bar transparency. +Launch the visual editor with `omarchy launch bar settings` / `omarchy plugin bar edit` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand. For scriptable changes, use `omarchy plugin bar list`, `omarchy plugin bar add`, `omarchy plugin bar move`, `omarchy plugin bar remove`, and `omarchy plugin bar set`. You can also right-click empty space to the left or right of the centered clock module to open it; double-left-click the same empty space to toggle bar transparency. Example `shell.json` (bar subtree only shown): @@ -26,20 +26,20 @@ Example `shell.json` (bar subtree only shown): "bar": { "position": "top", "transparent": false, - "centerAnchor": "Clock", + "centerAnchor": "omarchy.clock", "layout": { "left": [ - { "id": "Omarchy" }, - { "id": "Spacer", "size": 12 }, - { "id": "Workspaces" } + { "id": "omarchy.menu" }, + { "id": "omarchy.spacer", "size": 12 }, + { "id": "omarchy.workspaces" } ], "center": [ - { "id": "Media" }, - { "id": "Clock", "format": "HH:mm" } + { "id": "omarchy.media" }, + { "id": "omarchy.clock", "format": "HH:mm" } ], "right": [ - { "id": "AudioPanel" }, - { "id": "PowerPanel" } + { "id": "omarchy.audio" }, + { "id": "omarchy.power" } ] } } @@ -54,17 +54,17 @@ Example `shell.json` (bar subtree only shown): | Name | What it does | Interactions | |---|---|---| -| `Omarchy` | Omarchy menu launcher | left = menu · right = terminal | -| `Workspaces` | Hyprland workspace switcher | left = focus workspace | -| `Clock` | Date/time label | left = alternate format · right = timezone selector | -| `Media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup | -| `Indicators` | Manual state indicators | left = indicator action | -| `NotificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND | -| `SystemUpdate` | Available update indicator | left = update | -| `SystemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal | -| `Tray` | System tray | hover = reveal drawer · right on chevron = manage | -| `Weather` | Weather icon + popup with forecast | left = popup · right = full notification | -| `Microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio panel · scroll = source volume | +| `omarchy.menu` | Omarchy menu launcher | left = menu · right = terminal | +| `omarchy.workspaces` | Hyprland workspace switcher | left = focus workspace | +| `omarchy.clock` | Date/time label | left = alternate format · right = timezone selector | +| `omarchy.media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup | +| `omarchy.indicators` | Manual state indicators | left = indicator action | +| `omarchy.notifications` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND | +| `omarchy.system-update` | Available update indicator | left = update | +| `omarchy.system-stats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal | +| `omarchy.tray` | System tray | hover = reveal drawer · right on chevron = manage | +| `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/`) @@ -76,7 +76,7 @@ Example `shell.json` (bar subtree only shown): | `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 `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 `Indicators` instances are allowed, so different sections can show different subsets. Rich panels such as `PowerPanel`, `NetworkPanel`, and `AudioPanel` 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. Rich popup widgets such as `omarchy.power`, `omarchy.network`, and `omarchy.audio` live in `../panels/` above. ## Orientation @@ -94,9 +94,9 @@ Command module: "bar": { "layout": { "right": [ - { "id": "Tray" }, + { "id": "omarchy.tray" }, { "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" }, - { "id": "AudioPanel" } + { "id": "omarchy.audio" } ] } } @@ -118,7 +118,7 @@ QML module: "layout": { "right": [ { "id": "gpu", "type": "qml" }, - { "id": "AudioPanel" } + { "id": "omarchy.audio" } ] } } @@ -169,15 +169,16 @@ 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`; first-party panels +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 use UpperCamelCase names such as `AudioPanel`, -`NetworkPanel`, and so on, and are picked up by the shell's -`BarWidgetRegistry` at startup; reference one by `id` in any layout list. +`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. Third-party widgets ship as separate plugins under `~/.config/omarchy/plugins//` with their own `manifest.json` declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See -[../../README.md](../../README.md) for the manifest schema. Enable or -rescan third-party plugins with `omarchy-shell shell setPluginEnabled` -and `omarchy-shell shell rescanPlugins`. +[../../README.md](../../README.md) for the manifest schema. Enable, +rescan, and place third-party plugins with `omarchy plugin enable`, +`omarchy plugin rescan`, and `omarchy plugin bar add`. diff --git a/shell/plugins/bar/widgets/Tray.qml b/shell/plugins/bar/widgets/Tray.qml index 7ae853ef..386356c9 100644 --- a/shell/plugins/bar/widgets/Tray.qml +++ b/shell/plugins/bar/widgets/Tray.qml @@ -70,7 +70,8 @@ BarWidget { function persistTrayState(pinned, hidden) { if (!root.bar || !root.bar.shell || typeof root.bar.shell.updateEntryInline !== "function") return - root.bar.shell.updateEntryInline("Tray", { id: "Tray", pinned: pinned, hidden: hidden }) + var id = root.moduleName || "omarchy.tray" + root.bar.shell.updateEntryInline(id, { id: id, pinned: pinned, hidden: hidden }) } function togglePin(iid) { diff --git a/shell/plugins/settings/SettingsPanel.qml b/shell/plugins/settings/SettingsPanel.qml index 1283589d..1d00e086 100644 --- a/shell/plugins/settings/SettingsPanel.qml +++ b/shell/plugins/settings/SettingsPanel.qml @@ -128,16 +128,16 @@ Item { bar: { position: "top", transparent: false, - centerAnchor: "Clock", + centerAnchor: "omarchy.clock", layout: { - left: [{ id: "Omarchy" }, { id: "Workspaces" }], + left: [{ id: "omarchy.menu" }, { id: "omarchy.workspaces" }], center: [ - { id: "Clock", format: "dddd HH:mm", formatAlt: "dd MMMM 'W'ww yyyy", verticalFormat: "HH\n\u2014\nmm" }, - { id: "Weather" }, { id: "Indicators" }, { id: "SystemUpdate" } + { id: "omarchy.clock", format: "dddd HH:mm", formatAlt: "dd MMMM 'W'ww yyyy", verticalFormat: "HH\n\u2014\nmm" }, + { id: "omarchy.weather" }, { id: "omarchy.indicators" }, { id: "omarchy.system-update" } ], right: [ - { id: "Tray" }, { id: "BluetoothPanel" }, { id: "NetworkPanel" }, - { id: "AudioPanel" }, { id: "MonitorPanel" }, { id: "PowerPanel" } + { id: "omarchy.tray" }, { id: "omarchy.bluetooth" }, { id: "omarchy.network" }, + { id: "omarchy.audio" }, { id: "omarchy.monitor" }, { id: "omarchy.power" } ] } }, @@ -145,7 +145,7 @@ Item { }) property var defaultConfig: builtinShellConfig - property var draft: ({ version: 1, bar: { position: "top", transparent: false, centerAnchor: "Clock", layout: { left: [], center: [], right: [] } }, plugins: [] }) + property var draft: ({ version: 1, bar: { position: "top", transparent: false, centerAnchor: "omarchy.clock", layout: { left: [], center: [], right: [] } }, plugins: [] }) property int draftRevision: 0 property bool suppressReload: false @@ -179,7 +179,7 @@ Item { bar: { position: String(bar.position || "top"), transparent: bar.transparent === true, - centerAnchor: String(bar.centerAnchor || ""), + centerAnchor: Util.canonicalWidgetId(bar.centerAnchor || ""), layout: Util.normalizeLayout(bar.layout || {}) }, plugins: plugins @@ -320,7 +320,7 @@ Item { } function canonicalWidgetId(id) { - return String(id || "") + return Util.canonicalWidgetId(id) } function widgetMetadata(id) { @@ -380,7 +380,7 @@ Item { function widgetAllowsMultiple(id) { var meta = widgetMetadata(id) if (meta.allowMultiple === true) return true - return String(id) === "Spacer" + return canonicalWidgetId(id) === "omarchy.spacer" } function catalogIds() { diff --git a/shell/services/PluginRegistry.qml b/shell/services/PluginRegistry.qml index a2bf5f7e..18cd4034 100644 --- a/shell/services/PluginRegistry.qml +++ b/shell/services/PluginRegistry.qml @@ -229,11 +229,13 @@ QtObject { var merged = {} for (var fk in firstParty) merged[fk] = firstParty[fk] - // Third-party plugins never shadow a first-party one with the same id. + // Third-party plugins never shadow first-party ids. The whole + // `omarchy.*` namespace is reserved for built-ins, including bar widgets + // registered outside the manifest-based plugin registry. for (var tk in thirdParty) { - if (firstParty[tk]) { + if (firstParty[tk] || String(tk).indexOf("omarchy.") === 0) { console.warn("PluginRegistry: plugin " + tk - + " rejected: id collides with first-party plugin") + + " rejected: id is reserved for first-party Omarchy plugins") continue } merged[tk] = thirdParty[tk] diff --git a/shell/shell-defaults.json b/shell/shell-defaults.json index 08d086d9..abb2bf5f 100644 --- a/shell/shell-defaults.json +++ b/shell/shell-defaults.json @@ -7,51 +7,51 @@ "bar": { "position": "top", "transparent": false, - "centerAnchor": "Clock", + "centerAnchor": "omarchy.clock", "layout": { "left": [ { - "id": "Omarchy" + "id": "omarchy.menu" }, { - "id": "Workspaces" + "id": "omarchy.workspaces" } ], "center": [ { - "id": "Clock", + "id": "omarchy.clock", "format": "dddd HH:mm", "formatAlt": "dd MMMM 'W'ww yyyy", "verticalFormat": "HH\n\u2014\nmm" }, { - "id": "Weather" + "id": "omarchy.weather" }, { - "id": "Indicators" + "id": "omarchy.indicators" }, { - "id": "SystemUpdate" + "id": "omarchy.system-update" } ], "right": [ { - "id": "Tray" + "id": "omarchy.tray" }, { - "id": "BluetoothPanel" + "id": "omarchy.bluetooth" }, { - "id": "NetworkPanel" + "id": "omarchy.network" }, { - "id": "AudioPanel" + "id": "omarchy.audio" }, { - "id": "MonitorPanel" + "id": "omarchy.monitor" }, { - "id": "PowerPanel" + "id": "omarchy.power" } ] } diff --git a/shell/shell.qml b/shell/shell.qml index 21efcf9c..b0cf618f 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -44,11 +44,11 @@ ShellRoot { bar: { position: "top", transparent: false, - centerAnchor: "Clock", + centerAnchor: "omarchy.clock", layout: { - left: [{ id: "Omarchy" }, { id: "Workspaces" }], - center: [{ id: "Clock", format: "dddd HH:mm" }], - right: [{ id: "AudioPanel" }] + left: [{ id: "omarchy.menu" }, { id: "omarchy.workspaces" }], + center: [{ id: "omarchy.clock", format: "dddd HH:mm" }], + right: [{ id: "omarchy.audio" }] } }, plugins: [] @@ -264,7 +264,7 @@ ShellRoot { // new shellConfig in a local clone, and only persist if anything actually // changed so reactive bindings do not dirty shell.json unnecessarily. function updateEntryInline(moduleName, settings) { - var stripped = String(moduleName) + var stripped = Util.canonicalWidgetId(moduleName) var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig)) if (!Util.isPlainObject(copy.bar)) copy.bar = { layout: { left: [], center: [], right: [] } } if (!Util.isPlainObject(copy.bar.layout)) copy.bar.layout = { left: [], center: [], right: [] } @@ -276,7 +276,7 @@ ShellRoot { for (var s = 0; s < sections.length; s++) { var arr = copy.bar.layout[sections[s]] || [] for (var i = 0; i < arr.length; i++) { - if (arr[i] && arr[i].id === stripped) { + if (arr[i] && Util.canonicalWidgetId(arr[i].id) === stripped) { var next = { id: stripped } for (var k in settings) if (k !== "id") next[k] = settings[k] if (JSON.stringify(arr[i]) !== JSON.stringify(next)) {