The model segment collapsed effort and model into one string with no way to tell them apart at a glance once models like Opus started supporting multiple effort levels; append it as "+xhigh" instead, omitted when the model doesn't support the parameter. Move the git branch/worktree segment to a second line so it isn't pushed off screen by the usage segments on a long first line. Also unset LC_ALL before forcing LC_NUMERIC=C: LC_ALL overrides LC_NUMERIC per POSIX precedence, so if it's set the locale fix was silently ineffective and $cost -- the only usage signal on a credit-pool account -- would render wrong or fail. Documented all three in docs/config/claude.md.
202 lines
6.4 KiB
Shell
Executable file
202 lines
6.4 KiB
Shell
Executable file
#!/bin/bash
|
|
#
|
|
# Claude Code status line.
|
|
#
|
|
# Opus 5+xhigh | 43% | $1.23 | ⚡99% | 5h 4% (1h 30m) | 7d 90% (2d 15h) | !err
|
|
# ^effort ^ctx ^cost ^cache ^------ only on Claude.ai Pro/Max ------^ ^on failure
|
|
# feat: br
|
|
# ^--- git branch/worktree, on its own line, omitted entirely outside a repo
|
|
#
|
|
# `effort` is omitted when the current model doesn't support the effort
|
|
# parameter. Every segment after the cost is optional and is omitted entirely
|
|
# -- separator included -- when its data is absent. In particular
|
|
# `rate_limits` is sent only to Claude.ai subscribers (Pro/Max) and each
|
|
# window can be absent independently, so on a Console/corporate account billed
|
|
# against a credit pool the line collapses to "Model | ctx% | $cost". The
|
|
# status line payload carries no credit-balance field, so $cost is the only
|
|
# usage signal there.
|
|
|
|
# printf(1) is locale-aware: under LC_NUMERIC=es_ES it renders the cost as
|
|
# "1,23" and rejects "31.2" outright with "invalid number". LC_ALL, when set,
|
|
# overrides LC_NUMERIC per POSIX precedence, so it must be cleared first or
|
|
# the export below is silently ineffective -- on a credit-pool/API-billed
|
|
# account $cost is the only usage signal, so this segment failing is worse
|
|
# than usual.
|
|
unset LC_ALL
|
|
export LC_NUMERIC=C
|
|
|
|
input=$(cat)
|
|
|
|
RESET="\033[0m"
|
|
GREEN="\033[32m"
|
|
YELLOW="\033[33m"
|
|
RED="\033[31m"
|
|
|
|
# --- error surfacing -------------------------------------------------------
|
|
# A status line's stderr is discarded, so a broken jq filter or a missing
|
|
# dependency would silently render a blank segment. Failures are collected and
|
|
# shown as a trailing marker instead.
|
|
ERRORS=""
|
|
note_error() {
|
|
case ",$ERRORS," in
|
|
*",$1,"*) return 0 ;;
|
|
esac
|
|
ERRORS="${ERRORS:+$ERRORS,}$1"
|
|
}
|
|
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo -e "${RED}status line: jq not installed${RESET}"
|
|
exit 0
|
|
fi
|
|
|
|
# jqr <filter> [fallback] -- read a value into $JQ_OUT, defaulting when the key
|
|
# is absent, null, or the filter fails.
|
|
#
|
|
# The result is returned via a global rather than stdout on purpose: called as
|
|
# $(jqr ...) the function would run in a subshell, and note_error's assignment
|
|
# to ERRORS would be discarded with it -- so the failure marker could never
|
|
# appear. Same reason rate_window writes to $RATE_OUT.
|
|
JQ_OUT=""
|
|
jqr() {
|
|
if ! JQ_OUT=$(printf '%s' "$input" | jq -r "$1" 2>/dev/null); then
|
|
note_error "jq"
|
|
JQ_OUT="${2-}"
|
|
return 0
|
|
fi
|
|
if [ -z "$JQ_OUT" ] || [ "$JQ_OUT" = "null" ]; then
|
|
JQ_OUT="${2-}"
|
|
fi
|
|
}
|
|
|
|
# threshold_color <pct> -- green under 50, yellow under 70, red above.
|
|
threshold_color() {
|
|
if [ "$1" -lt 50 ]; then
|
|
printf '%s' "$GREEN"
|
|
elif [ "$1" -lt 70 ]; then
|
|
printf '%s' "$YELLOW"
|
|
else
|
|
printf '%s' "$RED"
|
|
fi
|
|
}
|
|
|
|
# cache_color <pct> -- inverted thresholds: for a cache hit rate, high is good.
|
|
cache_color() {
|
|
if [ "$1" -ge 80 ]; then
|
|
printf '%s' "$GREEN"
|
|
elif [ "$1" -ge 50 ]; then
|
|
printf '%s' "$YELLOW"
|
|
else
|
|
printf '%s' "$RED"
|
|
fi
|
|
}
|
|
|
|
# human_duration <seconds> -- "2d 15h", "1h 30m", "10m", or "now".
|
|
human_duration() {
|
|
local secs=$1 d h m
|
|
[ "$secs" -le 0 ] && { echo "now"; return 0; }
|
|
d=$((secs / 86400))
|
|
h=$(((secs % 86400) / 3600))
|
|
m=$(((secs % 3600) / 60))
|
|
if [ "$d" -gt 0 ]; then
|
|
echo "${d}d ${h}h"
|
|
elif [ "$h" -gt 0 ]; then
|
|
echo "${h}h ${m}m"
|
|
else
|
|
echo "${m}m"
|
|
fi
|
|
}
|
|
|
|
# rate_window <json-key> <label> -- sets $RATE_OUT to "5h 4% (1h 30m)", or to
|
|
# the empty string when that window is absent.
|
|
RATE_OUT=""
|
|
rate_window() {
|
|
local pct resets reset_str
|
|
RATE_OUT=""
|
|
|
|
jqr ".rate_limits.${1}.used_percentage"
|
|
[ -z "$JQ_OUT" ] && return 0
|
|
pct=$(printf "%.0f" "$JQ_OUT")
|
|
|
|
reset_str=""
|
|
jqr ".rate_limits.${1}.resets_at"
|
|
if [ -n "$JQ_OUT" ]; then
|
|
resets=$(printf "%.0f" "$JQ_OUT")
|
|
reset_str=" ($(human_duration $((resets - $(date +%s)))))"
|
|
fi
|
|
|
|
RATE_OUT=$(printf '%s %s%s%%%s%s' "$2" "$(threshold_color "$pct")" "$pct" "$RESET" "$reset_str")
|
|
}
|
|
|
|
# cache_segment -- sets $CACHE_OUT to the prompt-cache hit rate of the last API
|
|
# call, or leaves it empty when that cannot be computed.
|
|
#
|
|
# Cache reads bill at roughly a tenth of base input, cache writes at rather more
|
|
# than base. So a falling hit rate alongside rising cache_creation means
|
|
# something early in the context keeps changing and invalidating the prefix,
|
|
# re-billing the same tokens at many times the price -- a shift that $cost alone
|
|
# does not reveal until the bill arrives.
|
|
#
|
|
# current_usage is null before the first API call of a session, and again after
|
|
# /compact until the next call, so the segment simply does not render then.
|
|
CACHE_OUT=""
|
|
cache_segment() {
|
|
local pct
|
|
CACHE_OUT=""
|
|
|
|
jqr '.context_window.current_usage
|
|
| if type != "object" then empty
|
|
else
|
|
((.input_tokens // 0)
|
|
+ (.cache_creation_input_tokens // 0)
|
|
+ (.cache_read_input_tokens // 0)) as $in
|
|
| if $in > 0
|
|
then (((.cache_read_input_tokens // 0) * 100 / $in) | floor)
|
|
else empty
|
|
end
|
|
end'
|
|
[ -z "$JQ_OUT" ] && return 0
|
|
|
|
pct=$JQ_OUT
|
|
CACHE_OUT="$(cache_color "$pct")⚡${pct}%${RESET}"
|
|
}
|
|
|
|
# --- segments --------------------------------------------------------------
|
|
parts=()
|
|
|
|
jqr '.model.display_name' 'Claude'
|
|
MODEL="$JQ_OUT"
|
|
jqr '.effort.level'
|
|
[ -n "$JQ_OUT" ] && MODEL="${MODEL}+${JQ_OUT}"
|
|
parts+=("$MODEL")
|
|
|
|
jqr '.context_window.used_percentage' 0
|
|
USED=$(printf "%.0f" "$JQ_OUT")
|
|
parts+=("$(threshold_color "$USED")${USED}%${RESET}")
|
|
|
|
jqr '.cost.total_cost_usd' 0
|
|
parts+=("\$$(printf "%.2f" "$JQ_OUT")")
|
|
|
|
cache_segment && [ -n "$CACHE_OUT" ] && parts+=("$CACHE_OUT")
|
|
|
|
rate_window five_hour 5h && [ -n "$RATE_OUT" ] && parts+=("$RATE_OUT")
|
|
rate_window seven_day 7d && [ -n "$RATE_OUT" ] && parts+=("$RATE_OUT")
|
|
|
|
[ -n "$ERRORS" ] && parts+=("${RED}!${ERRORS}${RESET}")
|
|
|
|
# "<worktree>: <branch>" inside a worktree, otherwise just "<branch>". Kept
|
|
# off the first line and rendered on its own so it doesn't get pushed off
|
|
# screen by the usage segments on a long line.
|
|
GIT_LINE=""
|
|
if BRANCH=$(git branch --show-current 2>/dev/null) && [ -n "$BRANCH" ]; then
|
|
jqr '.worktree.name // .workspace.git_worktree'
|
|
GIT_LINE="${JQ_OUT:+${JQ_OUT}: }${BRANCH}"
|
|
fi
|
|
|
|
# --- render ----------------------------------------------------------------
|
|
line=""
|
|
for part in "${parts[@]}"; do
|
|
line="${line:+$line | }$part"
|
|
done
|
|
[ -n "$GIT_LINE" ] && line="${line}
|
|
${GIT_LINE}"
|
|
echo -e "$line"
|