Simplify omarchy-bar into a bar-settings and bar-plugin split

The omarchy-bar command had grown three overlapping ways to inspect the
bar (show/layout/list/options, plus selected/active/available/widgets
aliases) and mixed layout mutation in with bar-level settings. Untangle
it into two focused commands:

- omarchy-bar keeps only the bar-level settings that write shell.json:
  use, reset, position, transparent, and settings. reset now delegates
  to `use omarchy.bar` rather than duplicating the del(.bar.id) write.
- omarchy-bar-plugin owns all layout mutation: add, move, remove, set,
  and replace, with the placement flags and jq resolve/anchor helpers.
  `omarchy bar plugin ...` routes here via the dispatcher.

Drop the inspection commands entirely: the layout is visible on the bar,
the config is shell.json, and widget/option ids come from
`omarchy plugin list`. Nothing consumed the show output programmatically
except tests. This also removes omarchy-bar-position, whose jq write was
a duplicate of `omarchy bar position`.

Strip environment-invariant guards (require_command, require_omarchy_path)
that defended against jq or OMARCHY_PATH being absent — neither happens on
a real system. Keep the user-input validation (--section/--index) and the
atomic shell.json write.

Update callers (service install/remove, refresh-shell, plugin-clone,
plugin enable), keybindings, the menu, tests, and docs to the new split.
This commit is contained in:
David Heinemeier Hansson
2026-07-17 16:28:17 -07:00
parent f8fec7cccf
commit 182330465a
18 changed files with 604 additions and 841 deletions
+37 -720
View File
@@ -1,9 +1,9 @@
#!/bin/bash
# omarchy:summary=Mutate the user shell.json bar layout, active bar option, and bar settings
# omarchy:summary=Set the active bar option, position, and transparency
# omarchy:group=bar
# omarchy:args=show | layout [--json] | list [--json] [--all] | options [--json] | use <plugin> | reset | defaults | add [plugin] [placement] | move <plugin> [placement] | remove [plugin] [placement] | set <plugin> <key> <value> [--json] [placement] | replace <old> <new> | position <top|bottom|left|right> | transparent <true|false> | settings
# omarchy:examples=omarchy bar show | omarchy bar list | omarchy bar add omarchy.tailscale | omarchy bar add omarchy.clock --section center --before omarchy.weather | omarchy bar move omarchy.clock --section center --index 0 | omarchy bar remove omarchy.tailscale | omarchy bar set omarchy.clock format HH:mm | omarchy bar use local.neon-bar | omarchy bar position top | omarchy bar transparent true
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false> | settings
# omarchy:examples=omarchy bar use local.neon-bar | omarchy bar reset | omarchy bar defaults | omarchy bar position top | omarchy bar transparent true
set -euo pipefail
@@ -15,58 +15,16 @@ usage() {
cat <<USAGE
Usage: omarchy bar <command> [args...]
Inspect:
show Current bar JSON
layout [--json] Current bar layout (section/index/widget)
list [--json] [--all] Addable bar widgets (alias: available, widgets)
options [--json] Bar replacement options
Select:
use <id> Use a bar option as the active bar
reset Return to the built-in Omarchy bar
defaults Restore the default bar and service widgets
Edit layout:
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
Other:
replace <old-id> <new-id> Replace the first instance of a widget id
position <top|bottom|left|right> Bar position
transparent <true|false> Bar transparency
settings Open the inline bar config panel
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).
Bar widgets are added, moved, removed, and configured with 'omarchy bar plugin'.
Examples:
omarchy bar show
omarchy bar layout
omarchy bar list
omarchy bar add omarchy.tailscale
omarchy bar add omarchy.clock --section center --before omarchy.weather
omarchy bar move omarchy.clock --section center --index 0
omarchy bar remove omarchy.tailscale
omarchy bar remove omarchy.tailscale --all
omarchy bar set omarchy.clock format HH:mm
omarchy bar set omarchy.indicators items '["Dnd","NightLight"]' --json
omarchy bar use local.neon-bar
omarchy bar reset
omarchy bar defaults
@@ -80,14 +38,6 @@ fail() {
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}
require_omarchy_path() {
[[ -n $OMARCHY_ROOT ]] || fail "OMARCHY_PATH is not set"
}
refresh_shell_config() {
if ! omarchy-shell shell reloadConfig >/dev/null 2>&1; then
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
@@ -95,80 +45,18 @@ refresh_shell_config() {
}
source_file() {
local source="$CONFIG_FILE"
if [[ ! -s $source ]]; then
require_omarchy_path
source="$DEFAULTS_FILE"
if [[ -s $CONFIG_FILE ]]; then
printf '%s\n' "$CONFIG_FILE"
else
printf '%s\n' "$DEFAULTS_FILE"
fi
[[ -s $source ]] || fail "could not find shell config or defaults"
printf '%s\n' "$source"
}
# jq defs shared across mutations. All `def`s come first so the pipeline that
# follows them stays valid jq. `entry_id` and the shape helpers are used by both
# the normalize pipeline and the resolve_target/resolve_source helpers.
JQ_DEFS='
# jq pipeline that normalizes shell.json into a well-shaped object with
# version=1, bar.layout.{left,center,right} arrays, and plugins array.
NORMALIZE='
def object_or_empty: if type == "object" then . else {} end;
def array_or_empty: if type == "array" then . else [] end;
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;
'
# jq pipeline that normalizes shell.json into a well-shaped object with
# version=1, bar.layout.{left,center,right} arrays, and plugins array. Every
# mutation pipes through this so downstream jq can assume structure.
NORMALIZE='
object_or_empty
| .version = 1
| .bar = (.bar | object_or_empty)
@@ -193,295 +81,21 @@ commit() {
shift
mkdir -p "$(dirname "$CONFIG_FILE")"
_BAR_TMP=$(mktemp)
if ! jq -S -e "$@" "$program" "$(source_file)" >"$_BAR_TMP"; then
rm -f "$_BAR_TMP"
_BAR_TMP=""
fail "could not update shell config"
fi
jq -S -e "$@" "$program" "$(source_file)" >"$_BAR_TMP" || fail "could not update shell config"
mv "$_BAR_TMP" "$CONFIG_FILE"
_BAR_TMP=""
refresh_shell_config
}
section_valid() {
[[ $1 =~ ^(left|center|right)$ ]]
}
# ------------------------------------------------------------------ validation
validate_section() {
section_valid "$1" || fail "section must be left, center, or right"
}
validate_index() {
[[ $1 =~ ^[0-9]+$ ]] || fail "index must be a non-negative integer"
}
# --------------------------------------------------------------------- catalog
catalog_json() {
require_omarchy_path
require_command jq
omarchy-plugin-catalog
}
widget_is_known() {
local id="$1"
catalog_json | jq -e --arg id "$id" '
any(.[]; (.kinds | index("bar-widget")) and .id == $id)
bar_option_exists() {
omarchy-plugin-catalog | jq -e --arg id "$1" '
any(.[]; (.kinds | index("bar")) and .barPath != null and .id == $id)
' >/dev/null
}
current_bar_ids_json() {
jq -c '
def array_or_empty: if type == "array" then . else [] end;
def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;
["left", "center", "right"] as $sections |
[ $sections[] as $section |
((.bar.layout[$section] // []) | array_or_empty)[] |
entry_id |
select(length > 0)
]
' "$(source_file)"
}
current_bar_entries_json() {
jq -c '
def array_or_empty: if type == "array" then . else [] end;
def entry_id($entry): if ($entry | type) == "object" then ($entry.id // "" | tostring) else ($entry | tostring) end;
["left", "center", "right"] as $sections |
[ $sections[] as $section |
((.bar.layout[$section] // []) | array_or_empty | to_entries[]) |
{ section: $section, index: .key, id: entry_id(.value) } |
select(.id | length > 0)
]
' "$(source_file)"
}
available_widgets_json() {
local include_all="$1"
local current_ids
current_ids=$(current_bar_ids_json)
catalog_json | jq --argjson currentIds "$current_ids" --argjson includeAll "$include_all" '
map(select((.kinds | index("bar-widget")) and .barWidgetPath != null))
| map({
id: .id,
name: (.barWidget.displayName // .name // .id),
description: (.barWidget.description // .description // ""),
category: (.barWidget.category // "Plugin"),
allowMultiple: ((.barWidget.allowMultiple // false) == true)
})
| unique_by(.id)
| map(. as $widget |
$widget + {
inBar: (($currentIds | index($widget.id)) != null),
addable: ($widget.allowMultiple or (($currentIds | index($widget.id)) == null))
})
| map(select($includeAll or .addable))
| sort_by(.category, .name, .id)
'
}
print_available_widgets() {
local include_all="false"
local json="false"
while (( $# > 0 )); do
case "$1" in
--all) include_all="true" ;;
--json) json="true" ;;
-h | --help) usage; exit 0 ;;
*) fail "unknown list option: $1" ;;
esac
shift
done
if [[ $json == "true" ]]; then
available_widgets_json "$include_all"
return
fi
available_widgets_json "$include_all" | jq -r '
.[] |
[
.id,
(if .inBar and .allowMultiple then "addable*" elif .inBar then "in-bar" else "addable" end),
.category,
.name,
.description
] | @tsv
' | awk -F '\t' '
BEGIN { printf "%-28s %-10s %-14s %-22s %s\n", "ID", "STATUS", "CATEGORY", "NAME", "DESCRIPTION" }
{ printf "%-28s %-10s %-14s %-22s %s\n", $1, $2, $3, $4, $5 }
'
}
# ------------------------------------------------------------------- bar options
current_bar_option_id() {
jq -r '(.bar.id // "omarchy.bar") | tostring' "$(source_file)"
}
available_bar_options_json() {
local current_id
current_id=$(current_bar_option_id)
catalog_json | jq --arg currentId "$current_id" '
map(select((.kinds | index("bar")) and .barPath != null))
| map({
id: .id,
name: (.name // .id),
description: (.description // ""),
firstParty: .firstParty,
active: (.id == $currentId)
})
| unique_by(.id)
| sort_by((if .id == "omarchy.bar" then 0 else 1 end), .name, .id)
'
}
print_bar_options() {
local json="false"
while (( $# > 0 )); do
case "$1" in
--json) json="true" ;;
-h | --help) usage; exit 0 ;;
*) fail "unknown options option: $1" ;;
esac
shift
done
if [[ $json == "true" ]]; then
available_bar_options_json
return
fi
available_bar_options_json | jq -r '
.[] |
[
.id,
(if .active then "active" else "available" end),
(if .firstParty then "built-in" else "plugin" end),
.name,
.description
] | @tsv
' | awk -F '\t' '
BEGIN { printf "%-32s %-10s %-8s %-22s %s\n", "ID", "STATUS", "SOURCE", "NAME", "DESCRIPTION" }
{ printf "%-32s %-10s %-8s %-22s %s\n", $1, $2, $3, $4, $5 }
'
}
bar_option_exists() {
available_bar_options_json | jq -e --arg id "$1" 'any(.[]; .id == $id)' >/dev/null
}
# --------------------------------------------------------------------- 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: inspect
cmd_show() {
(( $# == 0 )) || fail "show does not take arguments"
jq '.bar // {}' "$(source_file)"
}
cmd_layout() {
local json="false"
while (( $# > 0 )); do
case "$1" in
--json) json="true" ;;
-h | --help) usage; exit 0 ;;
*) fail "unknown layout option: $1" ;;
esac
shift
done
if [[ $json == "true" ]]; then
jq '.bar.layout // { left: [], center: [], right: [] }' "$(source_file)"
return
fi
jq -r '
(.bar.layout // { left: [], center: [], right: [] }) as $layout |
["left", "center", "right"][] as $section |
($layout[$section] // []) as $entries |
if ($entries | length) == 0 then
"\($section)\t-\t-"
else
$entries | to_entries[] | "\($section)\t\(.key)\t\(.value.id // "<missing>")"
end
' "$(source_file)" | awk '
BEGIN { printf "%-8s %-5s %s\n", "SECTION", "INDEX", "WIDGET" }
{ printf "%-8s %-5s %s\n", $1, $2, $3 }
'
}
# --------------------------------------------------------------- commands: select
# -------------------------------------------------------------------- commands
cmd_use() {
local plugin="${1:-}"
@@ -490,309 +104,36 @@ cmd_use() {
if [[ $plugin == "default" || $plugin == "built-in" ]]; then
plugin="omarchy.bar"
fi
bar_option_exists "$plugin" || fail "$plugin is not a known bar option; run 'omarchy bar options'"
bar_option_exists "$plugin" || fail "$plugin is not a known bar option; run 'omarchy plugin list'"
if [[ $plugin == "omarchy.bar" ]]; then
commit "$JQ_DEFS $NORMALIZE | del(.bar.id)"
commit "$NORMALIZE | del(.bar.id)"
else
commit "$JQ_DEFS $NORMALIZE | .bar.id = \$plugin" --arg plugin "$plugin"
commit "$NORMALIZE | .bar.id = \$plugin" --arg plugin "$plugin"
fi
echo "Using $plugin as the active bar"
}
cmd_reset() {
(( $# == 0 )) || fail "reset does not take arguments"
commit "$JQ_DEFS $NORMALIZE | del(.bar.id)"
echo "Reset to the built-in Omarchy bar"
}
cmd_defaults() {
local service
local services=(dropbox tailscale)
(( $# == 0 )) || fail "defaults does not take arguments"
require_omarchy_path
[[ -s $DEFAULTS_FILE ]] || fail "could not find bar defaults"
commit "$JQ_DEFS $NORMALIZE | .bar = \$defaults[0].bar" --slurpfile defaults "$DEFAULTS_FILE"
for service in "${services[@]}"; do
# Restore the whole default bar, then re-add widgets for services the user
# actually has installed (they aren't in the shipped default layout).
commit "$NORMALIZE | .bar = \$defaults[0].bar" --slurpfile defaults "$DEFAULTS_FILE"
local service
for service in dropbox tailscale; do
if "omarchy-installed-service-$service"; then
cmd_add "omarchy.$service"
omarchy-bar-plugin add "omarchy.$service"
fi
done
echo "Restored the default Omarchy bar"
}
# --------------------------------------------------------------- commands: edit
cmd_add() {
local id="${1:-}"
[[ -n $id ]] || fail "bar 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
require_omarchy_path
widget_is_known "$id" || fail "$id is not a known bar widget; run 'omarchy bar list' to see valid ids"
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.
local default_section="${PLACEMENT_SECTION:-right}"
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)) 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" \
--arg anchor "$(anchor_for_section "$default_section")"
else
local default_section="${PLACEMENT_SECTION:-right}"
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 'bar 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"
}
anchor_for_section() {
case "$1" in
left) printf 'omarchy.workspaces' ;;
center) printf 'omarchy.weather' ;;
right) printf 'omarchy.tray' ;;
*) fail "section must be left, center, or right" ;;
esac
}
cmd_move() {
local id="${1:-}"
[[ -n $id ]] || fail "bar move requires a widget id"
shift
parse_placement "$@"
local default_section="${PLACEMENT_SECTION:-}"
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 "$PLACEMENT_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 bar 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 "bar set requires a widget id"
[[ -n $key ]] || fail "bar set requires a setting key"
(( $# >= 3 )) || fail "bar 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 -e . <<<"$value" >/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 bar 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 "bar replace requires the source widget id"
[[ -n $new ]] || fail "bar replace requires the replacement widget id"
[[ $# -eq 2 ]] || fail "bar replace takes two widget ids"
local prog
prog=$(cat <<JQ
$JQ_DEFS $NORMALIZE
| resolve_source as \$source
| if \$source.entry | type != "object" then error("widget entry must be an object") else . end
| .bar.layout[\$source.section][\$source.index].id = \$new
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"
}
cmd_position() {
local position="${1:-}"
[[ -n $position ]] || fail "position is required"
[[ $# -eq 1 ]] || fail "position takes a single value"
[[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right"
commit "$JQ_DEFS $NORMALIZE | .bar.position = \$position" --arg position "$position"
commit "$NORMALIZE | .bar.position = \$position" --arg position "$position"
echo "Bar position set to $position"
}
@@ -801,7 +142,7 @@ cmd_transparent() {
[[ -n $transparent ]] || fail "transparent is required"
[[ $# -eq 1 ]] || fail "transparent takes a single value"
[[ $transparent =~ ^(true|false)$ ]] || fail "transparent must be true or false"
commit "$JQ_DEFS $NORMALIZE | .bar.transparent = \$transparent" --argjson transparent "$transparent"
commit "$NORMALIZE | .bar.transparent = \$transparent" --argjson transparent "$transparent"
echo "Bar transparency set to $transparent"
}
@@ -811,56 +152,32 @@ command="${1:-}"
[[ $# -gt 0 ]] && shift || true
case "$command" in
show | current)
cmd_show "$@"
;;
layout)
cmd_layout "$@"
;;
list | available | widgets)
print_available_widgets "$@"
;;
options | option)
print_bar_options "$@"
;;
selected | active)
(( $# == 0 )) || fail "$command does not take arguments"
current_bar_option_id
;;
use)
cmd_use "$@"
;;
reset)
cmd_reset "$@"
(( $# == 0 )) || fail "reset does not take arguments"
cmd_use omarchy.bar
;;
defaults)
cmd_defaults "$@"
;;
add)
cmd_add "$@"
;;
move)
cmd_move "$@"
;;
remove | rm | drop)
cmd_remove "$@"
;;
set)
cmd_set "$@"
;;
replace)
cmd_replace "$@"
;;
position)
cmd_position "$@"
;;
transparent)
cmd_transparent "$@"
;;
settings | edit)
settings)
(( $# == 0 )) || fail "settings does not take arguments"
exec omarchy-launch-bar-settings
;;
plugin)
exec omarchy-bar-plugin "$@"
;;
add | move | remove | rm | drop | set | replace)
fail "bar widgets are managed with: omarchy bar plugin $command ..."
;;
-h | --help | help | "")
usage
;;
+524
View File
@@ -0,0 +1,524 @@
#!/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 --section center --before omarchy.weather | 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
CONFIG_FILE="$HOME/.config/omarchy/shell.json"
OMARCHY_ROOT="${OMARCHY_PATH:-}"
DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json"
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 --section center --before omarchy.weather
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
}
fail() {
echo "omarchy-bar-plugin: $*" >&2
exit 1
}
refresh_shell_config() {
if ! omarchy-shell shell reloadConfig >/dev/null 2>&1; then
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
fi
}
source_file() {
if [[ -s $CONFIG_FILE ]]; then
printf '%s\n' "$CONFIG_FILE"
else
printf '%s\n' "$DEFAULTS_FILE"
fi
}
# jq defs shared across mutations. All `def`s come first so the pipeline that
# follows them stays valid jq. `entry_id` and the shape helpers are used by both
# the normalize pipeline and the resolve_target/resolve_source helpers.
JQ_DEFS='
def object_or_empty: if type == "object" then . else {} end;
def array_or_empty: if type == "array" then . else [] end;
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;
'
# jq pipeline that normalizes shell.json into a well-shaped object with
# version=1, bar.layout.{left,center,right} arrays, and plugins array. Every
# mutation pipes through this so downstream jq can assume structure.
NORMALIZE='
object_or_empty
| .version = 1
| .bar = (.bar | object_or_empty)
| .bar.layout = (.bar.layout | object_or_empty)
| .bar.layout.left = (.bar.layout.left | array_or_empty)
| .bar.layout.center = (.bar.layout.center | array_or_empty)
| .bar.layout.right = (.bar.layout.right | array_or_empty)
| .plugins = (.plugins | array_or_empty)
'
# Apply a jq program to the source file and atomically write the result to the
# user config, then refresh the running shell. Extra args after the program are
# forwarded to jq (e.g. --arg/--argjson).
_BAR_TMP=""
cleanup_bar_tmp() {
if [[ -n ${_BAR_TMP:-} ]]; then rm -f "$_BAR_TMP"; fi
}
trap cleanup_bar_tmp EXIT
commit() {
local program="$1"
shift
mkdir -p "$(dirname "$CONFIG_FILE")"
_BAR_TMP=$(mktemp)
jq -S -e "$@" "$program" "$(source_file)" >"$_BAR_TMP" || fail "could not update shell config"
mv "$_BAR_TMP" "$CONFIG_FILE"
_BAR_TMP=""
refresh_shell_config
}
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
}
# --------------------------------------------------------------------- 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:-right}"
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
parse_placement "$@"
local default_section="${PLACEMENT_SECTION:-}"
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 "$PLACEMENT_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 -e . <<<"$value" >/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"
[[ $# -eq 2 ]] || fail "replace takes two widget ids"
local prog
prog=$(cat <<JQ
$JQ_DEFS $NORMALIZE
| resolve_source as \$source
| if \$source.entry | type != "object" then error("widget entry must be an object") else . end
| .bar.layout[\$source.section][\$source.index].id = \$new
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:-}"
[[ $# -gt 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
-37
View File
@@ -1,37 +0,0 @@
#!/bin/bash
# omarchy:summary=Set bar position
# omarchy:args=<top|bottom|left|right>
# omarchy:examples=omarchy-bar-position top | omarchy bar position bottom
set -e
CONFIG_FILE="$HOME/.config/omarchy/shell.json"
DEFAULTS_FILE="$OMARCHY_PATH/config/omarchy/shell.json"
position=$1
if [[ ! $position =~ ^(top|bottom|left|right)$ ]]; then
echo "Usage: omarchy-bar-position top|bottom|left|right" >&2
exit 1
fi
mkdir -p "$(dirname "$CONFIG_FILE")"
tmp=$(mktemp)
# Seed from the default shell.json when the user has no shell.json yet (or it's
# malformed). Writing just `{ bar: { position } }` would leave the bar with
# no layout, since the shell treats any valid user config as authoritative
# and does not deep-merge defaults.
if [[ -s $CONFIG_FILE ]] && jq -e 'type == "object"' "$CONFIG_FILE" >/dev/null 2>&1; then
source="$CONFIG_FILE"
else
source="$DEFAULTS_FILE"
fi
jq --arg position "$position" '
.version = 1 |
.bar = ((.bar // {}) | if type == "object" then . else {} end) |
.bar.position = $position
' "$source" >"$tmp"
mv "$tmp" "$CONFIG_FILE"
+1 -1
View File
@@ -6,7 +6,7 @@ echo "Installing all dependencies..."
omarchy-pkg-add dropbox dropbox-cli libappindicator-gtk3 python-gpgme nautilus-dropbox
echo "Adding Dropbox to the bar..."
omarchy-bar add omarchy.dropbox
omarchy-bar-plugin add omarchy.dropbox
echo "Starting Dropbox..."
uwsm-app -- dropbox-cli start &>/dev/null &
+1 -1
View File
@@ -14,6 +14,6 @@ echo -e "\nAllowing $USER to manage Tailscale..."
sudo tailscale set --operator="$USER"
echo -e "\nAdding Tailscale to the bar..."
omarchy-bar add omarchy.tailscale
omarchy-bar-plugin add 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
+4 -31
View File
@@ -2,8 +2,8 @@
# omarchy:summary=Manage Omarchy shell plugins and bar widgets
# omarchy:group=plugin
# omarchy:args=<list|rescan|enable|disable|add|update|remove|clone|edit|validate|bar> [...]
# omarchy:examples=omarchy plugin list | omarchy plugin add https://github.com/acme/omarchy-weather.git | omarchy plugin update --all | omarchy plugin clone omarchy.clock local.clock | omarchy plugin bar settings
# omarchy:args=<list|rescan|enable|disable|add|update|remove|clone|edit|validate> [...]
# omarchy:examples=omarchy plugin list | omarchy plugin add https://github.com/acme/omarchy-weather.git | omarchy plugin update --all | omarchy plugin clone omarchy.clock local.clock
set -euo pipefail
@@ -36,10 +36,7 @@ Make your own:
edit [id] Open a user plugin directory in a shell
validate <plugin-folder> Check a plugin's manifest (for authors)
Bar commands (delegate to 'omarchy bar'; run 'omarchy bar --help' for details):
bar settings Open the inline bar config panel
bar list|layout|options|use|reset|add|move|remove|set|position|transparent
widget|widgets Alias of 'bar list'
Bar widgets are placed in the layout with 'omarchy bar plugin'.
Examples:
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable
@@ -169,7 +166,7 @@ plugin_enabled() {
[[ $result == "ok" ]] || fail "plugin '$id' is not known; run: omarchy plugin rescan"
if [[ $enabled == "true" && $# -gt 0 ]]; then
omarchy-bar move "$id" "$@"
omarchy-bar-plugin move "$id" "$@"
echo "Enabled and moved $id"
elif [[ $enabled == "true" ]]; then
echo "Enabled $id"
@@ -460,26 +457,6 @@ plugin_edit() {
printf '%s\n' "$dir"
}
# ---------------------------------------------------------------- bar
bar_command() {
local command="${1:-list}"
[[ $# -gt 0 ]] && shift || true
case "$command" in
edit | settings)
(( $# == 0 )) || fail "bar settings does not take arguments"
exec omarchy-launch-bar-settings
;;
-h | --help | help)
usage
;;
*)
exec omarchy-bar "$command" "$@"
;;
esac
}
command="${1:-}"
case "$command" in
list | ls)
@@ -522,10 +499,6 @@ validate)
shift
exec omarchy-plugin-validate "$@"
;;
bar | widget | widgets)
shift
bar_command "$@"
;;
-h | --help | help | "")
usage
;;
+4 -3
View File
@@ -7,9 +7,10 @@
# Walks $OMARCHY_PATH/shell/plugins and ~/.config/omarchy/plugins, reads every
# manifest.json / *.manifest.json, and emits one JSON object per plugin with
# computed fields (sourceDir, barWidgetPath, barPath, firstParty). This is the
# single source of truth that omarchy-bar (widget/option enumeration and add
# validation) and omarchy-plugin-clone (source enumeration) read, so the bar
# and plugin commands never re-implement manifest walking.
# single source of truth that omarchy-bar (widget/option enumeration),
# omarchy-bar-plugin (add validation), and omarchy-plugin-clone (source
# enumeration) read, so the bar and plugin commands never re-implement
# manifest walking.
set -o pipefail
+2 -2
View File
@@ -341,10 +341,10 @@ clone_command() {
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
if [[ $replace == "true" ]]; then
omarchy-bar replace "$source_id" "$new_id"
omarchy-bar-plugin replace "$source_id" "$new_id"
echo "Cloned $source_id to $new_id and replaced the first bar instance"
elif [[ $add == "true" ]]; then
omarchy-bar add "$new_id" "${placement[@]}"
omarchy-bar-plugin add "$new_id" "${placement[@]}"
echo "Cloned $source_id to $new_id"
elif [[ $use_bar == "true" ]]; then
omarchy-bar use "$new_id"
+1 -1
View File
@@ -4,7 +4,7 @@
# omarchy:requires-sudo=true
dropbox-cli stop 2>/dev/null || true
omarchy-bar drop omarchy.dropbox
omarchy-bar-plugin remove omarchy.dropbox
omarchy-pkg-drop dropbox dropbox-cli libappindicator-gtk3 python-gpgme nautilus-dropbox
echo ""
+1 -1
View File
@@ -5,7 +5,7 @@
tailscale down 2>/dev/null || true
sudo systemctl disable --now tailscaled.service 2>/dev/null || true
omarchy-bar drop omarchy.tailscale
omarchy-bar-plugin remove omarchy.tailscale
omarchy-webapp-remove "Tailscale" 2>/dev/null || true
omarchy-pkg-drop tailscale
+4 -4
View File
@@ -12,10 +12,10 @@ o.bind("SUPER + ALT + K", "Show Tmux key bindings", "omarchy-menu-tmux-keybindin
o.bind("XF86Calculator", "Calculator", "gnome-calculator")
o.bind_toggle("SUPER + SHIFT + SPACE", "Toggle top bar", "bar")
o.bind("SUPER + SHIFT + CTRL + UP", "Move bar to top", "omarchy-bar-position top")
o.bind("SUPER + SHIFT + CTRL + DOWN", "Move bar to bottom", "omarchy-bar-position bottom")
o.bind("SUPER + SHIFT + CTRL + LEFT", "Move bar to left", "omarchy-bar-position left")
o.bind("SUPER + SHIFT + CTRL + RIGHT", "Move bar to right", "omarchy-bar-position right")
o.bind("SUPER + SHIFT + CTRL + UP", "Move bar to top", "omarchy-bar position top")
o.bind("SUPER + SHIFT + CTRL + DOWN", "Move bar to bottom", "omarchy-bar position bottom")
o.bind("SUPER + SHIFT + CTRL + LEFT", "Move bar to left", "omarchy-bar position left")
o.bind("SUPER + SHIFT + CTRL + RIGHT", "Move bar to right", "omarchy-bar position right")
o.bind("SUPER + CTRL + SPACE", "Background switcher", "omarchy-menu toggle background")
o.bind("SUPER + SHIFT + CTRL + SPACE", "Theme menu", "omarchy-menu toggle theme")
o.bind("SUPER + BACKSPACE", "Toggle window transparency", "omarchy-hyprland-window-transparency-toggle")
+4 -4
View File
@@ -95,10 +95,10 @@
"style.screensaver": {"icon":"󱄄","label":"Screensaver","keywords":"branding"},
"style.about": {"icon":"","label":"About","keywords":"branding"},
"style.install": {"icon":"󰉉","label":"Install","keywords":"theme background font"},
"style.bar.top": {"icon":"󰁝","label":"Top","keywords":"position","action":"omarchy-bar-position top"},
"style.bar.bottom": {"icon":"󰁅","label":"Bottom","keywords":"position","action":"omarchy-bar-position bottom"},
"style.bar.left": {"icon":"󰁍","label":"Left","keywords":"position","action":"omarchy-bar-position left"},
"style.bar.right": {"icon":"󰁔","label":"Right","keywords":"position","action":"omarchy-bar-position right"},
"style.bar.top": {"icon":"󰁝","label":"Top","keywords":"position","action":"omarchy-bar position top"},
"style.bar.bottom": {"icon":"󰁅","label":"Bottom","keywords":"position","action":"omarchy-bar position bottom"},
"style.bar.left": {"icon":"󰁍","label":"Left","keywords":"position","action":"omarchy-bar position left"},
"style.bar.right": {"icon":"󰁔","label":"Right","keywords":"position","action":"omarchy-bar position right"},
"style.about.text": {"icon":"","label":"Edit Text","keywords":"branding","action":"omarchy-branding-about text"},
"style.about.image": {"icon":"","label":"Set From Image","keywords":"branding","action":"omarchy-branding-about image"},
"style.about.default": {"icon":"","label":"Restore Default","keywords":"branding","action":"omarchy-branding-about reset"},
+1 -1
View File
@@ -60,7 +60,7 @@ arguments — add `--yes` to skip every prompt (the path for scripts and agents)
You can still install by hand: drop a plugin into
`~/.config/omarchy/plugins/<id>/`, run `omarchy plugin rescan`, then
`omarchy plugin enable <id>` (bar widgets also need `omarchy bar add <id>`;
`omarchy plugin enable <id>` (bar widgets also need `omarchy bar plugin add <id>`;
full bar replacements are selected with `omarchy bar use <id>`).
The lower-level IPC methods remain available through `omarchy-shell shell ...`.
+1 -1
View File
@@ -131,7 +131,7 @@ You can still drop a plugin in without git:
1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json`
plus the QML referenced from its `entryPoints`.
2. `omarchy plugin rescan`.
3. `omarchy plugin enable <id>` (bar widgets also need `omarchy bar add <id>`; full bar replacements are selected with `omarchy bar use <id>`).
3. `omarchy plugin enable <id>` (bar widgets also need `omarchy bar plugin add <id>`; full bar replacements are selected with `omarchy bar use <id>`).
The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`,
`omarchy-shell shell setPluginEnabled <id> true`, and `omarchy-shell shell listPlugins`.
+3 -3
View File
@@ -10,13 +10,13 @@ the shell for its whole session.
- `widgets/` holds simple first-party bar widgets with sibling manifests.
- Feature plugins such as `../panels/audio/`, `../panels/network/`, `../panels/power/`, and `../model-usage/` provide richer popup bar plugins.
- The bar receives its config from the host shell as a `barConfig` property; the host loads it from `~/.config/omarchy/shell.json` (or `config/omarchy/shell.json` when the user has no file).
- `omarchy-bar-position` updates only the user shell.json file.
- `omarchy bar position` updates only the user shell.json file.
## Customizing
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 inline bar config panel, `omarchy bar ...`, or by editing shell.json directly, your file is canonical — there is no deep-merge.
Open quick position and transparency controls with `omarchy bar settings` (or run `omarchy-launch-bar-settings`). You can also hover the centered clock module to reveal the inline bar config button. For scriptable widget changes, use `omarchy bar list`, `omarchy bar add`, `omarchy bar move`, `omarchy bar remove`, and `omarchy bar set`. Double-left-click empty center-bar space to toggle bar transparency.
Open quick position and transparency controls with `omarchy bar settings` (or run `omarchy-launch-bar-settings`). You can also hover the centered clock module to reveal the inline bar config button. 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`). Double-left-click empty center-bar space to toggle bar transparency.
Example `shell.json` (bar subtree only shown):
@@ -181,4 +181,4 @@ Third-party widgets ship as separate plugins under
declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See
[../../README.md](../../README.md) for the manifest schema. Enable,
rescan, and place third-party plugins with `omarchy plugin enable`,
`omarchy plugin rescan`, and `omarchy bar add`.
`omarchy plugin rescan`, and `omarchy bar plugin add`.
+1 -1
View File
@@ -35,4 +35,4 @@ Renders the Tailscale mark natively as a theme-colored 3×3 dot grid, matching t
## Add to the bar
This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy bar 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 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.
+14 -29
View File
@@ -208,11 +208,10 @@ cat >"$TMPDIR/home/.config/omarchy/plugins/local.demo-bar/manifest.json" <<'JSON
JSON
touch "$TMPDIR/home/.config/omarchy/plugins/local.demo-bar/Bar.qml"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar options --json | jq -e '
any(.[]; .id == "omarchy.bar" and .active == true) and
any(.[]; .id == "local.demo-bar" and .active == false)
' >/dev/null
pass "shell config lists bar options"
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar use local.nonexistent-bar 2>/dev/null; then
fail "bar use accepted an unknown bar option"
fi
pass "bar use rejects an unknown bar option"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar use local.demo-bar
jq -e '.bar.id == "local.demo-bar"' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
@@ -222,52 +221,38 @@ HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar reset
jq -e '.bar.id == null' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config resets to built-in bar option"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar add omarchy.tailscale
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add omarchy.tailscale
jq -e '
def ids: map(.id // .);
.bar.layout.right | ids == ["omarchy.tray", "omarchy.tailscale", "omarchy.bluetooth"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config appends widgets to right by default"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar add omarchy.active-window left
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add omarchy.active-window left
jq -e '
def ids: map(.id // .);
.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces", "omarchy.active-window"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config appends left widgets after workspaces"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar add omarchy.system-update center
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"]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config appends center widgets after weather"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar add omarchy.microphone right
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.tailscale", "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 show | jq -e '
def ids: map(.id // .);
(.layout.right | ids == ["omarchy.tray", "omarchy.microphone", "omarchy.tailscale", "omarchy.bluetooth"]) and
has("version") | not
' >/dev/null
pass "shell config shows only bar json"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar list --json | jq -e '
any(.[]; .id == "omarchy.keyboard-layout" and .addable == true and .inBar == false) and
all(.[]; .id != "omarchy.tailscale")
' >/dev/null
pass "shell config lists addable bar widgets"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar list --json --all | jq -e '
any(.[]; .id == "omarchy.tailscale" and .inBar == true and .addable == false) and
any(.[]; .id == "omarchy.indicators" and .addable == true)
' >/dev/null
pass "shell config list --all includes current widget status"
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
jq -e '
@@ -284,7 +269,7 @@ jq -e '
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config sets bar transparency"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar drop omarchy.active-window
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin drop omarchy.active-window
jq -e '
def ids: map(.id // .);
(.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces"]) and
@@ -293,7 +278,7 @@ jq -e '
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "shell config drops widgets from any section"
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar remove omarchy.system-update
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin remove omarchy.system-update
jq -e '
def ids: map(.id // .);
.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather"]
+1 -1
View File
@@ -220,7 +220,7 @@ for panel_id in omarchy.audio omarchy.bluetooth omarchy.monitor omarchy.network
done
pass "direct panel IPC opens and closes default panels"
HOME="$test_home" OMARCHY_PATH="$test_root" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-bar" remove omarchy.audio
HOME="$test_home" OMARCHY_PATH="$test_root" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-bar-plugin" remove omarchy.audio
for _ in {1..80}; do
shell_config=$(shell_ipc shell listShellConfig 2>/dev/null || true)