linux/claude-mode was 3,035 lines. It is now 177: the paths and flags, a loader, and the command dispatch. Everything else moved, verbatim, into twelve files under linux/lib/, one per concern - output, core, preflight, sessions, switch, catalogue, commands, doctor, presets, menu, setup, repair. The move was done by line range with a check that every original line landed in exactly one file; the only thing that changed place is the switch's running-sessions question, which now sits with session detection. The CLI finds lib/, cm-json.py and cm-vault.sh beside itself, following the ~/.local/bin symlink, so a checkout runs its own code rather than the installed version's (it used to mix the two). The key helper path written into settings.json is still the installed one. linux/install.sh ships bin/lib/, clearing old modules first so a removed one cannot linger. The package build copies linux/ recursively, which a flat copy would not. tests/cli/test_install.sh runs the real installer into a sandbox home: every file lands, the symlink runs, a reinstall keeps edited presets and drops a stale module. docs/architecture.md lists the modules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
278 lines
13 KiB
Bash
278 lines
13 KiB
Bash
# shellcheck shell=bash
|
|
# linux/lib/output.sh - colours (a palette derived from the desktop theme), output helpers, the banner and usage.
|
|
#
|
|
# Part of the claude-mode CLI: sourced by linux/claude-mode, which sets the
|
|
# CM_* paths, PY and JSON used here. Not meant to run on its own.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Colour / output
|
|
#
|
|
# The palette follows the desktop theme when there is one to follow. Omarchy
|
|
# publishes its active theme as a flat colors.toml, so on those systems the
|
|
# menu paints in the same colours as the bar and the terminal instead of in
|
|
# whatever the sixteen ANSI slots happen to mean today.
|
|
#
|
|
# That indirection is not decoration. The ANSI slots carry no guarantee about
|
|
# relative brightness, and monochrome themes exploit it. Under Omarchy's
|
|
# Solitude, slot 36 (headings) resolves to #707070 and slot 31 (FAIL) to
|
|
# #565d60; against #cacccc body text on a #101315 ground those are 3.8:1 and
|
|
# 2.8:1 where the body text is 11.6:1 - so headings render as fine print and
|
|
# an error message becomes the quietest thing on screen. Exactly backwards.
|
|
#
|
|
# Deriving from the theme instead lets every role be measured against the
|
|
# background it will actually be drawn on, and lifted toward the foreground
|
|
# when it comes up short. Hue is preserved where the theme has any; where it
|
|
# does not, roles resolve to weight rather than to invisible colour.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CM_THEME_FILE="${CLAUDE_MODE_THEME:-$HOME/.local/state/omarchy/current/theme/colors.toml}"
|
|
|
|
# Flat `key = "#rrggbb"` lookup. Quotes are optional so this also reads the
|
|
# handful of themes that ship the file unquoted.
|
|
cm_theme_get() {
|
|
[ -f "$CM_THEME_FILE" ] || return 1
|
|
sed -n "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*\"\{0,1\}\([^\"#]*#\{0,1\}[0-9A-Fa-f]*\)\"\{0,1\}[[:space:]]*\$/\1/p" \
|
|
"$CM_THEME_FILE" 2>/dev/null | head -n1
|
|
}
|
|
|
|
cm_hex_ok() { case "$1" in \#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]) return 0 ;; *) return 1 ;; esac; }
|
|
|
|
# Relative brightness, 0-255. Deliberately the linear Rec.709 weighting without
|
|
# the sRGB gamma step: bash has no floats, and this only ever has to answer
|
|
# "which of these is further from the background", which it does correctly.
|
|
cm_lum() {
|
|
local h="${1#\#}" r g b
|
|
r=$((16#${h:0:2})); g=$((16#${h:2:2})); b=$((16#${h:4:2}))
|
|
printf '%d' $(( (2126 * r + 7152 * g + 722 * b) / 10000 ))
|
|
}
|
|
|
|
# Distance from the theme background, which is what legibility actually is.
|
|
cm_dist() { local a b; a="$(cm_lum "$1")"; b="$(cm_lum "$CM_BG")"; printf '%d' $(( a > b ? a - b : b - a )); }
|
|
|
|
cm_mix() {
|
|
local x="${1#\#}" y="${2#\#}" p="$3" r g b
|
|
r=$(( (16#${x:0:2} * (100 - p) + 16#${y:0:2} * p) / 100 ))
|
|
g=$(( (16#${x:2:2} * (100 - p) + 16#${y:2:2} * p) / 100 ))
|
|
b=$(( (16#${x:4:2} * (100 - p) + 16#${y:4:2} * p) / 100 ))
|
|
printf '#%02x%02x%02x' "$r" "$g" "$b"
|
|
}
|
|
|
|
# Blend a colour toward the foreground until it clears `pct` of the
|
|
# foreground's own separation from the background. Hue survives the lift, so a
|
|
# themed accent stays recognisably itself; a grey one just ends up brighter.
|
|
cm_lift() {
|
|
local c="$1" pct="$2" need step out
|
|
need=$(( $(cm_dist "$CM_FG") * pct / 100 ))
|
|
out="$c"
|
|
for step in 0 15 30 45 60 75 90 100; do
|
|
out="$(cm_mix "$c" "$CM_FG" "$step")"
|
|
[ "$(cm_dist "$out")" -ge "$need" ] && break
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# Push a colour toward the background - for text that is meant to recede but
|
|
# still be readable. Floored, so "dim" never becomes "gone".
|
|
cm_sink() {
|
|
local c="$1" pct="$2" need out step
|
|
need=$(( $(cm_dist "$CM_FG") * pct / 100 ))
|
|
out="$c"
|
|
[ "$(cm_dist "$out")" -ge "$need" ] && { printf '%s' "$out"; return; }
|
|
for step in 85 70 55 40 25 10 0; do
|
|
out="$(cm_mix "$CM_BG" "$CM_FG" $((100 - step)))"
|
|
[ "$(cm_dist "$out")" -ge "$need" ] && break
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# Saturation as a 0-100 proxy. Monochrome themes define `red` as a desaturated
|
|
# slate, which carries none of the meaning the role needs.
|
|
cm_sat() {
|
|
local h="${1#\#}" r g b mx mn
|
|
r=$((16#${h:0:2})); g=$((16#${h:2:2})); b=$((16#${h:4:2}))
|
|
mx=$r; [ "$g" -gt "$mx" ] && mx=$g; [ "$b" -gt "$mx" ] && mx=$b
|
|
mn=$r; [ "$g" -lt "$mn" ] && mn=$g; [ "$b" -lt "$mn" ] && mn=$b
|
|
[ "$mx" -eq 0 ] && { printf '0'; return; }
|
|
printf '%d' $(( (mx - mn) * 100 / mx ))
|
|
}
|
|
|
|
cm_sgr() { local h="${1#\#}"; printf '\033[38;2;%d;%d;%dm' $((16#${h:0:2})) $((16#${h:2:2})) $((16#${h:4:2})); }
|
|
|
|
# Pick a themed colour for a role, falling back through the theme's own keys
|
|
# and finally to the foreground. `minPct` is the share of the foreground's
|
|
# contrast the role has to clear before it is allowed on screen.
|
|
cm_role() {
|
|
local minPct="$1" want c
|
|
shift
|
|
for want in "$@"; do
|
|
c="$(cm_theme_get "$want")"
|
|
cm_hex_ok "$c" || continue
|
|
cm_sgr "$(cm_lift "$c" "$minPct")"
|
|
return 0
|
|
done
|
|
cm_sgr "$CM_FG"
|
|
}
|
|
|
|
cm_truecolor() { case "${COLORTERM:-}" in truecolor|24bit) return 0 ;; *) return 1 ;; esac; }
|
|
|
|
cm_theme_palette() {
|
|
local bg fg red
|
|
|
|
cm_truecolor || return 1
|
|
bg="$(cm_theme_get background)"; fg="$(cm_theme_get foreground)"
|
|
cm_hex_ok "$bg" && cm_hex_ok "$fg" || return 1
|
|
|
|
CM_BG="$bg"; CM_FG="$fg"
|
|
CM_THEME_MODE="$(cm_theme_get mode)"
|
|
|
|
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'
|
|
|
|
# Body text sets the bar every other role is measured against.
|
|
C_WHITE="$(cm_sgr "$CM_FG")"
|
|
C_GRAY="$(cm_sgr "$(cm_mix "$CM_FG" "$CM_BG" 20)")"
|
|
|
|
# Headings and the selection accent. Lifted to 85% of body contrast: a
|
|
# heading that is dimmer than the text beneath it is not a heading.
|
|
C_CYAN="$(cm_role 85 accent blue cyan)"
|
|
# Banner shading - subordinate to the heading but still structural.
|
|
C_DKCYAN="$(cm_sgr "$(cm_sink "$(cm_mix "$(cm_theme_get accent)" "$CM_BG" 35)" 42)")"
|
|
|
|
# Deliberately recessive: help lines, hints, detail rows. The floor keeps
|
|
# it off the background rather than merged into it.
|
|
C_DIM="$(cm_sgr "$(cm_sink "$(cm_theme_get muted)" 30)")"
|
|
|
|
C_GREEN="$(cm_role 55 green bright_green)"
|
|
C_YELLOW="$(cm_role 65 yellow bright_yellow)"
|
|
C_MAGENTA="$(cm_role 70 magenta bright_magenta)"
|
|
|
|
# FAIL has to out-shout everything else, so it takes the theme's saturated
|
|
# red where one exists and a lifted fallback where it does not. Solitude's
|
|
# `red` is #565d60 - a slate with no hue left in it - which is why the
|
|
# saturated `bright_red` is preferred over the nominal one here.
|
|
red="$(cm_theme_get bright_red)"
|
|
if ! cm_hex_ok "$red" || [ "$(cm_sat "$red")" -lt 25 ]; then
|
|
red="$(cm_theme_get red)"
|
|
fi
|
|
if cm_hex_ok "$red" && [ "$(cm_sat "$red")" -ge 25 ]; then
|
|
C_RED="$(cm_sgr "$(cm_lift "$red" 50)")"
|
|
else
|
|
# No usable red anywhere in the theme. Weight carries the role instead,
|
|
# over a hue that at least leans warm.
|
|
C_RED="$C_BOLD$(cm_sgr "$(cm_lift '#d2685f' 60)")"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
if [ ! -t 1 ] || [ -n "${NO_COLOR:-}" ]; then
|
|
C_RESET=''; C_DIM=''; C_CYAN=''; C_GREEN=''; C_YELLOW=''; C_RED=''
|
|
C_MAGENTA=''; C_WHITE=''; C_GRAY=''; C_DKCYAN=''; C_BOLD=''
|
|
CM_THEME_MODE=''
|
|
elif ! cm_theme_palette; then
|
|
# No theme to read, or a terminal that cannot render one. Same sixteen
|
|
# slots as before with the two roles the slots get wrong corrected:
|
|
# bright red for FAIL, because slot 31 is a muted maroon under a good many
|
|
# palettes, and bold on headings, which no palette can take away.
|
|
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'
|
|
C_DIM=$'\033[90m'; C_CYAN=$'\033[36m'; C_GREEN=$'\033[32m'
|
|
C_YELLOW=$'\033[33m'; C_RED=$'\033[91m'; C_MAGENTA=$'\033[35m'; C_WHITE=$'\033[97m'
|
|
C_GRAY=$'\033[37m'; C_DKCYAN=$'\033[36;2m'
|
|
CM_THEME_MODE=''
|
|
fi
|
|
|
|
say() { printf ' %s\n' "$*"; }
|
|
ok() { printf ' %sok %s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
|
|
# warn/err go to stderr: several of these functions run inside $( ), where
|
|
# anything on stdout is captured as the return value instead of being shown.
|
|
warn() { printf ' %swarn%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
|
|
err() { printf ' %sFAIL%s %s\n' "$C_BOLD$C_RED" "$C_RESET" "$*" >&2; }
|
|
head_() { printf '\n%s%s%s\n' "$C_BOLD$C_CYAN" "$*" "$C_RESET"; }
|
|
|
|
mode_color() {
|
|
[ "$1" = anthropic ] && { printf '%s' "$C_MAGENTA"; return; }
|
|
case "$(prov_field "$1" 5)" in
|
|
cyan) printf '%s' "$C_CYAN" ;;
|
|
green) printf '%s' "$C_GREEN" ;;
|
|
yellow) printf '%s' "$C_YELLOW" ;;
|
|
magenta) printf '%s' "$C_MAGENTA" ;;
|
|
white) printf '%s' "$C_WHITE" ;;
|
|
dkcyan) printf '%s' "$C_DKCYAN" ;;
|
|
*) printf '%s' "$C_GRAY" ;;
|
|
esac
|
|
}
|
|
|
|
mode_label() {
|
|
[ "$1" = anthropic ] && { printf 'Anthropic - your subscription login, no gateway'; return; }
|
|
prov_field "$1" 4
|
|
}
|
|
|
|
# Pure ASCII on purpose - renders identically in every terminal and locale.
|
|
show_banner() {
|
|
local mode="$1" preset="$2" tag
|
|
printf '\n'
|
|
printf '%s ____ _ _ __ __ _ %s\n' "$C_DKCYAN" "$C_RESET"
|
|
printf '%s / ___| | __ _ _ _ __| | ___ | \\/ | ___ __| | ___ %s\n' "$C_CYAN" "$C_RESET"
|
|
printf '%s | | | |/ _` | | | |/ _` |/ _ \\ | |\\/| |/ _ \\ / _` |/ _ \\%s\n' "$C_CYAN" "$C_RESET"
|
|
printf '%s | |___| | (_| | |_| | (_| | __/ | | | | (_) | (_| | __/%s\n' "$C_CYAN" "$C_RESET"
|
|
printf '%s \\____|_|\\__,_|\\__,_|\\__,_|\\___| |_| |_|\\___/ \\__,_|\\___|%s\n' "$C_DKCYAN" "$C_RESET"
|
|
printf ' %s-----------------------------------------------------------%s\n' "$C_DIM" "$C_RESET"
|
|
tag="$mode"; [ -n "$preset" ] && tag="$mode / $preset"
|
|
printf ' %snow%s %s%s%s %sswitch Claude Code between providers%s\n' \
|
|
"$C_DIM" "$C_RESET" "$(mode_color "$mode")" "$tag" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
claude-mode - switch Claude Code between Anthropic and gateway providers
|
|
|
|
claude-mode interactive menu
|
|
claude-mode status active mode, preset, model map
|
|
|
|
claude-mode anthropic native login (clears all gateway config)
|
|
EOF
|
|
# One line per provider in providers.json, so a new one documents itself.
|
|
local id
|
|
for id in $(provider_ids); do
|
|
printf ' claude-mode %-26s %s (default: %s)\n' "$id [preset]" \
|
|
"$(prov_field "$id" 4 | sed 's/^[^-]*- //')" "$(default_preset_for "$id")"
|
|
done
|
|
cat <<'EOF'
|
|
|
|
claude-mode presets list presets
|
|
claude-mode preset show <name>
|
|
claude-mode preset new <name> [from] create a preset (copies 'from', else 'default')
|
|
claude-mode preset new <name> --provider <p> [--blank]
|
|
copy that provider's default, or start empty
|
|
claude-mode preset rename <name> <new-name>
|
|
claude-mode preset default [provider] [name|--clear]
|
|
which preset 'claude-mode <provider>' picks
|
|
claude-mode preset set <name> <tier> <model-id>
|
|
claude-mode preset all <name> <model-id>
|
|
claude-mode preset url <name> <base-url> point a preset at another server
|
|
claude-mode preset auth <name> none|key [ref] whether that server needs a key
|
|
claude-mode preset rm <name>
|
|
|
|
claude-mode set-key [ref] [key] store an API key (hidden prompt; key for scripts)
|
|
claude-mode models [filter] models available from the active provider
|
|
claude-mode models --preset <name> [--refresh|--json]
|
|
that preset's provider instead; --refresh only
|
|
updates the panel's cached list
|
|
claude-mode doctor verify auth, endpoint, model ids, env
|
|
claude-mode repair strip [1m] tags from cached model ids
|
|
claude-mode health refresh health.json (machine-readable state)
|
|
claude-mode setup <mode> [--terminal] first-run setup: key, server, models
|
|
claude-mode preflight <mode> [preset] check a mode can actually serve, without switching
|
|
claude-mode sessions [--stop|--restart]
|
|
running sessions; close or reopen them
|
|
claude-mode repair-session [id] [--apply]
|
|
make a session resumable again after a bad switch,
|
|
keeping the cut turns as markdown + a context note
|
|
claude-mode repair-session --ignore <id> | --unignore <id> | --unignore-all | --ignored
|
|
stop (or resume) counting one broken session
|
|
claude-mode repair-session --all [--max-age <days>]
|
|
broken sessions untouched for longer are hidden
|
|
(default 7; 0 shows them all)
|
|
claude-mode <mode> --force switch even if preflight says no
|
|
claude-mode <mode> --yes switch without asking about running sessions
|
|
EOF
|
|
}
|