#!/bin/bash # omarchy:summary=Mutate the user shell.json bar layout, active bar option, and bar settings # omarchy:group=bar # omarchy:args=show | layout [--json] | list [--json] [--all] | options [--json] | use | reset | add [plugin] [placement] | move [placement] | remove [plugin] [placement] | set [--json] [placement] | replace | position | transparent | 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 set -euo pipefail CONFIG_FILE="$HOME/.config/omarchy/shell.json" OMARCHY_ROOT="${OMARCHY_PATH:-}" DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json" usage() { cat < [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 Use a bar option as the active bar reset Return to the built-in Omarchy bar Edit layout: add [placement] Add a bar widget move [placement] [--from-section S --from-index N] Move a widget within/between sections remove [placement] (alias: rm, drop) Remove a widget set [--json] [placement] Set a per-widget option Other: replace Replace the first instance of a widget id position Bar position transparent Bar transparency settings Open the inline bar config panel Placement (for add/move/remove/set): --section Target section --index Insert/remove at index in the target section --before Insert before the first matching widget --after Insert after the first matching widget --from-section
Source section (move/remove/set) --from-index 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). 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 position top omarchy bar transparent true USAGE } fail() { echo "omarchy-bar: $*" >&2 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 fi } source_file() { local source="$CONFIG_FILE" if [[ ! -s $source ]]; then require_omarchy_path source="$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=' 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) if ! jq -S -e "$@" "$program" "$(source_file)" >"$_BAR_TMP"; then rm -f "$_BAR_TMP" _BAR_TMP="" fail "could not update shell config" fi mv "$_BAR_TMP" "$CONFIG_FILE" _BAR_TMP="" refresh_shell_config } section_valid() { [[ $1 =~ ^(left|center|right)$ ]] } 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) ' >/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 // "")" end ' "$(source_file)" | awk ' BEGIN { printf "%-8s %-5s %s\n", "SECTION", "INDEX", "WIDGET" } { printf "%-8s %-5s %s\n", $1, $2, $3 } ' } # --------------------------------------------------------------- commands: select cmd_use() { local plugin="${1:-}" [[ -n $plugin ]] || fail "bar option id is required" [[ $# -eq 1 ]] || fail "use takes a single bar option id" 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'" if [[ $plugin == "omarchy.bar" ]]; then commit "$JQ_DEFS $NORMALIZE | del(.bar.id)" else commit "$JQ_DEFS $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" } # --------------------------------------------------------------- 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 < 0 )) && [[ $1 != --* ]]; then id="$1" shift fi parse_placement "$@" local prog prog=$(cat <= 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 <