Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-4

This commit is contained in:
Ryan Hughes
2026-06-05 18:35:34 -04:00
10 changed files with 430 additions and 57 deletions
+175 -5
View File
@@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Mutate the user shell.json bar layout # omarchy:summary=Mutate the user shell.json bar layout and active bar option
# omarchy:args=show | list [--json] [--all] | add [plugin] [left|center|right] | remove [plugin] | drop [plugin] | position <top|bottom|left|right> | transparent <true|false> # omarchy:args=show | options [--json] | use <plugin> | reset | list [--json] [--all] | add [plugin] [left|center|right] | remove [plugin] | drop [plugin] | position <top|bottom|left|right> | transparent <true|false>
# omarchy:examples=omarchy config shell bar show | omarchy config shell bar list | omarchy config shell bar add | omarchy config shell bar remove | omarchy config shell bar add omarchy.tailscale | omarchy config shell bar position top | omarchy config shell bar transparent true # omarchy:examples=omarchy config shell bar show | omarchy config shell bar options | omarchy config shell bar use local.neon-bar | omarchy config shell bar reset | omarchy config shell bar list | omarchy config shell bar add omarchy.tailscale | omarchy config shell bar position top | omarchy config shell bar transparent true
set -euo pipefail set -euo pipefail
@@ -11,7 +11,7 @@ OMARCHY_ROOT="${OMARCHY_PATH:-}"
DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json" DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json"
usage() { usage() {
echo "Usage: omarchy-config-shell-bar show | list [--json] [--all] | add [plugin] [left|center|right] | remove [plugin] | drop [plugin] | position <top|bottom|left|right> | transparent <true|false>" >&2 echo "Usage: omarchy-config-shell-bar show | options [--json] | use <plugin> | reset | list [--json] [--all] | add [plugin] [left|center|right] | remove [plugin] | drop [plugin] | position <top|bottom|left|right> | transparent <true|false>" >&2
} }
fail() { fail() {
@@ -49,6 +49,95 @@ bar_widget_manifest_paths() {
} | sort -u } | sort -u
} }
bar_option_manifest_paths() {
{
if [[ -n $OMARCHY_ROOT && -d $OMARCHY_ROOT/shell/plugins ]]; then
find "$OMARCHY_ROOT/shell/plugins" -mindepth 2 -maxdepth 4 -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null
fi
if [[ -d $HOME/.config/omarchy/plugins ]]; then
find -L "$HOME/.config/omarchy/plugins" -mindepth 2 -maxdepth 2 -type f -name manifest.json 2>/dev/null
fi
} | sort -u
}
current_bar_option_id() {
jq -r '(.bar.id // "omarchy.bar") | tostring' "$(source_file)"
}
available_bar_options_json() {
local current_id
local manifest_paths=()
current_id=$(current_bar_option_id)
mapfile -t manifest_paths < <(bar_option_manifest_paths)
if (( ${#manifest_paths[@]} == 0 )); then
printf '[]\n'
return
fi
jq -s --arg currentId "$current_id" '
map(
select(((.kinds // []) | index("bar")) and ((.entryPoints.bar // "") != "")) |
{
id: (.id // ""),
name: (.name // .id // ""),
description: (.description // ""),
firstParty: ((.id // "") | startswith("omarchy.")),
active: ((.id // "") == $currentId)
} |
select(.id != "")
) |
unique_by(.id) |
sort_by((if .id == "omarchy.bar" then 0 else 1 end), .name, .id)
' "${manifest_paths[@]}"
}
print_bar_options() {
local json="false"
while (( $# > 0 )); do
case "$1" in
--json)
json="true"
;;
-h|--help)
usage
exit 0
;;
*)
fail "unknown options option: $1"
;;
esac
shift
done
if [[ $json == "true" ]]; then
available_bar_options_json
return
fi
available_bar_options_json | jq -r '
.[] |
[
.id,
(if .active then "active" else "available" end),
(if .firstParty then "built-in" else "plugin" end),
.name,
.description
] | @tsv
' | awk -F '\t' '
BEGIN { printf "%-32s %-10s %-8s %-22s %s\n", "ID", "STATUS", "SOURCE", "NAME", "DESCRIPTION" }
{ printf "%-32s %-10s %-8s %-22s %s\n", $1, $2, $3, $4, $5 }
'
}
bar_option_exists() {
local id="$1"
available_bar_options_json | jq -e --arg id "$id" 'any(.[]; .id == $id)' >/dev/null
}
current_bar_ids_json() { current_bar_ids_json() {
jq -c ' jq -c '
def array_or_empty: if type == "array" then . else [] end; def array_or_empty: if type == "array" then . else [] end;
@@ -256,7 +345,88 @@ case "$command" in
jq '.bar // {}' "$(source_file)" jq '.bar // {}' "$(source_file)"
;; ;;
list|available|options) selected|active)
if (( $# != 1 )); then
usage
exit 1
fi
current_bar_option_id
;;
options|option)
shift
print_bar_options "$@"
;;
use)
if (( $# != 2 )); then
usage
exit 1
fi
plugin="${2:-}"
[[ -n $plugin ]] || fail "bar option id is required"
if [[ $plugin == "default" || $plugin == "built-in" ]]; then
plugin="omarchy.bar"
fi
bar_option_exists "$plugin" || fail "$plugin is not a known bar option; run 'omarchy config shell bar options'"
mkdir -p "$(dirname "$CONFIG_FILE")"
source="$(source_file)"
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
if [[ $plugin == "omarchy.bar" ]]; then
jq '
def object_or_empty: if type == "object" then . else {} end;
object_or_empty
| .version = 1
| .bar = (.bar | object_or_empty)
| del(.bar.id)
' "$source" >"$tmp"
else
jq --arg plugin "$plugin" '
def object_or_empty: if type == "object" then . else {} end;
object_or_empty
| .version = 1
| .bar = (.bar | object_or_empty)
| .bar.id = $plugin
' "$source" >"$tmp"
fi
mv "$tmp" "$CONFIG_FILE"
trap - EXIT
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
refresh_shell_config
;;
reset)
if (( $# != 1 )); then
usage
exit 1
fi
mkdir -p "$(dirname "$CONFIG_FILE")"
source="$(source_file)"
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
jq '
def object_or_empty: if type == "object" then . else {} end;
object_or_empty
| .version = 1
| .bar = (.bar | object_or_empty)
| del(.bar.id)
' "$source" >"$tmp"
mv "$tmp" "$CONFIG_FILE"
trap - EXIT
refresh_shell_config
;;
list|available|widgets)
shift shift
print_available_widgets "$@" print_available_widgets "$@"
;; ;;
+25 -1
View File
@@ -41,9 +41,13 @@ Clone options:
--prompt <prompt> Initial prompt when opening with AI --prompt <prompt> Initial prompt when opening with AI
--replace Replace the first source bar instance --replace Replace the first source bar instance
--add [placement] Add cloned bar widget to the bar --add [placement] Add cloned bar widget to the bar
--use Use cloned bar options as the active bar
Bar widget commands: Bar widget commands:
bar settings Open the inline bar config panel bar settings Open the inline bar config panel
bar options [--json] List available bar options
bar use <id> Use a bar option as the active bar
bar reset Return to the built-in Omarchy bar
bar list [--json] List current bar layout bar list [--json] List current bar layout
bar add <id> [placement] [--duplicate] bar add <id> [placement] [--duplicate]
bar move <id> [placement] [--from-section SECTION] [--from-index N] bar move <id> [placement] [--from-section SECTION] [--from-index N]
@@ -65,6 +69,9 @@ Examples:
omarchy plugin edit local.clock --with editor omarchy plugin edit local.clock --with editor
omarchy plugin edit local.clock --with ai --prompt "Customize this clock" omarchy plugin edit local.clock --with ai --prompt "Customize this clock"
omarchy plugin enable acme.weather --section right omarchy plugin enable acme.weather --section right
omarchy plugin bar options
omarchy plugin bar use local.neon-bar
omarchy plugin bar reset
omarchy plugin bar add acme.weather --section right --before omarchy.tray omarchy plugin bar add acme.weather --section right --before omarchy.tray
omarchy plugin bar move omarchy.clock --section center --index 0 omarchy plugin bar move omarchy.clock --section center --index 0
omarchy plugin bar remove acme.weather omarchy plugin bar remove acme.weather
@@ -705,7 +712,7 @@ clone_source_options() {
first_party_manifest_paths | while IFS= read -r manifest; do first_party_manifest_paths | while IFS= read -r manifest; do
id=$(jq -r '.id // empty' "$manifest" 2>/dev/null || true) id=$(jq -r '.id // empty' "$manifest" 2>/dev/null || true)
name=$(jq -r '.barWidget.displayName // .name // .id // empty' "$manifest" 2>/dev/null || true) name=$(jq -r '.barWidget.displayName // .name // .id // empty' "$manifest" 2>/dev/null || true)
[[ -n $id && $id != "omarchy.bar" ]] || continue [[ -n $id ]] || continue
if jq -e '(.kinds // []) | index("bar-widget")' "$manifest" >/dev/null 2>&1; then if jq -e '(.kinds // []) | index("bar-widget")' "$manifest" >/dev/null 2>&1; then
printf '%s\tBuilt-in widget\t%s\n' "$id" "${name:-$id}" printf '%s\tBuilt-in widget\t%s\n' "$id" "${name:-$id}"
else else
@@ -1030,6 +1037,7 @@ clone_command() {
local prompt="" local prompt=""
local replace="false" local replace="false"
local add="false" local add="false"
local use_bar="false"
local placement=() local placement=()
while (( $# > 0 )); do while (( $# > 0 )); do
@@ -1061,6 +1069,10 @@ clone_command() {
add="true" add="true"
shift shift
;; ;;
--use)
use_bar="true"
shift
;;
--section | --index | --before | --after) --section | --index | --before | --after)
placement+=("$1" "${2:-}") placement+=("$1" "${2:-}")
shift 2 shift 2
@@ -1155,6 +1167,9 @@ clone_command() {
elif [[ $add == "true" ]]; then elif [[ $add == "true" ]]; then
bar_add "$new_id" "${placement[@]}" bar_add "$new_id" "${placement[@]}"
echo "Cloned $source_id to $new_id" echo "Cloned $source_id to $new_id"
elif [[ $use_bar == "true" ]]; then
omarchy-config-shell-bar use "$new_id"
echo "Cloned $source_id to $new_id and selected it as the active bar option"
else else
echo "Cloned $source_id to $new_id" echo "Cloned $source_id to $new_id"
fi fi
@@ -1176,6 +1191,15 @@ bar_command() {
(( $# == 0 )) || fail "bar settings does not take arguments" (( $# == 0 )) || fail "bar settings does not take arguments"
exec omarchy-launch-bar-settings exec omarchy-launch-bar-settings
;; ;;
options | option)
exec omarchy-config-shell-bar options "$@"
;;
use)
exec omarchy-config-shell-bar use "$@"
;;
reset)
exec omarchy-config-shell-bar reset "$@"
;;
list) list)
bar_list "$@" bar_list "$@"
;; ;;
+22 -13
View File
@@ -25,12 +25,15 @@ first call.
| Kind | What it is | | Kind | What it is |
|--------------|---------------------------------------------| |--------------|---------------------------------------------|
| `bar-widget` | Component the bar drops into a section | | `bar-widget` | Component the active bar drops into a section |
| `panel` | Floating window (e.g. OSD) | | `bar` | Full bar option that can replace `omarchy.bar` |
| `overlay` | Fullscreen overlay (e.g. background picker) | | `panel` | Floating window (e.g. OSD) |
| `menu` | Summoned menu surface | | `overlay` | Fullscreen overlay (e.g. background picker) |
| `service` | Headless singleton, no UI | | `menu` | Summoned menu surface |
| `service` | Headless singleton, no UI |
Only one full bar option is active at a time. The built-in `omarchy.bar` is
used when `bar.id` is omitted or when a selected third-party bar cannot load.
Panels, overlays, and menus are loaded when summoned. Plugins can set Panels, overlays, and menus are loaded when summoned. Plugins can set
`keepLoaded: true` to survive between summons. First-party services are `keepLoaded: true` to survive between summons. First-party services are
loaded at startup. loaded at startup.
@@ -59,7 +62,8 @@ every prompt (the path for scripts and agents).
Sources are recorded in `~/.config/omarchy/plugins/sources.json` and cloned into Sources are recorded in `~/.config/omarchy/plugins/sources.json` and cloned into
`~/.cache/omarchy/plugin-sources/`. You can still install by hand: drop a plugin `~/.cache/omarchy/plugin-sources/`. You can still install by hand: drop a plugin
into `~/.config/omarchy/plugins/<id>/`, run `omarchy plugin rescan`, then into `~/.config/omarchy/plugins/<id>/`, run `omarchy plugin rescan`, then
`omarchy plugin enable <id>` (bar widgets also need `omarchy plugin bar add <id>`). `omarchy plugin enable <id>` (bar widgets also need `omarchy plugin bar add <id>`;
full bar replacements are selected with `omarchy plugin bar use <id>`).
The lower-level IPC methods remain available through `omarchy-shell shell ...`. The lower-level IPC methods remain available through `omarchy-shell shell ...`.
## IPC ## IPC
@@ -91,6 +95,7 @@ individual plugins (`bar`, `image-selector`, …).
"lock": 300 "lock": 300
}, },
"bar": { "bar": {
"id": "omarchy.bar",
"position": "top", "position": "top",
"transparent": false, "transparent": false,
"centerAnchor": "calendar", "centerAnchor": "calendar",
@@ -109,16 +114,20 @@ individual plugins (`bar`, `image-selector`, …).
Rules: Rules:
1. Every plugin instance is one entry — `bar.layout.<section>` for 1. The active bar option is `bar.id`. Omit it or set it to `omarchy.bar` for
the built-in bar; set it to a plugin whose manifest declares `kind: "bar"`
to replace the full bar.
2. Every plugin instance is one entry — `bar.layout.<section>` for
bar widgets, `plugins[]` for everything else. bar widgets, `plugins[]` for everything else.
2. Settings are inline on the entry. No `config:` sub-object, no 3. Settings are inline on the entry. No `config:` sub-object, no
merge layers. merge layers.
3. Built-in bar widget ids are namespaced (`omarchy.clock`, `omarchy.audio`, …). 4. Built-in bar widget ids are namespaced (`omarchy.clock`, `omarchy.audio`, …).
The migration rewrites older ids such as `Clock` and `AudioPanel` forward. The migration rewrites older ids such as `Clock` and `AudioPanel` forward.
4. Third-party enabled ⇔ present; first-party plugins are always enabled. 5. Third-party enabled ⇔ present; for full bar options that means `bar.id`.
5. `allowMultiple: true` in the manifest permits multiple instances. First-party non-bar plugins are always enabled.
6. `idle.screensaver` and `idle.lock` are seconds since user idle began. 6. `allowMultiple: true` in the manifest permits multiple instances.
7. `version: 1` is required. 7. `idle.screensaver` and `idle.lock` are seconds since user idle began.
8. `version: 1` is required.
`config/omarchy/shell.json` describes the fresh-install state. When no `config/omarchy/shell.json` describes the fresh-install state. When no
user `shell.json` exists, defaults are used verbatim. Once the user user `shell.json` exists, defaults are used verbatim. Once the user
+23 -14
View File
@@ -75,13 +75,15 @@ Supported `kinds`:
| Kind | What it is | | Kind | What it is |
|--------------|--------------------------------------------------------------| |--------------|--------------------------------------------------------------|
| `bar-widget` | A component that the bar can drop into a section | | `bar-widget` | A component that the active bar can drop into a section |
| `panel` | A persistent or summoned floating window (e.g. OSD) | | `panel` | A persistent or summoned floating window (e.g. OSD) |
| `overlay` | A fullscreen overlay (e.g. background switcher) | | `overlay` | A fullscreen overlay (e.g. background switcher) |
| `menu` | A summoned menu surface | | `menu` | A summoned menu surface |
| `service` | A headless singleton, no UI | | `service` | A headless singleton, no UI |
| `bar` | Reserved for the first-party bar host (`omarchy.bar`). Third-party plugins should ship `bar-widget`s; they do not replace the host bar. | | `bar` | A full bar option that can replace the built-in `omarchy.bar` |
Only one `bar` plugin is active at a time. Missing or invalid selections fall
back to the built-in `omarchy.bar`, so users always have a safe path home.
Panels, overlays, and menus are loaded when summoned. Plugins that need Panels, overlays, and menus are loaded when summoned. Plugins that need
to outlive a single summon can set `keepLoaded: true` (e.g. the image to outlive a single summon can set `keepLoaded: true` (e.g. the image
picker keeps its overlay window mounted between summons). First-party picker keeps its overlay window mounted between summons). First-party
@@ -132,7 +134,7 @@ You can still drop a plugin in without a source:
1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json` 1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json`
plus the QML referenced from its `entryPoints`. plus the QML referenced from its `entryPoints`.
2. `omarchy plugin rescan`. 2. `omarchy plugin rescan`.
3. `omarchy plugin enable <id>` (bar widgets also need `omarchy plugin bar add <id>`). 3. `omarchy plugin enable <id>` (bar widgets also need `omarchy plugin bar add <id>`; full bar replacements are selected with `omarchy plugin bar use <id>`).
The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`, The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`,
`omarchy-shell shell setPluginEnabled <id> true`, and `omarchy-shell shell listPlugins`. `omarchy-shell shell setPluginEnabled <id> true`, and `omarchy-shell shell listPlugins`.
@@ -151,7 +153,9 @@ omarchy plugin edit local.clock --with ai # edit with `omarchy launch ai`
``` ```
First-party plugins under `shell/plugins/` First-party plugins under `shell/plugins/`
are discovered the same way and cannot be disabled. are discovered the same way and cannot be disabled, except that the built-in
bar option can become inactive while a third-party `kind: "bar"` plugin is the
selected bar.
## IPC contract ## IPC contract
@@ -224,6 +228,7 @@ becomes the authoritative file — we do **not** deep-merge defaults back in.
"lock": 300 "lock": 300
}, },
"bar": { "bar": {
"id": "omarchy.bar",
"position": "top", "position": "top",
"transparent": false, "transparent": false,
"centerAnchor": "omarchy.clock", "centerAnchor": "omarchy.clock",
@@ -241,27 +246,31 @@ becomes the authoritative file — we do **not** deep-merge defaults back in.
### Storage rules ### Storage rules
1. **Every plugin instance is one entry.** Either in `bar.layout.<section>` 1. **The active bar option is `bar.id`.** Omit it or set it to `omarchy.bar`
to use the built-in bar. Set it to another plugin id whose manifest declares
`kind: "bar"` to replace the full bar.
2. **Every plugin instance is one entry.** Either in `bar.layout.<section>`
for bar widgets, or in `plugins[]` for panels, overlays, services, for bar widgets, or in `plugins[]` for panels, overlays, services,
menus, and anything else non-bar. menus, and anything else non-bar.
2. **Settings are inline on the entry.** No `config:` sub-object, no 3. **Settings are inline on the entry.** No `config:` sub-object, no
separate per-plugin settings file, no merge layers. The fields on each separate per-plugin settings file, no merge layers. The fields on each
entry are the values the plugin sees. entry are the values the plugin sees.
3. **Built-in widget ids are namespaced.** Use ids such as `omarchy.clock`, 4. **Built-in widget ids are namespaced.** Use ids such as `omarchy.clock`,
`omarchy.audio`, and `omarchy.network`. The migration rewrites older ids `omarchy.audio`, and `omarchy.network`. The migration rewrites older ids
like `Clock` and `AudioPanel` forward. like `Clock` and `AudioPanel` forward.
4. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff 5. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff
its id appears somewhere in shell.json. For bar widgets, the bar its id appears somewhere in shell.json. For full bar options, that means
settings UI adds/removes layout entries; other plugin kinds are enabled `bar.id`; for bar widgets, the bar settings UI adds/removes layout entries;
with the shell IPC. First-party plugins are always enabled. other plugin kinds are enabled with the shell IPC. First-party non-bar
5. **Multiple instances** are allowed when a manifest sets plugins are always enabled.
6. **Multiple instances** are allowed when a manifest sets
`allowMultiple: true`. Each instance is independent — e.g. two clock `allowMultiple: true`. Each instance is independent — e.g. two clock
widgets in different timezones are just two `{"id":"omarchy.clock", "timezone": ...}` widgets in different timezones are just two `{"id":"omarchy.clock", "timezone": ...}`
entries with their own values. entries with their own values.
6. **Idle timings are top-level.** `idle.screensaver` and `idle.lock` 7. **Idle timings are top-level.** `idle.screensaver` and `idle.lock`
are seconds since user idle began, so the default lock fires at 300s are seconds since user idle began, so the default lock fires at 300s
even if the 150s screensaver starts first. even if the 150s screensaver starts first.
7. **`version: 1` is required** at the top level. The shell will fall back 8. **`version: 1` is required** at the top level. The shell will fall back
to defaults rather than load an unknown version. to defaults rather than load an unknown version.
## Implementation history ## Implementation history
+6 -4
View File
@@ -2,9 +2,11 @@
These plugins ship with Omarchy and are discovered by the shell at startup. These plugins ship with Omarchy and are discovered by the shell at startup.
They use the same `manifest.json` contract as third-party plugins; the They use the same `manifest.json` contract as third-party plugins; the
only difference is that the shell flags them with `__isFirstParty: true` only difference is that the shell flags them with `__isFirstParty: true`.
so they are always enabled. Services and keep-loaded panels are mounted at First-party non-bar plugins are always enabled; `omarchy.bar` is the default
startup; other panels, overlays, and menus are loaded on demand. bar option and becomes inactive only while a third-party `kind: "bar"` plugin is
selected. Services and keep-loaded panels are mounted at startup; other panels,
overlays, and menus are loaded on demand.
User-installed plugins live alongside these conceptually but on disk under User-installed plugins live alongside these conceptually but on disk under
`~/.config/omarchy/plugins/<plugin-id>/` rather than in this directory. `~/.config/omarchy/plugins/<plugin-id>/` rather than in this directory.
@@ -40,7 +42,7 @@ own plugin directories, each with its own `manifest.json`.
## Bar ## Bar
The status bar. Mounted at startup, lives forever. Layout lives in the The built-in status bar and default full-bar option. Layout lives in the
top-level `bar:` subtree of `~/.config/omarchy/shell.json` (with the shell top-level `bar:` subtree of `~/.config/omarchy/shell.json` (with the shell
providing [`config/omarchy/shell.json`](../../config/omarchy/shell.json) when providing [`config/omarchy/shell.json`](../../config/omarchy/shell.json) when
the user has no file). See [`bar/README.md`](bar/README.md) for the widget catalogue the user has no file). See [`bar/README.md`](bar/README.md) for the widget catalogue
+3
View File
@@ -22,6 +22,9 @@ Item {
// Injected by the host shell. Used for shell-wide actions such as opening // Injected by the host shell. Used for shell-wide actions such as opening
// settings and persisting inline widget state. // settings and persisting inline widget state.
property var shell: null property var shell: null
// Manifest for the active bar option. Present for custom bars and useful for
// diagnostics; the built-in bar does not otherwise need it.
property var manifest: null
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without // Mirrors the on-disk `bar-off` flag so the user can hide the bar without
// killing the entire shell. Wired to BarPanel.visible below; updated by the // killing the entire shell. Wired to BarPanel.visible below; updated by the
// FileView watcher further down. // FileView watcher further down.
+30 -7
View File
@@ -97,29 +97,41 @@ QtObject {
} }
// Enabled = the plugin id is referenced somewhere in shell.json. That can // Enabled = the plugin id is referenced somewhere in shell.json. That can
// be either a layout entry inside `bar.layout.*` (bar widgets) or a top-level // be either the active bar option in `bar.id`, a layout entry inside
// entry in `plugins[]` (panels, overlays, services). // `bar.layout.*` (bar widgets), or a top-level entry in `plugins[]` (panels,
// overlays, services).
// //
// Special cases (implicitly always enabled, no shell.json entry needed): // Special cases (implicitly always enabled, no shell.json entry needed):
// - plugins whose `kinds` contains "bar" are mounted directly by the host. // - the built-in bar option (`omarchy.bar`) is active when `bar.id` is
// - first-party plugins are shell infrastructure (settings, // missing or set to `omarchy.bar`.
// - first-party non-bar plugins are shell infrastructure (settings,
// image-picker, ...). Requiring users to add them to plugins[] just to // image-picker, ...). Requiring users to add them to plugins[] just to
// summon them was a footgun: a stock shell.json with `plugins: []` would // summon them was a footgun: a stock shell.json with `plugins: []` would
// silently make `omarchy launch bar-settings` a no-op. // silently make `omarchy launch bar-settings` a no-op.
function isEnabled(id) { function isEnabled(id) {
var key = String(id) var key = String(id)
var manifest = installedPlugins[key] var manifest = installedPlugins[key]
var config = shellConfigProvider ? shellConfigProvider() : null
if (manifest) { if (manifest) {
if (Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1) return true if (Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1) {
var selectedBar = ""
if (Util.isPlainObject(config) && Util.isPlainObject(config.bar))
selectedBar = Util.canonicalWidgetId(String(config.bar.id || ""))
if (!selectedBar) selectedBar = "omarchy.bar"
return selectedBar === key
}
if (manifest.__isFirstParty) return true if (manifest.__isFirstParty) return true
} }
var config = shellConfigProvider ? shellConfigProvider() : null
return findEntryLocation(config, key).found return findEntryLocation(config, key).found
} }
function findEntryLocation(config, id) { function findEntryLocation(config, id) {
if (!Util.isPlainObject(config)) return { found: false } if (!Util.isPlainObject(config)) return { found: false }
var key = Util.canonicalWidgetId(String(id)) var key = Util.canonicalWidgetId(String(id))
if (Util.isPlainObject(config.bar)) {
var selectedBar = Util.canonicalWidgetId(String(config.bar.id || ""))
if (selectedBar === key) return { found: true, kind: "bar-option" }
}
if (Util.isPlainObject(config.bar) && Util.isPlainObject(config.bar.layout)) { if (Util.isPlainObject(config.bar) && Util.isPlainObject(config.bar.layout)) {
var sections = ["left", "center", "right"] var sections = ["left", "center", "right"]
for (var s = 0; s < sections.length; s++) { for (var s = 0; s < sections.length; s++) {
@@ -148,12 +160,23 @@ QtObject {
return return
} }
var manifest = installedPlugins[key] var manifest = installedPlugins[key]
var isBarOption = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1
var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1 var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
shellConfigMutator(function(config) { shellConfigMutator(function(config) {
// Ensure shape exists. // Ensure shape exists.
if (!Util.isPlainObject(config.bar)) config.bar = { layout: { left: [], center: [], right: [] } } if (!Util.isPlainObject(config.bar)) config.bar = { layout: { left: [], center: [], right: [] } }
if (!Util.isPlainObject(config.bar.layout)) config.bar.layout = { left: [], center: [], right: [] } if (!Util.isPlainObject(config.bar.layout)) config.bar.layout = { left: [], center: [], right: [] }
if (!Array.isArray(config.plugins)) config.plugins = [] if (!Array.isArray(config.plugins)) config.plugins = []
if (isBarOption) {
if (value) {
config.bar.id = key
} else if (Util.canonicalWidgetId(String(config.bar.id || "")) === key) {
delete config.bar.id
}
return
}
var location = findEntryLocation(config, key) var location = findEntryLocation(config, key)
if (value && !location.found) { if (value && !location.found) {
var entry = { id: key } var entry = { id: key }
@@ -166,7 +189,7 @@ QtObject {
} else if (!value && location.found) { } else if (!value && location.found) {
if (location.kind === "bar") { if (location.kind === "bar") {
config.bar.layout[location.section].splice(location.index, 1) config.bar.layout[location.section].splice(location.index, 1)
} else { } else if (location.kind === "plugin") {
config.plugins.splice(location.index, 1) config.plugins.splice(location.index, 1)
} }
} }
+103 -10
View File
@@ -56,6 +56,7 @@ ShellRoot {
property bool suppressUserReload: false property bool suppressUserReload: false
onShellConfigChanged: { onShellConfigChanged: {
if (failedBarId !== "") failedBarId = ""
pluginRegistry.registryRevision++ pluginRegistry.registryRevision++
pluginRegistry.pluginsChanged() pluginRegistry.pluginsChanged()
} }
@@ -105,6 +106,7 @@ ShellRoot {
} }
readonly property var barConfig: shellConfig && Util.isPlainObject(shellConfig.bar) ? shellConfig.bar : builtinShellConfig.bar readonly property var barConfig: shellConfig && Util.isPlainObject(shellConfig.bar) ? shellConfig.bar : builtinShellConfig.bar
onBarConfigChanged: if (bar && "barConfig" in bar) bar.barConfig = shell.barConfig
FileView { FileView {
id: defaultsFile id: defaultsFile
path: shell.defaultsPath path: shell.defaultsPath
@@ -159,15 +161,102 @@ ShellRoot {
} }
// Exposed as a property so child plugins (notifications, future panels) // Exposed as a property so child plugins (notifications, future panels)
// can read barSize/barHidden/position to anchor relative to the bar. // can read barSize/barHidden/position to anchor relative to the active bar.
property alias bar: bar readonly property string defaultBarId: "omarchy.bar"
readonly property string selectedBarId: {
var config = shell.barConfig
if (Util.isPlainObject(config)) {
var configured = Util.canonicalWidgetId(String(config.id || ""))
if (configured) return configured
}
return shell.defaultBarId
}
property string failedBarId: ""
readonly property bool selectedBarAvailable: {
var revision = shell.pluginRegistry.registryRevision
return shell.barOptionAvailable(shell.selectedBarId)
}
readonly property string activeBarId: selectedBarId !== failedBarId && selectedBarAvailable ? selectedBarId : defaultBarId
readonly property var activeBarManifest: {
var revision = shell.pluginRegistry.registryRevision
return shell.barManifestFor(shell.activeBarId)
}
readonly property string activeBarSourceUrl: activeBarId === defaultBarId ? "" : shell.pluginRegistry.entryPointUrl(activeBarManifest, "bar")
property var bar: null
Bar { onSelectedBarIdChanged: if (failedBarId !== "") failedBarId = ""
id: bar
omarchyPath: shell.omarchyPath function barManifestFor(pluginId) {
barWidgetRegistry: shell.barWidgetRegistry var plugins = shell.pluginRegistry ? shell.pluginRegistry.installedPlugins : null
barConfig: shell.barConfig return plugins ? plugins[String(pluginId || "")] || null : null
shell: shell }
function isBarOptionManifest(manifest) {
return manifest
&& Array.isArray(manifest.kinds)
&& manifest.kinds.indexOf("bar") !== -1
&& manifest.entryPoints
&& manifest.entryPoints.bar
}
function barOptionAvailable(pluginId) {
var id = String(pluginId || "")
if (id === "" || id === shell.defaultBarId) return true
var manifest = shell.barManifestFor(id)
return shell.isBarOptionManifest(manifest) && shell.pluginRegistry.entryPointUrl(manifest, "bar") !== ""
}
function isActiveBarOption(pluginId) {
return String(pluginId || "") === shell.activeBarId
}
function configureBar(target, manifest) {
if (!target) return
if ("omarchyPath" in target) target.omarchyPath = shell.omarchyPath
if ("shell" in target) target.shell = shell
if ("manifest" in target) target.manifest = manifest
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.barWidgetRegistry
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistry
if ("barConfig" in target) target.barConfig = shell.barConfig
shell.bar = target
}
Component {
id: defaultBarComponent
Bar {
omarchyPath: shell.omarchyPath
barWidgetRegistry: shell.barWidgetRegistry
barConfig: shell.barConfig
shell: shell
manifest: shell.barManifestFor(shell.defaultBarId)
}
}
Loader {
id: defaultBarLoader
active: shell.activeBarId === shell.defaultBarId
sourceComponent: defaultBarComponent
onLoaded: shell.configureBar(item, shell.barManifestFor(shell.defaultBarId))
onActiveChanged: if (!active && shell.activeBarId !== shell.defaultBarId) shell.bar = null
}
Loader {
id: pluginBarLoader
active: shell.activeBarId !== shell.defaultBarId && shell.activeBarSourceUrl !== ""
source: shell.activeBarId !== shell.defaultBarId ? shell.activeBarSourceUrl : ""
asynchronous: true
onLoaded: shell.configureBar(item, shell.activeBarManifest)
onActiveChanged: if (!active && shell.activeBarId === shell.defaultBarId) shell.bar = null
onStatusChanged: {
if (status === Loader.Error) {
var detail = errorString && errorString() ? errorString() : ""
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId + ":", detail)
shell.failedBarId = shell.activeBarId
}
}
} }
// ------------------------------------------------------------- services // ------------------------------------------------------------- services
@@ -639,11 +728,15 @@ ShellRoot {
var out = [] var out = []
var plugins = shell.pluginRegistry.installedPlugins var plugins = shell.pluginRegistry.installedPlugins
for (var id in plugins) { for (var id in plugins) {
var kinds = plugins[id].kinds || []
var isBarOption = Array.isArray(kinds) && kinds.indexOf("bar") !== -1
var active = isBarOption && shell.isActiveBarOption(id)
out.push({ out.push({
id: id, id: id,
name: plugins[id].name, name: plugins[id].name,
kinds: plugins[id].kinds, kinds: kinds,
enabled: shell.pluginRegistry.isEnabled(id), enabled: isBarOption ? active : shell.pluginRegistry.isEnabled(id),
active: active,
firstParty: !!plugins[id].__isFirstParty firstParty: !!plugins[id].__isFirstParty
}) })
} }
+29
View File
@@ -157,6 +157,35 @@ cat >"$TMPDIR/home/.config/omarchy/shell.json" <<'JSON'
} }
JSON JSON
mkdir -p "$TMPDIR/home/.config/omarchy/plugins/local.demo-bar"
cat >"$TMPDIR/home/.config/omarchy/plugins/local.demo-bar/manifest.json" <<'JSON'
{
"schemaVersion": 1,
"id": "local.demo-bar",
"name": "Demo bar",
"version": "1.0.0",
"author": "Test",
"description": "Replacement bar for config tests",
"kinds": ["bar"],
"entryPoints": { "bar": "Bar.qml" }
}
JSON
touch "$TMPDIR/home/.config/omarchy/plugins/local.demo-bar/Bar.qml"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar options --json | jq -e '
any(.[]; .id == "omarchy.bar" and .active == true) and
any(.[]; .id == "local.demo-bar" and .active == false)
' >/dev/null
pass "shell config lists bar options"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar use local.demo-bar
jq -e '.bar.id == "local.demo-bar"' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config selects a bar option"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar reset
jq -e '.bar.id == null' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config resets to built-in bar option"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar add omarchy.tailscale HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar add omarchy.tailscale
jq -e ' jq -e '
def ids: map(.id // .); def ids: map(.id // .);
@@ -78,10 +78,11 @@ ShellRoot {
function runChecks() { function runChecks() {
var scan = "" var scan = ""
scan += block("firstparty", "/first/widgets/clock", manifest("omarchy.first-widget", ["bar-widget"], { barWidget: "Widget.qml" })) scan += block("firstparty", "/first/widgets/clock", manifest("omarchy.first-widget", ["bar-widget"], { barWidget: "Widget.qml" }))
scan += block("firstparty", "/first/bar", manifest("omarchy.first-bar", ["bar"], { bar: "Bar.qml" })) scan += block("firstparty", "/first/bar", manifest("omarchy.bar", ["bar"], { bar: "Bar.qml" }))
scan += block("firstparty", "/first/panels/grouped", manifest("omarchy.grouped-panel", ["panel"], { panel: "Panel.qml" })) scan += block("firstparty", "/first/panels/grouped", manifest("omarchy.grouped-panel", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/panel", manifest("third.panel", ["panel"], { panel: "Panel.qml" })) scan += block("thirdparty", "/third/panel", manifest("third.panel", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/widget", manifest("third.widget", ["bar-widget"], { barWidget: "Widget.qml" })) scan += block("thirdparty", "/third/widget", manifest("third.widget", ["bar-widget"], { barWidget: "Widget.qml" }))
scan += block("thirdparty", "/third/bar", manifest("third.bar", ["bar"], { bar: "Bar.qml" }))
scan += block("thirdparty", "/third/shadow", manifest("omarchy.first-widget", ["panel"], { panel: "Panel.qml" })) scan += block("thirdparty", "/third/shadow", manifest("omarchy.first-widget", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/reserved", manifest("omarchy.reserved", ["panel"], { panel: "Panel.qml" })) scan += block("thirdparty", "/third/reserved", manifest("omarchy.reserved", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/unsafe", manifest("third.unsafe", ["panel"], { panel: "../Panel.qml" })) scan += block("thirdparty", "/third/unsafe", manifest("third.unsafe", ["panel"], { panel: "../Panel.qml" }))
@@ -92,9 +93,10 @@ ShellRoot {
registry.parseScanOutput(scan) registry.parseScanOutput(scan)
root.assertDeepEqual(pluginIds(), [ root.assertDeepEqual(pluginIds(), [
"omarchy.first-bar", "omarchy.bar",
"omarchy.first-widget", "omarchy.first-widget",
"omarchy.grouped-panel", "omarchy.grouped-panel",
"third.bar",
"third.panel", "third.panel",
"third.widget" "third.widget"
], "registry merges valid first-party and third-party manifests") ], "registry merges valid first-party and third-party manifests")
@@ -111,9 +113,18 @@ ShellRoot {
root.assertTrue(!has("third.schema"), "unsupported schema versions are rejected") root.assertTrue(!has("third.schema"), "unsupported schema versions are rejected")
root.assertTrue(registry.isEnabled("omarchy.first-widget"), "first-party plugins are implicitly enabled") root.assertTrue(registry.isEnabled("omarchy.first-widget"), "first-party plugins are implicitly enabled")
root.assertTrue(registry.isEnabled("omarchy.first-bar"), "bar plugins are implicitly enabled") root.assertTrue(registry.isEnabled("omarchy.bar"), "built-in bar option is active by default")
root.assertTrue(!registry.isEnabled("third.bar"), "third-party bar options start inactive")
root.assertTrue(!registry.isEnabled("third.panel"), "third-party plugins start disabled") root.assertTrue(!registry.isEnabled("third.panel"), "third-party plugins start disabled")
registry.setEnabled("third.bar", true)
root.assertEqual(root.config.bar.id, "third.bar", "enabling third-party bar options writes bar id")
root.assertTrue(registry.isEnabled("third.bar"), "selected third-party bar options are enabled")
root.assertTrue(!registry.isEnabled("omarchy.bar"), "selecting third-party bar options deactivates built-in bar")
registry.setEnabled("third.bar", false)
root.assertTrue(root.config.bar.id === undefined, "disabling active bar options resets to built-in")
root.assertTrue(registry.isEnabled("omarchy.bar"), "built-in bar option returns after reset")
registry.setEnabled("third.panel", true) registry.setEnabled("third.panel", true)
root.assertDeepEqual(root.config.plugins, [{ id: "third.panel" }], "enabling third-party panels writes plugins array") root.assertDeepEqual(root.config.plugins, [{ id: "third.panel" }], "enabling third-party panels writes plugins array")
root.assertTrue(registry.isEnabled("third.panel"), "enabled third-party panels are found") root.assertTrue(registry.isEnabled("third.panel"), "enabled third-party panels are found")