# 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 claude-mode preset new [from] create a preset (copies 'from', else 'default') claude-mode preset new --provider

[--blank] copy that provider's default, or start empty claude-mode preset rename claude-mode preset default [provider] [name|--clear] which preset 'claude-mode ' picks claude-mode preset set claude-mode preset all claude-mode preset url point a preset at another server claude-mode preset auth none|key [ref] whether that server needs a key claude-mode preset rm 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 [--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 [--terminal] first-run setup: key, server, models claude-mode preflight [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 | --unignore | --unignore-all | --ignored stop (or resume) counting one broken session claude-mode repair-session --all [--max-age ] broken sessions untouched for longer are hidden (default 7; 0 shows them all) claude-mode --force switch even if preflight says no claude-mode --yes switch without asking about running sessions EOF }