From 055ffaccd43d232f1180d03acad9c275df6b014f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 2 Jul 2026 11:10:24 -0700 Subject: [PATCH] Split omarchy-plugin into siblings and consolidate bar config into omarchy-bar omarchy-plugin mixed plugin lifecycle, clone/edit, and bar-layout mutation. The bar layout was mutated by two separate engines (an embedded Python mutate_bar here and jq in omarchy-config-shell-bar), each with its own manifest walker. - omarchy-plugin is now a ~220-line router; clone and edit move to omarchy-plugin-clone and omarchy-plugin-edit, matching the existing plugin-{source,add,update,remove,available,validate,scan} sibling pattern - omarchy-config-shell-bar is replaced by omarchy-bar, the single bar engine (show/layout/list/options/use/reset/add/move/remove/set/replace/position/ transparent/settings) with one jq pipeline; the Python mutate_bar and its python3 dependency are gone - omarchy-plugin-catalog is the shared manifest scanner used by omarchy-bar (widget/option enumeration, add validation) and omarchy-plugin-clone (source enumeration), replacing three find|jq re-implementations - service install/remove and refresh-shell call omarchy-bar; docs and tests updated to the new command surface --- bin/omarchy | 1 + bin/omarchy-bar | 844 ++++++++++++++++++ bin/omarchy-config-shell-bar | 601 ------------- bin/omarchy-install-service-dropbox | 2 +- bin/omarchy-install-service-tailscale | 2 +- bin/omarchy-plugin | 1011 +--------------------- bin/omarchy-plugin-catalog | 67 ++ bin/omarchy-plugin-clone | 377 ++++++++ bin/omarchy-plugin-edit | 157 ++++ bin/omarchy-refresh-shell | 4 +- bin/omarchy-remove-service-dropbox | 2 +- bin/omarchy-remove-service-tailscale | 2 +- docs/omarchy-shell.md | 4 +- shell/README.md | 2 +- shell/plugins/bar/README.md | 6 +- shell/plugins/panels/tailscale/README.md | 2 +- test/shell.d/config-test.sh | 40 +- test/shell.d/runtime-smoke-test.sh | 6 +- 18 files changed, 1494 insertions(+), 1636 deletions(-) create mode 100755 bin/omarchy-bar delete mode 100755 bin/omarchy-config-shell-bar create mode 100755 bin/omarchy-plugin-catalog create mode 100755 bin/omarchy-plugin-clone create mode 100755 bin/omarchy-plugin-edit diff --git a/bin/omarchy b/bin/omarchy index 4505f568..945492dd 100755 --- a/bin/omarchy +++ b/bin/omarchy @@ -27,6 +27,7 @@ declare -A BINARY_TO_KEY declare -A GROUP_DESCRIPTIONS GROUP_DESCRIPTIONS[audio]="Audio input and output controls" +GROUP_DESCRIPTIONS[bar]="Omarchy shell bar layout and settings" GROUP_DESCRIPTIONS[battery]="Battery status helpers" GROUP_DESCRIPTIONS[bluetooth]="Bluetooth device controls" GROUP_DESCRIPTIONS[branch]="Omarchy git branch management" diff --git a/bin/omarchy-bar b/bin/omarchy-bar new file mode 100755 index 00000000..261dbf21 --- /dev/null +++ b/bin/omarchy-bar @@ -0,0 +1,844 @@ +#!/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 < | reset | list [--json] [--all] | add [plugin] [left|center|right] | remove [plugin] | drop [plugin] | position | transparent -# omarchy:examples=omarchy config shell bar show | omarchy config shell bar options | omarchy config shell bar use local.neon-bar | omarchy config shell bar reset | omarchy config shell bar list | omarchy config shell bar add omarchy.tailscale | omarchy config shell bar position top | omarchy config shell bar transparent true - -set -euo pipefail - -CONFIG_FILE="$HOME/.config/omarchy/shell.json" -OMARCHY_ROOT="${OMARCHY_PATH:-}" -DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json" - -usage() { - echo "Usage: omarchy-config-shell-bar show | options [--json] | use | reset | list [--json] [--all] | add [plugin] [left|center|right] | remove [plugin] | drop [plugin] | position | transparent " >&2 -} - -fail() { - echo "omarchy-config-shell-bar: $*" >&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() { - local source="$CONFIG_FILE" - - if [[ ! -s $source ]]; then - [[ -n $OMARCHY_ROOT ]] || fail "OMARCHY_PATH is not set" - source="$DEFAULTS_FILE" - fi - - [[ -s $source ]] || fail "could not find shell config or defaults" - printf '%s\n' "$source" -} - -bar_widget_manifest_paths() { - { - if [[ -n $OMARCHY_ROOT && -d $OMARCHY_ROOT/shell/plugins ]]; then - find "$OMARCHY_ROOT/shell/plugins" -mindepth 2 -maxdepth 4 -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null - fi - - if [[ -d $HOME/.config/omarchy/plugins ]]; then - find -L "$HOME/.config/omarchy/plugins" -mindepth 2 -maxdepth 2 -type f -name manifest.json 2>/dev/null - fi - } | sort -u -} - -bar_option_manifest_paths() { - { - if [[ -n $OMARCHY_ROOT && -d $OMARCHY_ROOT/shell/plugins ]]; then - find "$OMARCHY_ROOT/shell/plugins" -mindepth 2 -maxdepth 4 -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null - fi - - if [[ -d $HOME/.config/omarchy/plugins ]]; then - find -L "$HOME/.config/omarchy/plugins" -mindepth 2 -maxdepth 2 -type f -name manifest.json 2>/dev/null - fi - } | sort -u -} - -current_bar_option_id() { - jq -r '(.bar.id // "omarchy.bar") | tostring' "$(source_file)" -} - -available_bar_options_json() { - local current_id - local manifest_paths=() - - current_id=$(current_bar_option_id) - mapfile -t manifest_paths < <(bar_option_manifest_paths) - - if (( ${#manifest_paths[@]} == 0 )); then - printf '[]\n' - return - fi - - jq -s --arg currentId "$current_id" ' - map( - select(((.kinds // []) | index("bar")) and ((.entryPoints.bar // "") != "")) | - { - id: (.id // ""), - name: (.name // .id // ""), - description: (.description // ""), - firstParty: ((.id // "") | startswith("omarchy.")), - active: ((.id // "") == $currentId) - } | - select(.id != "") - ) | - unique_by(.id) | - sort_by((if .id == "omarchy.bar" then 0 else 1 end), .name, .id) - ' "${manifest_paths[@]}" -} - -print_bar_options() { - local json="false" - - while (( $# > 0 )); do - case "$1" in - --json) - json="true" - ;; - -h|--help) - usage - exit 0 - ;; - *) - fail "unknown options option: $1" - ;; - esac - shift - done - - if [[ $json == "true" ]]; then - available_bar_options_json - return - fi - - available_bar_options_json | jq -r ' - .[] | - [ - .id, - (if .active then "active" else "available" end), - (if .firstParty then "built-in" else "plugin" end), - .name, - .description - ] | @tsv - ' | awk -F '\t' ' - BEGIN { printf "%-32s %-10s %-8s %-22s %s\n", "ID", "STATUS", "SOURCE", "NAME", "DESCRIPTION" } - { printf "%-32s %-10s %-8s %-22s %s\n", $1, $2, $3, $4, $5 } - ' -} - -bar_option_exists() { - local id="$1" - available_bar_options_json | jq -e --arg id "$id" 'any(.[]; .id == $id)' >/dev/null -} - -current_bar_ids_json() { - 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 - local manifest_paths=() - - current_ids=$(current_bar_ids_json) - mapfile -t manifest_paths < <(bar_widget_manifest_paths) - - if (( ${#manifest_paths[@]} == 0 )); then - printf '[]\n' - return - fi - - jq -s --argjson currentIds "$current_ids" --argjson includeAll "$include_all" ' - map( - select(((.kinds // []) | index("bar-widget")) and ((.entryPoints.barWidget // "") != "")) | - { - id: (.id // ""), - name: (.barWidget.displayName // .name // .id // ""), - description: (.barWidget.description // .description // ""), - category: (.barWidget.category // "Plugin"), - allowMultiple: (.barWidget.allowMultiple == true) - } | - select(.id != "") - ) | - 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) - ' "${manifest_paths[@]}" -} - -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 } - ' -} - -choose_widget() { - local widgets - local options=() - local selected="" - - widgets=$(available_widgets_json false) - - if ! jq -e 'length > 0' <<<"$widgets" >/dev/null; then - fail "no addable bar widgets found" - fi - - mapfile -t options < <(jq -r ' - .[] | - [.id, .name, .category, .description] | @tsv - ' <<<"$widgets" | awk -F '\t' '{ printf "%-28s %-22s [%s] %s\n", $1, $2, $3, $4 }') - - if [[ -t 0 || -t 2 ]]; then - selected=$(printf '%s\n' "${options[@]}" | gum filter --header "Add bar widget" --placeholder "Search addable widgets..." --limit 1 --height 14) || return 1 - else - print_available_widgets >&2 - fail "widget id is required; use list to see addable widgets" - fi - - selected="${selected%%[[:space:]]*}" - [[ -n $selected ]] || return 1 - printf '%s\n' "$selected" -} - -choose_removal() { - local entries - local widgets - local options=() - local selected="" - local location="" - local section="" - local index="" - - entries=$(current_bar_entries_json) - widgets=$(available_widgets_json true) - - if ! jq -e 'length > 0' <<<"$entries" >/dev/null; then - fail "no bar widgets are currently configured" - fi - - mapfile -t options < <(jq -r --argjson widgets "$widgets" ' - def widget_meta($id): first($widgets[]? | select(.id == $id)) // {}; - - .[] as $entry | - (widget_meta($entry.id)) as $meta | - [ - "\($entry.section)[\($entry.index)]", - $entry.id, - ($meta.name // $entry.id), - ($meta.category // "") - ] | @tsv - ' <<<"$entries" | awk -F '\t' '{ printf "%-10s %-28s %-22s %s\n", $1, $2, $3, $4 }') - - if [[ -t 0 || -t 2 ]]; then - selected=$(printf '%s\n' "${options[@]}" | gum filter --header "Remove bar widget" --placeholder "Search current widgets..." --limit 1 --height 14) || return 1 - else - jq -r '.[] | "\(.section)[\(.index)]\t\(.id)"' <<<"$entries" >&2 - fail "widget id is required; use show to see current widgets" - fi - - location="${selected%%[[:space:]]*}" - section="${location%%[*}" - index="${location#*[}" - index="${index%]}" - - [[ $section =~ ^(left|center|right)$ ]] || return 1 - [[ $index =~ ^[0-9]+$ ]] || return 1 - printf '%s\t%s\n' "$section" "$index" -} - -command="${1:-}" - -case "$command" in - show|current) - if (( $# != 1 )); then - usage - exit 1 - fi - - jq '.bar // {}' "$(source_file)" - ;; - selected|active) - if (( $# != 1 )); then - usage - exit 1 - fi - - current_bar_option_id - ;; - options|option) - shift - print_bar_options "$@" - ;; - use) - if (( $# != 2 )); then - usage - exit 1 - fi - - plugin="${2:-}" - [[ -n $plugin ]] || fail "bar option id is required" - if [[ $plugin == "default" || $plugin == "built-in" ]]; then - plugin="omarchy.bar" - fi - bar_option_exists "$plugin" || fail "$plugin is not a known bar option; run 'omarchy config shell bar options'" - - mkdir -p "$(dirname "$CONFIG_FILE")" - - source="$(source_file)" - tmp=$(mktemp) - trap 'rm -f "$tmp"' EXIT - - if [[ $plugin == "omarchy.bar" ]]; then - jq ' - def object_or_empty: if type == "object" then . else {} end; - - object_or_empty - | .version = 1 - | .bar = (.bar | object_or_empty) - | del(.bar.id) - ' "$source" >"$tmp" - else - jq --arg plugin "$plugin" ' - def object_or_empty: if type == "object" then . else {} end; - - object_or_empty - | .version = 1 - | .bar = (.bar | object_or_empty) - | .bar.id = $plugin - ' "$source" >"$tmp" - fi - - mv "$tmp" "$CONFIG_FILE" - trap - EXIT - omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true - refresh_shell_config - ;; - reset) - if (( $# != 1 )); then - usage - exit 1 - fi - - mkdir -p "$(dirname "$CONFIG_FILE")" - - source="$(source_file)" - tmp=$(mktemp) - trap 'rm -f "$tmp"' EXIT - - jq ' - def object_or_empty: if type == "object" then . else {} end; - - object_or_empty - | .version = 1 - | .bar = (.bar | object_or_empty) - | del(.bar.id) - ' "$source" >"$tmp" - - mv "$tmp" "$CONFIG_FILE" - trap - EXIT - refresh_shell_config - ;; - list|available|widgets) - shift - print_available_widgets "$@" - ;; - add) - if (( $# > 3 )); then - usage - exit 1 - fi - - if (( $# == 1 )); then - plugin=$(choose_widget) || exit 1 - else - plugin="${2:-}" - fi - section="${3:-right}" - - [[ -n $plugin ]] || fail "plugin is required" - [[ $section =~ ^(left|center|right)$ ]] || fail "section must be left, center, or right" - - mkdir -p "$(dirname "$CONFIG_FILE")" - - source="$(source_file)" - tmp=$(mktemp) - trap 'rm -f "$tmp"' EXIT - - jq --arg section "$section" --arg plugin "$plugin" ' - 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 append_anchor($section): - { - left: "omarchy.workspaces", - center: "omarchy.weather", - right: "omarchy.tray" - }[$section]; - def insert_after_anchor($entries; $entry; $anchor): - ($entries | map(entry_id) | index($anchor)) as $index - | if $index == null then - $entries + [$entry] - else - $entries[0:$index + 1] + [$entry] + $entries[$index + 1:] - end; - - 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 | map(select(entry_id != $plugin))) - | .bar.layout.center = (.bar.layout.center | array_or_empty | map(select(entry_id != $plugin))) - | .bar.layout.right = (.bar.layout.right | array_or_empty | map(select(entry_id != $plugin))) - | .plugins = (.plugins | array_or_empty) - | .bar.layout[$section] = insert_after_anchor(.bar.layout[$section]; { id: $plugin }; append_anchor($section)) - ' "$source" >"$tmp" - - mv "$tmp" "$CONFIG_FILE" - trap - EXIT - refresh_shell_config - ;; - position) - if (( $# != 2 )); then - usage - exit 1 - fi - - position="${2:-}" - - [[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right" - - mkdir -p "$(dirname "$CONFIG_FILE")" - - source="$(source_file)" - tmp=$(mktemp) - trap 'rm -f "$tmp"' EXIT - - jq --arg position "$position" ' - def object_or_empty: if type == "object" then . else {} end; - - object_or_empty - | .version = 1 - | .bar = (.bar | object_or_empty) - | .bar.position = $position - ' "$source" >"$tmp" - - mv "$tmp" "$CONFIG_FILE" - trap - EXIT - refresh_shell_config - ;; - transparent) - if (( $# != 2 )); then - usage - exit 1 - fi - - transparent="${2:-}" - - [[ $transparent =~ ^(true|false)$ ]] || fail "transparent must be true or false" - - mkdir -p "$(dirname "$CONFIG_FILE")" - - source="$(source_file)" - tmp=$(mktemp) - trap 'rm -f "$tmp"' EXIT - - jq --argjson transparent "$transparent" ' - def object_or_empty: if type == "object" then . else {} end; - - object_or_empty - | .version = 1 - | .bar = (.bar | object_or_empty) - | .bar.transparent = $transparent - ' "$source" >"$tmp" - - mv "$tmp" "$CONFIG_FILE" - trap - EXIT - refresh_shell_config - ;; - drop|remove|rm) - if (( $# > 2 )); then - usage - exit 1 - fi - - plugin="" - target_section="" - target_index="" - - if (( $# == 1 )); then - removal=$(choose_removal) || exit 1 - target_section="${removal%%$'\t'*}" - target_index="${removal#*$'\t'}" - else - plugin="${2:-}" - [[ -n $plugin ]] || fail "plugin is required" - fi - - mkdir -p "$(dirname "$CONFIG_FILE")" - - source="$(source_file)" - tmp=$(mktemp) - trap 'rm -f "$tmp"' EXIT - - if [[ -n $target_section ]]; then - jq --arg section "$target_section" --argjson index "$target_index" ' - def object_or_empty: if type == "object" then . else {} end; - def array_or_empty: if type == "array" then . else [] end; - - 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) - | if (.bar.layout[$section] | length) <= $index then - error("no widget at " + $section + "[" + ($index | tostring) + "]") - else - .bar.layout[$section] = (.bar.layout[$section][0:$index] + .bar.layout[$section][($index + 1):]) - end - | .plugins = (.plugins | array_or_empty) - ' "$source" >"$tmp" - else - jq --arg plugin "$plugin" ' - 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; - - 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 | map(select(entry_id != $plugin))) - | .bar.layout.center = (.bar.layout.center | array_or_empty | map(select(entry_id != $plugin))) - | .bar.layout.right = (.bar.layout.right | array_or_empty | map(select(entry_id != $plugin))) - | .plugins = (.plugins | array_or_empty) - ' "$source" >"$tmp" - fi - - mv "$tmp" "$CONFIG_FILE" - trap - EXIT - refresh_shell_config - ;; - *) - usage - exit 1 - ;; -esac diff --git a/bin/omarchy-install-service-dropbox b/bin/omarchy-install-service-dropbox index 9de23983..fd5b647d 100755 --- a/bin/omarchy-install-service-dropbox +++ b/bin/omarchy-install-service-dropbox @@ -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-config-shell-bar add omarchy.dropbox +omarchy-bar add omarchy.dropbox echo "Starting Dropbox..." uwsm-app -- dropbox-cli start &>/dev/null & diff --git a/bin/omarchy-install-service-tailscale b/bin/omarchy-install-service-tailscale index c93295c0..b1b84ecf 100755 --- a/bin/omarchy-install-service-tailscale +++ b/bin/omarchy-install-service-tailscale @@ -14,6 +14,6 @@ echo -e "\nAllowing $USER to manage Tailscale..." sudo tailscale set --operator="$USER" echo -e "\nAdding Tailscale to the bar..." -omarchy-config-shell-bar add omarchy.tailscale +omarchy-bar 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 diff --git a/bin/omarchy-plugin b/bin/omarchy-plugin index fe905467..0ef6fce2 100755 --- a/bin/omarchy-plugin +++ b/bin/omarchy-plugin @@ -7,8 +7,6 @@ set -euo pipefail -CONFIG_FILE="$HOME/.config/omarchy/shell.json" - usage() { cat < [args...] @@ -34,45 +32,19 @@ Add from sources (see 'omarchy plugin --help'): Source/add commands run their own binaries; the rest are handled here. Plugins are unsandboxed code — review what you add and enable. -Clone options: - --name Display name for the clone - --replace Replace the first source bar instance - --add [placement] Add cloned bar widget to the bar - --use Use cloned bar options as the active bar - -Bar widget commands: +Bar commands (delegate to 'omarchy bar'; run 'omarchy bar --help' for details): bar settings Open the inline bar config panel - bar options [--json] List available bar options - bar use Use a bar option as the active bar - bar reset Return to the built-in Omarchy bar - bar list [--json] List current bar layout - bar add [placement] [--duplicate] - bar move [placement] [--from-section SECTION] [--from-index N] - bar remove [--section SECTION] [--all] - bar remove --section SECTION --index N - bar set [--json] [--section SECTION] [--index N] - -Placement options: - --section Target bar section - --index Insert at index in the target section - --before Insert before the first matching widget - --after Insert after the first matching widget + bar list|layout|options|use|reset|add|move|remove|set|position|transparent + widget|widgets Alias of 'bar list' Examples: omarchy plugin list omarchy plugin rescan - omarchy plugin clone omarchy plugin clone omarchy.clock local.clock --name "My Clock" --replace omarchy plugin edit local.clock omarchy plugin enable acme.weather --section right - omarchy plugin bar options - omarchy plugin bar use local.neon-bar - omarchy plugin bar reset - omarchy plugin bar add acme.weather --section right --before omarchy.tray - omarchy plugin bar move omarchy.clock --section center --index 0 - omarchy plugin bar remove acme.weather + omarchy plugin bar add acme.weather --section right omarchy plugin bar set omarchy.clock format HH:mm - omarchy plugin bar set omarchy.indicators items '["Dnd","NightLight"]' --json USAGE } @@ -86,8 +58,7 @@ require_omarchy_path() { } require_command() { - local command_name="$1" - command -v "$command_name" >/dev/null 2>&1 || fail "$command_name is required" + command -v "$1" >/dev/null 2>&1 || fail "$1 is required" } plugin_discovered() { @@ -105,32 +76,6 @@ wait_for_plugin_discovery() { return 1 } -section_valid() { - [[ $1 =~ ^(left|center|right)$ ]] -} - -validate_section() { - local section="$1" - section_valid "$section" || fail "section must be left, center, or right" -} - -validate_index() { - local value="$1" - [[ $value =~ ^[0-9]+$ ]] || fail "index must be a non-negative integer" -} - -canonical_widget_id() { - printf '%s' "$1" -} - -validate_plugin_id() { - local id="$1" - [[ $id =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || fail "plugin id must contain only letters, numbers, dots, underscores, and dashes" - [[ $id == *.* ]] || fail "plugin id must be namespaced, e.g. local.clock" - [[ $id != omarchy.* ]] || fail "plugin ids beginning with omarchy. are reserved for built-ins" - [[ $(canonical_widget_id "$id") == "$id" ]] || fail "$id is a reserved built-in widget id" -} - print_plugins() { local json="false" @@ -189,8 +134,6 @@ plugin_enabled() { fail "disable does not take placement options" fi - id=$(canonical_widget_id "$id") - omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true if [[ $enabled == "true" ]]; then require_command jq @@ -202,918 +145,12 @@ plugin_enabled() { [[ $result == "ok" ]] || fail "plugin '$id' is not known; run: omarchy plugin rescan" if [[ $enabled == "true" && $# -gt 0 ]]; then - mutate_bar move --id "$id" "$@" + omarchy-bar move "$id" "$@" echo "Enabled and moved $id" + elif [[ $enabled == "true" ]]; then + echo "Enabled $id" else - if [[ $enabled == "true" ]]; then - echo "Enabled $id" - else - echo "Disabled $id" - fi - fi -} - -config_source() { - require_omarchy_path - local defaults_file="$OMARCHY_PATH/config/omarchy/shell.json" - - if [[ -s $CONFIG_FILE ]] && jq -e 'type == "object" and .version == 1' "$CONFIG_FILE" >/dev/null 2>&1; then - printf '%s' "$CONFIG_FILE" - elif [[ -s $defaults_file ]] && jq -e 'type == "object" and .version == 1' "$defaults_file" >/dev/null 2>&1; then - printf '%s' "$defaults_file" - else - fail "could not find a valid shell config or defaults file" - fi -} - -bar_list() { - local json="false" - - while (( $# > 0 )); do - case "$1" in - --json) - json="true" - ;; - -h | --help) - usage - exit 0 - ;; - *) - fail "unknown bar list option: $1" - ;; - esac - shift - done - - require_command jq - local source - source=$(config_source) - - if [[ $json == "true" ]]; then - jq '.bar.layout // { left: [], center: [], right: [] }' "$source" - 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" | awk ' - BEGIN { printf "%-8s %-5s %s\n", "SECTION", "INDEX", "WIDGET" } - { printf "%-8s %-5s %s\n", $1, $2, $3 } - ' -} - -mutate_bar() { - require_command jq - require_command python3 - require_omarchy_path - - local operation="$1" - shift - local id="" - local section="" - local index="" - local before="" - local after="" - local from_section="" - local from_index="" - local duplicate="false" - local all="false" - local key="" - local value="" - local value_is_json="false" - local new_id="" - - while (( $# > 0 )); do - case "$1" in - --id) - id="${2-}" - shift 2 - ;; - --section) - section="${2:-}" - validate_section "$section" - shift 2 - ;; - --index) - index="${2:-}" - validate_index "$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 - ;; - --from-section) - from_section="${2:-}" - validate_section "$from_section" - shift 2 - ;; - --from-index) - from_index="${2:-}" - validate_index "$from_index" - shift 2 - ;; - --duplicate) - duplicate="true" - shift - ;; - --all) - all="true" - shift - ;; - --key) - key="${2:-}" - [[ -n $key ]] || fail "--key requires a value" - shift 2 - ;; - --new-id) - new_id="${2:-}" - [[ -n $new_id ]] || fail "--new-id requires a value" - shift 2 - ;; - --value) - value="${2:-}" - shift 2 - ;; - --json) - value_is_json="true" - shift - ;; - *) - fail "unknown option: $1" - ;; - esac - done - - if [[ -n $before && -n $after ]]; then - fail "use only one of --before or --after" - fi - - local source tmp - source=$(config_source) - mkdir -p "$(dirname "$CONFIG_FILE")" - tmp=$(mktemp) - - OPERATION="$operation" \ - WIDGET_ID="$id" \ - TARGET_SECTION="$section" \ - TARGET_INDEX="$index" \ - TARGET_BEFORE="$before" \ - TARGET_AFTER="$after" \ - FROM_SECTION="$from_section" \ - FROM_INDEX="$from_index" \ - ALLOW_DUPLICATE="$duplicate" \ - REMOVE_ALL="$all" \ - SET_KEY="$key" \ - SET_VALUE="$value" \ - SET_VALUE_IS_JSON="$value_is_json" \ - NEW_WIDGET_ID="$new_id" \ - python3 - "$source" >"$tmp" <<'PY' -import json -import os -import sys -from copy import deepcopy - -source = sys.argv[1] -operation = os.environ["OPERATION"] -def canonical(value): - return str(value or "") - - -widget_id = canonical(os.environ.get("WIDGET_ID", "")) -target_section = os.environ.get("TARGET_SECTION", "") -target_index = os.environ.get("TARGET_INDEX", "") -target_before = canonical(os.environ.get("TARGET_BEFORE", "")) -target_after = canonical(os.environ.get("TARGET_AFTER", "")) -from_section = os.environ.get("FROM_SECTION", "") -from_index = os.environ.get("FROM_INDEX", "") -allow_duplicate = os.environ.get("ALLOW_DUPLICATE") == "true" -remove_all = os.environ.get("REMOVE_ALL") == "true" -set_key = os.environ.get("SET_KEY", "") -set_value = os.environ.get("SET_VALUE", "") -set_value_is_json = os.environ.get("SET_VALUE_IS_JSON") == "true" -new_widget_id = os.environ.get("NEW_WIDGET_ID", "") -sections = ["left", "center", "right"] - - -def die(message): - print(f"omarchy-plugin: {message}", file=sys.stderr) - sys.exit(1) - - -def load_config(path): - with open(path, encoding="utf-8") as handle: - data = json.load(handle) - if not isinstance(data, dict): - die("shell config must be a JSON object") - data = deepcopy(data) - data["version"] = 1 - data.setdefault("bar", {}) - if not isinstance(data["bar"], dict): - data["bar"] = {} - data["bar"].setdefault("layout", {}) - if not isinstance(data["bar"]["layout"], dict): - data["bar"]["layout"] = {} - for section in sections: - entries = data["bar"]["layout"].get(section) - if not isinstance(entries, list): - data["bar"]["layout"][section] = [] - data.setdefault("plugins", []) - if not isinstance(data["plugins"], list): - data["plugins"] = [] - return data - - -config = load_config(source) -layout = config["bar"]["layout"] - - -def find_entries(id_value=None, only_section=""): - found = [] - for section in sections: - if only_section and section != only_section: - continue - for index, entry in enumerate(layout[section]): - if id_value is None or (isinstance(entry, dict) and canonical(entry.get("id")) == id_value): - found.append((section, index, entry)) - return found - - -def entry_at(section, index): - if section not in sections: - die("section must be left, center, or right") - if index < 0 or index >= len(layout[section]): - die(f"no widget at {section}[{index}]") - return layout[section][index] - - -def parse_index(value): - if value == "": - return None - try: - parsed = int(value) - except ValueError: - die("index must be a non-negative integer") - if parsed < 0: - die("index must be a non-negative integer") - return parsed - - -def resolve_target(default_section="right"): - if target_before or target_after: - target_id = target_before or target_after - matches = find_entries(target_id, target_section) - if not matches: - scope = f" in {target_section}" if target_section else "" - die(f"could not find target widget {target_id}{scope}") - section, index, _entry = matches[0] - if target_after: - index += 1 - return section, index - - section = target_section or default_section - if section not in sections: - die("section must be left, center, or right") - index = parse_index(target_index) - if index is None: - index = len(layout[section]) - if index > len(layout[section]): - index = len(layout[section]) - return section, index - - -def source_location(): - if from_index != "": - if not from_section: - die("--from-index requires --from-section") - index = parse_index(from_index) - entry = entry_at(from_section, index) - if widget_id and (not isinstance(entry, dict) or canonical(entry.get("id")) != widget_id): - die(f"widget at {from_section}[{index}] is not {widget_id}") - return from_section, index, entry - - if not widget_id: - die("widget id is required") - matches = find_entries(widget_id, from_section) - if not matches: - scope = f" in {from_section}" if from_section else "" - die(f"could not find widget {widget_id}{scope}") - return matches[0] - - -if operation == "add": - if not widget_id: - die("widget id is required") - if not allow_duplicate and find_entries(widget_id): - die(f"{widget_id} is already in the bar; use 'bar move' or pass --duplicate") - section, index = resolve_target("right") - layout[section].insert(index, {"id": widget_id}) - -elif operation == "move": - old_section, old_index, entry = source_location() - entry = deepcopy(entry) - if isinstance(entry, dict): - entry["id"] = widget_id - del layout[old_section][old_index] - default_section = target_section or old_section - section, index = resolve_target(default_section) - layout[section].insert(index, entry) - -elif operation == "remove": - if from_index != "": - old_section, old_index, _entry = source_location() - del layout[old_section][old_index] - elif target_index != "": - if not target_section: - die("--index requires --section for bar remove") - index = parse_index(target_index) - entry = entry_at(target_section, index) - if widget_id and (not isinstance(entry, dict) or canonical(entry.get("id")) != widget_id): - die(f"widget at {target_section}[{index}] is not {widget_id}") - del layout[target_section][index] - else: - if not widget_id: - die("widget id is required") - matches = find_entries(widget_id, target_section) - if not matches: - scope = f" in {target_section}" if target_section else "" - die(f"could not find widget {widget_id}{scope}") - if remove_all: - for section, index, _entry in reversed(matches): - del layout[section][index] - else: - section, index, _entry = matches[0] - del layout[section][index] - -elif operation == "replace": - if not widget_id: - die("source widget id is required") - if not new_widget_id: - die("replacement widget id is required") - section, index, entry = source_location() - if not isinstance(entry, dict): - die("widget entry must be an object") - entry["id"] = new_widget_id - layout[section][index] = entry - -elif operation == "set": - if not widget_id: - die("widget id is required") - if not set_key: - die("setting key is required") - if from_index != "": - section, index, entry = source_location() - elif target_index != "": - if not target_section: - die("--index requires --section for bar set") - index = parse_index(target_index) - entry = entry_at(target_section, index) - if not isinstance(entry, dict) or canonical(entry.get("id")) != widget_id: - die(f"widget at {target_section}[{index}] is not {widget_id}") - section = target_section - else: - matches = find_entries(widget_id, target_section) - if not matches: - scope = f" in {target_section}" if target_section else "" - die(f"could not find widget {widget_id}{scope}") - section, index, entry = matches[0] - if not isinstance(entry, dict): - die("widget entry must be an object") - if set_value_is_json: - try: - parsed_value = json.loads(set_value) - except json.JSONDecodeError as error: - die(f"invalid JSON value: {error}") - else: - parsed_value = set_value - layout[section][index][set_key] = parsed_value - -else: - die(f"unknown operation: {operation}") - -json.dump(config, sys.stdout, indent=2) -sys.stdout.write("\n") -PY - - mv "$tmp" "$CONFIG_FILE" - omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true -} - -# Canonical ids of every known bar widget, read from manifests on disk -# (first-party + user). This is the same set `omarchy plugin list` reports, but -# read straight from disk so it works with the shell down and never races an -# in-flight rescan. -bar_widget_ids() { - require_command jq - first_party_manifest_paths | while IFS= read -r manifest; do - jq -r 'select((.kinds // []) | index("bar-widget")) | .id' "$manifest" 2>/dev/null - done - local user_dir="$HOME/.config/omarchy/plugins" - if [[ -d $user_dir ]]; then - find "$user_dir" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) 2>/dev/null | while IFS= read -r dir; do - [[ -f $dir/manifest.json ]] || continue - jq -r 'select((.kinds // []) | index("bar-widget")) | .id' "$dir/manifest.json" 2>/dev/null - done - fi -} - -bar_add() { - local id="${1:-}" - [[ -n $id ]] || fail "bar add requires a widget id" - shift - id=$(canonical_widget_id "$id") - # Only let real bar widgets into the layout — a typo'd or non-widget id just - # renders as a dead entry (this is how a stray "--help" once got in). - if ! bar_widget_ids | grep -qxF -- "$id"; then - fail "$id is not a known bar widget; run 'omarchy plugin list' to see valid ids" - fi - mutate_bar add --id "$id" "$@" - echo "Added $id to the bar" -} - -bar_move() { - local id="${1:-}" - [[ -n $id ]] || fail "bar move requires a widget id" - shift - mutate_bar move --id "$id" "$@" - echo "Moved $id" -} - -bar_remove() { - local id="" - if (( $# > 0 )) && [[ $1 != --* ]]; then - id="$1" - shift - fi - mutate_bar remove --id "$id" "$@" - if [[ -n $id ]]; then - echo "Removed $id from the bar" - else - echo "Removed bar widget" - fi -} - -bar_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 - mutate_bar set --id "$id" --key "$key" --value "$value" "$@" - echo "Set $key on $(canonical_widget_id "$id")" -} - -slug_id() { - tr '[:upper:]' '[:lower:]' <<<"$1" | sed -E 's/^omarchy\.//; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' -} - -first_party_manifest_paths() { - require_omarchy_path - find "$OMARCHY_PATH/shell/plugins" -mindepth 2 -maxdepth 3 -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null | sort -} - -manifest_source_dir() { - local manifest="$1" - if [[ ${manifest##*/} == "manifest.json" ]]; then - dirname -- "$manifest" - else - dirname -- "$manifest" - fi -} - -builtin_widget_info() { - require_command jq - local wanted - wanted=$(canonical_widget_id "$1") - - local manifest id entry source_dir source_path name category multiple - while IFS= read -r manifest; do - if ! jq -e --arg id "$wanted" ' - (.id == $id or ((.barWidget.aliases // []) | index($id))) and - ((.kinds // []) | index("bar-widget")) - ' "$manifest" >/dev/null 2>&1; then - continue - fi - - id=$(jq -r '.id' "$manifest") - entry=$(jq -r '.entryPoints.barWidget // empty' "$manifest") - [[ -n $entry ]] || return 1 - source_dir=$(manifest_source_dir "$manifest") - source_path="$source_dir/$entry" - name=$(jq -r '.barWidget.displayName // .name // .id' "$manifest") - category=$(jq -r '.barWidget.category // "Plugin"' "$manifest") - multiple=$(jq -r 'if .barWidget.allowMultiple == true then "true" else "false" end' "$manifest") - printf '%s\t%s\t%s\t%s\t%s\n' "$id" "$source_path" "$name" "$category" "$multiple" - return 0 - done < <(first_party_manifest_paths) - - return 1 -} - -clone_source_options() { - local id name - first_party_manifest_paths | while IFS= read -r manifest; do - id=$(jq -r '.id // empty' "$manifest" 2>/dev/null || true) - name=$(jq -r '.barWidget.displayName // .name // .id // empty' "$manifest" 2>/dev/null || true) - [[ -n $id ]] || continue - if jq -e '(.kinds // []) | index("bar-widget")' "$manifest" >/dev/null 2>&1; then - printf '%s\tBuilt-in widget\t%s\n' "$id" "${name:-$id}" - else - printf '%s\tBuilt-in plugin\t%s\n' "$id" "${name:-$id}" - fi - done - - if [[ -d $HOME/.config/omarchy/plugins ]]; then - find "$HOME/.config/omarchy/plugins" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) 2>/dev/null | sort | while IFS= read -r dir; do - [[ -f $dir/manifest.json ]] || continue - id=$(jq -r '.id // empty' "$dir/manifest.json" 2>/dev/null || true) - name=$(jq -r '.name // .id // empty' "$dir/manifest.json" 2>/dev/null || true) - [[ -n $id ]] && printf '%s\tUser plugin\t%s\n' "$id" "${name:-$id}" - done - fi -} - -choose_value() { - local prompt="$1" - shift - gum choose --header "$prompt" "$@" -} - -input_value() { - local prompt="$1" - local value="${2:-}" - gum input --prompt "$prompt " --value "$value" -} - -choose_clone_source() { - local selected - selected=$(clone_source_options | awk -F '\t' '{ printf "%-32s %-15s %s\n", $3, $2, $1 }' | gum filter --header "Clone plugin" --placeholder "Search built-in and user plugins..." --limit 1) || return 1 - awk '{ print $NF }' <<<"$selected" -} - -edit_source_options() { - if [[ -d $HOME/.config/omarchy/plugins ]]; then - find "$HOME/.config/omarchy/plugins" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) 2>/dev/null | sort | while IFS= read -r dir; do - [[ -f $dir/manifest.json ]] || continue - id=$(jq -r '.id // empty' "$dir/manifest.json" 2>/dev/null || true) - name=$(jq -r '.name // .id // empty' "$dir/manifest.json" 2>/dev/null || true) - [[ -n $id ]] && printf '%s\t%s\t%s\n' "$id" "${name:-$id}" "$dir" - done - fi -} - -choose_edit_source() { - local selected - selected=$(edit_source_options | awk -F '\t' '{ printf "%-32s %s\n", $2, $1 }' | gum filter --header "Edit plugin" --placeholder "Search user plugins..." --limit 1) || return 1 - awk '{ print $NF }' <<<"$selected" -} - -validate_user_plugin_dir() { - local dir="$1" - local requested_id="${2:-}" - local root="$HOME/.config/omarchy/plugins" - local dir_abs root_abs manifest_id - - require_command jq - require_command python3 - [[ -d $dir ]] || fail "plugin directory not found: $dir" - - dir_abs=$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).expanduser().resolve(strict=False))' "$dir") - root_abs=$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).expanduser().resolve(strict=False))' "$root") - [[ $dir_abs == $root_abs/* ]] || fail "plugin edit only opens user plugins under $root" - [[ -f $dir_abs/manifest.json ]] || fail "plugin directory missing manifest.json: $dir_abs" - - manifest_id=$(jq -r '.id // empty' "$dir_abs/manifest.json" 2>/dev/null || true) - [[ -n $manifest_id ]] || fail "plugin manifest missing id: $dir_abs/manifest.json" - [[ $manifest_id != omarchy.* && $(canonical_widget_id "$manifest_id") == $manifest_id ]] || fail "$manifest_id is built in; clone it first with: omarchy plugin clone $manifest_id local.$(slug_id "$manifest_id")" - if [[ -n $requested_id && $manifest_id != $requested_id ]]; then - fail "plugin id mismatch: requested $requested_id but manifest declares $manifest_id" - fi - - printf '%s' "$dir_abs" -} - -plugin_dir_for_id() { - local id="$1" - - if [[ $id == */* ]]; then - validate_user_plugin_dir "$id" - return - fi - - id=$(canonical_widget_id "$id") - if builtin_widget_info "$id" >/dev/null 2>&1 || [[ $id == omarchy.* ]]; then - fail "$id is built in; clone it first with: omarchy plugin clone $id local.$(slug_id "$id")" - fi - - validate_user_plugin_dir "$HOME/.config/omarchy/plugins/$id" "$id" -} - -edit_command() { - local id="" - if (( $# > 0 )) && [[ $1 != --* ]]; then - id="$1" - shift - fi - - while (( $# > 0 )); do - case "$1" in - -h | --help) - usage - return - ;; - *) - fail "unknown edit option: $1" - ;; - esac - done - - if [[ -z $id ]]; then - if [[ -t 0 && -t 1 ]]; then - id=$(choose_edit_source) || fail "edit cancelled" - else - fail "plugin id is required" - fi - fi - - local dir - dir=$(plugin_dir_for_id "$id") - - if [[ -t 0 && -t 1 ]]; then - cd "$dir" - exec "${SHELL:-bash}" - fi - printf '%s\n' "$dir" -} - -rewrite_cloned_qml() { - local file="$1" - local id="$2" - local source_path="$3" - python3 - "$file" "$id" "$source_path" <<'PY' -import pathlib -import re -import sys -path, plugin_id, source_path = sys.argv[1], sys.argv[2], pathlib.Path(sys.argv[3]) -text = open(path, encoding="utf-8").read() -text = re.sub(r'moduleName:\s*"[^"]+"', f'moduleName: "{plugin_id}"', text, count=1) - - -def resolve_relative(match): - rel = match.group(1) - if rel.startswith(("../", "./")) or (not rel.startswith("/") and "://" not in rel): - resolved = (source_path.parent / rel).resolve() - return '"' + resolved.as_uri() + '"' - return match.group(0) - -text = re.sub(r'Qt\.resolvedUrl\("([^"]+)"\)', resolve_relative, text) - -# Indicators dynamically loads sibling indicator components via string -# concatenation, so the simple Qt.resolvedUrl("literal") rewrite above cannot -# see it. Point the clone back at Omarchy's bundled indicator directory. -if source_path.name == "Indicators.qml": - indicators_url = (source_path.parent.parent / "indicators").resolve().as_uri() + "/" - text = text.replace( - 'Qt.resolvedUrl("../indicators/" + indicatorSlot.indicatorId + ".qml")', - f'"{indicators_url}" + indicatorSlot.indicatorId + ".qml"', - ) - -# Panel-backed clones should not register the same global IPC target as the -# built-in widget. Direct panel clones can disable Panel.manageIpc themselves; -# wrapper widgets such as Weather disable it on the loaded panel instance. -if "ipcTarget:" in text and "manageIpc:" not in text: - text = text.replace(" id: root\n", " id: root\n manageIpc: false\n", 1) -if "function injectPanel()" in text and '"manageIpc" in target' not in text: - text = text.replace( - " if (!target) return\n", - " if (!target) return\n if (\"manageIpc\" in target) target.manageIpc = false\n", - 1, - ) - -open(path, "w", encoding="utf-8").write(text) -PY -} - -write_clone_manifest() { - local file="$1" - local id="$2" - local name="$3" - local description="$4" - local category="$5" - local multiple="$6" - local source_id="$7" - local source_path="$8" - python3 - "$file" "$id" "$name" "$description" "$category" "$multiple" "$source_id" "$source_path" <<'PY' -import json -import sys -file, plugin_id, name, description, category, multiple, source_id, source_path = sys.argv[1:] -data = { - "schemaVersion": 1, - "id": plugin_id, - "name": name, - "version": "1.0.0", - "author": "Local", - "description": description, - "kinds": ["bar-widget"], - "entryPoints": {"barWidget": "Widget.qml"}, - "barWidget": { - "displayName": name, - "description": description, - "category": category, - "allowMultiple": multiple == "true" - }, - "omarchy": { - "clonedFrom": source_id, - "clonedFromPath": source_path - } -} -open(file, "w", encoding="utf-8").write(json.dumps(data, indent=2) + "\n") -PY -} - -update_cloned_manifest() { - local file="$1" - local id="$2" - local name="$3" - local source_id="$4" - local source_path="$5" - python3 - "$file" "$id" "$name" "$source_id" "$source_path" <<'PY' -import json -import sys -file, plugin_id, name, source_id, source_path = sys.argv[1:] -with open(file, encoding="utf-8") as handle: - data = json.load(handle) -data["id"] = plugin_id -data["name"] = name -if "barWidget" in data and isinstance(data["barWidget"], dict): - data["barWidget"]["displayName"] = name -meta = data.get("omarchy") if isinstance(data.get("omarchy"), dict) else {} -meta.update({"clonedFrom": source_id, "clonedFromPath": source_path}) -data["omarchy"] = meta -open(file, "w", encoding="utf-8").write(json.dumps(data, indent=2) + "\n") -PY -} - -clone_command() { - require_command jq - require_command python3 - require_omarchy_path - - local source_id="" - local new_id="" - if (( $# > 0 )) && [[ $1 != --* ]]; then - source_id="$1" - shift - fi - if (( $# > 0 )) && [[ $1 != --* ]]; then - new_id="$1" - shift - fi - - local display_name="" - local replace="false" - local add="false" - local use_bar="false" - local placement=() - - while (( $# > 0 )); do - case "$1" in - --name) - display_name="${2:-}" - [[ -n $display_name ]] || fail "--name requires a value" - shift 2 - ;; - --replace) - replace="true" - shift - ;; - --add) - add="true" - shift - ;; - --use) - use_bar="true" - shift - ;; - --section | --index | --before | --after) - placement+=("$1" "${2:-}") - shift 2 - ;; - -h | --help) - usage - return - ;; - *) - fail "unknown clone option: $1" - ;; - esac - done - - if [[ -z $source_id ]]; then - if [[ -t 0 && -t 1 ]]; then - source_id=$(choose_clone_source) || fail "clone cancelled" - else - fail "clone source is required" - fi - fi - source_id=$(canonical_widget_id "$source_id") - - local default_slug - default_slug=$(slug_id "$source_id") - if [[ -z $new_id ]]; then - if [[ -t 0 && -t 1 ]]; then - new_id=$(input_value "New plugin id:" "local.$default_slug") - else - fail "new plugin id is required" - fi - fi - validate_plugin_id "$new_id" - - local target_dir="$HOME/.config/omarchy/plugins/$new_id" - [[ ! -e $target_dir && ! -L $target_dir ]] || fail "$target_dir already exists" - - local source_path="" - local source_name="" - local source_category="Plugin" - local source_multiple="false" - local source_type="" - - if IFS=$'\t' read -r _ source_path source_name source_category source_multiple < <(builtin_widget_info "$source_id"); then - source_type="builtin-widget" - elif [[ -f $HOME/.config/omarchy/plugins/$source_id/manifest.json ]]; then - source_type="user-plugin" - source_path="$HOME/.config/omarchy/plugins/$source_id" - source_name=$(jq -r '.name // .id' "$source_path/manifest.json") - else - source_path=$(find "$OMARCHY_PATH/shell/plugins" -path '*/manifest.json' -print0 | xargs -0 jq -r --arg id "$source_id" 'select(.id == $id) | input_filename' 2>/dev/null | head -1 || true) - if [[ -n $source_path ]]; then - source_type="builtin-plugin" - source_path=$(dirname "$source_path") - source_name=$(jq -r '.name // .id' "$source_path/manifest.json") - fi - fi - - [[ -n $source_type ]] || fail "unknown clone source: $source_id" - if [[ -z $display_name ]]; then - if [[ -t 0 && -t 1 ]]; then - display_name=$(input_value "Display name:" "${source_name:-$new_id}") - else - display_name="${source_name:-$new_id}" - fi - fi - [[ -n $display_name ]] || fail "display name is required" - - mkdir -p "$target_dir" - if [[ $source_type == "builtin-widget" ]]; then - cp "$source_path" "$target_dir/Widget.qml" - rewrite_cloned_qml "$target_dir/Widget.qml" "$new_id" "$source_path" - write_clone_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "Cloned from $source_id" "$source_category" "$source_multiple" "$source_id" "$source_path" - else - cp -aL "$source_path/." "$target_dir/" - update_cloned_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "$source_id" "$source_path" - fi - - { - printf '# %s\n\n' "$display_name" - printf 'Cloned from `%s`.\n\n' "$source_id" - printf 'Source: `%s`\n\n' "$source_path" - printf 'The original remains built into Omarchy. Remove this plugin directory or swap\n' - printf 'your bar entry back to `%s` whenever you want to return to the built-in.\n' "$source_id" - } >"$target_dir/UPSTREAM.md" - - omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true - - if [[ $replace == "true" ]]; then - mutate_bar replace --id "$source_id" --new-id "$new_id" - echo "Cloned $source_id to $new_id and replaced the first bar instance" - elif [[ $add == "true" ]]; then - bar_add "$new_id" "${placement[@]}" - echo "Cloned $source_id to $new_id" - elif [[ $use_bar == "true" ]]; then - omarchy-config-shell-bar use "$new_id" - echo "Cloned $source_id to $new_id and selected it as the active bar option" - else - echo "Cloned $source_id to $new_id" - fi - - if [[ -t 0 && -t 1 ]]; then - cd "$target_dir" - exec "${SHELL:-bash}" + echo "Disabled $id" fi } @@ -1126,35 +163,11 @@ bar_command() { (( $# == 0 )) || fail "bar settings does not take arguments" exec omarchy-launch-bar-settings ;; - options | option) - exec omarchy-config-shell-bar options "$@" - ;; - use) - exec omarchy-config-shell-bar use "$@" - ;; - reset) - exec omarchy-config-shell-bar reset "$@" - ;; - list) - bar_list "$@" - ;; - add) - bar_add "$@" - ;; - move) - bar_move "$@" - ;; - remove | rm) - bar_remove "$@" - ;; - set) - bar_set "$@" - ;; -h | --help | help) usage ;; *) - fail "unknown bar command: $command" + exec omarchy-bar "$command" "$@" ;; esac } @@ -1179,11 +192,11 @@ disable) ;; clone) shift - clone_command "$@" + exec omarchy-plugin-clone "$@" ;; edit) shift - edit_command "$@" + exec omarchy-plugin-edit "$@" ;; bar | widget | widgets) shift diff --git a/bin/omarchy-plugin-catalog b/bin/omarchy-plugin-catalog new file mode 100755 index 00000000..f9e7148d --- /dev/null +++ b/bin/omarchy-plugin-catalog @@ -0,0 +1,67 @@ +#!/bin/bash + +# omarchy:summary=Emit every first-party and user plugin manifest as JSON +# omarchy:group=plugin +# omarchy:hidden=true + +# 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. + +set -o pipefail + +OMARCHY_PATH="${OMARCHY_PATH:-}" +command -v jq >/dev/null 2>&1 || { echo "omarchy-plugin-catalog: jq is required" >&2; exit 1; } + +paths=() + +if [[ -n $OMARCHY_PATH && -d $OMARCHY_PATH/shell/plugins ]]; then + while IFS= read -r manifest; do + paths+=("$manifest") + done < <(find "$OMARCHY_PATH/shell/plugins" -mindepth 2 -maxdepth 4 -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null | sort) +fi + +user_dir="$HOME/.config/omarchy/plugins" +if [[ -d $user_dir ]]; then + while IFS= read -r manifest; do + paths+=("$manifest") + done < <(find -L "$user_dir" -mindepth 2 -maxdepth 2 -type f -name manifest.json 2>/dev/null | sort) +fi + +if (( ${#paths[@]} == 0 )); then + echo "[]" + exit 0 +fi + +jq -n ' + [ inputs + | . as $manifest + | input_filename as $manifestPath + | ($manifestPath | sub("/manifest.json$"; "") | sub("/[^/]+\\.manifest\\.json$"; "")) as $sourceDir + | { + id: ($manifest.id // ""), + name: ($manifest.name // $manifest.id // ""), + description: ($manifest.description // ""), + kinds: ($manifest.kinds // []), + firstParty: (($manifest.id // "") | startswith("omarchy.")), + manifestPath: $manifestPath, + sourceDir: $sourceDir, + entryPoints: ($manifest.entryPoints // {}), + barWidget: ($manifest.barWidget // null), + bar: ($manifest.bar // null) + } + | .barWidgetPath = ( + if (.entryPoints.barWidget // null) != null + then (.sourceDir + "/" + .entryPoints.barWidget) + else null end) + | .barPath = ( + if (.entryPoints.bar // null) != null + then (.sourceDir + "/" + .entryPoints.bar) + else null end) + | select(.id != "") + ] + | unique_by(.id) +' "${paths[@]}" diff --git a/bin/omarchy-plugin-clone b/bin/omarchy-plugin-clone new file mode 100755 index 00000000..fae92cdb --- /dev/null +++ b/bin/omarchy-plugin-clone @@ -0,0 +1,377 @@ +#!/bin/bash + +# omarchy:summary=Clone a built-in or user Omarchy shell plugin into your own config +# omarchy:group=plugin +# omarchy:args=[source-id] [new-id] [--name ] [--replace] [--add [placement]] [--use] +# omarchy:examples=omarchy plugin clone | omarchy plugin clone omarchy.clock local.clock | omarchy plugin clone omarchy.clock local.clock --name "My Clock" --replace + +set -euo pipefail + +PLUGINS_DIR="$HOME/.config/omarchy/plugins" + +fail() { + echo "omarchy-plugin-clone: $*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "$1 is required" +} + +require_omarchy_path() { + [[ -n ${OMARCHY_PATH:-} ]] || fail "OMARCHY_PATH is not set" +} + +interactive() { + [[ -t 0 && -t 1 ]] +} + +canonical_widget_id() { + printf '%s' "$1" +} + +slug_id() { + tr '[:upper:]' '[:lower:]' <<<"$1" | sed -E 's/^omarchy\.//; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' +} + +validate_plugin_id() { + local id="$1" + [[ $id =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || fail "plugin id must contain only letters, numbers, dots, underscores, and dashes" + [[ $id == *.* ]] || fail "plugin id must be namespaced, e.g. local.clock" + [[ $id != omarchy.* ]] || fail "plugin ids beginning with omarchy. are reserved for built-ins" + [[ $(canonical_widget_id "$id") == "$id" ]] || fail "$id is a reserved built-in widget id" +} + +catalog_json() { + require_omarchy_path + require_command jq + omarchy-plugin-catalog +} + +# Emit: idbarWidgetPathnamecategoryallowMultiple for a +# built-in bar widget matching $1 (by id or alias). Used to find the source QML +# file and metadata to copy when cloning a widget. +builtin_widget_info() { + local wanted + wanted=$(canonical_widget_id "$1") + + catalog_json | jq -r --arg id "$wanted" ' + .[] | select((.kinds | index("bar-widget")) and .barWidgetPath != null) + | select(.id == $id or (((.barWidget.aliases // [])) | index($id))) + | [.id, .barWidgetPath, (.barWidget.displayName // .name // .id), (.barWidget.category // "Plugin"), (if .barWidget.allowMultiple == true then "true" else "false" end)] + | @tsv + ' | head -1 +} + +clone_source_options() { + catalog_json | jq -r ' + .[] | "\(.id)\t" + + (if .firstParty then (if (.kinds | index("bar-widget")) then "Built-in widget" else "Built-in plugin" end) else "User plugin" end) + + "\t" + ((.barWidget.displayName // .name // .id) // .id) + ' +} + +choose_clone_source() { + local selected + selected=$(clone_source_options | awk -F '\t' '{ printf "%-32s %-15s %s\n", $3, $2, $1 }' \ + | gum filter --header "Clone plugin" --placeholder "Search built-in and user plugins..." --limit 1) || return 1 + awk '{ print $NF }' <<<"$selected" +} + +choose_value() { + local prompt="$1" + shift + gum choose --header "$prompt" "$@" +} + +input_value() { + local prompt="$1" + local value="${2:-}" + gum input --prompt "$prompt " --value "$value" +} + +rewrite_cloned_qml() { + local file="$1" + local id="$2" + local source_path="$3" + python3 - "$file" "$id" "$source_path" <<'PY' +import pathlib +import re +import sys +path, plugin_id, source_path = sys.argv[1], sys.argv[2], pathlib.Path(sys.argv[3]) +text = open(path, encoding="utf-8").read() +text = re.sub(r'moduleName:\s*"[^"]+"', f'moduleName: "{plugin_id}"', text, count=1) + + +def resolve_relative(match): + rel = match.group(1) + if rel.startswith(("../", "./")) or (not rel.startswith("/") and "://" not in rel): + resolved = (source_path.parent / rel).resolve() + return '"' + resolved.as_uri() + '"' + return match.group(0) + +text = re.sub(r'Qt\.resolvedUrl\("([^"]+)"\)', resolve_relative, text) + +# Indicators dynamically loads sibling indicator components via string +# concatenation, so the simple Qt.resolvedUrl("literal") rewrite above cannot +# see it. Point the clone back at Omarchy's bundled indicator directory. +if source_path.name == "Indicators.qml": + indicators_url = (source_path.parent.parent / "indicators").resolve().as_uri() + "/" + text = text.replace( + 'Qt.resolvedUrl("../indicators/" + indicatorSlot.indicatorId + ".qml")', + f'"{indicators_url}" + indicatorSlot.indicatorId + ".qml"', + ) + +# Panel-backed clones should not register the same global IPC target as the +# built-in widget. Direct panel clones can disable Panel.manageIpc themselves; +# wrapper widgets such as Weather disable it on the loaded panel instance. +if "ipcTarget:" in text and "manageIpc:" not in text: + text = text.replace(" id: root\n", " id: root\n manageIpc: false\n", 1) +if "function injectPanel()" in text and '"manageIpc" in target' not in text: + text = text.replace( + " if (!target) return\n", + " if (!target) return\n if (\"manageIpc\" in target) target.manageIpc = false\n", + 1, + ) + +open(path, "w", encoding="utf-8").write(text) +PY +} + +write_clone_manifest() { + local file="$1" + local id="$2" + local name="$3" + local description="$4" + local category="$5" + local multiple="$6" + local source_id="$7" + local source_path="$8" + python3 - "$file" "$id" "$name" "$description" "$category" "$multiple" "$source_id" "$source_path" <<'PY' +import json +import sys +file, plugin_id, name, description, category, multiple, source_id, source_path = sys.argv[1:] +data = { + "schemaVersion": 1, + "id": plugin_id, + "name": name, + "version": "1.0.0", + "author": "Local", + "description": description, + "kinds": ["bar-widget"], + "entryPoints": {"barWidget": "Widget.qml"}, + "barWidget": { + "displayName": name, + "description": description, + "category": category, + "allowMultiple": multiple == "true" + }, + "omarchy": { + "clonedFrom": source_id, + "clonedFromPath": source_path + } +} +open(file, "w", encoding="utf-8").write(json.dumps(data, indent=2) + "\n") +PY +} + +update_cloned_manifest() { + local file="$1" + local id="$2" + local name="$3" + local source_id="$4" + local source_path="$5" + python3 - "$file" "$id" "$name" "$source_id" "$source_path" <<'PY' +import json +import sys +file, plugin_id, name, source_id, source_path = sys.argv[1:] +with open(file, encoding="utf-8") as handle: + data = json.load(handle) +data["id"] = plugin_id +data["name"] = name +if "barWidget" in data and isinstance(data["barWidget"], dict): + data["barWidget"]["displayName"] = name +meta = data.get("omarchy") if isinstance(data.get("omarchy"), dict) else {} +meta.update({"clonedFrom": source_id, "clonedFromPath": source_path}) +data["omarchy"] = meta +open(file, "w", encoding="utf-8").write(json.dumps(data, indent=2) + "\n") +PY +} + +usage() { + cat </ so you +can edit it freely. The original stays built into Omarchy. + +Options: + --name Display name for the clone + --replace Replace the first bar instance of the source with the clone + --add [placement] Add the cloned bar widget to the bar + --use Use the clone as the active bar option (bar plugins) + +Placement (with --add): + --section + --index + --before + --after + +Examples: + omarchy plugin clone + omarchy plugin clone omarchy.clock local.clock + omarchy plugin clone omarchy.clock local.clock --name "My Clock" --replace +USAGE +} + +clone_command() { + require_command jq + require_command python3 + require_omarchy_path + + local source_id="" + local new_id="" + if (( $# > 0 )) && [[ $1 != --* ]]; then + source_id="$1" + shift + fi + if (( $# > 0 )) && [[ $1 != --* ]]; then + new_id="$1" + shift + fi + + local display_name="" + local replace="false" + local add="false" + local use_bar="false" + local placement=() + + while (( $# > 0 )); do + case "$1" in + --name) + display_name="${2:-}" + [[ -n $display_name ]] || fail "--name requires a value" + shift 2 + ;; + --replace) + replace="true" + shift + ;; + --add) + add="true" + shift + ;; + --use) + use_bar="true" + shift + ;; + --section | --index | --before | --after) + placement+=("$1" "${2:-}") + shift 2 + ;; + -h | --help) + usage + return + ;; + *) + fail "unknown clone option: $1" + ;; + esac + done + + if [[ -z $source_id ]]; then + if interactive; then + source_id=$(choose_clone_source) || fail "clone cancelled" + else + fail "clone source is required" + fi + fi + source_id=$(canonical_widget_id "$source_id") + + local default_slug + default_slug=$(slug_id "$source_id") + if [[ -z $new_id ]]; then + if interactive; then + new_id=$(input_value "New plugin id:" "local.$default_slug") + else + fail "new plugin id is required" + fi + fi + validate_plugin_id "$new_id" + + local target_dir="$PLUGINS_DIR/$new_id" + [[ ! -e $target_dir && ! -L $target_dir ]] || fail "$target_dir already exists" + + local source_path="" + local source_name="" + local source_category="Plugin" + local source_multiple="false" + local source_type="" + + if IFS=$'\t' read -r _ source_path source_name source_category source_multiple < <(builtin_widget_info "$source_id"); then + source_type="builtin-widget" + elif [[ -f $PLUGINS_DIR/$source_id/manifest.json ]]; then + source_type="user-plugin" + source_path="$PLUGINS_DIR/$source_id" + source_name=$(jq -r '.name // .id' "$source_path/manifest.json") + else + # Non-widget built-in plugin (e.g. a panel or bar): find it in the catalog. + local hit + hit=$(catalog_json | jq -r --arg id "$source_id" '.[] | select(.id == $id) | .sourceDir' | head -1) + if [[ -n $hit ]]; then + source_type="builtin-plugin" + source_path="$hit" + source_name=$(jq -r '.name // .id' "$source_path/manifest.json") + fi + fi + + [[ -n $source_type ]] || fail "unknown clone source: $source_id" + if [[ -z $display_name ]]; then + if interactive; then + display_name=$(input_value "Display name:" "${source_name:-$new_id}") + else + display_name="${source_name:-$new_id}" + fi + fi + [[ -n $display_name ]] || fail "display name is required" + + mkdir -p "$target_dir" + if [[ $source_type == "builtin-widget" ]]; then + cp "$source_path" "$target_dir/Widget.qml" + rewrite_cloned_qml "$target_dir/Widget.qml" "$new_id" "$source_path" + write_clone_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "Cloned from $source_id" "$source_category" "$source_multiple" "$source_id" "$source_path" + else + cp -aL "$source_path/." "$target_dir/" + update_cloned_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "$source_id" "$source_path" + fi + + { + printf '# %s\n\n' "$display_name" + printf 'Cloned from `%s`.\n\n' "$source_id" + printf 'Source: `%s`\n\n' "$source_path" + printf 'The original remains built into Omarchy. Remove this plugin directory or swap\n' + printf 'your bar entry back to `%s` whenever you want to return to the built-in.\n' "$source_id" + } >"$target_dir/UPSTREAM.md" + + omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true + + if [[ $replace == "true" ]]; then + omarchy-bar 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[@]}" + echo "Cloned $source_id to $new_id" + elif [[ $use_bar == "true" ]]; then + omarchy-bar use "$new_id" + echo "Cloned $source_id to $new_id and selected it as the active bar option" + else + echo "Cloned $source_id to $new_id" + fi + + if interactive; then + cd "$target_dir" + exec "${SHELL:-bash}" + fi +} + +clone_command "$@" diff --git a/bin/omarchy-plugin-edit b/bin/omarchy-plugin-edit new file mode 100755 index 00000000..9553dfc1 --- /dev/null +++ b/bin/omarchy-plugin-edit @@ -0,0 +1,157 @@ +#!/bin/bash + +# omarchy:summary=Open a user Omarchy shell plugin directory in a shell +# omarchy:group=plugin +# omarchy:args=[plugin-id] +# omarchy:examples=omarchy plugin edit | omarchy plugin edit local.clock + +set -euo pipefail + +PLUGINS_DIR="$HOME/.config/omarchy/plugins" + +fail() { + echo "omarchy-plugin-edit: $*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "$1 is required" +} + +interactive() { + [[ -t 0 && -t 1 ]] +} + +canonical_widget_id() { + printf '%s' "$1" +} + +slug_id() { + tr '[:upper:]' '[:lower:]' <<<"$1" | sed -E 's/^omarchy\.//; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' +} + +require_omarchy_path() { + [[ -n ${OMARCHY_PATH:-} ]] || fail "OMARCHY_PATH is not set" +} + +# A built-in id is one the catalog knows as first-party (or an omarchy.* id). +builtin_id_known() { + local id="$1" + require_command jq + require_omarchy_path + omarchy-plugin-catalog | jq -e --arg id "$id" \ + 'any(.[]; .id == $id and .firstParty)' >/dev/null 2>&1 +} + +validate_user_plugin_dir() { + local dir="$1" + local requested_id="${2:-}" + local root="$PLUGINS_DIR" + local dir_abs root_abs manifest_id + + require_command jq + require_command python3 + [[ -d $dir ]] || fail "plugin directory not found: $dir" + + dir_abs=$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).expanduser().resolve(strict=False))' "$dir") + root_abs=$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).expanduser().resolve(strict=False))' "$root") + [[ $dir_abs == $root_abs/* ]] || fail "plugin edit only opens user plugins under $root" + [[ -f $dir_abs/manifest.json ]] || fail "plugin directory missing manifest.json: $dir_abs" + + manifest_id=$(jq -r '.id // empty' "$dir_abs/manifest.json" 2>/dev/null || true) + [[ -n $manifest_id ]] || fail "plugin manifest missing id: $dir_abs/manifest.json" + [[ $manifest_id != omarchy.* && $(canonical_widget_id "$manifest_id") == $manifest_id ]] \ + || fail "$manifest_id is built in; clone it first with: omarchy plugin clone $manifest_id local.$(slug_id "$manifest_id")" + if [[ -n $requested_id && $manifest_id != $requested_id ]]; then + fail "plugin id mismatch: requested $requested_id but manifest declares $manifest_id" + fi + + printf '%s' "$dir_abs" +} + +plugin_dir_for_id() { + local id="$1" + + if [[ $id == */* ]]; then + validate_user_plugin_dir "$id" + return + fi + + id=$(canonical_widget_id "$id") + if builtin_id_known "$id" || [[ $id == omarchy.* ]]; then + fail "$id is built in; clone it first with: omarchy plugin clone $id local.$(slug_id "$id")" + fi + + validate_user_plugin_dir "$PLUGINS_DIR/$id" "$id" +} + +edit_source_options() { + [[ -d $PLUGINS_DIR ]] || return 0 + find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) 2>/dev/null | sort | while IFS= read -r dir; do + [[ -f $dir/manifest.json ]] || continue + id=$(jq -r '.id // empty' "$dir/manifest.json" 2>/dev/null || true) + name=$(jq -r '.name // .id // empty' "$dir/manifest.json" 2>/dev/null || true) + [[ -n $id ]] && printf '%s\t%s\t%s\n' "$id" "${name:-$id}" "$dir" + done +} + +choose_edit_source() { + local selected + selected=$(edit_source_options | awk -F '\t' '{ printf "%-32s %s\n", $2, $1 }' \ + | gum filter --header "Edit plugin" --placeholder "Search user plugins..." --limit 1) || return 1 + awk '{ print $NF }' <<<"$selected" +} + +usage() { + cat <) in a shell so you +can edit it. Built-in plugins must be cloned first with 'omarchy plugin clone'. + +Without an id in a terminal, an interactive picker lists your user plugins. + +Examples: + omarchy plugin edit + omarchy plugin edit local.clock +USAGE +} + +edit_command() { + local id="" + if (( $# > 0 )) && [[ $1 != --* ]]; then + id="$1" + shift + fi + + while (( $# > 0 )); do + case "$1" in + -h | --help) + usage + return + ;; + *) + fail "unknown edit option: $1" + ;; + esac + done + + if [[ -z $id ]]; then + if interactive; then + id=$(choose_edit_source) || fail "edit cancelled" + else + fail "plugin id is required" + fi + fi + + local dir + dir=$(plugin_dir_for_id "$id") + + if interactive; then + cd "$dir" + exec "${SHELL:-bash}" + fi + printf '%s\n' "$dir" +} + +edit_command "$@" diff --git a/bin/omarchy-refresh-shell b/bin/omarchy-refresh-shell index ae5611c5..13dc68fb 100755 --- a/bin/omarchy-refresh-shell +++ b/bin/omarchy-refresh-shell @@ -8,11 +8,11 @@ set -euo pipefail omarchy-refresh-config omarchy/shell.json if omarchy-installed-service-dropbox; then - omarchy-config-shell-bar add omarchy.dropbox + omarchy-bar add omarchy.dropbox fi if omarchy-installed-service-tailscale; then - omarchy-config-shell-bar add omarchy.tailscale + omarchy-bar add omarchy.tailscale fi omarchy-restart-shell diff --git a/bin/omarchy-remove-service-dropbox b/bin/omarchy-remove-service-dropbox index c94c2a71..c7922ad6 100644 --- a/bin/omarchy-remove-service-dropbox +++ b/bin/omarchy-remove-service-dropbox @@ -4,7 +4,7 @@ # omarchy:requires-sudo=true dropbox-cli stop 2>/dev/null || true -omarchy-config-shell-bar drop omarchy.dropbox +omarchy-bar drop omarchy.dropbox omarchy-pkg-drop dropbox dropbox-cli libappindicator-gtk3 python-gpgme nautilus-dropbox echo "" diff --git a/bin/omarchy-remove-service-tailscale b/bin/omarchy-remove-service-tailscale index 1fa4e321..f484d5e6 100644 --- a/bin/omarchy-remove-service-tailscale +++ b/bin/omarchy-remove-service-tailscale @@ -5,7 +5,7 @@ tailscale down 2>/dev/null || true sudo systemctl disable --now tailscaled.service 2>/dev/null || true -omarchy-config-shell-bar drop omarchy.tailscale +omarchy-bar drop omarchy.tailscale omarchy-webapp-remove "Tailscale" 2>/dev/null || true omarchy-pkg-drop tailscale diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index 0885bb26..56a01962 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -62,8 +62,8 @@ every prompt (the path for scripts and agents). Sources are recorded in `~/.config/omarchy/plugins/sources.json` and cloned into `~/.cache/omarchy/plugin-sources/`. You can still install by hand: drop a plugin into `~/.config/omarchy/plugins//`, run `omarchy plugin rescan`, then -`omarchy plugin enable ` (bar widgets also need `omarchy plugin bar add `; -full bar replacements are selected with `omarchy plugin bar use `). +`omarchy plugin enable ` (bar widgets also need `omarchy bar add `; +full bar replacements are selected with `omarchy bar use `). The lower-level IPC methods remain available through `omarchy-shell shell ...`. ## IPC diff --git a/shell/README.md b/shell/README.md index 0a762678..57d28219 100644 --- a/shell/README.md +++ b/shell/README.md @@ -134,7 +134,7 @@ You can still drop a plugin in without a source: 1. Put it in `~/.config/omarchy/plugins//` with a `manifest.json` plus the QML referenced from its `entryPoints`. 2. `omarchy plugin rescan`. -3. `omarchy plugin enable ` (bar widgets also need `omarchy plugin bar add `; full bar replacements are selected with `omarchy plugin bar use `). +3. `omarchy plugin enable ` (bar widgets also need `omarchy bar add `; full bar replacements are selected with `omarchy bar use `). The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`, `omarchy-shell shell setPluginEnabled true`, and `omarchy-shell shell listPlugins`. diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index 9afe9e40..ed006da0 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -14,9 +14,9 @@ the shell for its whole session. ## 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 plugin 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 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 launch bar settings` / `omarchy plugin 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 plugin bar list`, `omarchy plugin bar add`, `omarchy plugin bar move`, `omarchy plugin bar remove`, and `omarchy plugin 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 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. 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 plugin bar add`. +`omarchy plugin rescan`, and `omarchy bar add`. diff --git a/shell/plugins/panels/tailscale/README.md b/shell/plugins/panels/tailscale/README.md index 955759e7..aca6139f 100644 --- a/shell/plugins/panels/tailscale/README.md +++ b/shell/plugins/panels/tailscale/README.md @@ -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 plugin 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 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. diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 480f8888..904b3a2e 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -208,75 +208,75 @@ 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-config-shell-bar options --json | jq -e ' +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" -HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar use local.demo-bar +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 pass "shell config selects a bar option" -HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar reset +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-config-shell-bar add omarchy.tailscale +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar 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-config-shell-bar add local.left left +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar add omarchy.active-window left jq -e ' def ids: map(.id // .); - .bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces", "local.left"] + .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-config-shell-bar add local.center center +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar add omarchy.system-update center jq -e ' def ids: map(.id // .); - .bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "local.center"] + .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-config-shell-bar add local.first right +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar add omarchy.microphone right jq -e ' def ids: map(.id // .); - .bar.layout.right | ids == ["omarchy.tray", "local.first", "omarchy.tailscale", "omarchy.bluetooth"] + .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-config-shell-bar show | jq -e ' +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar show | jq -e ' def ids: map(.id // .); - (.layout.right | ids == ["omarchy.tray", "local.first", "omarchy.tailscale", "omarchy.bluetooth"]) and + (.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-config-shell-bar list --json | jq -e ' +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-config-shell-bar list --json --all | jq -e ' +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" -HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar position bottom +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar position bottom jq -e ' .bar.position == "bottom" and .plugins == [] ' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null pass "shell config sets bar position" -HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar transparent true +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar transparent true jq -e ' .bar.transparent == true and .bar.position == "bottom" and @@ -284,16 +284,16 @@ jq -e ' ' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null pass "shell config sets bar transparency" -HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar drop local.left +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar drop omarchy.active-window jq -e ' def ids: map(.id // .); (.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces"]) and - (.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "local.center"]) and - (.bar.layout.right | ids == ["omarchy.tray", "local.first", "omarchy.tailscale", "omarchy.bluetooth"]) + (.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "omarchy.system-update"]) and + (.bar.layout.right | ids == ["omarchy.tray", "omarchy.microphone", "omarchy.tailscale", "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-config-shell-bar remove local.center +HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar remove omarchy.system-update jq -e ' def ids: map(.id // .); .bar.layout.center | ids == ["omarchy.clock", "omarchy.weather"] diff --git a/test/shell.d/runtime-smoke-test.sh b/test/shell.d/runtime-smoke-test.sh index 2210373b..998b85b5 100755 --- a/test/shell.d/runtime-smoke-test.sh +++ b/test/shell.d/runtime-smoke-test.sh @@ -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-config-shell-bar" remove omarchy.audio +HOME="$test_home" OMARCHY_PATH="$test_root" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-bar" remove omarchy.audio for _ in {1..80}; do shell_config=$(shell_ipc shell listShellConfig 2>/dev/null || true) @@ -237,7 +237,7 @@ done jq -e 'all(.bar.layout.right[]; (.id // .) != "omarchy.audio")' <<<"$shell_config" >/dev/null || { printf 'Shell config after reload:\n%s\n' "$shell_config" | jq . >&2 - fail_with_log "config shell bar remove reloads shell config" + fail_with_log "bar remove reloads shell config" } jq -e 'all(.[]; .id != "omarchy.audio")' <<<"$geometry" >/dev/null || { @@ -246,4 +246,4 @@ jq -e 'all(.[]; .id != "omarchy.audio")' <<<"$geometry" >/dev/null || { fail_with_log "runtime bar layout updates after shell config reload" } -pass "config shell bar remove reloads shell config and updates bar layout" +pass "bar remove reloads shell config and updates bar layout"