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 #!/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:group=bar
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle> # 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 reset | omarchy bar defaults | omarchy bar position top | omarchy bar transparent true # 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 set -euo pipefail
@@ -18,15 +18,26 @@ Usage: omarchy bar <command> [args...]
defaults Restore the default bar and service widgets defaults Restore the default bar and service widgets
position <top|bottom|left|right> Bar position position <top|bottom|left|right> Bar position
transparent <true|false|toggle> Bar transparency 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: Examples:
omarchy bar use local.neon-bar omarchy bar use local.neon-bar
omarchy bar reset omarchy bar move omarchy.media left
omarchy bar defaults omarchy bar move omarchy.clock --section center --index 0
omarchy bar position top omarchy bar set omarchy.clock format HH:mm
omarchy bar transparent true
USAGE USAGE
} }
@@ -38,6 +49,87 @@ bar_option_exists() {
' >/dev/null ' >/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 # -------------------------------------------------------------------- commands
cmd_use() { cmd_use() {
@@ -59,15 +151,48 @@ cmd_use() {
cmd_defaults() { cmd_defaults() {
(( $# == 0 )) || fail "defaults does not take arguments" (( $# == 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). local catalog
commit "$NORMALIZE | .bar = \$defaults[0].bar" --slurpfile defaults "$DEFAULTS_FILE" local optional_widgets="[]"
local service local service widget
catalog=$(omarchy-plugin-catalog)
for service in dropbox tailscale; do for service in dropbox tailscale; do
if "omarchy-installed-service-$service"; then 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 fi
done 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" echo "Restored the default Omarchy bar"
} }
@@ -94,6 +219,67 @@ cmd_transparent() {
fi 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 # --------------------------------------------------------------------- dispatch
command="${1:-}" command="${1:-}"
@@ -116,11 +302,11 @@ case "$command" in
transparent) transparent)
cmd_transparent "$@" cmd_transparent "$@"
;; ;;
plugin) move)
exec omarchy-bar-plugin "$@" cmd_move "$@"
;; ;;
add | move | remove | rm | drop | set | replace) set)
fail "bar widgets are managed with: omarchy bar plugin $command ..." cmd_set "$@"
;; ;;
-h | --help | help | "") -h | --help | help | "")
usage 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 omarchy-pkg-add dropbox dropbox-cli libappindicator-gtk3 python-gpgme nautilus-dropbox
echo "Adding Dropbox to the bar..." echo "Adding Dropbox to the bar..."
omarchy-bar-plugin add omarchy.dropbox omarchy-plugin-enable omarchy.dropbox
echo "Starting Dropbox..." echo "Starting Dropbox..."
uwsm-app -- dropbox-cli start &>/dev/null & 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 systemctl --user enable --now omarchy-tailscale-receive.service
echo -e "\nAdding Tailscale to the bar..." 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 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 fi
} }
place_bar_widget() { ENABLE_PLACEMENT=()
select_bar_widget_placement() {
local id="$1" local id="$1"
local section local section
local default_section local default_section
@@ -47,8 +49,7 @@ place_bar_widget() {
section=$(printf '%s\n' left center right | section=$(printf '%s\n' left center right |
gum choose --header="Place $id in which bar section?" --selected "$default_section") || return 0 gum choose --header="Place $id in which bar section?" --selected "$default_section") || return 0
[[ -n $section ]] || return 0 [[ -n $section ]] || return 0
omarchy-bar-plugin move "$id" --section "$section" >/dev/null && ENABLE_PLACEMENT=(--section "$section")
echo "Placed $id in the $section section"
} }
plugin_id_manifest() { plugin_id_manifest() {
@@ -151,20 +152,17 @@ if [[ -z $enable_after ]]; then
fi fi
if [[ $enable_after == true ]]; then if [[ $enable_after == true ]]; then
select_bar_widget_placement "$id"
discovered=0
for (( attempt = 0; attempt < 40; attempt++ )); do for (( attempt = 0; attempt < 40; attempt++ )); do
result=$(omarchy-shell shell setPluginEnabled "$id" true) if omarchy-plugin-list --json | jq -e --arg id "$id" 'any(.[]; .id == $id)' >/dev/null; then
[[ $result == "ok" ]] && break discovered=1
break
fi
sleep 0.05 sleep 0.05
done done
[[ $result == "ok" ]] || fail "plugin '$id' is not known" (( discovered )) || fail "plugin '$id' is not known"
if omarchy-plugin-catalog | jq -e --arg id "$id" ' omarchy-plugin-enable "$id" "${ENABLE_PLACEMENT[@]}"
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"
else else
echo "Enable it later with: omarchy plugin enable $id" echo "Enable it later with: omarchy plugin enable $id"
fi fi
+2 -4
View File
@@ -7,10 +7,8 @@
# Walks $OMARCHY_PATH/shell/plugins and ~/.config/omarchy/plugins, reads every # Walks $OMARCHY_PATH/shell/plugins and ~/.config/omarchy/plugins, reads every
# manifest.json / *.manifest.json, and emits one JSON object per plugin with # manifest.json / *.manifest.json, and emits one JSON object per plugin with
# computed fields (sourceDir, barWidgetPath, barPath, firstParty). This is the # computed fields (sourceDir, barWidgetPath, barPath, firstParty). This is the
# single source of truth that omarchy-bar (widget/option enumeration), # single source of truth used by bar selection and plugin enable/clone, so
# omarchy-bar-plugin (add validation), and omarchy-plugin-clone (source # those commands never re-implement manifest walking.
# enumeration) read, so the bar and plugin commands never re-implement
# manifest walking.
set -o pipefail set -o pipefail
+3 -36
View File
@@ -79,36 +79,6 @@ update_manifest() {
mv "$manifest.tmp" "$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() { usage() {
cat <<USAGE cat <<USAGE
Usage: omarchy plugin clone <source-id> Usage: omarchy plugin clone <source-id>
@@ -145,14 +115,11 @@ source_info=$(omarchy-plugin-catalog | jq -r --arg id "$source_id" '
| [ | [
.sourceDir, .sourceDir,
.manifestPath, .manifestPath,
(.name // .id), (.name // .id)
((.kinds | index("bar")) != null),
((.kinds | index("bar-widget")) != null),
(any(.kinds[]; . != "bar-widget"))
] | @tsv ] | @tsv
') ')
[[ -n $source_info ]] || fail "unknown built-in plugin: $source_id" [[ -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.}" new_id="local.${source_id#omarchy.}"
display_name="My $source_name" display_name="My $source_name"
@@ -182,7 +149,7 @@ for (( attempt = 0; attempt < 40; attempt++ )); do
sleep 0.05 sleep 0.05
done done
(( discovered )) || fail "cloned plugin '$new_id' was not discovered" (( 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 clone_complete=1
omarchy-notification-send -g 󰐱 \ omarchy-notification-send -g 󰐱 \
"Editing Cloned Plugin" \ "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" fail "'$id' is a bar; it replaces the bar in use rather than taking a place in one"
fi fi
result=$(omarchy-shell shell setPluginEnabled "$id" true) section=""
[[ $result == "ok" ]] || index=""
fail "plugin '$id' is not known; run: omarchy-shell shell rescanPlugins" 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 [[ -z $section || $section =~ ^(left|center|right)$ ]] ||
omarchy-bar-plugin move "$id" "$@" 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" echo "Enabled and moved $id"
elif omarchy-plugin-catalog | jq -e --arg id "$id" ' elif omarchy-plugin-catalog | jq -e --arg id "$id" '
any(.[]; .id == $id and (.kinds | index("bar"))) any(.[]; .id == $id and (.kinds | index("bar")))
+2 -20
View File
@@ -80,7 +80,6 @@ if [[ -n $shell_plugins ]]; then
fi fi
cloned_from="" cloned_from=""
restored_source=0
if [[ -f $target/manifest.json ]]; then if [[ -f $target/manifest.json ]]; then
cloned_from=$(jq -r '.omarchy.clonedFrom // empty' "$target/manifest.json") cloned_from=$(jq -r '.omarchy.clonedFrom // empty' "$target/manifest.json")
fi fi
@@ -93,24 +92,7 @@ else
confirm "Remove '$id'? The folder will be backed up." || fail "aborted" confirm "Remove '$id'? The folder will be backed up." || fail "aborted"
fi fi
if [[ -n $cloned_from ]]; then if [[ $was_enabled == "true" ]]; 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
omarchy-shell shell setPluginEnabled "$id" false >/dev/null omarchy-shell shell setPluginEnabled "$id" false >/dev/null
fi fi
@@ -134,7 +116,7 @@ fi
omarchy-shell shell rescanPlugins >/dev/null omarchy-shell shell rescanPlugins >/dev/null
if (( restored_source )); then if [[ -n $cloned_from && $was_enabled == "true" ]]; then
echo "Restored $cloned_from." echo "Restored $cloned_from."
elif [[ $was_enabled == "true" ]]; then elif [[ $was_enabled == "true" ]]; then
echo "Plugin was enabled and was unloaded from omarchy-shell." echo "Plugin was enabled and was unloaded from omarchy-shell."
+1 -1
View File
@@ -4,7 +4,7 @@
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
dropbox-cli stop 2>/dev/null || 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 omarchy-pkg-drop dropbox dropbox-cli libappindicator-gtk3 python-gpgme nautilus-dropbox
echo "" echo ""
+1 -1
View File
@@ -6,7 +6,7 @@
tailscale down 2>/dev/null || true tailscale down 2>/dev/null || true
systemctl --user disable --now omarchy-tailscale-receive.service 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 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-webapp-remove "Tailscale" 2>/dev/null || true
omarchy-pkg-drop tailscale 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 restart` | Restart a service/app | `omarchy restart shell` |
| `omarchy toggle` | Toggle feature on/off | `omarchy toggle nightlight` | | `omarchy toggle` | Toggle feature on/off | `omarchy toggle nightlight` |
| `omarchy theme` | Theme management | `omarchy theme set <name>` | | `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 plugin` | Manage/clone shell plugins | `omarchy plugin clone omarchy.clock` |
| `omarchy hook` | Install automation hooks | `omarchy hook install theme-set <script>` | | `omarchy hook` | Install automation hooks | `omarchy hook install theme-set <script>` |
| `omarchy install` | Install optional software / packages | `omarchy install docker dbs` | | `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 You can still install by hand: drop a plugin into
`~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then `~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then
`omarchy plugin enable <id>`. A bar widget starts in its declared default `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. replaces the one in use.
The lower-level IPC methods remain available through `omarchy-shell shell ...`. 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 | | `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code |
| `reloadConfig` | reload shell.json | | `reloadConfig` | reload shell.json |
| `setPluginEnabled <id> <"true"\|…>` | flip enabled bit (`ok` / `unknown`) | | `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 | | `listPlugins` | JSON of every discovered plugin |
`setPluginEnabled` takes a string; only literal `"true"` enables. `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`, …). 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.
5. Third-party enabled ⇔ present; for full bar options that means `bar.id`. 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. 6. `allowMultiple: true` in the manifest permits multiple instances.
7. `idle.screensaver` and `idle.lock` are seconds since user idle began. 7. `idle.screensaver` and `idle.lock` are seconds since user idle began.
8. `version: 1` is required. 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`. 2. `omarchy-shell shell rescanPlugins`.
3. `omarchy plugin enable <id>`. Bar widgets start in 3. `omarchy plugin enable <id>`. Bar widgets start in
`barWidget.defaultSection`, or in the center when it is omitted, and can be `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`, 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 enablePlugin <id> '{}'`, and `omarchy-shell shell listPlugins`.
The `omarchy plugin` commands wrap those calls and can also edit the persisted The `omarchy plugin` commands wrap those calls. `omarchy bar move` and
bar layout in `shell.json`. `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 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 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. like `Clock` and `AudioPanel` forward.
5. **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 full bar options, that means 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; `bar.id`; for bar widgets, plugin enable/disable adds/removes layout entries;
other plugin kinds are enabled with the shell IPC. First-party non-bar other plugin kinds are enabled the same way. First-party non-bar plugins
plugins are always enabled. are enabled unless listed in `disabledPlugins[]`.
6. **Multiple instances** are allowed when a manifest sets 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": ...}`
+4 -4
View File
@@ -3,10 +3,10 @@
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`.
First-party non-bar plugins are always enabled; `omarchy.bar` is the default First-party non-bar plugins are enabled unless listed in `disabledPlugins[]`;
bar option and becomes inactive only while a third-party `kind: "bar"` plugin is `omarchy.bar` is the default bar option and becomes inactive only while another
selected. Services and keep-loaded panels are mounted at startup; other panels, `kind: "bar"` plugin is selected. Services and keep-loaded panels are mounted
overlays, and menus are loaded on demand. 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.
+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 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): Example `shell.json` (bar subtree only shown):
@@ -178,6 +178,6 @@ namespaced ids.
Third-party widgets ship as separate plugins under Third-party widgets ship as separate plugins under
`~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json` `~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json`
declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See
[../../README.md](../../README.md) for the manifest schema. Enable, [../../README.md](../../README.md) for the manifest schema. Rescan, enable,
rescan, and place third-party plugins with `omarchy plugin enable`, and place third-party plugins with `omarchy-shell shell rescanPlugins`,
`omarchy-shell shell rescanPlugins`, and `omarchy bar plugin add`. `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 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 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 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 ## Providers
@@ -52,7 +52,7 @@ falls back to local stats only.
Settings live in the widget's entry in `~/.config/omarchy/shell.json`. The Settings live in the widget's entry in `~/.config/omarchy/shell.json`. The
top-level keys can be set with 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 | | 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: Numbers need `--json`, or they land in `shell.json` as strings:
```bash ```bash
omarchy bar plugin set omarchy.model-usage refreshIntervalSec 300 --json omarchy bar set omarchy.model-usage refreshIntervalSec 300 --json
omarchy bar plugin set omarchy.model-usage syncDir '~/Sync/model-usage' omarchy bar set omarchy.model-usage syncDir '~/Sync/model-usage'
``` ```
Per-provider settings are nested, and `set` writes its key literally rather 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): edit `shell.json` directly):
```bash ```bash
omarchy bar plugin set omarchy.model-usage providers '{ omarchy bar set omarchy.model-usage providers '{
"claude": { "claude": {
"enabled": true, "enabled": true,
"statsPath": "~/.claude/stats-cache.json", "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 ## 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 var installedPlugins: ({})
property int registryRevision: 0 property int registryRevision: 0
property bool scanning: false property bool scanning: false
property string lastEnableError: ""
signal pluginsChanged() signal pluginsChanged()
signal scanFinished() signal scanFinished()
@@ -171,6 +172,27 @@ QtObject {
return ["left", "center", "right"].indexOf(section) !== -1 ? section : "center" 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) { 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))
@@ -179,14 +201,8 @@ QtObject {
if (selectedBar === key) return { found: true, kind: "bar-option" } 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 barLocation = findBarLocation(config, key, "")
for (var s = 0; s < sections.length; s++) { if (barLocation.found) return barLocation
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 }
}
}
} }
if (Array.isArray(config.plugins)) { if (Array.isArray(config.plugins)) {
for (var j = 0; j < config.plugins.length; j++) { for (var j = 0; j < config.plugins.length; j++) {
@@ -196,13 +212,210 @@ QtObject {
return { found: false } 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 // Bar widgets use the default section declared in their manifest, falling
// back to center. Panels/overlays/menus/services go into the plugins[] array. // back to center. Panels/overlays/menus/services go into the plugins[] array.
// Built-ins are already loaded, so shell.json only ever records the // Built-ins are already loaded, so shell.json only ever records the
// deviation: an added third-party plugin in plugins[], a switched-off // deviation: an added third-party plugin in plugins[], a switched-off
// built-in in disabledPlugins[]. // 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)) var key = Util.canonicalWidgetId(String(id))
lastEnableError = ""
if (!shellConfigMutator) { if (!shellConfigMutator) {
console.warn("PluginRegistry.setEnabled called before shellConfigMutator wired") console.warn("PluginRegistry.setEnabled called before shellConfigMutator wired")
return false return false
@@ -216,17 +429,33 @@ QtObject {
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
var hasNonWidgetKind = manifest && Array.isArray(manifest.kinds) var hasNonWidgetKind = manifest && Array.isArray(manifest.kinds)
&& manifest.kinds.some(function(kind) { return kind !== "bar-widget" }) && 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) { shellConfigMutator(function(config) {
// Ensure shape exists. 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: [] } if (value && placement && (placement.before || placement.after)) {
if (!Array.isArray(config.plugins)) config.plugins = [] 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 (isBarOption) {
if (value) { if (value) {
config.bar.id = key config.bar.id = key
} else if (Util.canonicalWidgetId(String(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 return
} }
@@ -235,33 +464,45 @@ QtObject {
var location = findEntryLocation(config, key) var location = findEntryLocation(config, key)
if (value) { if (value) {
// Leave shell.json without the key once nothing is switched off, so a removeDisabled(config, key)
// 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
var entry = { id: key } var entry = { id: key }
if (isBarWidget) { var insertedWithPlacement = false
var section = defaultBarWidgetSection(manifest) if (!location.found && isBarWidget) {
if (!Array.isArray(config.bar.layout[section])) config.bar.layout[section] = [] var sourceLocation = clonedFrom ? findEntryLocation(config, clonedFrom) : { found: false }
config.bar.layout[section].push(entry) if (sourceLocation.kind === "bar") {
} else if (!isFirstParty) { 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) 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 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) else if (location.kind === "plugin") config.plugins.splice(location.index, 1)
// Dropping the layout entry is the whole story for a widget. Anything // 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. // else built-in loads by default, so switching it off has to be stated.
if (isFirstParty && (!isBarWidget || hasNonWidgetKind) && !isDisabled(config, key)) { if (isFirstParty && !isBarWidget) addDisabled(config, key)
if (!Array.isArray(config.disabledPlugins)) config.disabledPlugins = []
config.disabledPlugins.push(key)
}
}) })
if (lastEnableError) return false
registryRevision++ registryRevision++
pluginsChanged() pluginsChanged()
return true return true
+31 -9
View File
@@ -54,7 +54,6 @@ ShellRoot {
property var defaultsConfig: builtinShellConfig property var defaultsConfig: builtinShellConfig
property var shellConfig: builtinShellConfig property var shellConfig: builtinShellConfig
property bool suppressUserReload: false
property bool pluginReloading: false property bool pluginReloading: false
property bool pluginReloadPending: false property bool pluginReloadPending: false
@@ -107,7 +106,6 @@ ShellRoot {
} }
function persistShellConfig(nextConfig) { function persistShellConfig(nextConfig) {
suppressUserReload = true
var payload = JSON.parse(JSON.stringify(nextConfig)) var payload = JSON.parse(JSON.stringify(nextConfig))
payload.version = 1 payload.version = 1
shellConfig = payload shellConfig = payload
@@ -135,13 +133,7 @@ ShellRoot {
watchChanges: true watchChanges: true
atomicWrites: true atomicWrites: true
printErrors: false printErrors: false
onLoaded: { onLoaded: shell.applyShellConfig()
if (shell.suppressUserReload) {
shell.suppressUserReload = false
return
}
shell.applyShellConfig()
}
onLoadFailed: function(error) { shell.applyShellConfig() } onLoadFailed: function(error) { shell.applyShellConfig() }
onFileChanged: reload() onFileChanged: reload()
} }
@@ -908,6 +900,36 @@ ShellRoot {
return shell.pluginRegistry.setEnabled(id, enabled === "true") ? "ok" : "unknown" 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 { function listPlugins(): string {
var out = [] var out = []
var plugins = shell.pluginRegistry.installedPlugins var plugins = shell.pluginRegistry.installedPlugins
+62 -108
View File
@@ -208,14 +208,27 @@ pass "Hyprland bootstrap reloads cached Omarchy config modules"
TMPDIR=$(mktemp -d) TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/home/.config/omarchy" 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' cat >"$TMPDIR/home/.config/omarchy/shell.json" <<'JSON'
{ {
"version": 1, "version": 1,
"bar": { "bar": {
"layout": { "layout": {
"left": [{ "id": "omarchy.menu" }, { "id": "omarchy.workspaces" }], "left": [{ "id": "omarchy.menu" }, { "id": "omarchy.workspaces" }, { "id": "omarchy.active-window" }],
"center": [{ "id": "omarchy.clock" }, { "id": "omarchy.weather" }], "center": [{ "id": "omarchy.clock" }, { "id": "omarchy.weather" }, { "id": "omarchy.system-update" }, { "id": "omarchy.tailscale" }],
"right": [{ "id": "omarchy.tray" }, { "id": "omarchy.bluetooth" }] "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.microphone" }, { "id": "omarchy.bluetooth" }]
} }
}, },
"plugins": [] "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 jq -e '.bar.id == null' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config resets to built-in bar option" pass "shell config resets to built-in bar option"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add omarchy.tailscale HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar move omarchy.active-window right
jq -e ' grep -Fqx 'shell moveBarWidget omarchy.active-window {"section":"right"}' \
def ids: map(.id // .); "$TMPDIR/home/.local/state/omarchy/shell-ipc-calls"
.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "omarchy.tailscale"] pass "bar move accepts a positional target section"
' "$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-plugin add omarchy.active-window HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar move omarchy.active-window left
jq -e ' grep -Fqx 'shell moveBarWidget omarchy.active-window {"section":"left"}' \
def ids: map(.id // .); "$TMPDIR/home/.local/state/omarchy/shell-ipc-calls"
.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces", "omarchy.active-window"] pass "bar move can restore a widget with positional syntax"
' "$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-plugin add omarchy.media if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar move omarchy.active-window left --section right 2>/dev/null; then
jq -e ' fail "bar move accepted positional and flagged target sections"
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"
fi fi
pass "bar plugin move rejects conflicting target section syntax" pass "bar 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"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar position bottom HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar position bottom
jq -e ' 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 jq -e '.bar.transparent == false' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config toggles bar transparency" pass "shell config toggles bar transparency"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin drop omarchy.active-window HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar set omarchy.bluetooth enabled false --json
jq -e ' grep -Fqx 'shell setBarWidget omarchy.bluetooth enabled false {}' \
def ids: map(.id // .); "$TMPDIR/home/.local/state/omarchy/shell-ipc-calls"
(.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces"]) and pass "bar set accepts false JSON values"
(.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-plugin remove omarchy.system-update HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar set omarchy.bluetooth optional null --json
jq -e ' grep -Fqx 'shell setBarWidget omarchy.bluetooth optional null {}' \
def ids: map(.id // .); "$TMPDIR/home/.local/state/omarchy/shell-ipc-calls"
.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "omarchy.tailscale"] pass "bar set accepts null JSON values"
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config removes widgets with remove alias"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth enabled false --json if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar set omarchy.bluetooth broken '{' --json 2>/dev/null; then
jq -e ' fail "bar set accepted malformed JSON"
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"
fi 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 if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar set omarchy.bluetooth broken 'false null' --json 2>/dev/null; then
fail "bar plugin set accepted multiple JSON values" fail "bar set accepted multiple JSON values"
fi fi
pass "bar plugin set rejects multiple JSON values" pass "bar set rejects multiple JSON values"
mock_bin="$TMPDIR/mock-bin" mock_bin="$TMPDIR/mock-bin"
mkdir -p "$mock_bin" mkdir -p "$mock_bin"
@@ -400,7 +340,8 @@ SH
cat >"$mock_bin/omarchy-shell" <<'SH' cat >"$mock_bin/omarchy-shell" <<'SH'
#!/bin/bash #!/bin/bash
exit 0 [[ ${OMARCHY_TEST_SHELL_DOWN:-0} == "1" ]] && exit 1
printf 'ok\n'
SH SH
cat >"$mock_bin/omarchy-installed-service-dropbox" <<'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 HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$mock_path" OMARCHY_TEST_DROPBOX=1 OMARCHY_TEST_TAILSCALE=1 omarchy-bar defaults
jq -e ' jq -e '
def ids: map(.id // .); def ids: map(.id // .);
(.bar.layout.right | ids | index("omarchy.dropbox") != null) and (.bar.layout.center | ids) as $center |
(.bar.layout.center | ids | index("omarchy.tailscale") != null) (.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 ' "$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 HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$mock_path" OMARCHY_TEST_DROPBOX=0 OMARCHY_TEST_TAILSCALE=0 omarchy-refresh-shell
jq -e ' 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 HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$mock_path" OMARCHY_TEST_DROPBOX=1 OMARCHY_TEST_TAILSCALE=1 omarchy-refresh-shell
jq -e ' jq -e '
def ids: map(.id // .); def ids: map(.id // .);
(.bar.layout.right | ids | index("omarchy.dropbox") != null) and (.bar.layout.center | ids) as $center |
(.bar.layout.center | ids | index("omarchy.tailscale") != null) (.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 ' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
[[ -f $TMPDIR/home/.local/state/omarchy/restart-shell-called ]] || fail "shell refresh restarts shell" [[ -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 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" 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("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/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/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" }) var localWidget = manifest("local.first-widget", ["bar-widget"], { barWidget: "Widget.qml" })
localWidget.omarchy = { clonedFrom: "omarchy.first-widget" } localWidget.omarchy = { clonedFrom: "omarchy.first-widget" }
scan += block("thirdparty", "/third/local-widget", localWidget) 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/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" }))
@@ -100,13 +111,18 @@ ShellRoot {
registry.parseScanOutput(scan) registry.parseScanOutput(scan)
root.assertDeepEqual(pluginIds(), [ root.assertDeepEqual(pluginIds(), [
"local.bar",
"local.first-widget", "local.first-widget",
"local.grouped-panel",
"local.hybrid",
"omarchy.bar", "omarchy.bar",
"omarchy.first-widget", "omarchy.first-widget",
"omarchy.grouped-panel", "omarchy.grouped-panel",
"omarchy.hybrid", "omarchy.hybrid",
"third.bar", "third.bar",
"third.center-widget",
"third.panel", "third.panel",
"third.right-widget",
"third.widget" "third.widget"
], "registry merges valid first-party and third-party manifests") ], "registry merges valid first-party and third-party manifests")
@@ -148,9 +164,126 @@ ShellRoot {
registry.setEnabled("third.widget", false) registry.setEnabled("third.widget", false)
root.assertDeepEqual(root.config.bar.layout.left, [], "disabling bar widgets removes layout entry") 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) 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") 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) 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 = { root.config = {
version: 1, version: 1,
@@ -201,8 +334,8 @@ ShellRoot {
} }
registry.setEnabled("omarchy.hybrid", false) 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.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(root.config.disabledPlugins === undefined, "disabling a multi-kind widget records nothing else")
root.assertTrue(!registry.isEnabled("omarchy.hybrid"), "a disabled multi-kind built-in is not loadable") root.assertTrue(registry.isEnabled("omarchy.hybrid"), "a multi-kind built-in remains loadable without its widget")
var localBase = registry.pluginsDir + "/local.clock" var localBase = registry.pluginsDir + "/local.clock"
root.assertEqual(registry.localPluginIdForPath(localBase + "/BarWidget.qml"), "local.clock", "local clone changes are watched") 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') const pluginEnable = fs.readFileSync(path.join(root, 'bin/omarchy-plugin-enable'), 'utf8')
assert( assert(
/Now using \$id as the bar/.test(pluginEnable) /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' 'plugin enable reports a bar as replacing the one in use, whether enabled or freshly added'
) )
assert( 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 | find "$HOME/.config/omarchy/plugins" -mindepth 2 -maxdepth 2 -name manifest.json -print0 |
xargs -0 -r jq -s 'map({id: .id, enabled: true})' xargs -0 -r jq -s 'map({id: .id, enabled: true})'
fi fi
elif [[ $* == *"setPluginEnabled"* ]]; then
printf 'omarchy-shell %s\n' "$*" >>"$FAKE_CALLS"
printf 'ok\n'
fi fi
exit 0 exit 0
SH SH
for command in omarchy-bar omarchy-bar-plugin; 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"
exec "$OMARCHY_TEST_ROOT/bin/${0##*/}" "$@"
SH
done
for command in omarchy-plugin-enable omarchy-plugin-disable omarchy-notification-send; do
cat >"$TMPDIR/bin/$command" <<'SH' cat >"$TMPDIR/bin/$command" <<'SH'
#!/bin/bash #!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS" printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
@@ -87,36 +82,16 @@ jq -e '
' "$clock/manifest.json" >/dev/null || fail "clock clone manifest is incorrect" ' "$clock/manifest.json" >/dev/null || fail "clock clone manifest is incorrect"
pass "clone updates identity without replacing the manifest" pass "clone updates identity without replacing the manifest"
grep -qx 'omarchy-bar-plugin replace omarchy.clock local.clock' "$CALLS" || grep -qx 'omarchy-plugin-enable local.clock' "$CALLS" ||
fail "clone does not replace an active bar widget" 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" || 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" fail "clone does not notify that the editable clone is active"
jq -e ' pass "clone enables bar widgets and confirms the editable clone"
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"
jq '.bar.layout.center += ["omarchy.keyboard-layout"]' \ clone_plugin omarchy.keyboard-layout >/dev/null
"$TMPDIR/home/.config/omarchy/shell.json" >"$TMPDIR/shell.json" grep -qx 'omarchy-plugin-enable local.keyboard-layout' "$CALLS" ||
mv "$TMPDIR/shell.json" "$TMPDIR/home/.config/omarchy/shell.json" fail "clone does not enable a clone of a legacy string-form bar entry"
FAKE_CLONE_CONFIG='{ pass "clone enables clones of legacy string-form bar entries"
"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.menu >/dev/null clone_plugin omarchy.menu >/dev/null
menu="$TMPDIR/home/.config/omarchy/plugins/local.menu" menu="$TMPDIR/home/.config/omarchy/plugins/local.menu"
@@ -130,29 +105,26 @@ jq -e '
.entryPoints.menu == "Menu.qml" and .entryPoints.menu == "Menu.qml" and
.entryPoints.barWidget == "BarWidget.qml" .entryPoints.barWidget == "BarWidget.qml"
' "$menu/manifest.json" >/dev/null || fail "menu clone loses plugin kinds" ' "$menu/manifest.json" >/dev/null || fail "menu clone loses plugin kinds"
grep -qx 'omarchy-plugin-disable omarchy.menu' "$CALLS" || grep -qx 'omarchy-plugin-enable local.menu' "$CALLS" ||
fail "clone leaves the built-in half of a multi-kind plugin enabled" fail "clone does not enable a multi-kind plugin"
pass "clone preserves multi-kind plugins" 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" \ FAKE_CALLS="$CALLS" OMARCHY_TEST_ROOT="$ROOT" \
omarchy-plugin-remove local.menu --yes >/dev/null omarchy-plugin-remove local.menu --yes)
grep -qx 'omarchy-bar-plugin replace local.menu omarchy.menu' "$CALLS" && grep -qx 'omarchy-shell shell setPluginEnabled local.menu false' "$CALLS" ||
grep -qx 'omarchy-plugin-enable omarchy.menu' "$CALLS" || fail "removing an enabled clone does not disable it first"
fail "removing a clone does not restore its built-in source" grep -q 'Restored omarchy.menu.' <<<"$remove_output" ||
pass "removing a clone restores its built-in source" 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 clone_plugin omarchy.active-window >/dev/null
[[ -f $TMPDIR/home/.config/omarchy/plugins/local.active-window/ActiveWindow.qml ]] || [[ -f $TMPDIR/home/.config/omarchy/plugins/local.active-window/ActiveWindow.qml ]] ||
fail "flat bar plugin clone is incomplete" fail "flat bar plugin clone is incomplete"
pass "flat bar plugins clone from adjacent manifests" 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" 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" pass "clone activates an absent bar widget"
clone_plugin omarchy.indicators >/dev/null clone_plugin omarchy.indicators >/dev/null
@@ -170,17 +142,13 @@ clone_plugin omarchy.tray >/dev/null
pass "flat bar plugins keep local script dependencies" pass "flat bar plugins keep local script dependencies"
clone_plugin omarchy.bar >/dev/null 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" 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" pass "clone switches full bars"
clone_plugin omarchy.background >/dev/null clone_plugin omarchy.background >/dev/null
grep -qx 'omarchy-plugin-enable local.background' "$CALLS" || grep -qx 'omarchy-plugin-enable local.background' "$CALLS" ||
fail "clone does not enable an ordinary cloned plugin" 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" pass "clone switches ordinary plugins"
mkdir -p "$TMPDIR/home/.config/omarchy/plugins/acme.example" 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 fi
pass "each widget registers its IPC handler once per screen" 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 for _ in {1..80}; do
shell_config=$(shell_ipc shell listShellConfig 2>/dev/null || true) 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 || { 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 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 || { jq -e 'all(.[]; .id != "omarchy.audio")' <<<"$geometry" >/dev/null || {