* Add agent usage collectors that write display-ready data files
One omarchy-agent-usage-scan-<agent> collector per AI coding agent prints a
complete display-ready usage record — identity, tier, status, rate limits,
and today/week/all-time stats. omarchy-agent-usage-update runs every
collector it finds and writes the records atomically to
~/.local/state/omarchy/agents/usage/, so anything that displays usage only
ever reads JSON from there.
The Claude collector absorbs what the shell previously did in-process:
transcript scanning, the stats-cache/history fallback, credentials parsing,
and the OAuth limits probe, now with a probe throttle and last-good limits
kept across network failures. The Codex collector is the existing scanner
reshaped to the shared record contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Redo the model-usage plugin as omarchy.agents watching usage data files
The panel is now strictly a display. It discovers the JSON records that
omarchy-agent-usage-update maintains under
~/.local/state/omarchy/agents/usage/, watches them for changes, and draws
whatever appears — so adding an agent means shipping a collector, never
touching the panel. Marks resolve by convention (assets/<id>.svg with an
optional -light twin), the limits meters read a generic limits array, and
the per-provider QML adapters and in-plugin scanner scripts are gone.
Cross-device sync aggregation stays in the shell and keeps the snapshot
field names older versions wrote, so mixed-version fleets still merge in
both directions.
With the provider fan-out gone, the widget takes its real name: the plugin
id becomes omarchy.agents. A migration renames it wherever a user's config
mentions it — layout entries keep their settings and position, a disabled
widget stays disabled — then primes the data files once and drops the old
scanner cache. The migration test also drops a stale assertion that expected
migrations to restart the shell themselves, which c992cdff moved to
omarchy update.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address Codex review: synced-only tabs, limits retry, history fallback
Three data-availability gaps from review. An agent whose records only exist
in synced snapshots — a collector installed on just one machine — now gets
its tab by unioning the synced aggregate into the provider list, with rate
limits blank since those never travel. A Claude limits probe that reaches no
server at all writes retryAdvised into its record, and the shell honors it
with one 30-second retry instead of waiting out the full refresh interval,
restoring the old boot-before-DHCP behavior. And a machine with only
history.jsonl — no transcripts, no stats-cache — still reports today's
prompt and session counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address second Codex pass: history-only visibility, targeted retries
Today's prompt and session counts now count toward an agent's presence in
the bar, so a machine whose only Claude source is history.jsonl shows up
without waiting for limits. And the 30-second limits retry passes the
advising agent ids to the updater, so an outage at one provider no longer
puts every other collector on a retry treadmill.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop omarchy-cmd-present jq guards from the agents migrations
jq ships in the default package set, which makes it a runtime invariant per
AGENTS.md — call it directly. The migration tests lose their now-unused
omarchy-cmd-present stubs with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the scan infix from the collector command names
Collectors are omarchy-agent-usage-<agent>; the updater skips its own name
when globbing them, and the update test proves it with a decoy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep the credential store out of the printed usage record
The Claude collector now reads .credentials.json once into three scalars —
the access token, its expiry, and the plan label — instead of passing the
parsed store around. The token reaches nothing but the Authorization header
of the limits probe, and only the plan label may travel into the record,
which is what CodeQL's clear-text-logging alert on the record print was
unable to see when the whole dict flowed through.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1067 lines
27 KiB
Bash
Executable File
1067 lines
27 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -o pipefail
|
|
|
|
OMARCHY_BIN_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
METADATA_SCAN_LIMIT=80
|
|
|
|
COMMAND_KEYS=()
|
|
ROUTE_COLLISIONS=()
|
|
declare -A COMMAND_ROUTE
|
|
declare -A COMMAND_FALLBACK_ROUTE
|
|
declare -A COMMAND_BINARY
|
|
declare -A COMMAND_GROUP
|
|
declare -A COMMAND_NAME
|
|
declare -A COMMAND_SUMMARY
|
|
declare -A COMMAND_USAGE
|
|
declare -A COMMAND_ARGS
|
|
declare -A COMMAND_EXAMPLES
|
|
declare -A COMMAND_REQUIRES_SUDO
|
|
declare -A COMMAND_HIDDEN
|
|
declare -A COMMAND_ALIASES
|
|
declare -A COMMAND_HAS_SUMMARY
|
|
declare -A COMMAND_METADATA_ERRORS
|
|
declare -A ROUTE_TO_KEY
|
|
declare -A ROUTE_IS_ALIAS
|
|
declare -A BINARY_TO_KEY
|
|
declare -A GROUP_DESCRIPTIONS
|
|
|
|
GROUP_DESCRIPTIONS[agent]="AI coding agent usage data"
|
|
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"
|
|
GROUP_DESCRIPTIONS[branding]="About and screensaver branding"
|
|
GROUP_DESCRIPTIONS[brightness]="Display and keyboard brightness"
|
|
GROUP_DESCRIPTIONS[capture]="Screenshots and screen recording"
|
|
GROUP_DESCRIPTIONS[channel]="Omarchy release channel management"
|
|
GROUP_DESCRIPTIONS[clipboard]="Clipboard helpers"
|
|
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
|
|
GROUP_DESCRIPTIONS[config]="System configuration helpers"
|
|
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
|
|
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
|
|
GROUP_DESCRIPTIONS[default]="Default application selection"
|
|
GROUP_DESCRIPTIONS[dev]="Omarchy development tools"
|
|
GROUP_DESCRIPTIONS[display]="Display and text scaling"
|
|
GROUP_DESCRIPTIONS[dns]="DNS resolver configuration"
|
|
GROUP_DESCRIPTIONS[drive]="Drive selection and encryption"
|
|
GROUP_DESCRIPTIONS[file]="File selection helpers"
|
|
GROUP_DESCRIPTIONS[font]="Font management"
|
|
GROUP_DESCRIPTIONS[games]="Game launchers and helpers"
|
|
GROUP_DESCRIPTIONS[hibernation]="Hibernation setup and removal"
|
|
GROUP_DESCRIPTIONS[hook]="User hook runner"
|
|
GROUP_DESCRIPTIONS[hw]="Hardware detection and controls"
|
|
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
|
|
GROUP_DESCRIPTIONS[install]="Optional software installers"
|
|
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
|
|
GROUP_DESCRIPTIONS[launch]="Application launchers"
|
|
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
|
|
GROUP_DESCRIPTIONS[migrate]="Migration runner"
|
|
GROUP_DESCRIPTIONS[monitor]="Monitor status helpers"
|
|
GROUP_DESCRIPTIONS[network]="Network status helpers"
|
|
GROUP_DESCRIPTIONS[notification]="Notification helpers"
|
|
GROUP_DESCRIPTIONS[mise]="Mise tool wrappers"
|
|
GROUP_DESCRIPTIONS[osd]="On-screen display status helpers"
|
|
GROUP_DESCRIPTIONS[pkg]="Package management helpers"
|
|
GROUP_DESCRIPTIONS[plugin]="Omarchy shell plugin and bar widget management"
|
|
GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
|
|
GROUP_DESCRIPTIONS[power]="Power supply detection"
|
|
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
|
|
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
|
|
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
|
|
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
|
|
GROUP_DESCRIPTIONS[remove]="Removal workflows"
|
|
GROUP_DESCRIPTIONS[restart]="Restart Omarchy components"
|
|
GROUP_DESCRIPTIONS[setup]="Interactive setup wizards"
|
|
GROUP_DESCRIPTIONS[shell]="Omarchy shell IPC helpers"
|
|
GROUP_DESCRIPTIONS[screensaver]="Screensaver branding and animation"
|
|
GROUP_DESCRIPTIONS[snapshot]="System snapshots"
|
|
GROUP_DESCRIPTIONS[style]="Global UI style controls"
|
|
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
|
|
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
|
|
GROUP_DESCRIPTIONS[tailscale]="Tailscale helpers"
|
|
GROUP_DESCRIPTIONS[theme]="Theme management"
|
|
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
|
|
GROUP_DESCRIPTIONS[transcode]="Image and video transcoding"
|
|
GROUP_DESCRIPTIONS[tui]="Terminal UI launchers"
|
|
GROUP_DESCRIPTIONS[tz]="Timezone selection"
|
|
GROUP_DESCRIPTIONS[update]="Omarchy and system updates"
|
|
GROUP_DESCRIPTIONS[version]="Version and channel information"
|
|
GROUP_DESCRIPTIONS[voxtype]="Voxtype dictation"
|
|
GROUP_DESCRIPTIONS[weather]="Weather status"
|
|
GROUP_DESCRIPTIONS[webapp]="Web app launchers"
|
|
GROUP_DESCRIPTIONS[wifi]="Wi-Fi helpers"
|
|
GROUP_DESCRIPTIONS[windows]="Windows VM management"
|
|
|
|
join_words() {
|
|
local separator="$1"
|
|
shift
|
|
local joined=""
|
|
|
|
for word in "$@"; do
|
|
if [[ -z $joined ]]; then
|
|
joined="$word"
|
|
else
|
|
joined+="$separator$word"
|
|
fi
|
|
done
|
|
|
|
printf '%s' "$joined"
|
|
}
|
|
|
|
append_pipe_value() {
|
|
local current="$1"
|
|
local addition="$2"
|
|
|
|
if [[ -z $current ]]; then
|
|
printf '%s' "$addition"
|
|
else
|
|
printf '%s|%s' "$current" "$addition"
|
|
fi
|
|
}
|
|
|
|
register_route() {
|
|
local route="$1"
|
|
local key="$2"
|
|
local is_alias="${3:-false}"
|
|
|
|
[[ -z $route || -z $key ]] && return
|
|
|
|
if [[ -n ${ROUTE_TO_KEY[$route]} && ${ROUTE_TO_KEY[$route]} != "$key" ]]; then
|
|
ROUTE_COLLISIONS+=("$route -> ${ROUTE_TO_KEY[$route]} conflicts with $key")
|
|
return
|
|
fi
|
|
|
|
ROUTE_TO_KEY["$route"]="$key"
|
|
if [[ $is_alias == "true" ]]; then
|
|
ROUTE_IS_ALIAS["$route"]="true"
|
|
fi
|
|
}
|
|
|
|
register_command() {
|
|
local file="$1"
|
|
local file_binary="${file##*/}"
|
|
local group=""
|
|
local name=""
|
|
local summary=""
|
|
local usage=""
|
|
local args=""
|
|
local examples=""
|
|
local aliases=""
|
|
local requires_sudo=""
|
|
local hidden=""
|
|
local line=""
|
|
local metadata_key=""
|
|
local metadata_value=""
|
|
local line_count=0
|
|
local name_seen="false"
|
|
local fallback_summary=""
|
|
local has_summary="false"
|
|
local metadata_errors=""
|
|
|
|
while IFS= read -r line && (( line_count < METADATA_SCAN_LIMIT )); do
|
|
line_count=$((line_count + 1))
|
|
|
|
if (( line_count == 1 )) && [[ $line == "#!"* ]]; then
|
|
continue
|
|
fi
|
|
|
|
if [[ $line =~ ^[[:space:]]*$ ]]; then
|
|
continue
|
|
fi
|
|
|
|
if [[ ! $line =~ ^[[:space:]]*# ]]; then
|
|
break
|
|
fi
|
|
|
|
if [[ $line =~ ^[[:space:]]*#[[:space:]]*omarchy:([[:alnum:]_-]+)=(.*)$ ]]; then
|
|
metadata_key="${BASH_REMATCH[1]}"
|
|
metadata_value="${BASH_REMATCH[2]}"
|
|
metadata_value="${metadata_value%$'\r'}"
|
|
metadata_value="${metadata_value//$'\t'/ }"
|
|
|
|
case "$metadata_key" in
|
|
group)
|
|
group="$metadata_value"
|
|
;;
|
|
name)
|
|
name="$metadata_value"
|
|
name_seen="true"
|
|
;;
|
|
summary)
|
|
summary="$metadata_value"
|
|
[[ -n $metadata_value ]] && has_summary="true"
|
|
;;
|
|
args)
|
|
args="$metadata_value"
|
|
;;
|
|
examples)
|
|
examples="$metadata_value"
|
|
;;
|
|
alias | aliases)
|
|
aliases="$metadata_value"
|
|
;;
|
|
requires-sudo)
|
|
requires_sudo="$metadata_value"
|
|
[[ $metadata_value == "true" ]] || metadata_errors=$(append_pipe_value "$metadata_errors" "requires-sudo must be omitted or true")
|
|
;;
|
|
hidden)
|
|
hidden="$metadata_value"
|
|
[[ $metadata_value == "true" ]] || metadata_errors=$(append_pipe_value "$metadata_errors" "hidden must be omitted or true")
|
|
;;
|
|
*)
|
|
;;
|
|
esac
|
|
elif [[ -z $fallback_summary && $line =~ ^[[:space:]]*#[[:space:]]*(.+)$ ]]; then
|
|
fallback_summary="${BASH_REMATCH[1]}"
|
|
fallback_summary="${fallback_summary%$'\r'}"
|
|
fallback_summary="${fallback_summary//$'\t'/ }"
|
|
|
|
if [[ -z $fallback_summary || $fallback_summary == omarchy:* ]]; then
|
|
fallback_summary=""
|
|
fi
|
|
fi
|
|
done <"$file"
|
|
|
|
local stem="${file_binary#omarchy-}"
|
|
local fallback_group="$stem"
|
|
local fallback_name=""
|
|
local fallback_route="omarchy ${stem//-/ }"
|
|
|
|
if [[ $stem == *-* ]]; then
|
|
fallback_group="${stem%%-*}"
|
|
fallback_name="${stem#*-}"
|
|
fallback_name="${fallback_name//-/ }"
|
|
fi
|
|
|
|
[[ -z $group ]] && group="$fallback_group"
|
|
[[ $name_seen != "true" ]] && name="$fallback_name"
|
|
[[ -z $summary && -n $fallback_summary ]] && summary="$fallback_summary"
|
|
[[ -z $summary ]] && summary="Run the ${stem//-/ } command"
|
|
local route="omarchy $group"
|
|
if [[ -n $name ]]; then
|
|
route+=" $name"
|
|
fi
|
|
|
|
usage="$route"
|
|
if [[ -n $args ]]; then
|
|
usage+=" $args"
|
|
fi
|
|
|
|
[[ $requires_sudo == "true" ]] || requires_sudo="false"
|
|
[[ $hidden == "true" ]] || hidden="false"
|
|
|
|
local key="$file_binary"
|
|
COMMAND_KEYS+=("$key")
|
|
COMMAND_ROUTE["$key"]="$route"
|
|
COMMAND_FALLBACK_ROUTE["$key"]="$fallback_route"
|
|
COMMAND_BINARY["$key"]="$file_binary"
|
|
COMMAND_GROUP["$key"]="$group"
|
|
COMMAND_NAME["$key"]="$name"
|
|
COMMAND_SUMMARY["$key"]="$summary"
|
|
COMMAND_USAGE["$key"]="$usage"
|
|
COMMAND_ARGS["$key"]="$args"
|
|
COMMAND_EXAMPLES["$key"]="$examples"
|
|
COMMAND_REQUIRES_SUDO["$key"]="$requires_sudo"
|
|
COMMAND_HIDDEN["$key"]="$hidden"
|
|
COMMAND_HAS_SUMMARY["$key"]="$has_summary"
|
|
COMMAND_METADATA_ERRORS["$key"]="$metadata_errors"
|
|
|
|
BINARY_TO_KEY["$file_binary"]="$key"
|
|
register_route "$route" "$key"
|
|
register_route "$fallback_route" "$key"
|
|
|
|
local old_ifs="$IFS"
|
|
local alias_route=""
|
|
IFS='|'
|
|
for alias_route in $aliases; do
|
|
alias_route="${alias_route# }"
|
|
alias_route="${alias_route% }"
|
|
[[ -z $alias_route || $alias_route == "$route" ]] && continue
|
|
register_route "$alias_route" "$key" true
|
|
COMMAND_ALIASES["$key"]=$(append_pipe_value "${COMMAND_ALIASES[$key]}" "$alias_route")
|
|
done
|
|
IFS="$old_ifs"
|
|
}
|
|
|
|
load_commands() {
|
|
local file=""
|
|
|
|
for file in "$OMARCHY_BIN_DIR"/omarchy-*; do
|
|
[[ -f $file && -x $file ]] || continue
|
|
register_command "$file"
|
|
done
|
|
|
|
}
|
|
|
|
load_command_by_binary() {
|
|
local binary="$1"
|
|
local file="$OMARCHY_BIN_DIR/$binary"
|
|
|
|
[[ -f $file && -x $file ]] || return 1
|
|
register_command "$file"
|
|
}
|
|
|
|
load_child_commands_by_binary() {
|
|
local binary="$1"
|
|
local file=""
|
|
|
|
for file in "$OMARCHY_BIN_DIR/$binary"-*; do
|
|
[[ -f $file && -x $file ]] || continue
|
|
register_command "$file"
|
|
done
|
|
}
|
|
|
|
group_has_child_commands() {
|
|
local group="$1"
|
|
local file=""
|
|
|
|
for file in "$OMARCHY_BIN_DIR/omarchy-$group"-*; do
|
|
[[ -f $file && -x $file ]] && return 0
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
group_has_binary_commands() {
|
|
local group="$1"
|
|
|
|
[[ -x $OMARCHY_BIN_DIR/omarchy-$group ]] && return 0
|
|
group_has_child_commands "$group"
|
|
}
|
|
|
|
command_requires_args() {
|
|
local key="$1"
|
|
local args="${COMMAND_ARGS[$key]}"
|
|
local required="$args"
|
|
|
|
while [[ $required =~ ^(.*)\[[^][]*\](.*)$ ]]; do
|
|
required="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
|
|
done
|
|
|
|
required="${required// /}"
|
|
[[ -n $required ]]
|
|
}
|
|
|
|
load_group_commands() {
|
|
local group="$1"
|
|
local file=""
|
|
|
|
if [[ -f $OMARCHY_BIN_DIR/omarchy-$group && -x $OMARCHY_BIN_DIR/omarchy-$group ]]; then
|
|
register_command "$OMARCHY_BIN_DIR/omarchy-$group"
|
|
fi
|
|
|
|
for file in "$OMARCHY_BIN_DIR/omarchy-$group"-*; do
|
|
[[ -f $file && -x $file ]] || continue
|
|
register_command "$file"
|
|
done
|
|
|
|
}
|
|
|
|
resolve_direct_route() {
|
|
local argc="$1"
|
|
shift
|
|
local args=("$@")
|
|
local prefix_count=0
|
|
local route=""
|
|
local binary=""
|
|
local candidate=""
|
|
|
|
for (( prefix_count = argc; prefix_count >= 1; prefix_count-- )); do
|
|
route="omarchy $(join_words " " "${args[@]:0:prefix_count}")"
|
|
|
|
binary="omarchy-$(join_words "-" "${args[@]:0:prefix_count}")"
|
|
candidate="$OMARCHY_BIN_DIR/$binary"
|
|
if [[ -f $candidate && -x $candidate ]]; then
|
|
DIRECT_RESOLVED_BINARY="$binary"
|
|
DIRECT_RESOLVED_COUNT="$prefix_count"
|
|
DIRECT_RESOLVED_ROUTE="$route"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
sorted_keys() {
|
|
local include_all="$1"
|
|
local key=""
|
|
|
|
for key in "${COMMAND_KEYS[@]}"; do
|
|
if [[ $include_all != "true" && ${COMMAND_HIDDEN[$key]} == "true" ]]; then
|
|
continue
|
|
fi
|
|
printf '%s\t%s\n' "${COMMAND_ROUTE[$key]}" "$key"
|
|
done | sort -u | cut -f2-
|
|
}
|
|
|
|
fallback_group_for_key() {
|
|
local key="$1"
|
|
local fallback="${COMMAND_FALLBACK_ROUTE[$key]#omarchy }"
|
|
|
|
printf '%s' "${fallback%% *}"
|
|
}
|
|
|
|
command_route_for_group() {
|
|
local key="$1"
|
|
local group="$2"
|
|
|
|
if [[ $(fallback_group_for_key "$key") == "$group" && ${COMMAND_GROUP[$key]} != "$group" ]]; then
|
|
printf '%s' "${COMMAND_FALLBACK_ROUTE[$key]}"
|
|
else
|
|
printf '%s' "${COMMAND_ROUTE[$key]}"
|
|
fi
|
|
}
|
|
|
|
command_usage_for_group() {
|
|
local key="$1"
|
|
local group="$2"
|
|
local route=""
|
|
|
|
route=$(command_route_for_group "$key" "$group")
|
|
if [[ -n ${COMMAND_ARGS[$key]} ]]; then
|
|
route+=" ${COMMAND_ARGS[$key]}"
|
|
fi
|
|
|
|
printf '%s' "$route"
|
|
}
|
|
|
|
sorted_group_keys() {
|
|
local group="$1"
|
|
local include_all="$2"
|
|
local key=""
|
|
local fallback_group=""
|
|
local route=""
|
|
|
|
for key in "${COMMAND_KEYS[@]}"; do
|
|
if [[ $include_all != "true" && ${COMMAND_HIDDEN[$key]} == "true" ]]; then
|
|
continue
|
|
fi
|
|
|
|
fallback_group=$(fallback_group_for_key "$key")
|
|
if [[ ${COMMAND_GROUP[$key]} != "$group" && $fallback_group != "$group" ]]; then
|
|
continue
|
|
fi
|
|
|
|
route=$(command_route_for_group "$key" "$group")
|
|
printf '%s\t%s\n' "$route" "$key"
|
|
done | sort -u | cut -f2-
|
|
}
|
|
|
|
examples_as_lines() {
|
|
local examples="$1"
|
|
local old_ifs="$IFS"
|
|
local example=""
|
|
IFS='|'
|
|
|
|
for example in $examples; do
|
|
example="${example# }"
|
|
example="${example% }"
|
|
[[ -n $example ]] && printf '%s\n' "$example"
|
|
done
|
|
|
|
IFS="$old_ifs"
|
|
}
|
|
|
|
pipe_values_as_lines() {
|
|
local values="$1"
|
|
local old_ifs="$IFS"
|
|
local value=""
|
|
IFS='|'
|
|
|
|
for value in $values; do
|
|
value="${value# }"
|
|
value="${value% }"
|
|
[[ -n $value ]] && printf '%s\n' "$value"
|
|
done
|
|
|
|
IFS="$old_ifs"
|
|
}
|
|
|
|
show_group_list() {
|
|
local sorted_group=""
|
|
|
|
printf '%s\n' "${!GROUP_DESCRIPTIONS[@]}" | sort | while IFS= read -r sorted_group; do
|
|
[[ -n $sorted_group ]] || continue
|
|
printf ' %-14s %s\n' "$sorted_group" "${GROUP_DESCRIPTIONS[$sorted_group]}"
|
|
done
|
|
}
|
|
|
|
show_main_help() {
|
|
cat <<'EOF'
|
|
Omarchy command center
|
|
|
|
Usage:
|
|
omarchy <command> [args...]
|
|
omarchy commands [--all] [--json] [--check]
|
|
omarchy <group> --help
|
|
omarchy <group> <command> --help
|
|
|
|
Common commands:
|
|
omarchy update Update Omarchy and system packages
|
|
omarchy theme list List available themes
|
|
omarchy theme set <name> Apply a theme
|
|
omarchy font list List available fonts
|
|
omarchy screenshot Take a screenshot
|
|
omarchy debug Print debugging information
|
|
|
|
Groups:
|
|
EOF
|
|
show_group_list
|
|
cat <<'EOF'
|
|
|
|
Discovery:
|
|
omarchy commands List all commands
|
|
omarchy commands --all Include commands explicitly marked hidden
|
|
omarchy commands --json Machine-readable command list
|
|
omarchy commands --check Validate command metadata and routes
|
|
EOF
|
|
}
|
|
|
|
show_commands_help() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
omarchy commands [--all] [--json] [--markdown] [--check]
|
|
|
|
List commands known to the Omarchy command center.
|
|
|
|
Options:
|
|
--all Include commands explicitly marked hidden
|
|
--json Emit machine-readable JSON
|
|
--markdown Emit a Markdown command table
|
|
--check Validate command metadata and route collisions
|
|
EOF
|
|
}
|
|
|
|
print_command_table() {
|
|
local rows="$1"
|
|
local max_width=0
|
|
local width=0
|
|
local command=""
|
|
local rest=""
|
|
local summary=""
|
|
|
|
while IFS=$'\t' read -r command rest; do
|
|
[[ -n $command ]] || continue
|
|
width=${#command}
|
|
(( width > max_width )) && max_width=$width
|
|
done <<<"$rows"
|
|
|
|
while IFS=$'\t' read -r command rest; do
|
|
[[ -n $command ]] || continue
|
|
|
|
printf ' %-*s %s\n' "$max_width" "$command" "$rest"
|
|
done <<<"$rows"
|
|
}
|
|
|
|
show_commands() {
|
|
local include_all="$1"
|
|
local key=""
|
|
local route=""
|
|
local usage=""
|
|
local rows=""
|
|
local alias_rows=""
|
|
|
|
if [[ $include_all == "true" ]]; then
|
|
echo "Omarchy commands (all):"
|
|
else
|
|
echo "Omarchy commands:"
|
|
fi
|
|
|
|
while IFS= read -r key; do
|
|
[[ -n $key ]] || continue
|
|
usage="${COMMAND_USAGE[$key]}"
|
|
rows+="$usage"$'\t'"${COMMAND_SUMMARY[$key]}"$'\n'
|
|
done < <(sorted_keys "$include_all")
|
|
|
|
print_command_table "$rows"
|
|
|
|
for route in "${!ROUTE_IS_ALIAS[@]}"; do
|
|
key="${ROUTE_TO_KEY[$route]}"
|
|
if [[ $include_all != "true" && ${COMMAND_HIDDEN[$key]} == "true" ]]; then
|
|
continue
|
|
fi
|
|
alias_rows+="$route"$'\t'"${COMMAND_ROUTE[$key]}"$'\n'
|
|
done
|
|
|
|
if [[ -n $alias_rows ]]; then
|
|
echo ""
|
|
echo "Aliases:"
|
|
print_command_table "$(printf '%s' "$alias_rows" | sort)"
|
|
fi
|
|
}
|
|
|
|
markdown_escape() {
|
|
local value="$1"
|
|
value="${value//|/\\|}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
show_commands_markdown() {
|
|
local include_all="$1"
|
|
local key=""
|
|
|
|
echo "| Command | Binary | Summary |"
|
|
echo "| --- | --- | --- |"
|
|
|
|
while IFS= read -r key; do
|
|
[[ -n $key ]] || continue
|
|
printf '| `%s` | `%s` | %s |\n' \
|
|
"$(markdown_escape "${COMMAND_USAGE[$key]}")" \
|
|
"$(markdown_escape "${COMMAND_BINARY[$key]}")" \
|
|
"$(markdown_escape "${COMMAND_SUMMARY[$key]}")"
|
|
done < <(sorted_keys "$include_all")
|
|
}
|
|
|
|
emit_command_record() {
|
|
local key="$1"
|
|
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
"${COMMAND_ROUTE[$key]}" \
|
|
"${COMMAND_BINARY[$key]}" \
|
|
"${COMMAND_GROUP[$key]}" \
|
|
"${COMMAND_NAME[$key]}" \
|
|
"${COMMAND_SUMMARY[$key]}" \
|
|
"${COMMAND_REQUIRES_SUDO[$key]}" \
|
|
"${COMMAND_HIDDEN[$key]}" \
|
|
"${COMMAND_ARGS[$key]}" \
|
|
"${COMMAND_EXAMPLES[$key]}" \
|
|
"${COMMAND_ALIASES[$key]}" \
|
|
"${COMMAND_FALLBACK_ROUTE[$key]}" \
|
|
"${COMMAND_USAGE[$key]}"
|
|
}
|
|
|
|
emit_command_records() {
|
|
local include_all="$1"
|
|
local key=""
|
|
|
|
while IFS= read -r key; do
|
|
[[ -n $key ]] || continue
|
|
emit_command_record "$key"
|
|
done < <(sorted_keys "$include_all")
|
|
}
|
|
|
|
commands_json_filter() {
|
|
cat <<'EOF'
|
|
[inputs | split("\t") | {
|
|
route: .[0],
|
|
binary: .[1],
|
|
group: .[2],
|
|
name: .[3],
|
|
summary: .[4],
|
|
requires_sudo: (.[5] == "true"),
|
|
hidden: (.[6] == "true"),
|
|
args: .[7],
|
|
examples: (.[8] | split("|") | map(gsub("^ +| +$"; "")) | map(select(length > 0))),
|
|
aliases: (.[9] | split("|") | map(gsub("^ +| +$"; "")) | map(select(length > 0))),
|
|
filename_route: .[10],
|
|
routes: ([.[0], .[10]] + (.[9] | split("|") | map(gsub("^ +| +$"; "")) | map(select(length > 0))) | unique)
|
|
}] | {ok: true, commands: .}
|
|
EOF
|
|
}
|
|
|
|
show_commands_json() {
|
|
local include_all="$1"
|
|
|
|
emit_command_records "$include_all" | jq -Rn "$(commands_json_filter)"
|
|
}
|
|
|
|
show_commands_check() {
|
|
local failures=0
|
|
local collision=""
|
|
local key=""
|
|
local error=""
|
|
|
|
for collision in "${ROUTE_COLLISIONS[@]}"; do
|
|
echo "Route collision: $collision" >&2
|
|
failures=$((failures + 1))
|
|
done
|
|
|
|
for key in "${COMMAND_KEYS[@]}"; do
|
|
if [[ ${COMMAND_HAS_SUMMARY[$key]} != "true" ]]; then
|
|
echo "Missing metadata summary: ${COMMAND_BINARY[$key]}" >&2
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
if [[ -n ${COMMAND_METADATA_ERRORS[$key]} ]]; then
|
|
while IFS= read -r error; do
|
|
[[ -n $error ]] || continue
|
|
echo "Invalid metadata in ${COMMAND_BINARY[$key]}: $error" >&2
|
|
failures=$((failures + 1))
|
|
done < <(pipe_values_as_lines "${COMMAND_METADATA_ERRORS[$key]}")
|
|
fi
|
|
|
|
if [[ ! -x $OMARCHY_BIN_DIR/${COMMAND_BINARY[$key]} ]]; then
|
|
echo "Missing binary: ${COMMAND_BINARY[$key]}" >&2
|
|
failures=$((failures + 1))
|
|
fi
|
|
done
|
|
|
|
if (( failures > 0 )); then
|
|
echo "Command metadata check failed ($failures issues)" >&2
|
|
return 1
|
|
fi
|
|
|
|
echo "Command metadata check passed (${#COMMAND_KEYS[@]} commands)"
|
|
}
|
|
|
|
show_command_json() {
|
|
emit_command_record "$1" | jq -Rn "$(commands_json_filter) | {ok: true, command: .commands[0]}"
|
|
}
|
|
|
|
parse_commands_args() {
|
|
local include_all="false"
|
|
local json="false"
|
|
local markdown="false"
|
|
local check="false"
|
|
|
|
shift
|
|
|
|
while (( $# > 0 )); do
|
|
case "$1" in
|
|
--all)
|
|
include_all="true"
|
|
;;
|
|
--json)
|
|
json="true"
|
|
;;
|
|
--markdown)
|
|
markdown="true"
|
|
;;
|
|
--check)
|
|
check="true"
|
|
;;
|
|
--help | -h)
|
|
show_commands_help
|
|
return 0
|
|
;;
|
|
*)
|
|
echo "Unknown option for omarchy commands: $1" >&2
|
|
show_commands_help >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ $check == "true" ]]; then
|
|
show_commands_check
|
|
elif [[ $json == "true" ]]; then
|
|
show_commands_json "$include_all"
|
|
elif [[ $markdown == "true" ]]; then
|
|
show_commands_markdown "$include_all"
|
|
else
|
|
show_commands "$include_all"
|
|
fi
|
|
}
|
|
|
|
group_exists() {
|
|
local group="$1"
|
|
local key=""
|
|
|
|
for key in "${COMMAND_KEYS[@]}"; do
|
|
if [[ ${COMMAND_GROUP[$key]} == "$group" && -n ${COMMAND_NAME[$key]} ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
show_group_help() {
|
|
local group="$1"
|
|
local include_all="${2:-false}"
|
|
local key=""
|
|
local rows=""
|
|
local title="${GROUP_DESCRIPTIONS[$group]}"
|
|
|
|
if [[ -n $title ]]; then
|
|
echo "${group^} commands — $title:"
|
|
else
|
|
echo "${group^} commands:"
|
|
fi
|
|
|
|
while IFS= read -r key; do
|
|
[[ -n $key ]] || continue
|
|
rows+="$(command_usage_for_group "$key" "$group")"$'\t'$'\t'"${COMMAND_SUMMARY[$key]}"$'\n'
|
|
done < <(sorted_group_keys "$group" "$include_all")
|
|
|
|
if [[ -n $rows ]]; then
|
|
print_command_table "$rows"
|
|
else
|
|
echo " No documented commands found. Try: omarchy commands --all"
|
|
fi
|
|
}
|
|
|
|
show_prefix_help() {
|
|
local prefix="$1"
|
|
local prefix_with_space="$prefix "
|
|
local key=""
|
|
local rows=""
|
|
|
|
while IFS= read -r key; do
|
|
[[ -n $key ]] || continue
|
|
if [[ ${COMMAND_USAGE[$key]} == $prefix_with_space* ]]; then
|
|
rows+="${COMMAND_USAGE[$key]}"$'\t'$'\t'"${COMMAND_SUMMARY[$key]}"$'\n'
|
|
fi
|
|
done < <(sorted_keys false)
|
|
|
|
[[ -n $rows ]] || return 1
|
|
|
|
echo "${prefix#omarchy } commands:"
|
|
print_command_table "$rows"
|
|
}
|
|
|
|
show_related_commands() {
|
|
local key="$1"
|
|
local group="${COMMAND_GROUP[$key]}"
|
|
local child_key=""
|
|
local rows=""
|
|
|
|
[[ -z ${COMMAND_NAME[$key]} ]] || return
|
|
|
|
while IFS= read -r child_key; do
|
|
[[ -n $child_key && $child_key != "$key" ]] || continue
|
|
rows+="$(command_usage_for_group "$child_key" "$group")"$'\t'$'\t'"${COMMAND_SUMMARY[$child_key]}"$'\n'
|
|
done < <(sorted_group_keys "$group" false)
|
|
|
|
if [[ -n $rows ]]; then
|
|
echo ""
|
|
echo "Related commands:"
|
|
print_command_table "$rows"
|
|
fi
|
|
}
|
|
|
|
show_command_help() {
|
|
local key="$1"
|
|
local aliases="${COMMAND_ALIASES[$key]}"
|
|
local examples="${COMMAND_EXAMPLES[$key]}"
|
|
local args="${COMMAND_ARGS[$key]}"
|
|
local example=""
|
|
local alias=""
|
|
|
|
echo "Usage:"
|
|
echo " ${COMMAND_USAGE[$key]}"
|
|
echo ""
|
|
echo "${COMMAND_SUMMARY[$key]}"
|
|
|
|
if [[ -n $args ]]; then
|
|
echo ""
|
|
echo "Arguments:"
|
|
echo " $args"
|
|
fi
|
|
|
|
if [[ -n $examples ]]; then
|
|
echo ""
|
|
echo "Examples:"
|
|
while IFS= read -r example; do
|
|
printf ' %s\n' "$example"
|
|
done < <(examples_as_lines "$examples")
|
|
fi
|
|
|
|
if [[ -n $aliases ]]; then
|
|
echo ""
|
|
echo "Aliases:"
|
|
while IFS= read -r alias; do
|
|
printf ' %s\n' "$alias"
|
|
done < <(pipe_values_as_lines "$aliases")
|
|
fi
|
|
|
|
echo ""
|
|
echo "Binary:"
|
|
echo " ${COMMAND_BINARY[$key]}"
|
|
|
|
if [[ ${COMMAND_FALLBACK_ROUTE[$key]} != "${COMMAND_ROUTE[$key]}" ]]; then
|
|
echo ""
|
|
echo "Filename route:"
|
|
echo " ${COMMAND_FALLBACK_ROUTE[$key]}"
|
|
fi
|
|
|
|
show_related_commands "$key"
|
|
}
|
|
|
|
resolve_route() {
|
|
local argc="$1"
|
|
shift
|
|
local args=("$@")
|
|
local prefix_count=0
|
|
local route=""
|
|
local key=""
|
|
|
|
for (( prefix_count = argc; prefix_count >= 1; prefix_count-- )); do
|
|
route="omarchy $(join_words " " "${args[@]:0:prefix_count}")"
|
|
key="${ROUTE_TO_KEY[$route]}"
|
|
|
|
if [[ -n $key ]]; then
|
|
RESOLVED_KEY="$key"
|
|
RESOLVED_COUNT="$prefix_count"
|
|
RESOLVED_ROUTE="$route"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
suggest_command() {
|
|
local first="$1"
|
|
local candidate=""
|
|
|
|
for candidate in "${!ROUTE_TO_KEY[@]}"; do
|
|
candidate="${candidate#omarchy }"
|
|
if [[ $candidate == "$first"* ]]; then
|
|
printf '%s' "${candidate%% *}"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
dispatch_fast_or_help() {
|
|
local args=("$@")
|
|
local remaining=()
|
|
local key=""
|
|
local binary_path=""
|
|
|
|
if resolve_direct_route "$#" "${args[@]}"; then
|
|
remaining=("${args[@]:DIRECT_RESOLVED_COUNT}")
|
|
binary_path="$OMARCHY_BIN_DIR/$DIRECT_RESOLVED_BINARY"
|
|
|
|
if (( ${#remaining[@]} > 0 )) && [[ ${remaining[0]} == "--help" || ${remaining[0]} == "-h" ]]; then
|
|
if ! load_command_by_binary "$DIRECT_RESOLVED_BINARY"; then
|
|
echo "Binary is missing or not executable: $DIRECT_RESOLVED_BINARY" >&2
|
|
return 127
|
|
fi
|
|
|
|
if (( DIRECT_RESOLVED_COUNT == 1 )); then
|
|
load_child_commands_by_binary "$DIRECT_RESOLVED_BINARY"
|
|
fi
|
|
|
|
key="${BINARY_TO_KEY[$DIRECT_RESOLVED_BINARY]}"
|
|
if [[ " ${remaining[*]} " == *" --json "* ]]; then
|
|
show_command_json "$key"
|
|
else
|
|
show_command_help "$key"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
if (( ${#remaining[@]} == 0 )); then
|
|
load_command_by_binary "$DIRECT_RESOLVED_BINARY"
|
|
key="${BINARY_TO_KEY[$DIRECT_RESOLVED_BINARY]}"
|
|
|
|
if [[ -n $key ]] && command_requires_args "$key"; then
|
|
if (( DIRECT_RESOLVED_COUNT == 1 )) && group_has_child_commands "$1"; then
|
|
load_group_commands "$1"
|
|
show_group_help "$1"
|
|
else
|
|
show_command_help "$key"
|
|
fi
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
exec "$binary_path" "${remaining[@]}"
|
|
fi
|
|
|
|
if (( $# == 1 )) && group_has_binary_commands "$1"; then
|
|
load_group_commands "$1"
|
|
show_group_help "$1"
|
|
return 0
|
|
fi
|
|
|
|
if (( $# >= 2 )) && [[ $2 == "--help" || $2 == "-h" ]] && group_has_binary_commands "$1"; then
|
|
load_group_commands "$1"
|
|
show_group_help "$1"
|
|
return 0
|
|
fi
|
|
|
|
load_commands
|
|
dispatch_or_help "$@"
|
|
}
|
|
|
|
dispatch_or_help() {
|
|
local args=("$@")
|
|
local remaining=()
|
|
local key=""
|
|
local binary_path=""
|
|
local suggestion=""
|
|
|
|
if resolve_route "$#" "${args[@]}"; then
|
|
key="$RESOLVED_KEY"
|
|
remaining=("${args[@]:RESOLVED_COUNT}")
|
|
|
|
if (( ${#remaining[@]} > 0 )) && [[ ${remaining[0]} == "--help" || ${remaining[0]} == "-h" ]]; then
|
|
if [[ " ${remaining[*]} " == *" --json "* ]]; then
|
|
show_command_json "$key"
|
|
else
|
|
show_command_help "$key"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
binary_path="$OMARCHY_BIN_DIR/${COMMAND_BINARY[$key]}"
|
|
if [[ ! -x $binary_path ]]; then
|
|
echo "Binary is missing or not executable: ${COMMAND_BINARY[$key]}" >&2
|
|
return 127
|
|
fi
|
|
|
|
if (( ${#remaining[@]} == 0 )) && command_requires_args "$key"; then
|
|
if (( RESOLVED_COUNT == 1 )) && group_exists "$1"; then
|
|
show_group_help "$1"
|
|
else
|
|
show_command_help "$key"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
exec "$binary_path" "${remaining[@]}"
|
|
fi
|
|
|
|
if (( $# == 1 )) && group_exists "$1"; then
|
|
show_group_help "$1"
|
|
return 0
|
|
fi
|
|
|
|
if (( $# >= 2 )) && [[ $2 == "--help" || $2 == "-h" ]] && group_exists "$1"; then
|
|
show_group_help "$1"
|
|
return 0
|
|
fi
|
|
|
|
if show_prefix_help "omarchy ${args[*]}"; then
|
|
return 0
|
|
fi
|
|
|
|
echo "Unknown Omarchy command: omarchy ${args[*]}" >&2
|
|
suggestion=$(suggest_command "${args[0]}")
|
|
if [[ -n $suggestion ]]; then
|
|
echo "Did you mean: omarchy $suggestion ?" >&2
|
|
fi
|
|
echo "Run 'omarchy commands --all' to discover available commands." >&2
|
|
return 127
|
|
}
|
|
|
|
main() {
|
|
if (( $# == 0 )); then
|
|
show_main_help
|
|
return 0
|
|
fi
|
|
|
|
case "$1" in
|
|
--help | -h)
|
|
show_main_help
|
|
;;
|
|
commands)
|
|
load_commands
|
|
parse_commands_args "$@"
|
|
;;
|
|
*)
|
|
dispatch_fast_or_help "$@"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|