OpenClaw's desktop experience on Linux is its Control UI, served by the gateway the openclaw package runs, so the Install > AI entry installs the package and a web app launcher that routes through the new omarchy-launch-openclaw: first launch hands off to OpenClaw's own onboarding wizard, later launches start the gateway when needed and open the dashboard's single-use browser handoff URL as an app window. Remove > AI tears the gateway service down through OpenClaw's own gateway uninstall (falling back to systemctl by hand), aborts rather than dropping the package under a gateway that will not stop, and keeps the user's agent in ~/.openclaw. OpenClaw also joins Setup > Defaults > Agent through the same agent_installer seam Hermes carries: its CLI is the pacman package rather than a mise tool, so omarchy-install-openclaw-cli answers --check/--now with pacman, and omarchy-agent runs `openclaw chat`, seeding prompts through --message. The menu mark is a new U+E90C glyph traced from the package's lobster favicon; E90B stays free for the Perplexity mark still in flight on its own branch. The launcher recovers the gateway through `openclaw gateway install --force` (unit not enabled: missing, or an install that died after writing it) or `openclaw gateway start` (enabled but stopped), never `openclaw dashboard --yes`: as of OpenClaw 2026.9.1 that defers to "the owning supervisor" in both cases, and once the gateway is up it copies a one-time browser pairing URL into the clipboard. The dashboard probe is bounded so an app-grid launch cannot hang without a terminal to interrupt it. Removal treats only systemd's own "inactive"/"failed" as a stopped gateway, so an unreachable user manager aborts instead of dropping the package under a live process. All of it verified against a real 2026.9.1 install. Removal also takes down the node-host unit if OpenClaw ever installed one, and asks (default no, only on a terminal) whether ~/.openclaw should go too, with its size: the chats and credentials live there next to hundreds of megabytes of plugin runtimes and cache OpenClaw downloads for itself. Onboarding goes through omarchy-openclaw-onboard rather than bare `openclaw onboard`: as of 2026.9.1 the bare command is the guided flow, which ends by running a foreground gateway and handing off to a browser tab without returning, so the install script never reached the app launch and no service was installed. The helper runs the classic wizard (--flow quickstart --install-daemon --skip-ui) as a background job that keeps the terminal as its stdin, so its prompts render and take input as upstream draws them, and stops it once the gateway answers: upstream leaves the wizard running after its outro (only the TUI branch exits, and the model sign-in holds a socket open). Every quickstart prompt precedes the service install, so that point is safe. A gateway that never comes up after this run applies setup ends the wait as a failure instead of hanging, an already-running OpenClaw is left alone rather than mistaken for this run's success, a gateway answering on the port is only this run's once its process is the unit's own MainPID (an orphan from an earlier run) is not mistaken for the service this run installs, and a signal at the helper takes the wizard down with it.
1094 lines
27 KiB
Bash
Executable File
1094 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[ascii]="Text drawn as ASCII art"
|
|
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[crash]="Crash notification controls"
|
|
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[disk]="Disk performance helpers"
|
|
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[openclaw]="OpenClaw agent platform setup"
|
|
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[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[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
|
|
}
|
|
|
|
# A route can resolve to a binary before all args are consumed (`update aur` only matches the `update` binary, leaving `aur` and `--help` as leftovers)
|
|
# --help must never be lost among those leftovers and get forwarded into the real command, so this checks the whole remainder rather than just the first token.
|
|
# A `--` marks everything after it as belonging to the command itself, so scanning stops there.
|
|
remaining_has_help_flag() {
|
|
local token=""
|
|
|
|
for token in "$@"; do
|
|
[[ $token == "--" ]] && break
|
|
[[ $token == "--help" || $token == "-h" ]] && return 0
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
remaining_has_json_flag() {
|
|
local token=""
|
|
|
|
for token in "$@"; do
|
|
[[ $token == "--" ]] && break
|
|
[[ $token == "--json" ]] && return 0
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
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_has_help_flag "${remaining[@]}"; 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_has_json_flag "${remaining[@]}"; 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_has_help_flag "${remaining[@]}"; then
|
|
if remaining_has_json_flag "${remaining[@]}"; 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 "$@"
|