Simplify bar plugin management (#6435)

This commit is contained in:
David Heinemeier Hansson
2026-07-29 22:39:55 -04:00
committed by GitHub
parent 3304b79c92
commit 57ea0b4cd0
26 changed files with 886 additions and 835 deletions
+203 -17
View File
@@ -1,9 +1,9 @@
#!/bin/bash
# omarchy:summary=Set the active bar option, position, and transparency
# omarchy:summary=Configure the bar and its widget layout
# omarchy:group=bar
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle>
# omarchy:examples=omarchy bar use local.neon-bar | omarchy bar reset | omarchy bar defaults | omarchy bar position top | omarchy bar transparent true
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle> | move <id> [placement] | set <id> <key> <value> [--json] [placement]
# omarchy:examples=omarchy bar use local.neon-bar | omarchy bar move omarchy.clock --section center --index 0 | omarchy bar set omarchy.clock format HH:mm
set -euo pipefail
@@ -18,15 +18,26 @@ Usage: omarchy bar <command> [args...]
defaults Restore the default bar and service widgets
position <top|bottom|left|right> Bar position
transparent <true|false|toggle> Bar transparency
move <id> [placement] Move a widget within or between sections
set <id> <key> <value> [--json] [placement]
Set a per-widget option
Bar widgets are added, moved, removed, and configured with 'omarchy bar plugin'.
Placement:
--section <left|center|right> Target section
--index <n> Target index
--before <id> Insert before a widget
--after <id> Insert after a widget
--from-section <section> Source section
--from-index <n> Source index
Enable and disable widgets with 'omarchy plugin enable' and
'omarchy plugin disable'.
Examples:
omarchy bar use local.neon-bar
omarchy bar reset
omarchy bar defaults
omarchy bar position top
omarchy bar transparent true
omarchy bar move omarchy.media left
omarchy bar move omarchy.clock --section center --index 0
omarchy bar set omarchy.clock format HH:mm
USAGE
}
@@ -38,6 +49,87 @@ bar_option_exists() {
' >/dev/null
}
validate_section() {
[[ $1 =~ ^(left|center|right)$ ]] || fail "section must be left, center, or right"
}
validate_index() {
[[ $1 =~ ^[0-9]+$ ]] || fail "index must be a non-negative integer"
}
# --------------------------------------------------------------------- placement
PLACEMENT_SECTION=""
PLACEMENT_INDEX=""
PLACEMENT_BEFORE=""
PLACEMENT_AFTER=""
PLACEMENT_FROM_SECTION=""
PLACEMENT_FROM_INDEX=""
parse_placement() {
while (( $# > 0 )); do
case "$1" in
--section)
PLACEMENT_SECTION="${2:-}"
validate_section "$PLACEMENT_SECTION"
shift 2
;;
--index)
PLACEMENT_INDEX="${2:-}"
validate_index "$PLACEMENT_INDEX"
shift 2
;;
--before)
PLACEMENT_BEFORE="${2:-}"
[[ -n $PLACEMENT_BEFORE ]] || fail "--before requires a widget id"
shift 2
;;
--after)
PLACEMENT_AFTER="${2:-}"
[[ -n $PLACEMENT_AFTER ]] || fail "--after requires a widget id"
shift 2
;;
--from-section)
PLACEMENT_FROM_SECTION="${2:-}"
validate_section "$PLACEMENT_FROM_SECTION"
shift 2
;;
--from-index)
PLACEMENT_FROM_INDEX="${2:-}"
validate_index "$PLACEMENT_FROM_INDEX"
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
fail "unknown option: $1"
;;
esac
done
[[ -z $PLACEMENT_BEFORE || -z $PLACEMENT_AFTER ]] || fail "use only one of --before or --after"
}
placement_json() {
jq -cn \
--arg section "$PLACEMENT_SECTION" \
--arg index "$PLACEMENT_INDEX" \
--arg before "$PLACEMENT_BEFORE" \
--arg after "$PLACEMENT_AFTER" \
--arg fromSection "$PLACEMENT_FROM_SECTION" \
--arg fromIndex "$PLACEMENT_FROM_INDEX" '
{}
+ (if $section == "" then {} else {section: $section} end)
+ (if $index == "" then {} else {index: ($index | tonumber)} end)
+ (if $before == "" then {} else {before: $before} end)
+ (if $after == "" then {} else {after: $after} end)
+ (if $fromSection == "" then {} else {fromSection: $fromSection} end)
+ (if $fromIndex == "" then {} else {fromIndex: ($fromIndex | tonumber)} end)
'
}
# -------------------------------------------------------------------- commands
cmd_use() {
@@ -59,15 +151,48 @@ cmd_use() {
cmd_defaults() {
(( $# == 0 )) || fail "defaults does not take arguments"
# Restore the whole default bar, then re-add widgets for services the user
# actually has installed (they aren't in the shipped default layout).
commit "$NORMALIZE | .bar = \$defaults[0].bar" --slurpfile defaults "$DEFAULTS_FILE"
local service
local catalog
local optional_widgets="[]"
local service widget
catalog=$(omarchy-plugin-catalog)
for service in dropbox tailscale; do
if "omarchy-installed-service-$service"; then
omarchy-bar-plugin add "omarchy.$service"
widget=$(jq -c --arg id "omarchy.$service" '
map(select(.id == $id))[0] as $plugin
| {
id: $id,
section: (
$plugin.barWidget.defaultSection // "center"
| if IN("left", "center", "right") then . else "center" end
)
}
' <<<"$catalog")
optional_widgets=$(jq -c --argjson widget "$widget" '. + [$widget]' <<<"$optional_widgets")
fi
done
# This remains one file mutation so it also works during the headless
# Quattro upgrade and cannot race the shell's in-memory config.
commit "$NORMALIZE
| .bar = \$defaults[0].bar
| def entry_id: if type == \"object\" then (.id // \"\" | tostring) else tostring end;
def anchor_for(\$section): { left: \"omarchy.workspaces\", center: \"omarchy.weather\", right: \"omarchy.tray\" }[\$section];
reduce \$widgets[] as \$widget (.;
.bar.layout.left = (.bar.layout.left | map(select(entry_id != \$widget.id)))
| .bar.layout.center = (.bar.layout.center | map(select(entry_id != \$widget.id)))
| .bar.layout.right = (.bar.layout.right | map(select(entry_id != \$widget.id)))
| (.bar.layout[\$widget.section] | map(entry_id) | index(anchor_for(\$widget.section))) as \$anchor
| (\$anchor | if . == null then (.bar.layout[\$widget.section] | length) else . + 1 end) as \$index
| .bar.layout[\$widget.section] = (
.bar.layout[\$widget.section][0:\$index]
+ [{id: \$widget.id}]
+ .bar.layout[\$widget.section][\$index:]
)
)
" \
--slurpfile defaults "$DEFAULTS_FILE" \
--argjson widgets "$optional_widgets"
echo "Restored the default Omarchy bar"
}
@@ -94,6 +219,67 @@ cmd_transparent() {
fi
}
cmd_move() {
local id="${1:-}"
[[ -n $id ]] || fail "move requires a widget id"
shift
local positional_section=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
positional_section="$1"
validate_section "$positional_section"
shift
fi
parse_placement "$@"
[[ -z $positional_section || -z $PLACEMENT_SECTION ]] ||
fail "specify a section positionally or with --section, not both"
[[ -z $positional_section || -z $PLACEMENT_INDEX ]] ||
fail "specify a section positionally or use --index, not both"
[[ -z $positional_section || -z $PLACEMENT_BEFORE ]] ||
fail "specify a section positionally or use --before, not both"
[[ -z $positional_section || -z $PLACEMENT_AFTER ]] ||
fail "specify a section positionally or use --after, not both"
[[ -z $positional_section ]] || PLACEMENT_SECTION="$positional_section"
local result
result=$(omarchy-shell shell moveBarWidget "$id" "$(placement_json)")
[[ $result == "ok" ]] || fail "$result"
echo "Moved $id"
}
cmd_set() {
local id="${1:-}"
local key="${2:-}"
local value="${3:-}"
[[ -n $id ]] || fail "set requires a widget id"
[[ -n $key ]] || fail "set requires a setting key"
(( $# >= 3 )) || fail "set requires a value"
shift 3
local value_is_json="false"
if (( $# > 0 )) && [[ $1 == "--json" ]]; then
value_is_json="true"
shift
fi
parse_placement "$@"
[[ -z $PLACEMENT_BEFORE && -z $PLACEMENT_AFTER ]] ||
fail "set does not accept --before or --after"
local value_json
if [[ $value_is_json == "true" ]]; then
value_json=$(jq -cn --argjson value "$value" '$value') ||
fail "invalid JSON value: $value"
else
value_json=$(jq -cn --arg value "$value" '$value')
fi
local result
result=$(omarchy-shell shell setBarWidget "$id" "$key" "$value_json" "$(placement_json)")
[[ $result == "ok" ]] || fail "$result"
echo "Set $key on $id"
}
# --------------------------------------------------------------------- dispatch
command="${1:-}"
@@ -116,11 +302,11 @@ case "$command" in
transparent)
cmd_transparent "$@"
;;
plugin)
exec omarchy-bar-plugin "$@"
move)
cmd_move "$@"
;;
add | move | remove | rm | drop | set | replace)
fail "bar widgets are managed with: omarchy bar plugin $command ..."
set)
cmd_set "$@"
;;
-h | --help | help | "")
usage
-503
View File
@@ -1,503 +0,0 @@
#!/bin/bash
# omarchy:summary=Add, move, remove, and configure bar plugin widgets in the layout
# omarchy:group=bar
# omarchy:args=add <id> [placement] | move <id> [placement] | remove <id> [placement] | set <id> <key> <value> [--json] [placement] | replace <old-id> <new-id>
# omarchy:examples=omarchy bar plugin add omarchy.tailscale | omarchy bar plugin add omarchy.clock center | omarchy bar plugin move omarchy.media left | omarchy bar plugin move omarchy.clock --section center --index 0 | omarchy bar plugin remove omarchy.tailscale | omarchy bar plugin set omarchy.clock format HH:mm
set -euo pipefail
source omarchy-shell-config
usage() {
cat <<USAGE
Usage: omarchy bar plugin <command> [args...]
add <id> [placement] Add a bar widget
move <id> [placement] [--from-section S --from-index N]
Move a widget within/between sections
remove <id> [placement] (alias: rm, drop)
Remove a widget
set <id> <key> <value> [--json] [placement]
Set a per-widget option
replace <old-id> <new-id> Replace the first instance of a widget id
Placement (for add/move/remove/set):
--section <left|center|right> Target section
--index <n> Insert/remove at index in the target section
--before <id> Insert before the first matching widget
--after <id> Insert after the first matching widget
--from-section <section> Source section (move/remove/set)
--from-index <n> Source index (move/remove/set)
--duplicate add: allow a second instance of the widget
--all remove: remove every matching instance
Without placement flags, 'add' inserts after the section anchor (workspaces on
left, weather on center, tray on right) and dedupes across sections; 'move'
preserves the source section; 'remove' drops the first match (or every match
with --all).
Widget ids are listed by 'omarchy plugin list'.
Examples:
omarchy bar plugin add omarchy.tailscale
omarchy bar plugin add omarchy.clock center
omarchy bar plugin move omarchy.media left
omarchy bar plugin move omarchy.clock --section center --index 0
omarchy bar plugin remove omarchy.tailscale
omarchy bar plugin remove omarchy.tailscale --all
omarchy bar plugin set omarchy.clock format HH:mm
omarchy bar plugin set omarchy.indicators items '["Dnd","NightLight"]' --json
USAGE
}
# jq defs used by the mutation pipelines. All `def`s come first so the pipeline
# that follows them stays valid jq; NORMALIZE adds its own shape helpers.
JQ_DEFS='
def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;
def anchor_for($section): { left: "omarchy.workspaces", center: "omarchy.weather", right: "omarchy.tray" }[$section];
def find_all($id; $only):
[ ["left","center","right"][] as $section
| ((.bar.layout[$section] // []) | to_entries[])
| select((($only == "" or $section == $only)) and ((.value | entry_id) == $id))
| {section: $section, index: .key, entry: .value} ];
def entry_at($section; $index):
if $index < 0 or $index >= (.bar.layout[$section] | length) then
error("no widget at " + $section + "[" + ($index | tostring) + "]")
else .bar.layout[$section][$index] end;
def resolve_target($defaultSection):
. as $root
| (if $before != "" or $after != "" then
($root | find_all($before + $after; $targetSection) | .[0]
// error("could not find target widget " + ($before + $after) + ($targetSection | if . == "" then "" else " in " + . end)))
| .section as $section | (.index + (if $after == "" then 0 else 1 end)) as $index
| {section: $section, index: $index, anchor: false}
elif $targetSection != "" or $targetIndex != "" then
{ section: ($targetSection | if . == "" then $defaultSection else . end),
index: (if $targetIndex == "" then null else ($targetIndex | tonumber) end),
anchor: false }
else
{ section: $defaultSection, index: null, anchor: true }
end)
| .section as $section
| .index as $rawIndex
| (if $rawIndex == null then
if .anchor then
($root.bar.layout[$section] | map(entry_id) | index(anchor_for($section))) as $anchorIndex
| if $anchorIndex == null then ($root.bar.layout[$section] | length) else ($anchorIndex + 1) end
else ($root.bar.layout[$section] | length) end
elif $rawIndex > ($root.bar.layout[$section] | length) then ($root.bar.layout[$section] | length)
else $rawIndex end) as $index
| {section: $section, index: $index};
def resolve_source:
if $fromIndex != "" then
if $fromSection == "" then error("--from-index requires --from-section")
else
($fromIndex | tonumber) as $index
| entry_at($fromSection; $index) as $entry
| if $widgetId != "" and ($entry | entry_id) != $widgetId then
error("widget at " + $fromSection + "[" + ($index | tostring) + "] is not " + $widgetId)
else {section: $fromSection, index: $index, entry: $entry} end
end
else
(find_all($widgetId; $fromSection)
| if length == 0 then
error("could not find widget " + $widgetId + ($fromSection | if . == "" then "" else " in " + . end))
else .[0] end)
end;
'
validate_section() {
[[ $1 =~ ^(left|center|right)$ ]] || fail "section must be left, center, or right"
}
validate_index() {
[[ $1 =~ ^[0-9]+$ ]] || fail "index must be a non-negative integer"
}
widget_is_known() {
omarchy-plugin-catalog | jq -e --arg id "$1" '
any(.[]; (.kinds | index("bar-widget")) and .id == $id)
' >/dev/null
}
widget_default_section() {
omarchy-plugin-catalog | jq -r --arg id "$1" '
(map(select(.id == $id))[0].barWidget.defaultSection // "center") as $section
| if ["left", "center", "right"] | index($section) then $section else "center" end
'
}
# --------------------------------------------------------------------- placement
# Parse placement flags shared by add/move/remove/set. Sets the globals below.
PLACEMENT_SECTION=""
PLACEMENT_INDEX=""
PLACEMENT_BEFORE=""
PLACEMENT_AFTER=""
PLACEMENT_FROM_SECTION=""
PLACEMENT_FROM_INDEX=""
PLACEMENT_DUPLICATE="false"
PLACEMENT_ALL="false"
parse_placement() {
while (( $# > 0 )); do
case "$1" in
--section)
PLACEMENT_SECTION="${2:-}"
validate_section "$PLACEMENT_SECTION"
shift 2
;;
--index)
PLACEMENT_INDEX="${2:-}"
validate_index "$PLACEMENT_INDEX"
shift 2
;;
--before)
PLACEMENT_BEFORE="${2:-}"
[[ -n $PLACEMENT_BEFORE ]] || fail "--before requires a widget id"
shift 2
;;
--after)
PLACEMENT_AFTER="${2:-}"
[[ -n $PLACEMENT_AFTER ]] || fail "--after requires a widget id"
shift 2
;;
--from-section)
PLACEMENT_FROM_SECTION="${2:-}"
validate_section "$PLACEMENT_FROM_SECTION"
shift 2
;;
--from-index)
PLACEMENT_FROM_INDEX="${2:-}"
validate_index "$PLACEMENT_FROM_INDEX"
shift 2
;;
--duplicate)
PLACEMENT_DUPLICATE="true"
shift
;;
--all)
PLACEMENT_ALL="true"
shift
;;
-h | --help)
usage
exit 0
;;
*)
fail "unknown option: $1"
;;
esac
done
[[ -z $PLACEMENT_BEFORE || -z $PLACEMENT_AFTER ]] || fail "use only one of --before or --after"
}
# --------------------------------------------------------------------- commands
cmd_add() {
local id="${1:-}"
[[ -n $id ]] || fail "add requires a widget id"
shift
local positional_section=""
while (( $# > 0 )); do
if [[ $1 == --* ]]; then
break
fi
[[ -z $positional_section ]] || fail "unexpected argument: $1"
positional_section="$1"
shift
done
parse_placement "$@"
[[ -z $positional_section || -z $PLACEMENT_SECTION ]] || fail "specify a section positionally or with --section, not both"
[[ -z $positional_section || -z $PLACEMENT_INDEX ]] || fail "specify a section positionally or use --index, not both"
[[ -z $positional_section || -z $PLACEMENT_BEFORE ]] || fail "specify a section positionally or use --before, not both"
[[ -z $positional_section || -z $PLACEMENT_AFTER ]] || fail "specify a section positionally or use --after, not both"
[[ -z $positional_section || ( $PLACEMENT_DUPLICATE == false ) ]] || fail "--duplicate needs explicit placement flags"
if [[ -n $positional_section ]]; then
validate_section "$positional_section"
PLACEMENT_SECTION="$positional_section"
fi
widget_is_known "$id" || fail "$id is not a known bar widget; run 'omarchy plugin list' to see valid ids"
local default_section="${PLACEMENT_SECTION:-$(widget_default_section "$id")}"
local explicit="false"
if [[ -n $PLACEMENT_INDEX || -n $PLACEMENT_BEFORE || -n $PLACEMENT_AFTER || $PLACEMENT_DUPLICATE == true ]]; then
explicit="true"
fi
if [[ $explicit == "false" ]]; then
# Anchor-based dedupe insert: remove any existing instance across sections,
# then insert after the section anchor (workspaces/weather/tray) — or append
# when the anchor is absent.
commit "$JQ_DEFS $NORMALIZE
| .bar.layout.left = (.bar.layout.left | map(select(entry_id != \$id)))
| .bar.layout.center = (.bar.layout.center | map(select(entry_id != \$id)))
| .bar.layout.right = (.bar.layout.right | map(select(entry_id != \$id)))
| (.bar.layout[\$section] | map(entry_id) | index(anchor_for(\$section))) as \$anchorIndex
| .bar.layout[\$section] = (
if \$anchorIndex == null then .bar.layout[\$section] + [{id: \$id}]
else .bar.layout[\$section][0:\$anchorIndex+1] + [{id: \$id}] + .bar.layout[\$section][\$anchorIndex+1:] end)
" \
--arg id "$id" \
--arg section "$default_section"
else
local prog
prog=$(cat <<JQ
$JQ_DEFS $NORMALIZE
| (.bar.layout.left + .bar.layout.center + .bar.layout.right | map(entry_id) | index(\$id)) as \$existing
| if \$existing != null and \$allowDuplicate != "true" then
error(\$id + " is already in the bar; use 'omarchy bar plugin move' or pass --duplicate")
else . end
| resolve_target(\$defaultSection) as \$target
| .bar.layout[\$target.section] = (.bar.layout[\$target.section][0:\$target.index] + [{id: \$id}] + .bar.layout[\$target.section][\$target.index:])
JQ
)
commit "$prog" \
--arg id "$id" \
--arg defaultSection "$default_section" \
--arg targetSection "$PLACEMENT_SECTION" \
--arg targetIndex "$PLACEMENT_INDEX" \
--arg before "$PLACEMENT_BEFORE" \
--arg after "$PLACEMENT_AFTER" \
--arg allowDuplicate "$PLACEMENT_DUPLICATE"
fi
echo "Added $id to the bar"
}
cmd_move() {
local id="${1:-}"
[[ -n $id ]] || fail "move requires a widget id"
shift
local positional_section=""
while (( $# > 0 )); do
if [[ $1 == --* ]]; then
break
fi
[[ -z $positional_section ]] || fail "unexpected argument: $1"
positional_section="$1"
shift
done
parse_placement "$@"
[[ -z $positional_section || -z $PLACEMENT_SECTION ]] || fail "specify a section positionally or with --section, not both"
[[ -z $positional_section || -z $PLACEMENT_INDEX ]] || fail "specify a section positionally or use --index, not both"
[[ -z $positional_section || -z $PLACEMENT_BEFORE ]] || fail "specify a section positionally or use --before, not both"
[[ -z $positional_section || -z $PLACEMENT_AFTER ]] || fail "specify a section positionally or use --after, not both"
if [[ -n $positional_section ]]; then
validate_section "$positional_section"
PLACEMENT_SECTION="$positional_section"
fi
local default_section="${PLACEMENT_SECTION:-}"
# A bare section names the section, not the slot, so let it fall through to
# the same anchor placement 'add' uses. Passing it as an explicit target
# instead drops the widget on the far end of the row.
local target_section="$PLACEMENT_SECTION"
if [[ -z $PLACEMENT_INDEX && -z $PLACEMENT_BEFORE && -z $PLACEMENT_AFTER ]]; then
target_section=""
fi
local prog
prog=$(cat <<JQ
$JQ_DEFS $NORMALIZE
| resolve_source as \$source
| .bar.layout[\$source.section] = (.bar.layout[\$source.section][0:\$source.index] + .bar.layout[\$source.section][\$source.index+1:])
| (\$defaultSection | if . == "" then \$source.section else . end) as \$fallback
| resolve_target(\$fallback) as \$target
| (\$source.entry | if type == "object" then .id = \$id else . end) as \$entry
| .bar.layout[\$target.section] = (.bar.layout[\$target.section][0:\$target.index] + [\$entry] + .bar.layout[\$target.section][\$target.index:])
JQ
)
commit "$prog" \
--arg id "$id" \
--arg defaultSection "$default_section" \
--arg targetSection "$target_section" \
--arg targetIndex "$PLACEMENT_INDEX" \
--arg before "$PLACEMENT_BEFORE" \
--arg after "$PLACEMENT_AFTER" \
--arg fromSection "$PLACEMENT_FROM_SECTION" \
--arg fromIndex "$PLACEMENT_FROM_INDEX" \
--arg widgetId "$id"
echo "Moved $id"
}
cmd_remove() {
local id=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
id="$1"
shift
fi
parse_placement "$@"
local prog
prog=$(cat <<JQ
$JQ_DEFS $NORMALIZE
| if \$fromIndex != "" then
resolve_source as \$source
| .bar.layout[\$source.section] = (.bar.layout[\$source.section][0:\$source.index] + .bar.layout[\$source.section][\$source.index+1:])
elif \$targetIndex != "" then
if \$targetSection == "" then error("--index requires --section for remove") else . end
| (\$targetIndex | tonumber) as \$index
| .bar.layout[\$targetSection][\$index] as \$entry
| if \$widgetId != "" and (\$entry | entry_id) != \$widgetId then
error("widget at " + \$targetSection + "[" + (\$index | tostring) + "] is not " + \$widgetId)
else . end
| .bar.layout[\$targetSection] = (.bar.layout[\$targetSection][0:\$index] + .bar.layout[\$targetSection][\$index+1:])
else
find_all(\$widgetId; \$targetSection) as \$matches
| if (\$matches | length) == 0 then
error("could not find widget " + \$widgetId + (\$targetSection | if . == "" then "" else " in " + . end))
elif \$removeAll != "true" then
(\$matches | sort_by(.section) | reverse | .[0]) as \$first
| .bar.layout[\$first.section] = (.bar.layout[\$first.section][0:\$first.index] + .bar.layout[\$first.section][\$first.index+1:])
else
reduce (\$matches | sort_by(.section) | reverse | .[]) as \$m (.;
.bar.layout[\$m.section] = (.bar.layout[\$m.section][0:\$m.index] + .bar.layout[\$m.section][\$m.index+1:]))
end
end
JQ
)
commit "$prog" \
--arg widgetId "$id" \
--arg targetSection "$PLACEMENT_SECTION" \
--arg targetIndex "$PLACEMENT_INDEX" \
--arg fromSection "$PLACEMENT_FROM_SECTION" \
--arg fromIndex "$PLACEMENT_FROM_INDEX" \
--arg removeAll "$PLACEMENT_ALL"
if [[ -n $id ]]; then
echo "Removed $id from the bar"
else
echo "Removed bar widget"
fi
}
cmd_set() {
local id="${1:-}"
local key="${2:-}"
local value="${3:-}"
[[ -n $id ]] || fail "set requires a widget id"
[[ -n $key ]] || fail "set requires a setting key"
(( $# >= 3 )) || fail "set requires a value"
shift 3
local value_is_json="false"
while (( $# > 0 )); do
case "$1" in
--json) value_is_json="true" ;;
-h | --help) usage; exit 0 ;;
*) break ;;
esac
shift
done
parse_placement "$@"
if [[ $value_is_json == "true" ]]; then
jq -n --argjson value "$value" empty >/dev/null 2>&1 || fail "invalid JSON value: $value"
fi
local value_arg
if [[ $value_is_json == "true" ]]; then
value_arg=(--argjson value "$value")
else
value_arg=(--arg value "$value")
fi
local prog
prog=$(cat <<JQ
$JQ_DEFS $NORMALIZE
| if \$fromIndex != "" then
resolve_source as \$source
| .bar.layout[\$source.section][\$source.index] = (.bar.layout[\$source.section][\$source.index] | if type == "object" then .[\$key] = \$value else error("widget entry must be an object") end)
elif \$targetIndex != "" then
if \$targetSection == "" then error("--index requires --section for set") else . end
| (\$targetIndex | tonumber) as \$index
| .bar.layout[\$targetSection][\$index] as \$entry
| if (\$entry | entry_id) != \$widgetId then
error("widget at " + \$targetSection + "[" + (\$index | tostring) + "] is not " + \$widgetId)
elif \$entry | type != "object" then
error("widget entry must be an object")
else . end
| .bar.layout[\$targetSection][\$index][\$key] = \$value
else
find_all(\$widgetId; \$targetSection) as \$matches
| if (\$matches | length) == 0 then
error("could not find widget " + \$widgetId + (\$targetSection | if . == "" then "" else " in " + . end))
else
(\$matches | sort_by(.section) | .[0]) as \$first
| if \$first.entry | type != "object" then error("widget entry must be an object") else . end
| .bar.layout[\$first.section][\$first.index][\$key] = \$value
end
end
JQ
)
commit "$prog" \
--arg widgetId "$id" \
--arg key "$key" \
"${value_arg[@]}" \
--arg targetSection "$PLACEMENT_SECTION" \
--arg targetIndex "$PLACEMENT_INDEX" \
--arg fromSection "$PLACEMENT_FROM_SECTION" \
--arg fromIndex "$PLACEMENT_FROM_INDEX"
echo "Set $key on $id"
}
cmd_replace() {
local old="${1:-}"
local new="${2:-}"
[[ -n $old ]] || fail "replace requires the source widget id"
[[ -n $new ]] || fail "replace requires the replacement widget id"
(( $# == 2 )) || fail "replace takes two widget ids"
local prog
prog=$(cat <<JQ
$JQ_DEFS $NORMALIZE
| resolve_source as \$source
| .bar.layout[\$source.section][\$source.index] = (
\$source.entry | if type == "object" then .id = \$new else { id: \$new } end
)
JQ
)
# resolve_source finds by widget id when no placement is given; replace
# takes no placement flags, so pass empty placement explicitly.
commit "$prog" \
--arg widgetId "$old" \
--arg new "$new" \
--arg fromSection "" \
--arg fromIndex ""
echo "Replaced $old with $new"
}
# --------------------------------------------------------------------- dispatch
command="${1:-}"
(( $# > 0 )) && shift || true
case "$command" in
add)
cmd_add "$@"
;;
move)
cmd_move "$@"
;;
remove | rm | drop)
cmd_remove "$@"
;;
set)
cmd_set "$@"
;;
replace)
cmd_replace "$@"
;;
-h | --help | help | "")
usage
;;
*)
fail "unknown command: $command"
;;
esac
+1 -1
View File
@@ -6,7 +6,7 @@ echo "Installing all dependencies..."
omarchy-pkg-add dropbox dropbox-cli libappindicator-gtk3 python-gpgme nautilus-dropbox
echo "Adding Dropbox to the bar..."
omarchy-bar-plugin add omarchy.dropbox
omarchy-plugin-enable omarchy.dropbox
echo "Starting Dropbox..."
uwsm-app -- dropbox-cli start &>/dev/null &
+1 -1
View File
@@ -17,6 +17,6 @@ echo -e "\nReceiving Taildrop files in $HOME/Downloads..."
systemctl --user enable --now omarchy-tailscale-receive.service
echo -e "\nAdding Tailscale to the bar..."
omarchy-bar-plugin add omarchy.tailscale
omarchy-plugin-enable omarchy.tailscale
omarchy-webapp-install "Tailscale" "https://login.tailscale.com/admin/machines" https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/tailscale-light.png
+12 -14
View File
@@ -33,7 +33,9 @@ confirm() {
fi
}
place_bar_widget() {
ENABLE_PLACEMENT=()
select_bar_widget_placement() {
local id="$1"
local section
local default_section
@@ -47,8 +49,7 @@ place_bar_widget() {
section=$(printf '%s\n' left center right |
gum choose --header="Place $id in which bar section?" --selected "$default_section") || return 0
[[ -n $section ]] || return 0
omarchy-bar-plugin move "$id" --section "$section" >/dev/null &&
echo "Placed $id in the $section section"
ENABLE_PLACEMENT=(--section "$section")
}
plugin_id_manifest() {
@@ -151,20 +152,17 @@ if [[ -z $enable_after ]]; then
fi
if [[ $enable_after == true ]]; then
select_bar_widget_placement "$id"
discovered=0
for (( attempt = 0; attempt < 40; attempt++ )); do
result=$(omarchy-shell shell setPluginEnabled "$id" true)
[[ $result == "ok" ]] && break
if omarchy-plugin-list --json | jq -e --arg id "$id" 'any(.[]; .id == $id)' >/dev/null; then
discovered=1
break
fi
sleep 0.05
done
[[ $result == "ok" ]] || fail "plugin '$id' is not known"
if omarchy-plugin-catalog | jq -e --arg id "$id" '
any(.[]; .id == $id and (.kinds | index("bar")))
' >/dev/null; then
echo "Now using $id as the bar"
else
echo "Enabled $id"
fi
place_bar_widget "$id"
(( discovered )) || fail "plugin '$id' is not known"
omarchy-plugin-enable "$id" "${ENABLE_PLACEMENT[@]}"
else
echo "Enable it later with: omarchy plugin enable $id"
fi
+2 -4
View File
@@ -7,10 +7,8 @@
# Walks $OMARCHY_PATH/shell/plugins and ~/.config/omarchy/plugins, reads every
# manifest.json / *.manifest.json, and emits one JSON object per plugin with
# computed fields (sourceDir, barWidgetPath, barPath, firstParty). This is the
# single source of truth that omarchy-bar (widget/option enumeration),
# omarchy-bar-plugin (add validation), and omarchy-plugin-clone (source
# enumeration) read, so the bar and plugin commands never re-implement
# manifest walking.
# single source of truth used by bar selection and plugin enable/clone, so
# those commands never re-implement manifest walking.
set -o pipefail
+3 -36
View File
@@ -79,36 +79,6 @@ update_manifest() {
mv "$manifest.tmp" "$manifest"
}
switch_to_clone() {
local source_id="$1"
local new_id="$2"
local is_bar="$3"
local is_bar_widget="$4"
local has_non_widget_kind="$5"
if [[ $is_bar == "true" ]]; then
omarchy-bar use "$new_id"
elif [[ $is_bar_widget == "true" ]]; then
local shell_config
shell_config=$(omarchy-shell shell listShellConfig)
if jq -e --arg id "$source_id" '
[
.bar.layout.left[]?,
.bar.layout.center[]?,
.bar.layout.right[]?
] | any(.[]; (if type == "object" then .id else . end) == $id)
' <<<"$shell_config" >/dev/null; then
omarchy-bar-plugin replace "$source_id" "$new_id"
else
omarchy-bar-plugin add "$new_id"
fi
[[ $has_non_widget_kind == "false" ]] || omarchy-plugin-disable "$source_id"
else
omarchy-plugin-disable "$source_id"
omarchy-plugin-enable "$new_id"
fi
}
usage() {
cat <<USAGE
Usage: omarchy plugin clone <source-id>
@@ -145,14 +115,11 @@ source_info=$(omarchy-plugin-catalog | jq -r --arg id "$source_id" '
| [
.sourceDir,
.manifestPath,
(.name // .id),
((.kinds | index("bar")) != null),
((.kinds | index("bar-widget")) != null),
(any(.kinds[]; . != "bar-widget"))
(.name // .id)
] | @tsv
')
[[ -n $source_info ]] || fail "unknown built-in plugin: $source_id"
IFS=$'\t' read -r source_dir source_manifest source_name is_bar is_bar_widget has_non_widget_kind <<<"$source_info"
IFS=$'\t' read -r source_dir source_manifest source_name <<<"$source_info"
new_id="local.${source_id#omarchy.}"
display_name="My $source_name"
@@ -182,7 +149,7 @@ for (( attempt = 0; attempt < 40; attempt++ )); do
sleep 0.05
done
(( discovered )) || fail "cloned plugin '$new_id' was not discovered"
switch_to_clone "$source_id" "$new_id" "$is_bar" "$is_bar_widget" "$has_non_widget_kind"
omarchy-plugin-enable "$new_id" >/dev/null
clone_complete=1
omarchy-notification-send -g 󰐱 \
"Editing Cloned Plugin" \
+62 -5
View File
@@ -27,12 +27,69 @@ if (( $# > 0 )) && omarchy-plugin-catalog | jq -e --arg id "$id" '
fail "'$id' is a bar; it replaces the bar in use rather than taking a place in one"
fi
result=$(omarchy-shell shell setPluginEnabled "$id" true)
[[ $result == "ok" ]] ||
fail "plugin '$id' is not known; run: omarchy-shell shell rescanPlugins"
section=""
index=""
before=""
after=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
section="$1"
shift
fi
while (( $# > 0 )); do
case "$1" in
--section)
[[ -z $section ]] || fail "specify a section positionally or with --section, not both"
section="${2:-}"
[[ -n $section ]] || fail "--section requires a section"
shift 2
;;
--index)
index="${2:-}"
[[ -n $index ]] || fail "--index requires an index"
shift 2
;;
--before)
before="${2:-}"
[[ -n $before ]] || fail "--before requires a widget id"
shift 2
;;
--after)
after="${2:-}"
[[ -n $after ]] || fail "--after requires a widget id"
shift 2
;;
*)
fail "unknown placement option: $1"
;;
esac
done
if (( $# > 0 )); then
omarchy-bar-plugin move "$id" "$@"
[[ -z $section || $section =~ ^(left|center|right)$ ]] ||
fail "section must be left, center, or right"
[[ -z $index || $index =~ ^[0-9]+$ ]] ||
fail "index must be a non-negative integer"
[[ -z $before || -z $after ]] || fail "use only one of --before or --after"
placement=$(jq -cn \
--arg section "$section" \
--arg index "$index" \
--arg before "$before" \
--arg after "$after" '
{}
+ (if $section == "" then {} else {section: $section} end)
+ (if $index == "" then {} else {index: ($index | tonumber)} end)
+ (if $before == "" then {} else {before: $before} end)
+ (if $after == "" then {} else {after: $after} end)
')
result=$(omarchy-shell shell enablePlugin "$id" "$placement")
if [[ $result == "unknown" ]]; then
fail "plugin '$id' is not known; run: omarchy-shell shell rescanPlugins"
elif [[ $result != "ok" ]]; then
fail "$result"
fi
if [[ $placement != "{}" ]]; then
echo "Enabled and moved $id"
elif omarchy-plugin-catalog | jq -e --arg id "$id" '
any(.[]; .id == $id and (.kinds | index("bar")))
+2 -20
View File
@@ -80,7 +80,6 @@ if [[ -n $shell_plugins ]]; then
fi
cloned_from=""
restored_source=0
if [[ -f $target/manifest.json ]]; then
cloned_from=$(jq -r '.omarchy.clonedFrom // empty' "$target/manifest.json")
fi
@@ -93,24 +92,7 @@ else
confirm "Remove '$id'? The folder will be backed up." || fail "aborted"
fi
if [[ -n $cloned_from ]]; then
is_bar=$(jq -r '(.kinds | index("bar")) != null' "$target/manifest.json")
is_bar_widget=$(jq -r '(.kinds | index("bar-widget")) != null' "$target/manifest.json")
has_non_widget_kind=$(jq -r 'any(.kinds[]; . != "bar-widget")' "$target/manifest.json")
if [[ $is_bar == "true" && $was_enabled == "true" ]]; then
omarchy-bar use "$cloned_from"
restored_source=1
elif [[ $is_bar_widget == "true" && $was_enabled == "true" ]]; then
omarchy-bar-plugin replace "$id" "$cloned_from"
[[ $has_non_widget_kind == "false" ]] || omarchy-plugin-enable "$cloned_from"
restored_source=1
elif [[ $has_non_widget_kind == "true" ]]; then
[[ $was_enabled != "true" ]] || omarchy-shell shell setPluginEnabled "$id" false >/dev/null
omarchy-plugin-enable "$cloned_from"
restored_source=1
fi
elif [[ $was_enabled == "true" ]]; then
if [[ $was_enabled == "true" ]]; then
omarchy-shell shell setPluginEnabled "$id" false >/dev/null
fi
@@ -134,7 +116,7 @@ fi
omarchy-shell shell rescanPlugins >/dev/null
if (( restored_source )); then
if [[ -n $cloned_from && $was_enabled == "true" ]]; then
echo "Restored $cloned_from."
elif [[ $was_enabled == "true" ]]; then
echo "Plugin was enabled and was unloaded from omarchy-shell."
+1 -1
View File
@@ -4,7 +4,7 @@
# omarchy:requires-sudo=true
dropbox-cli stop 2>/dev/null || true
omarchy-bar-plugin remove omarchy.dropbox
omarchy-plugin-disable omarchy.dropbox
omarchy-pkg-drop dropbox dropbox-cli libappindicator-gtk3 python-gpgme nautilus-dropbox
echo ""
+1 -1
View File
@@ -6,7 +6,7 @@
tailscale down 2>/dev/null || true
systemctl --user disable --now omarchy-tailscale-receive.service 2>/dev/null || true
sudo systemctl disable --now tailscaled.service 2>/dev/null || true
omarchy-bar-plugin remove omarchy.tailscale
omarchy-plugin-disable omarchy.tailscale
omarchy-webapp-remove "Tailscale" 2>/dev/null || true
omarchy-pkg-drop tailscale
+1 -1
View File
@@ -129,7 +129,7 @@ Run `omarchy --help` for the full list. The most common groups:
| `omarchy restart` | Restart a service/app | `omarchy restart shell` |
| `omarchy toggle` | Toggle feature on/off | `omarchy toggle nightlight` |
| `omarchy theme` | Theme management | `omarchy theme set <name>` |
| `omarchy bar` | Bar layout and widgets | `omarchy bar plugin move omarchy.clock --section right` |
| `omarchy bar` | Bar layout and widgets | `omarchy bar move omarchy.clock --section right` |
| `omarchy plugin` | Manage/clone shell plugins | `omarchy plugin clone omarchy.clock` |
| `omarchy hook` | Install automation hooks | `omarchy hook install theme-set <script>` |
| `omarchy install` | Install optional software / packages | `omarchy install docker dbs` |
+5 -2
View File
@@ -81,7 +81,7 @@ arguments — add `--yes` to skip every prompt (the path for scripts and agents)
You can still install by hand: drop a plugin into
`~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then
`omarchy plugin enable <id>`. A bar widget starts in its declared default
section and can be moved with `omarchy bar plugin move`; enabling a full bar
section and can be moved with `omarchy bar move`; enabling a full bar
replaces the one in use.
The lower-level IPC methods remain available through `omarchy-shell shell ...`.
@@ -100,6 +100,9 @@ individual plugins (`bar`, `image-selector`, …).
| `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code |
| `reloadConfig` | reload shell.json |
| `setPluginEnabled <id> <"true"\|…>` | flip enabled bit (`ok` / `unknown`) |
| `enablePlugin <id> <placementJson>` | enable and place in one mutation |
| `moveBarWidget <id> <placementJson>` | move a configured widget |
| `setBarWidget <id> <key> <valueJson> <selectorJson>` | set an inline widget option |
| `listPlugins` | JSON of every discovered plugin |
`setPluginEnabled` takes a string; only literal `"true"` enables.
@@ -143,7 +146,7 @@ Rules:
4. Built-in bar widget ids are namespaced (`omarchy.clock`, `omarchy.audio`, …).
The migration rewrites older ids such as `Clock` and `AudioPanel` forward.
5. Third-party enabled ⇔ present; for full bar options that means `bar.id`.
First-party non-bar plugins are always enabled.
First-party non-bar plugins are enabled unless listed in `disabledPlugins[]`.
6. `allowMultiple: true` in the manifest permits multiple instances.
7. `idle.screensaver` and `idle.lock` are seconds since user idle began.
8. `version: 1` is required.
+7 -7
View File
@@ -133,12 +133,12 @@ You can still drop a plugin in without git:
2. `omarchy-shell shell rescanPlugins`.
3. `omarchy plugin enable <id>`. Bar widgets start in
`barWidget.defaultSection`, or in the center when it is omitted, and can be
moved with `omarchy bar plugin move`; a full bar replaces the one in use.
moved with `omarchy bar move`; a full bar replaces the one in use.
The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`,
`omarchy-shell shell setPluginEnabled <id> true`, and `omarchy-shell shell listPlugins`.
The `omarchy plugin` commands wrap those calls and can also edit the persisted
bar layout in `shell.json`.
`omarchy-shell shell enablePlugin <id> '{}'`, and `omarchy-shell shell listPlugins`.
The `omarchy plugin` commands wrap those calls. `omarchy bar move` and
`omarchy bar set` edit the persisted widget layout in `shell.json`.
To hack on a built-in plugin safely, clone it into user config instead of
editing the built-in source. The complete plugin directory is copied, including
@@ -265,9 +265,9 @@ becomes the authoritative file — we do **not** deep-merge defaults back in.
like `Clock` and `AudioPanel` forward.
5. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff
its id appears somewhere in shell.json. For full bar options, that means
`bar.id`; for bar widgets, `omarchy bar plugin` adds/removes layout entries;
other plugin kinds are enabled with the shell IPC. First-party non-bar
plugins are always enabled.
`bar.id`; for bar widgets, plugin enable/disable adds/removes layout entries;
other plugin kinds are enabled the same way. First-party non-bar plugins
are enabled unless listed in `disabledPlugins[]`.
6. **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":"omarchy.clock", "timezone": ...}`
+4 -4
View File
@@ -3,10 +3,10 @@
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
only difference is that the shell flags them with `__isFirstParty: true`.
First-party non-bar plugins are always enabled; `omarchy.bar` is the default
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.
First-party non-bar plugins are enabled unless listed in `disabledPlugins[]`;
`omarchy.bar` is the default bar option and becomes inactive only while another
`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
`~/.config/omarchy/plugins/<plugin-id>/` rather than in this directory.
+4 -4
View File
@@ -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 [`config/omarchy/shell.json`](../../../config/omarchy/shell.json). Once you customize anything via the bar gestures, `omarchy bar ...`, or by editing shell.json directly, your file is canonical — there is no deep-merge.
The bar is configured directly on the bar itself: drag empty bar space (or click-and-hold) to move the bar to another screen edge, double-left-click empty center-bar space to toggle transparency, and drag widgets to reorder them. The `omarchy bar position` and `omarchy bar transparent` commands do the same from scripts. For scriptable widget changes, use `omarchy bar plugin add`, `omarchy bar plugin move`, `omarchy bar plugin remove`, and `omarchy bar plugin set` (widget ids come from `omarchy plugin list`).
The bar is configured directly on the bar itself: drag empty bar space (or click-and-hold) to move the bar to another screen edge, double-left-click empty center-bar space to toggle transparency, and drag widgets to reorder them. The `omarchy bar position`, `omarchy bar transparent`, `omarchy bar move`, and `omarchy bar set` commands do the same from scripts. Enable or disable widgets with `omarchy plugin enable` and `omarchy plugin disable` (widget ids come from `omarchy plugin list`).
Example `shell.json` (bar subtree only shown):
@@ -178,6 +178,6 @@ namespaced ids.
Third-party widgets ship as separate plugins under
`~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json`
declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See
[../../README.md](../../README.md) for the manifest schema. Enable,
rescan, and place third-party plugins with `omarchy plugin enable`,
`omarchy-shell shell rescanPlugins`, and `omarchy bar plugin add`.
[../../README.md](../../README.md) for the manifest schema. Rescan, enable,
and place third-party plugins with `omarchy-shell shell rescanPlugins`,
`omarchy plugin enable`, and `omarchy bar move`.
+5 -5
View File
@@ -29,7 +29,7 @@ shows up at the next refresh, so nothing polls the disk waiting for it.
That self-hiding is why the widget ships in the default bar layout: a machine
that has never run Claude Code or Codex draws nothing, and the icon arrives on
its own the first time a scan finds usage. Drop it with
`omarchy bar plugin remove omarchy.model-usage`.
`omarchy plugin disable omarchy.model-usage`.
## Providers
@@ -52,7 +52,7 @@ falls back to local stats only.
Settings live in the widget's entry in `~/.config/omarchy/shell.json`. The
top-level keys can be set with
`omarchy bar plugin set omarchy.model-usage <key> <value>`:
`omarchy bar set omarchy.model-usage <key> <value>`:
| Key | Default | What it does |
|---|---|---|
@@ -65,8 +65,8 @@ top-level keys can be set with
Numbers need `--json`, or they land in `shell.json` as strings:
```bash
omarchy bar plugin set omarchy.model-usage refreshIntervalSec 300 --json
omarchy bar plugin set omarchy.model-usage syncDir '~/Sync/model-usage'
omarchy bar set omarchy.model-usage refreshIntervalSec 300 --json
omarchy bar set omarchy.model-usage syncDir '~/Sync/model-usage'
```
Per-provider settings are nested, and `set` writes its key literally rather
@@ -74,7 +74,7 @@ than walking a dotted path — so pass the whole `providers` object as JSON (or
edit `shell.json` directly):
```bash
omarchy bar plugin set omarchy.model-usage providers '{
omarchy bar set omarchy.model-usage providers '{
"claude": {
"enabled": true,
"statsPath": "~/.claude/stats-cache.json",
+1 -1
View File
@@ -46,4 +46,4 @@ Renders the Tailscale mark natively as a theme-colored 3×3 dot grid, matching t
## Add to the bar
This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy bar plugin add omarchy.tailscale`, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json`; the shell reloads `shell.json` automatically.
This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy plugin enable omarchy.tailscale`, then place it with `omarchy bar move omarchy.tailscale` if desired.
+272 -31
View File
@@ -24,6 +24,7 @@ QtObject {
property var installedPlugins: ({})
property int registryRevision: 0
property bool scanning: false
property string lastEnableError: ""
signal pluginsChanged()
signal scanFinished()
@@ -171,6 +172,27 @@ QtObject {
return ["left", "center", "right"].indexOf(section) !== -1 ? section : "center"
}
function barEntryId(entry) {
return Util.canonicalWidgetId(String(Util.isPlainObject(entry) ? entry.id : entry || ""))
}
function findBarLocation(config, id, section) {
if (!Util.isPlainObject(config) || !Util.isPlainObject(config.bar)
|| !Util.isPlainObject(config.bar.layout)) return { found: false }
var key = Util.canonicalWidgetId(String(id))
var sections = ["left", "center", "right"]
for (var s = 0; s < sections.length; s++) {
if (section && sections[s] !== section) continue
var entries = config.bar.layout[sections[s]]
if (!Array.isArray(entries)) continue
for (var i = 0; i < entries.length; i++) {
if (barEntryId(entries[i]) === key)
return { found: true, kind: "bar", section: sections[s], index: i }
}
}
return { found: false }
}
function findEntryLocation(config, id) {
if (!Util.isPlainObject(config)) return { found: false }
var key = Util.canonicalWidgetId(String(id))
@@ -179,14 +201,8 @@ QtObject {
if (selectedBar === key) return { found: true, kind: "bar-option" }
}
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] && Util.canonicalWidgetId(arr[i].id) === key) return { found: true, kind: "bar", section: sections[s], index: i }
}
}
var barLocation = findBarLocation(config, key, "")
if (barLocation.found) return barLocation
}
if (Array.isArray(config.plugins)) {
for (var j = 0; j < config.plugins.length; j++) {
@@ -196,13 +212,210 @@ QtObject {
return { found: false }
}
function barTarget(config, placement, fallbackSection) {
var target = placement || {}
var section = ["left", "center", "right"].indexOf(String(target.section || "")) !== -1
? String(target.section) : fallbackSection
var relativeId = String(target.before || target.after || "")
if (relativeId) {
var relative = findBarLocation(config, relativeId, section && target.section ? section : "")
if (!relative.found) return { error: "could not find target widget " + relativeId }
return {
section: relative.section,
index: relative.index + (target.after ? 1 : 0)
}
}
if (!Array.isArray(config.bar.layout[section])) config.bar.layout[section] = []
if (target.index !== undefined && target.index !== null) {
var requested = Math.max(0, Math.floor(Number(target.index)))
return { section: section, index: Math.min(requested, config.bar.layout[section].length) }
}
var anchors = { left: "omarchy.workspaces", center: "omarchy.weather", right: "omarchy.tray" }
var anchor = findBarLocation(config, anchors[section], section)
return {
section: section,
index: anchor.found ? anchor.index + 1 : config.bar.layout[section].length
}
}
function moveBarEntry(config, id, placement) {
var key = Util.canonicalWidgetId(String(id))
var source
if (placement.fromIndex !== undefined && placement.fromIndex !== null) {
var fromSection = String(placement.fromSection || "")
if (!fromSection) return "from-index requires from-section"
var entries = config.bar.layout[fromSection]
var fromIndex = Math.floor(Number(placement.fromIndex))
if (!Array.isArray(entries) || fromIndex < 0 || fromIndex >= entries.length)
return "no widget at " + fromSection + "[" + fromIndex + "]"
if (barEntryId(entries[fromIndex]) !== key)
return "widget at " + fromSection + "[" + fromIndex + "] is not " + key
source = { found: true, section: fromSection, index: fromIndex }
} else {
source = findBarLocation(config, key, String(placement.fromSection || ""))
if (!source.found) return "could not find widget " + key
}
var entry = config.bar.layout[source.section][source.index]
config.bar.layout[source.section].splice(source.index, 1)
var target = barTarget(config, placement, source.section)
if (target.error) {
config.bar.layout[source.section].splice(source.index, 0, entry)
return target.error
}
config.bar.layout[target.section].splice(target.index, 0, entry)
return ""
}
function moveBarWidget(id, placement) {
var error = ""
shellConfigMutator(function(config) {
ensureConfigShape(config)
error = moveBarEntry(config, id, placement || {})
})
if (error) return error
registryRevision++
pluginsChanged()
return ""
}
function setBarWidget(id, key, value, selector) {
var error = ""
shellConfigMutator(function(config) {
ensureConfigShape(config)
var location
var requested = selector || {}
var section = String(requested.fromSection || requested.section || "")
var index = requested.fromIndex !== undefined && requested.fromIndex !== null
? requested.fromIndex : requested.index
if (index !== undefined && index !== null) {
if (!section) {
error = "index requires section"
return
}
var entries = config.bar.layout[section]
var numericIndex = Math.floor(Number(index))
if (!Array.isArray(entries) || numericIndex < 0 || numericIndex >= entries.length) {
error = "no widget at " + section + "[" + numericIndex + "]"
return
}
location = { found: true, section: section, index: numericIndex }
} else {
location = findBarLocation(config, id, section)
}
if (!location.found) {
error = "could not find widget " + id
return
}
if (barEntryId(config.bar.layout[location.section][location.index]) !== String(id)) {
error = "widget at " + location.section + "[" + location.index + "] is not " + id
return
}
var entry = config.bar.layout[location.section][location.index]
if (!Util.isPlainObject(entry)) {
error = "widget entry must be an object"
return
}
entry[String(key)] = value
})
if (error) return error
registryRevision++
pluginsChanged()
return ""
}
function ensureConfigShape(config) {
if (!Util.isPlainObject(config.bar)) config.bar = { layout: { left: [], center: [], right: [] } }
if (!Util.isPlainObject(config.bar.layout)) config.bar.layout = { left: [], center: [], right: [] }
var sections = ["left", "center", "right"]
for (var i = 0; i < sections.length; i++) {
if (!Array.isArray(config.bar.layout[sections[i]])) config.bar.layout[sections[i]] = []
}
if (!Array.isArray(config.plugins)) config.plugins = []
}
// Bar widgets use the default section declared in their manifest, falling
// back to center. Panels/overlays/menus/services go into the plugins[] array.
// Built-ins are already loaded, so shell.json only ever records the
// deviation: an added third-party plugin in plugins[], a switched-off
// built-in in disabledPlugins[].
function setEnabled(id, value) {
function removeDisabled(config, id) {
if (!Array.isArray(config.disabledPlugins)) return
config.disabledPlugins = config.disabledPlugins.filter(function(entry) { return entry !== id })
if (config.disabledPlugins.length === 0) delete config.disabledPlugins
}
function addDisabled(config, id) {
if (isDisabled(config, id)) return
if (!Array.isArray(config.disabledPlugins)) config.disabledPlugins = []
config.disabledPlugins.push(id)
}
function cloneShouldRestoreSource(config, id) {
return Array.isArray(config.cloneSourceRestores) && config.cloneSourceRestores.indexOf(id) !== -1
}
function setCloneShouldRestoreSource(config, id, value) {
var restores = Array.isArray(config.cloneSourceRestores) ? config.cloneSourceRestores : []
restores = restores.filter(function(entry) { return entry !== id })
if (value) restores.push(id)
if (restores.length) config.cloneSourceRestores = restores
else delete config.cloneSourceRestores
}
function activeCloneFor(config, sourceId) {
for (var candidate in installedPlugins) {
var candidateManifest = installedPlugins[candidate]
var candidateMetadata = candidateManifest && Util.isPlainObject(candidateManifest.omarchy)
? candidateManifest.omarchy : null
if (!candidateMetadata || String(candidateMetadata.clonedFrom || "") !== sourceId) continue
if (Array.isArray(candidateManifest.kinds) && candidateManifest.kinds.indexOf("bar") !== -1) {
if (Util.canonicalWidgetId(String(config.bar.id || "")) === candidate) return candidate
} else if (findEntryLocation(config, candidate).found) {
return candidate
}
}
return ""
}
function restoreCloneSource(config, cloneId, sourceId) {
var cloneManifest = installedPlugins[cloneId]
var isBarOption = cloneManifest && Array.isArray(cloneManifest.kinds)
&& cloneManifest.kinds.indexOf("bar") !== -1
if (isBarOption) {
if (sourceId === "omarchy.bar") delete config.bar.id
else config.bar.id = sourceId
} else {
var cloneLocation = findEntryLocation(config, cloneId)
if (cloneLocation.kind === "bar") {
var cloneEntry = config.bar.layout[cloneLocation.section][cloneLocation.index]
var sections = ["left", "center", "right"]
for (var s = 0; s < sections.length; s++) {
for (var i = config.bar.layout[sections[s]].length - 1; i >= 0; i--) {
if (barEntryId(config.bar.layout[sections[s]][i]) === sourceId)
config.bar.layout[sections[s]].splice(i, 1)
}
}
cloneLocation = findBarLocation(config, cloneId, "")
if (cloneLocation.found) {
var restoredEntry = Util.isPlainObject(cloneEntry) ? Util.cloneJson(cloneEntry) : {}
restoredEntry.id = sourceId
config.bar.layout[cloneLocation.section][cloneLocation.index] = restoredEntry
}
} else if (cloneLocation.kind === "plugin") {
config.plugins.splice(cloneLocation.index, 1)
}
}
if (cloneShouldRestoreSource(config, cloneId)) removeDisabled(config, sourceId)
setCloneShouldRestoreSource(config, cloneId, false)
}
function setEnabled(id, value, placement) {
var key = Util.canonicalWidgetId(String(id))
lastEnableError = ""
if (!shellConfigMutator) {
console.warn("PluginRegistry.setEnabled called before shellConfigMutator wired")
return false
@@ -216,17 +429,33 @@ QtObject {
var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
var hasNonWidgetKind = manifest && Array.isArray(manifest.kinds)
&& manifest.kinds.some(function(kind) { return kind !== "bar-widget" })
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
var clonedFrom = metadata ? Util.canonicalWidgetId(String(metadata.clonedFrom || "")) : ""
shellConfigMutator(function(config) {
// Ensure shape exists.
if (!Util.isPlainObject(config.bar)) 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 = []
ensureConfigShape(config)
if (value && placement && (placement.before || placement.after)) {
var relativeId = String(placement.before || placement.after)
if (!findBarLocation(config, relativeId, String(placement.section || "")).found) {
lastEnableError = "could not find target widget " + relativeId
return
}
}
if (value && manifest && manifest.__isFirstParty) {
var activeClone = activeCloneFor(config, key)
if (activeClone) {
restoreCloneSource(config, activeClone, key)
removeDisabled(config, key)
}
}
if (isBarOption) {
if (value) {
config.bar.id = key
} else if (Util.canonicalWidgetId(String(config.bar.id || "")) === key) {
delete config.bar.id
if (clonedFrom && clonedFrom !== "omarchy.bar") config.bar.id = clonedFrom
else delete config.bar.id
}
return
}
@@ -235,33 +464,45 @@ QtObject {
var location = findEntryLocation(config, key)
if (value) {
// Leave shell.json without the key once nothing is switched off, so a
// config that never disabled anything reads as it always did.
if (Array.isArray(config.disabledPlugins)) {
config.disabledPlugins = config.disabledPlugins.filter(function(entry) { return entry !== key })
if (config.disabledPlugins.length === 0) delete config.disabledPlugins
}
if (location.found) return
removeDisabled(config, key)
var entry = { id: key }
if (isBarWidget) {
var section = defaultBarWidgetSection(manifest)
if (!Array.isArray(config.bar.layout[section])) config.bar.layout[section] = []
config.bar.layout[section].push(entry)
} else if (!isFirstParty) {
var insertedWithPlacement = false
if (!location.found && isBarWidget) {
var sourceLocation = clonedFrom ? findEntryLocation(config, clonedFrom) : { found: false }
if (sourceLocation.kind === "bar") {
var sourceEntry = config.bar.layout[sourceLocation.section][sourceLocation.index]
var replacement = Util.isPlainObject(sourceEntry) ? Util.cloneJson(sourceEntry) : entry
replacement.id = key
config.bar.layout[sourceLocation.section][sourceLocation.index] = replacement
} else {
var section = defaultBarWidgetSection(manifest)
var target = barTarget(config, placement || {}, section)
config.bar.layout[target.section].splice(target.index, 0, entry)
insertedWithPlacement = true
}
} else if (!location.found && !isFirstParty) {
config.plugins.push(entry)
}
if (isBarWidget && !insertedWithPlacement && placement && Object.keys(placement).length)
moveBarEntry(config, key, placement)
if (clonedFrom && hasNonWidgetKind && !isDisabled(config, clonedFrom)) {
addDisabled(config, clonedFrom)
setCloneShouldRestoreSource(config, key, true)
}
return
}
if (location.kind === "bar") config.bar.layout[location.section].splice(location.index, 1)
if (clonedFrom) restoreCloneSource(config, key, clonedFrom)
else if (location.kind === "bar") config.bar.layout[location.section].splice(location.index, 1)
else if (location.kind === "plugin") config.plugins.splice(location.index, 1)
// Dropping the layout entry is the whole story for a widget. Anything
// else built-in loads by default, so switching it off has to be stated.
if (isFirstParty && (!isBarWidget || hasNonWidgetKind) && !isDisabled(config, key)) {
if (!Array.isArray(config.disabledPlugins)) config.disabledPlugins = []
config.disabledPlugins.push(key)
}
if (isFirstParty && !isBarWidget) addDisabled(config, key)
})
if (lastEnableError) return false
registryRevision++
pluginsChanged()
return true
+31 -9
View File
@@ -54,7 +54,6 @@ ShellRoot {
property var defaultsConfig: builtinShellConfig
property var shellConfig: builtinShellConfig
property bool suppressUserReload: false
property bool pluginReloading: false
property bool pluginReloadPending: false
@@ -107,7 +106,6 @@ ShellRoot {
}
function persistShellConfig(nextConfig) {
suppressUserReload = true
var payload = JSON.parse(JSON.stringify(nextConfig))
payload.version = 1
shellConfig = payload
@@ -135,13 +133,7 @@ ShellRoot {
watchChanges: true
atomicWrites: true
printErrors: false
onLoaded: {
if (shell.suppressUserReload) {
shell.suppressUserReload = false
return
}
shell.applyShellConfig()
}
onLoaded: shell.applyShellConfig()
onLoadFailed: function(error) { shell.applyShellConfig() }
onFileChanged: reload()
}
@@ -908,6 +900,36 @@ ShellRoot {
return shell.pluginRegistry.setEnabled(id, enabled === "true") ? "ok" : "unknown"
}
function enablePlugin(id: string, placementJson: string): string {
try {
var placement = JSON.parse(placementJson || "{}")
if (shell.pluginRegistry.setEnabled(id, true, placement)) return "ok"
return shell.pluginRegistry.lastEnableError || "unknown"
} catch (e) {
return "invalid placement: " + e
}
}
function moveBarWidget(id: string, placementJson: string): string {
try {
var error = shell.pluginRegistry.moveBarWidget(id, JSON.parse(placementJson || "{}"))
return error ? error : "ok"
} catch (e) {
return "invalid placement: " + e
}
}
function setBarWidget(id: string, key: string, valueJson: string, selectorJson: string): string {
try {
var value = JSON.parse(valueJson)
var selector = JSON.parse(selectorJson || "{}")
var error = shell.pluginRegistry.setBarWidget(id, key, value, selector)
return error ? error : "ok"
} catch (e) {
return "invalid widget setting: " + e
}
}
function listPlugins(): string {
var out = []
var plugins = shell.pluginRegistry.installedPlugins
+62 -108
View File
@@ -208,14 +208,27 @@ pass "Hyprland bootstrap reloads cached Omarchy config modules"
TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/home/.config/omarchy"
ipc_mock_bin="$TMPDIR/ipc-mock"
mkdir -p "$ipc_mock_bin"
cat >"$ipc_mock_bin/omarchy-shell" <<'SH'
#!/bin/bash
set -euo pipefail
mkdir -p "$HOME/.local/state/omarchy"
printf '%s\n' "$*" >>"$HOME/.local/state/omarchy/shell-ipc-calls"
printf 'ok\n'
SH
chmod +x "$ipc_mock_bin/omarchy-shell"
export PATH="$ipc_mock_bin:$PATH"
cat >"$TMPDIR/home/.config/omarchy/shell.json" <<'JSON'
{
"version": 1,
"bar": {
"layout": {
"left": [{ "id": "omarchy.menu" }, { "id": "omarchy.workspaces" }],
"center": [{ "id": "omarchy.clock" }, { "id": "omarchy.weather" }],
"right": [{ "id": "omarchy.tray" }, { "id": "omarchy.bluetooth" }]
"left": [{ "id": "omarchy.menu" }, { "id": "omarchy.workspaces" }, { "id": "omarchy.active-window" }],
"center": [{ "id": "omarchy.clock" }, { "id": "omarchy.weather" }, { "id": "omarchy.system-update" }, { "id": "omarchy.tailscale" }],
"right": [{ "id": "omarchy.tray" }, { "id": "omarchy.microphone" }, { "id": "omarchy.bluetooth" }]
}
},
"plugins": []
@@ -250,75 +263,20 @@ HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-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-bar-plugin add omarchy.tailscale
jq -e '
def ids: map(.id // .);
.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "omarchy.tailscale"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config defaults widgets without a section to center"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar move omarchy.active-window right
grep -Fqx 'shell moveBarWidget omarchy.active-window {"section":"right"}' \
"$TMPDIR/home/.local/state/omarchy/shell-ipc-calls"
pass "bar move accepts a positional target section"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add omarchy.active-window
jq -e '
def ids: map(.id // .);
.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces", "omarchy.active-window"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config uses a widget's default section"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar move omarchy.active-window left
grep -Fqx 'shell moveBarWidget omarchy.active-window {"section":"left"}' \
"$TMPDIR/home/.local/state/omarchy/shell-ipc-calls"
pass "bar move can restore a widget with positional syntax"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add omarchy.media
jq -e '
def ids: map(.id // .);
.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "omarchy.media", "omarchy.tailscale"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin remove omarchy.media >/dev/null
pass "shell config honors a center default section"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add omarchy.dropbox
jq -e '
def ids: map(.id // .);
.bar.layout.right | ids == ["omarchy.tray", "omarchy.dropbox", "omarchy.bluetooth"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin remove omarchy.dropbox >/dev/null
pass "shell config honors a right default section"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add omarchy.system-update center
jq -e '
def ids: map(.id // .);
.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "omarchy.system-update", "omarchy.tailscale"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config appends center widgets after weather"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add omarchy.microphone right
jq -e '
def ids: map(.id // .);
.bar.layout.right | ids == ["omarchy.tray", "omarchy.microphone", "omarchy.bluetooth"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config moves existing widgets without duplicates"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin move omarchy.active-window right
jq -e '
def ids: map(.id // .);
(.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces"]) and
(.bar.layout.right | ids == ["omarchy.tray", "omarchy.active-window", "omarchy.microphone", "omarchy.bluetooth"])
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "bar plugin move accepts a positional target section"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin move omarchy.active-window left
jq -e '
def ids: map(.id // .);
(.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces", "omarchy.active-window"]) and
(.bar.layout.right | ids == ["omarchy.tray", "omarchy.microphone", "omarchy.bluetooth"])
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "bar plugin move can restore a widget with positional syntax"
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin move omarchy.active-window left --section right 2>/dev/null; then
fail "bar plugin move accepted positional and flagged target sections"
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar move omarchy.active-window left --section right 2>/dev/null; then
fail "bar move accepted positional and flagged target sections"
fi
pass "bar plugin move rejects conflicting target section syntax"
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add local.nonexistent-widget 2>/dev/null; then
fail "bar plugin add accepted an unknown widget"
fi
pass "bar plugin add rejects an unknown widget"
pass "bar move rejects conflicting target section syntax"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar position bottom
jq -e '
@@ -339,43 +297,25 @@ HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar transparent toggle
jq -e '.bar.transparent == false' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config toggles bar transparency"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin drop omarchy.active-window
jq -e '
def ids: map(.id // .);
(.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces"]) and
(.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "omarchy.system-update", "omarchy.tailscale"]) and
(.bar.layout.right | ids == ["omarchy.tray", "omarchy.microphone", "omarchy.bluetooth"])
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config drops widgets from any section"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar set omarchy.bluetooth enabled false --json
grep -Fqx 'shell setBarWidget omarchy.bluetooth enabled false {}' \
"$TMPDIR/home/.local/state/omarchy/shell-ipc-calls"
pass "bar set accepts false JSON values"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin remove omarchy.system-update
jq -e '
def ids: map(.id // .);
.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "omarchy.tailscale"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config removes widgets with remove alias"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar set omarchy.bluetooth optional null --json
grep -Fqx 'shell setBarWidget omarchy.bluetooth optional null {}' \
"$TMPDIR/home/.local/state/omarchy/shell-ipc-calls"
pass "bar set accepts null JSON values"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth enabled false --json
jq -e '
any(.bar.layout.right[]; .id == "omarchy.bluetooth" and .enabled == false)
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "bar plugin set accepts false JSON values"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth optional null --json
jq -e '
any(.bar.layout.right[]; .id == "omarchy.bluetooth" and has("optional") and .optional == null)
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "bar plugin set accepts null JSON values"
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth broken '{' --json 2>/dev/null; then
fail "bar plugin set accepted malformed JSON"
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar set omarchy.bluetooth broken '{' --json 2>/dev/null; then
fail "bar set accepted malformed JSON"
fi
pass "bar plugin set rejects malformed JSON"
pass "bar set rejects malformed JSON"
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth broken 'false null' --json 2>/dev/null; then
fail "bar plugin set accepted multiple JSON values"
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar set omarchy.bluetooth broken 'false null' --json 2>/dev/null; then
fail "bar set accepted multiple JSON values"
fi
pass "bar plugin set rejects multiple JSON values"
pass "bar set rejects multiple JSON values"
mock_bin="$TMPDIR/mock-bin"
mkdir -p "$mock_bin"
@@ -400,7 +340,8 @@ SH
cat >"$mock_bin/omarchy-shell" <<'SH'
#!/bin/bash
exit 0
[[ ${OMARCHY_TEST_SHELL_DOWN:-0} == "1" ]] && exit 1
printf 'ok\n'
SH
cat >"$mock_bin/omarchy-installed-service-dropbox" <<'SH'
@@ -430,10 +371,22 @@ pass "bar defaults restores the stock bar"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$mock_path" OMARCHY_TEST_DROPBOX=1 OMARCHY_TEST_TAILSCALE=1 omarchy-bar defaults
jq -e '
def ids: map(.id // .);
(.bar.layout.right | ids | index("omarchy.dropbox") != null) and
(.bar.layout.center | ids | index("omarchy.tailscale") != null)
(.bar.layout.center | ids) as $center |
(.bar.layout.right | ids | index("omarchy.dropbox") == 1) and
($center | index("omarchy.tailscale") == (($center | index("omarchy.weather")) + 1))
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "bar defaults adds widgets for running optional services"
pass "bar defaults places plugins for running optional services"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$mock_path" \
OMARCHY_TEST_SHELL_DOWN=1 OMARCHY_TEST_DROPBOX=1 OMARCHY_TEST_TAILSCALE=1 \
omarchy-bar defaults
jq -e '
def ids: map(.id // .);
(.bar.layout.center | ids) as $center |
(.bar.layout.right | ids | index("omarchy.dropbox") == 1) and
($center | index("omarchy.tailscale") == (($center | index("omarchy.weather")) + 1))
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "bar defaults places service widgets without a running shell"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$mock_path" OMARCHY_TEST_DROPBOX=0 OMARCHY_TEST_TAILSCALE=0 omarchy-refresh-shell
jq -e '
@@ -446,11 +399,12 @@ pass "shell refresh keeps optional service widgets absent when services are unav
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$mock_path" OMARCHY_TEST_DROPBOX=1 OMARCHY_TEST_TAILSCALE=1 omarchy-refresh-shell
jq -e '
def ids: map(.id // .);
(.bar.layout.right | ids | index("omarchy.dropbox") != null) and
(.bar.layout.center | ids | index("omarchy.tailscale") != null)
(.bar.layout.center | ids) as $center |
(.bar.layout.right | ids | index("omarchy.dropbox") == 1) and
($center | index("omarchy.tailscale") == (($center | index("omarchy.weather")) + 1))
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
[[ -f $TMPDIR/home/.local/state/omarchy/restart-shell-called ]] || fail "shell refresh restarts shell"
pass "shell refresh adds optional service widgets when services are available"
pass "shell refresh places optional service widgets when services are available"
if grep -RIl 'upgrade-to-quattro\|Omarchy 4\.0 is upgraded' "$ROOT/migrations" >/dev/null; then
fail "4.0 upgrade is not modeled as a migration"
+135 -2
View File
@@ -85,9 +85,20 @@ ShellRoot {
scan += block("firstparty", "/first/hybrid", manifest("omarchy.hybrid", ["menu", "bar-widget"], { menu: "Menu.qml", barWidget: "Widget.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" }, { defaultSection: "left" }))
scan += block("thirdparty", "/third/center-widget", manifest("third.center-widget", ["bar-widget"], { barWidget: "Widget.qml" }))
scan += block("thirdparty", "/third/right-widget", manifest("third.right-widget", ["bar-widget"], { barWidget: "Widget.qml" }, { defaultSection: "right" }))
var localWidget = manifest("local.first-widget", ["bar-widget"], { barWidget: "Widget.qml" })
localWidget.omarchy = { clonedFrom: "omarchy.first-widget" }
scan += block("thirdparty", "/third/local-widget", localWidget)
var localHybrid = manifest("local.hybrid", ["menu", "bar-widget"], { menu: "Menu.qml", barWidget: "Widget.qml" })
localHybrid.omarchy = { clonedFrom: "omarchy.hybrid" }
scan += block("thirdparty", "/third/local-hybrid", localHybrid)
var localPanel = manifest("local.grouped-panel", ["panel"], { panel: "Panel.qml" })
localPanel.omarchy = { clonedFrom: "omarchy.grouped-panel" }
scan += block("thirdparty", "/third/local-panel", localPanel)
var localBar = manifest("local.bar", ["bar"], { bar: "Bar.qml" })
localBar.omarchy = { clonedFrom: "omarchy.bar" }
scan += block("thirdparty", "/third/local-bar", localBar)
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/reserved", manifest("omarchy.reserved", ["panel"], { panel: "Panel.qml" }))
@@ -100,13 +111,18 @@ ShellRoot {
registry.parseScanOutput(scan)
root.assertDeepEqual(pluginIds(), [
"local.bar",
"local.first-widget",
"local.grouped-panel",
"local.hybrid",
"omarchy.bar",
"omarchy.first-widget",
"omarchy.grouped-panel",
"omarchy.hybrid",
"third.bar",
"third.center-widget",
"third.panel",
"third.right-widget",
"third.widget"
], "registry merges valid first-party and third-party manifests")
@@ -148,9 +164,126 @@ ShellRoot {
registry.setEnabled("third.widget", false)
root.assertDeepEqual(root.config.bar.layout.left, [], "disabling bar widgets removes layout entry")
root.config = {
version: 1,
bar: {
layout: {
left: [{ id: "omarchy.workspaces" }, { id: "omarchy.menu" }],
center: [{ id: "omarchy.weather" }, { id: "omarchy.clock" }],
right: [{ id: "omarchy.tray" }]
}
},
plugins: []
}
registry.setEnabled("third.widget", true)
root.assertDeepEqual(
root.config.bar.layout.left,
[{ id: "omarchy.workspaces" }, { id: "third.widget" }, { id: "omarchy.menu" }],
"enabling a widget inserts it after its section anchor"
)
registry.setEnabled("third.center-widget", true)
root.assertDeepEqual(
root.config.bar.layout.center,
[{ id: "omarchy.weather" }, { id: "third.center-widget" }, { id: "omarchy.clock" }],
"widgets without a default section use the center anchor"
)
registry.setEnabled("third.right-widget", true)
root.assertDeepEqual(
root.config.bar.layout.right,
[{ id: "omarchy.tray" }, { id: "third.right-widget" }],
"right widgets use the right anchor"
)
root.config = { version: 1, bar: { layout: { left: [], center: [], right: [] } }, plugins: [] }
registry.setEnabled("third.right-widget", true)
root.assertDeepEqual(root.config.bar.layout.right, [{ id: "third.right-widget" }], "widgets append when their section anchor is absent")
root.config = {
version: 1,
bar: { layout: { left: [{ id: "third.widget", size: 3 }], center: [], right: [] } },
plugins: []
}
root.assertEqual(registry.moveBarWidget("third.widget", { section: "right" }), "", "registry moves widgets")
root.assertDeepEqual(root.config.bar.layout.right, [{ id: "third.widget", size: 3 }], "registry move preserves widget settings")
root.assertEqual(registry.setBarWidget("third.widget", "size", 7, {}), "", "registry sets widget options")
root.assertEqual(root.config.bar.layout.right[0].size, 7, "registry persists widget options")
root.config = { version: 1, bar: { layout: { left: [], center: [], right: [] } }, plugins: [] }
registry.setEnabled("third.widget", true, { section: "right", index: 0 })
root.assertDeepEqual(root.config.bar.layout.right, [{ id: "third.widget" }], "enabling with placement is one registry transition")
root.config = {
version: 1,
bar: { layout: { left: [], center: [{ id: "omarchy.first-widget", size: 4 }], right: [] } },
plugins: []
}
registry.setEnabled("local.first-widget", true)
root.assertDeepEqual(
root.config.bar.layout.center,
[{ id: "local.first-widget", size: 4 }],
"enabling a widget clone replaces its source in place"
)
root.assertEqual(registry.resolveEnabledId("omarchy.first-widget"), "local.first-widget", "enabled clones receive calls made to their source id")
registry.setEnabled("omarchy.first-widget", true)
root.assertDeepEqual(
root.config.bar.layout.center,
[{ id: "omarchy.first-widget", size: 4 }],
"enabling a clone source switches back without duplicates"
)
registry.setEnabled("local.first-widget", true)
registry.setEnabled("local.first-widget", false)
root.assertDeepEqual(
root.config.bar.layout.center,
[{ id: "omarchy.first-widget", size: 4 }],
"disabling a widget clone restores its source in place"
)
root.config = {
version: 1,
bar: { layout: { left: [], center: ["omarchy.first-widget"], right: [] } },
plugins: []
}
registry.setEnabled("local.first-widget", true)
root.assertDeepEqual(root.config.bar.layout.center, [{ id: "local.first-widget" }], "clone replacement normalizes string entries")
registry.setEnabled("local.first-widget", false)
root.assertDeepEqual(root.config.bar.layout.center, [{ id: "omarchy.first-widget" }], "clone restoration normalizes string entries")
root.config = {
version: 1,
bar: { layout: { left: [{ id: "omarchy.hybrid" }], center: [], right: [] } },
plugins: []
}
registry.setEnabled("local.hybrid", true)
root.assertDeepEqual(root.config.bar.layout.left, [{ id: "local.hybrid" }], "enabling a multi-kind clone replaces its widget")
root.assertDeepEqual(root.config.disabledPlugins, ["omarchy.hybrid"], "enabling a multi-kind clone disables the source")
registry.setEnabled("local.hybrid", false)
root.assertDeepEqual(root.config.bar.layout.left, [{ id: "omarchy.hybrid" }], "disabling a multi-kind clone restores its widget")
root.assertTrue(root.config.disabledPlugins === undefined, "disabling a multi-kind clone enables the source")
root.config = { version: 1, bar: { layout: { left: [], center: [], right: [] } }, plugins: [] }
registry.setEnabled("local.grouped-panel", true)
root.assertDeepEqual(root.config.plugins, [{ id: "local.grouped-panel" }], "enabling an ordinary clone adds it")
root.assertDeepEqual(root.config.disabledPlugins, ["omarchy.grouped-panel"], "enabling an ordinary clone disables the source")
registry.setEnabled("local.grouped-panel", false)
root.assertDeepEqual(root.config.plugins, [], "disabling an ordinary clone removes it")
root.assertTrue(root.config.disabledPlugins === undefined, "disabling an ordinary clone restores the source")
root.config = {
version: 1,
bar: { layout: { left: [], center: [], right: [] } },
plugins: [],
disabledPlugins: ["omarchy.grouped-panel"]
}
registry.setEnabled("local.grouped-panel", true)
root.assertDeepEqual(root.config.disabledPlugins, ["omarchy.grouped-panel"], "cloning an already-disabled source keeps it disabled")
root.assertTrue(root.config.cloneSourceRestores === undefined, "an already-disabled source is not marked for restoration")
registry.setEnabled("local.grouped-panel", false)
root.assertDeepEqual(root.config.disabledPlugins, ["omarchy.grouped-panel"], "disabling the clone preserves the source's prior disabled state")
registry.setEnabled("local.bar", true)
root.assertEqual(root.config.bar.id, "local.bar", "enabling a cloned bar selects it")
registry.setEnabled("local.bar", false)
root.assertTrue(root.config.bar.id === undefined, "disabling a cloned built-in bar restores it")
root.config = {
version: 1,
@@ -201,8 +334,8 @@ ShellRoot {
}
registry.setEnabled("omarchy.hybrid", false)
root.assertDeepEqual(root.config.bar.layout.left, [], "disabling a multi-kind built-in removes its widget")
root.assertDeepEqual(root.config.disabledPlugins, ["omarchy.hybrid"], "disabling a multi-kind built-in unloads its other kinds")
root.assertTrue(!registry.isEnabled("omarchy.hybrid"), "a disabled multi-kind built-in is not loadable")
root.assertTrue(root.config.disabledPlugins === undefined, "disabling a multi-kind widget records nothing else")
root.assertTrue(registry.isEnabled("omarchy.hybrid"), "a multi-kind built-in remains loadable without its widget")
var localBase = registry.pluginsDir + "/local.clock"
root.assertEqual(registry.localPluginIdForPath(localBase + "/BarWidget.qml"), "local.clock", "local clone changes are watched")
+1 -1
View File
@@ -221,7 +221,7 @@ const pluginAdd = fs.readFileSync(path.join(root, 'bin/omarchy-plugin-add'), 'ut
const pluginEnable = fs.readFileSync(path.join(root, 'bin/omarchy-plugin-enable'), 'utf8')
assert(
/Now using \$id as the bar/.test(pluginEnable)
&& /Now using \$id as the bar[\s\S]*?place_bar_widget/.test(pluginAdd),
&& /omarchy-plugin-enable "\$id" "\$\{ENABLE_PLACEMENT\[@\]\}"/.test(pluginAdd),
'plugin enable reports a bar as replacing the one in use, whether enabled or freshly added'
)
assert(
+23 -55
View File
@@ -24,19 +24,14 @@ elif [[ $* == *"listPlugins"* ]]; then
find "$HOME/.config/omarchy/plugins" -mindepth 2 -maxdepth 2 -name manifest.json -print0 |
xargs -0 -r jq -s 'map({id: .id, enabled: true})'
fi
elif [[ $* == *"setPluginEnabled"* ]]; then
printf 'omarchy-shell %s\n' "$*" >>"$FAKE_CALLS"
printf 'ok\n'
fi
exit 0
SH
for command in omarchy-bar omarchy-bar-plugin; do
cat >"$TMPDIR/bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
exec "$OMARCHY_TEST_ROOT/bin/${0##*/}" "$@"
SH
done
for command in omarchy-plugin-enable omarchy-plugin-disable omarchy-notification-send; do
for command in omarchy-plugin-enable omarchy-notification-send; do
cat >"$TMPDIR/bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
@@ -87,36 +82,16 @@ jq -e '
' "$clock/manifest.json" >/dev/null || fail "clock clone manifest is incorrect"
pass "clone updates identity without replacing the manifest"
grep -qx 'omarchy-bar-plugin replace omarchy.clock local.clock' "$CALLS" ||
fail "clone does not replace an active bar widget"
grep -qx 'omarchy-plugin-enable local.clock' "$CALLS" ||
fail "clone does not enable the editable copy"
grep -qx 'omarchy-notification-send -g 󰐱 Editing Cloned Plugin Original plugin has been replace by clone.' "$CALLS" ||
fail "clone does not notify that the editable clone is active"
jq -e '
any(.bar.layout.center[];
.id == "local.clock" and
.format == "dddd HH:mm" and
.formatAlt == "d MMMM \u0027W\u0027ww yyyy")
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
fail "clone does not preserve the active widget's placement and settings"
pass "clone switches bar widgets in place and confirms the editable clone"
pass "clone enables bar widgets and confirms the editable clone"
jq '.bar.layout.center += ["omarchy.keyboard-layout"]' \
"$TMPDIR/home/.config/omarchy/shell.json" >"$TMPDIR/shell.json"
mv "$TMPDIR/shell.json" "$TMPDIR/home/.config/omarchy/shell.json"
FAKE_CLONE_CONFIG='{
"bar": {
"layout": {
"left": [],
"center": ["omarchy.keyboard-layout"],
"right": []
}
}
}' clone_plugin omarchy.keyboard-layout >/dev/null
jq -e '
any(.bar.layout.center[]; .id == "local.keyboard-layout")
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
fail "clone does not replace a string-form bar entry"
pass "clone replaces legacy string-form bar entries"
clone_plugin omarchy.keyboard-layout >/dev/null
grep -qx 'omarchy-plugin-enable local.keyboard-layout' "$CALLS" ||
fail "clone does not enable a clone of a legacy string-form bar entry"
pass "clone enables clones of legacy string-form bar entries"
clone_plugin omarchy.menu >/dev/null
menu="$TMPDIR/home/.config/omarchy/plugins/local.menu"
@@ -130,29 +105,26 @@ jq -e '
.entryPoints.menu == "Menu.qml" and
.entryPoints.barWidget == "BarWidget.qml"
' "$menu/manifest.json" >/dev/null || fail "menu clone loses plugin kinds"
grep -qx 'omarchy-plugin-disable omarchy.menu' "$CALLS" ||
fail "clone leaves the built-in half of a multi-kind plugin enabled"
pass "clone preserves multi-kind plugins"
grep -qx 'omarchy-plugin-enable local.menu' "$CALLS" ||
fail "clone does not enable a multi-kind plugin"
pass "clone preserves and enables multi-kind plugins"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$TMPDIR/bin:$ROOT/bin:$PATH" \
remove_output=$(HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$TMPDIR/bin:$ROOT/bin:$PATH" \
FAKE_CALLS="$CALLS" OMARCHY_TEST_ROOT="$ROOT" \
omarchy-plugin-remove local.menu --yes >/dev/null
grep -qx 'omarchy-bar-plugin replace local.menu omarchy.menu' "$CALLS" &&
grep -qx 'omarchy-plugin-enable omarchy.menu' "$CALLS" ||
fail "removing a clone does not restore its built-in source"
pass "removing a clone restores its built-in source"
omarchy-plugin-remove local.menu --yes)
grep -qx 'omarchy-shell shell setPluginEnabled local.menu false' "$CALLS" ||
fail "removing an enabled clone does not disable it first"
grep -q 'Restored omarchy.menu.' <<<"$remove_output" ||
fail "removing a clone does not report its restored source"
pass "removing an enabled clone goes through plugin disable and reports its source"
clone_plugin omarchy.active-window >/dev/null
[[ -f $TMPDIR/home/.config/omarchy/plugins/local.active-window/ActiveWindow.qml ]] ||
fail "flat bar plugin clone is incomplete"
pass "flat bar plugins clone from adjacent manifests"
grep -qx 'omarchy-bar-plugin add local.active-window' "$CALLS" ||
grep -qx 'omarchy-plugin-enable local.active-window' "$CALLS" ||
fail "clone does not activate a bar widget whose source is absent"
jq -e '
any(.bar.layout.left[]; .id == "local.active-window")
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
fail "clone does not add the absent widget using its default placement"
pass "clone activates an absent bar widget"
clone_plugin omarchy.indicators >/dev/null
@@ -170,17 +142,13 @@ clone_plugin omarchy.tray >/dev/null
pass "flat bar plugins keep local script dependencies"
clone_plugin omarchy.bar >/dev/null
grep -qx 'omarchy-bar use local.bar' "$CALLS" ||
grep -qx 'omarchy-plugin-enable local.bar' "$CALLS" ||
fail "clone does not select a cloned bar"
jq -e '.bar.id == "local.bar"' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
fail "clone does not persist the cloned bar as active"
pass "clone switches full bars"
clone_plugin omarchy.background >/dev/null
grep -qx 'omarchy-plugin-enable local.background' "$CALLS" ||
fail "clone does not enable an ordinary cloned plugin"
grep -qx 'omarchy-plugin-disable omarchy.background' "$CALLS" ||
fail "clone does not disable the ordinary source plugin"
pass "clone switches ordinary plugins"
mkdir -p "$TMPDIR/home/.config/omarchy/plugins/acme.example"
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
mkdir -p "$TMPDIR/home" "$TMPDIR/bin"
calls="$TMPDIR/calls"
cat >"$TMPDIR/bin/omarchy-shell" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_CALLS"
printf 'ok\n'
SH
chmod +x "$TMPDIR/bin/omarchy-shell"
run_enable() {
HOME="$TMPDIR/home" \
OMARCHY_PATH="$ROOT" \
OMARCHY_TEST_CALLS="$calls" \
PATH="$TMPDIR/bin:$ROOT/bin:$PATH" \
omarchy-plugin-enable "$@"
}
run_enable omarchy.active-window --section right >/dev/null
grep -Fqx 'shell enablePlugin omarchy.active-window {"section":"right"}' "$calls" ||
fail "plugin enable did not combine activation and placement"
pass "plugin enable combines activation and placement in one shell mutation"
run_enable omarchy.clock --before omarchy.weather >/dev/null
grep -Fqx 'shell enablePlugin omarchy.clock {"before":"omarchy.weather"}' "$calls" ||
fail "plugin enable did not preserve relative placement"
pass "plugin enable forwards relative placement"
run_enable omarchy.dropbox >/dev/null
grep -Fqx 'shell enablePlugin omarchy.dropbox {}' "$calls" ||
fail "plugin enable did not use manifest-default placement"
pass "plugin enable leaves default placement to the registry"
if run_enable omarchy.bar --section right >/dev/null 2>&1; then
fail "plugin enable accepted placement for a full bar"
fi
pass "plugin enable rejects placement for full bars"
+2 -2
View File
@@ -296,7 +296,7 @@ if (( worst > screens - 1 )); then
fi
pass "each widget registers its IPC handler once per screen"
HOME="$test_home" OMARCHY_PATH="$test_root" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-bar-plugin" remove omarchy.audio
HOME="$test_home" OMARCHY_PATH="$test_root" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-plugin-disable" omarchy.audio
for _ in {1..80}; do
shell_config=$(shell_ipc shell listShellConfig 2>/dev/null || true)
@@ -313,7 +313,7 @@ done
jq -e 'all(.bar.layout.right[]; (.id // .) != "omarchy.audio")' <<<"$shell_config" >/dev/null || {
printf 'Shell config after reload:\n%s\n' "$shell_config" | jq . >&2
fail_with_log "bar remove reloads shell config"
fail_with_log "plugin disable reloads shell config"
}
jq -e 'all(.[]; .id != "omarchy.audio")' <<<"$geometry" >/dev/null || {