dotfiles/home/dot_claude/executable_statusline.sh
Felipe Martin 2ce9e1fcc8
Add effort level to status line, move git branch to its own line
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.
2026-08-19 12:32:22 +02:00

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"