|
#!/bin/sh |
|
# Claude Code statusline — 2-row grid: identity left, usage graphs right-aligned. |
|
# Layout (2 lines, right block flush to terminal edge via $COLUMNS): |
|
# row1 ~/path/to/cwd ↳ branch CONTX <bar> used% used/win |
|
# row2 model ·1M EFFORT flags MCP×N $total $turn →1:29H <bar> CACHE <bar> hit% HIT |
|
# |
|
# The context breakdown (sys / mem / msg) is ESTIMATED — the statusline JSON does |
|
# not expose exact per-category token sizes. Tune the constants below to taste. |
|
# All glyphs are width-1 (box-drawing + BMP punctuation), so ${#...} == rendered |
|
# width and the right-alignment math needs no per-glyph correction. |
|
|
|
input=$(cat) |
|
|
|
# Count characters (not bytes) in ${#...} and keep '.' decimals in awk. |
|
export LANG="${LANG:-en_US.UTF-8}" |
|
|
|
# ---- tunable estimation constants (approximate) ------------------------------- |
|
SYS_BASE=11000 # base system prompt + built-in tools |
|
MCP_PER=1800 # est. tokens added per connected MCP server |
|
PLUG_PER=1200 # est. tokens added per enabled plugin |
|
BAR_WIDTH=36 # cells wide for the context graph (~window-scaled) |
|
MIN_USED=1 # min full cells for the used portion when nonzero |
|
UCH="▬" # used cell glyph (full — centered thick rectangle) |
|
FCH="─" # free cell glyph (light horizontal rule) |
|
|
|
# ---- colors (256) ------------------------------------------------------------- |
|
RESET="\033[0m"; BOLD="\033[1m" |
|
DIM="\033[38;5;240m" |
|
C_MODEL="\033[1;38;5;255m" |
|
C_TAG="\033[38;5;244m" |
|
C_EFFORT="\033[38;5;221m" |
|
C_MCP="\033[38;5;44m" |
|
C_COST="\033[38;5;114m" |
|
C_ADD="\033[38;5;114m" |
|
C_DEL="\033[38;5;203m" |
|
C_CWD="\033[38;5;75m" |
|
C_GIT="\033[38;5;180m" |
|
C_FAST="\033[38;5;208m" |
|
C_SYS="\033[38;5;245m" # gray — system + tools + mcp |
|
C_MEM="\033[38;5;141m" # purple — memory / CLAUDE.md |
|
C_MSG="\033[38;5;114m" # green — conversation / messages |
|
C_FREE="\033[38;5;238m" # dim — free headroom |
|
C_READ="\033[38;5;114m" # green — cache read (warm / reused) |
|
C_NEW="\033[38;5;196m" # red — cache creation (written this turn) |
|
C_FRESH="\033[38;5;208m" # deep orange — fresh input (uncached) |
|
C_RLABEL="\033[38;5;244m" # dim — row labels |
|
C_VAL="\033[38;5;253m" # bright — metric values |
|
|
|
# pressure color from a percent (0-100) |
|
pcolor() { |
|
p="$1" |
|
if [ "$p" -ge 85 ]; then printf "\033[38;5;196m" |
|
elif [ "$p" -ge 70 ]; then printf "\033[38;5;208m" |
|
elif [ "$p" -ge 50 ]; then printf "\033[38;5;221m" |
|
elif [ "$p" -ge 30 ]; then printf "\033[38;5;155m" |
|
else printf "\033[38;5;114m"; fi |
|
} |
|
|
|
# ---- parse stdin (single jq pass, @sh-quoted for safe eval) ------------------- |
|
eval "$(printf '%s' "$input" | jq -r '@sh " |
|
CWD=\(.cwd // "") |
|
TRANSCRIPT=\(.transcript_path // "") |
|
MODEL=\(.model.display_name // "") |
|
EFFORT=\(.effort.level // "") |
|
FAST=\(.fast_mode // false) |
|
THINK=\(.thinking.enabled // false) |
|
WIN=\(.context_window.context_window_size // 0) |
|
USEDPCT=\(.context_window.used_percentage // 0) |
|
TOK_IN=\(.context_window.current_usage.input_tokens // 0) |
|
TOK_CC=\(.context_window.current_usage.cache_creation_input_tokens // 0) |
|
TOK_CR=\(.context_window.current_usage.cache_read_input_tokens // 0) |
|
TOK_OUT=\(.context_window.current_usage.output_tokens // 0) |
|
COST=\(.cost.total_cost_usd // 0) |
|
ADD=\(.cost.total_lines_added // 0) |
|
DEL=\(.cost.total_lines_removed // 0) |
|
RL5=\(.rate_limits.five_hour.used_percentage // -1) |
|
RL5RESET=\(.rate_limits.five_hour.resets_at // 0) |
|
RL7=\(.rate_limits.seven_day.used_percentage // -1) |
|
"')" |
|
|
|
[ -z "$WIN" ] && WIN=0 |
|
USED=$(( TOK_IN + TOK_CC + TOK_CR )) |
|
RL5=$(printf '%.0f' "$RL5" 2>/dev/null); [ -z "$RL5" ] && RL5=-1 |
|
|
|
# Fallback: after /compact stdin can go stale; recover used tokens from transcript. |
|
if [ "$USED" -le 0 ] && [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then |
|
USED=$(tail -n 200 "$TRANSCRIPT" 2>/dev/null | jq -r -n ' |
|
reduce (inputs | select(.type=="assistant" and (.message.usage//null)!=null)) as $x (null; $x) |
|
| if . == null then 0 else |
|
((.message.usage.input_tokens//0)+(.message.usage.cache_creation_input_tokens//0)+(.message.usage.cache_read_input_tokens//0)) |
|
end' 2>/dev/null) |
|
[ -z "$USED" ] && USED=0 |
|
fi |
|
|
|
# ---- slow-changing values (memory tokens, mcp + plugin counts), cached -------- |
|
CACHE="$HOME/.claude/cache/statusline.cache" |
|
sig="$CWD:$(stat -f %m "$HOME/.claude.json" 2>/dev/null):$(stat -f %m "$HOME/.claude/settings.json" 2>/dev/null):$(stat -f %m "$HOME/.claude/CLAUDE.md" 2>/dev/null)" |
|
cached=""; [ -f "$CACHE" ] && cached=$(cat "$CACHE" 2>/dev/null) |
|
csig=${cached%% *} |
|
if [ "$csig" = "$sig" ]; then |
|
rest=${cached#* } |
|
MEM_TOK=${rest%% *}; rest=${rest#* } |
|
MCP=${rest%% *}; PLUG=${rest##* } |
|
else |
|
enc=$(printf '%s' "$CWD" | sed 's#/#-#g') |
|
chars=0 |
|
for f in "$HOME/.claude/CLAUDE.md" "$CWD/CLAUDE.md" "$CWD/.claude/CLAUDE.md" "$HOME/.claude/projects/$enc/memory/MEMORY.md"; do |
|
[ -f "$f" ] && chars=$(( chars + $(wc -c < "$f" 2>/dev/null || echo 0) )) |
|
done |
|
MEM_TOK=$(( chars / 4 )) |
|
mcpg=$(jq '.mcpServers // {} | length' "$HOME/.claude.json" 2>/dev/null || echo 0) |
|
mcpp=$(jq --arg d "$CWD" '.projects[$d].mcpServers // {} | length' "$HOME/.claude.json" 2>/dev/null || echo 0) |
|
MCP=$(( mcpg + mcpp )) |
|
PLUG=$(jq '[(.enabledPlugins // {}) | to_entries[] | select(.value==true)] | length' "$HOME/.claude/settings.json" 2>/dev/null || echo 0) |
|
printf '%s\t%s\t%s\t%s' "$sig" "$MEM_TOK" "$MCP" "$PLUG" > "$CACHE" 2>/dev/null |
|
fi |
|
[ -z "$MEM_TOK" ] && MEM_TOK=0; [ -z "$MCP" ] && MCP=0; [ -z "$PLUG" ] && PLUG=0 |
|
|
|
# ---- derive segment token sizes ---------------------------------------------- |
|
SYS_TOK=$(( SYS_BASE + MCP * MCP_PER + PLUG * PLUG_PER )) |
|
if [ "$USED" -lt $(( SYS_TOK + MEM_TOK )) ]; then |
|
# early session: framework overhead dominates — cap to what's actually used |
|
s=$SYS_TOK; [ "$s" -gt "$USED" ] && s=$USED |
|
rem=$(( USED - s )); m=$MEM_TOK; [ "$m" -gt "$rem" ] && m=$rem |
|
SYS_SHOW=$s; MEM_SHOW=$m; MSG_SHOW=$(( USED - s - m )) |
|
else |
|
SYS_SHOW=$SYS_TOK; MEM_SHOW=$MEM_TOK; MSG_SHOW=$(( USED - SYS_TOK - MEM_TOK )) |
|
fi |
|
[ "$MSG_SHOW" -lt 0 ] && MSG_SHOW=0 |
|
FREE_TOK=$(( WIN - USED )); [ "$FREE_TOK" -lt 0 ] && FREE_TOK=0 |
|
upct_int=$(printf '%.0f' "$USEDPCT" 2>/dev/null || echo 0) |
|
MSG_COLOR=$(pcolor "$upct_int") |
|
|
|
# ---- helpers ------------------------------------------------------------------ |
|
kfmt() { awk -v n="$1" 'BEGIN{ |
|
if(n>=1000000){v=n/1000000; if(v==int(v))printf "%dM",v; else printf "%.1fM",v} |
|
else if(n>=1000)printf "%dk",(n/1000+0.5); |
|
else printf "%d",n }'; } |
|
|
|
# small pressure-colored bar for a percent. minibar <pct> <width> |
|
minibar() { |
|
awk -v p="$1" -v w="$2" -v col="$(pcolor "$1")" -v free="$C_FREE" \ |
|
-v rst="$RESET" -v uch="$UCH" -v fch="$FCH" 'BEGIN{ |
|
f=int(p/100*w+0.5); if(f>w)f=w; if(p>0&&f<1)f=1; e=w-f |
|
s=col; for(i=0;i<f;i++)s=s uch; s=s rst free; for(i=0;i<e;i++)s=s fch |
|
printf "%s%s", s, rst }' |
|
} |
|
|
|
# ---- one window-scaled segmented row: 3 colored segments + free --------------- |
|
# make_row <seg1> <seg2> <seg3> <col1> <col2> <col3> [overlay] [overlay_len] |
|
# overlay: pre-colored text drawn inside the free zone; overlay_len = its visible width |
|
make_row() { |
|
awk -v W="$BAR_WIDTH" -v MIN="$MIN_USED" -v win="$WIN" -v used="$USED" \ |
|
-v s1="$1" -v s2="$2" -v s3="$3" -v c1="$4" -v c2="$5" -v c3="$6" \ |
|
-v ov="$7" -v ovlen="$8" \ |
|
-v cfree="$C_FREE" -v rst="$RESET" -v uch="$UCH" -v fch="$FCH" ' |
|
function rep(n,ch, s,i){s="";for(i=0;i<n;i++)s=s ch;return s} |
|
BEGIN{ |
|
if(win<=0)win=1 |
|
ucf=used/win*W # fractional used cells |
|
uc=int(ucf+0.5) # round to nearest whole cell |
|
if(used>0 && uc<MIN)uc=MIN |
|
if(uc>W)uc=W |
|
if(used>0){h1=s1/used;h2=s2/used;h3=s3/used}else{h1=0;h2=0;h3=0} |
|
p1=int(h1*uc+0.5); p2=int((h1+h2)*uc+0.5) |
|
a=p1; b=p2-p1; c=uc-p2 |
|
if(s1>0 && h1>=0.05 && a<1)a=1 |
|
if(s2>0 && h2>=0.05 && b<1)b=1 |
|
if(s3>0 && h3>=0.05 && c<1)c=1 |
|
sum=a+b+c |
|
while(sum>uc){ |
|
if(a>=b && a>=c && a>1)a-- |
|
else if(b>=c && b>1)b-- |
|
else if(c>1)c-- |
|
else break |
|
sum=a+b+c |
|
} |
|
cf=W-(a+b+c); if(cf<0)cf=0 |
|
out = c1 rep(a,uch) rst c2 rep(b,uch) rst c3 rep(c,uch) rst |
|
if(ovlen>0){ |
|
pad = cf - ovlen # right-align overlay to the bar edge |
|
if(pad>=0) out = out cfree rep(pad,fch) rst ov |
|
else out = out " " ov # free zone too small: overflow with 1 gap |
|
} else { |
|
out = out cfree rep(cf,fch) rst |
|
} |
|
printf "%s", out |
|
}' |
|
} |
|
|
|
# usage label embedded in the CTX bar: "12% 122k/1M" |
|
usage_plain="${upct_int}% $(kfmt "$USED")/$(kfmt "$WIN")" |
|
usage_in_bar=$(printf "%b%s%%%b %b%s/%s%b" \ |
|
"$MSG_COLOR" "$upct_int" "$RESET" "$C_VAL" "$(kfmt "$USED")" "$(kfmt "$WIN")" "$RESET") |
|
|
|
# cache-hit label embedded in the CACHE bar: "93% HIT" |
|
hit_in_bar=""; hit_plain="" |
|
if [ "$USED" -gt 0 ]; then |
|
hit=$(awk -v r="$TOK_CR" -v u="$USED" 'BEGIN{printf "%.0f", r/u*100}') |
|
hit_plain="${hit}% HIT" |
|
hit_in_bar=$(printf "%b%s%% HIT%b" "$C_READ" "$hit" "$RESET") |
|
fi |
|
|
|
# ---- LEFT row 1: working directory + branch ----------------------------------- |
|
# Every glyph below is width-1, so ${#L*_plain} (LANG=UTF-8) is the rendered width. |
|
short_cwd=$(printf '%s' "${CWD:-$PWD}" | sed "s|^$HOME|~|") |
|
branch=$(git -C "${CWD:-$PWD}" symbolic-ref --short HEAD 2>/dev/null || git -C "${CWD:-$PWD}" rev-parse --short HEAD 2>/dev/null) |
|
L1=$(printf "%b%s%b" "$C_CWD" "$short_cwd" "$RESET") |
|
L1_plain="$short_cwd" |
|
if [ -n "$branch" ]; then |
|
L1=$(printf "%b %b↳ %s%b" "$L1" "$C_GIT" "$branch" "$RESET") |
|
L1_plain="$L1_plain ↳ $branch" |
|
fi |
|
|
|
# ---- LEFT row 2: model · effort · flags · mcp · $ · 5h ------------------------ |
|
model_base=${MODEL%% (*} |
|
L2=$(printf "%b%s%b" "$C_MODEL" "$model_base" "$RESET"); L2_plain="$model_base" |
|
case "$MODEL" in *1M*) L2=$(printf "%b %b·1M%b" "$L2" "$C_TAG" "$RESET"); L2_plain="$L2_plain ·1M";; esac |
|
if [ -n "$EFFORT" ]; then |
|
EFFORT_UP=$(printf '%s' "$EFFORT" | tr '[:lower:]' '[:upper:]') |
|
L2=$(printf "%b %b%s%b" "$L2" "$C_EFFORT" "$EFFORT_UP" "$RESET"); L2_plain="$L2_plain $EFFORT_UP" |
|
fi |
|
[ "$THINK" = "true" ] && { L2=$(printf "%b %bTHINK%b" "$L2" "$C_TAG" "$RESET"); L2_plain="$L2_plain THINK"; } |
|
[ "$FAST" = "true" ] && { L2=$(printf "%b %bFAST%b" "$L2" "$C_FAST" "$RESET"); L2_plain="$L2_plain FAST"; } |
|
cost_str=$(awk -v c="$COST" 'BEGIN{printf "%.2f",c}') |
|
# current-turn cost: price the live context usage at the model's per-MTok rates. |
|
# Cache creation uses the 1h rate (2x input) — Claude Code writes the 1h cache. |
|
# No >200k long-context premium on Opus 4.8 / Sonnet 4.6, so rates are flat. |
|
case "$MODEL" in |
|
*Haiku*) P_IN=1; P_OUT=5; P_CW=2; P_CR=0.1 ;; |
|
*Sonnet*) P_IN=3; P_OUT=15; P_CW=6; P_CR=0.3 ;; |
|
*) P_IN=5; P_OUT=25; P_CW=10; P_CR=0.5 ;; # Opus (default) |
|
esac |
|
turn=$(awk -v i="$TOK_IN" -v cc="$TOK_CC" -v cr="$TOK_CR" -v o="$TOK_OUT" \ |
|
-v pi="$P_IN" -v pcw="$P_CW" -v pcr="$P_CR" -v po="$P_OUT" \ |
|
'BEGIN{printf "%.2f", (i*pi + cc*pcw + cr*pcr + o*po)/1000000}') |
|
L2=$(printf "%b %b\$%s%b %b\$%s%b" "$L2" "$C_COST" "$cost_str" "$RESET" "$C_TAG" "$turn" "$RESET") |
|
L2_plain="$L2_plain \$$cost_str \$$turn" |
|
if [ "$RL5" -ge 0 ] 2>/dev/null; then |
|
rl5c=$(pcolor "$RL5") |
|
RL5LBL="5H" |
|
if [ "$RL5RESET" -gt 0 ] 2>/dev/null; then |
|
rem=$(( RL5RESET - $(date +%s) )); [ "$rem" -lt 0 ] && rem=0 |
|
RL5LBL=$(printf '\xe2\x86\x92%d:%02dH' $(( rem / 3600 )) $(( rem % 3600 / 60 ))) |
|
fi |
|
L2=$(printf "%b %b%s%b %s %b%s%%%b" "$L2" "$C_RLABEL" "$RL5LBL" "$RESET" "$(minibar "$RL5" 6)" "$rl5c" "$RL5" "$RESET") |
|
L2_plain="$L2_plain $RL5LBL ______ ${RL5}%" # ______ = 6 minibar cells (width placeholder) |
|
fi |
|
|
|
# ---- size bars from the ACTUAL left width so numbers never truncate ----------- |
|
# The right block worst case is "CONTX " (6) + bar + 1 + overlay (the overlay can |
|
# spill past the bar at high usage). Reserve all of it, plus MARGIN at the rim so |
|
# the host's own padding can never clip the trailing tokens. |
|
: "${COLUMNS:=120}" |
|
# Claude Code indents the statusline and reserves a few columns, so the usable |
|
# width is < $COLUMNS. Keep this many cells clear of the right edge or the |
|
# trailing CONTX/CACHE numbers get clipped with "…". Raise if still clipped. |
|
MARGIN=8 |
|
GAP=2 # minimum space between the left and right blocks |
|
edge=$(( COLUMNS - MARGIN )) |
|
LW1=${#L1_plain}; LW2=${#L2_plain} |
|
maxLW=$LW1; [ "$LW2" -gt "$maxLW" ] && maxLW=$LW2 |
|
op=${#usage_plain}; oc=${#hit_plain} |
|
ovmax=$op; [ "$oc" -gt "$ovmax" ] && ovmax=$oc |
|
BAR_WIDTH=$(( edge - maxLW - GAP - 6 - 1 - ovmax )) |
|
[ "$BAR_WIDTH" -gt 36 ] && BAR_WIDTH=36 |
|
[ "$BAR_WIDTH" -lt 10 ] && BAR_WIDTH=10 |
|
|
|
ROW_CTX=$(make_row "$SYS_SHOW" "$MEM_SHOW" "$MSG_SHOW" "$C_SYS" "$C_MEM" "$C_MSG" "$usage_in_bar" "$op") |
|
ROW_CACHE=$(make_row "$TOK_CR" "$TOK_CC" "$TOK_IN" "$C_READ" "$C_NEW" "$C_FRESH" "$hit_in_bar" "$oc") |
|
|
|
# ---- RIGHT column: CONTX (row1) + CACHE (row2), flush right ------------------- |
|
lbl_ctx=$(printf "%bCONTX%b" "$C_RLABEL" "$RESET") |
|
lbl_cache=$(printf "%bCACHE%b" "$C_RLABEL" "$RESET") |
|
RIGHT1=$(printf "%b %s" "$lbl_ctx" "$ROW_CTX") |
|
RIGHT2=$(printf "%b %s" "$lbl_cache" "$ROW_CACHE") |
|
|
|
# measure the right blocks exactly (every glyph is width-1) — no estimation error |
|
ESC=$(printf '\033') |
|
vwidth() { printf '%s' "$1" | sed "s/${ESC}\[[0-9;]*m//g" | LC_CTYPE=UTF-8 wc -m | tr -d ' '; } |
|
RW1=$(vwidth "$RIGHT1"); RW2=$(vwidth "$RIGHT2") |
|
|
|
# pad so each right block ends at `edge`, leaving MARGIN cells at the rim |
|
pad1=$(( edge - LW1 - RW1 )); [ "$pad1" -lt "$GAP" ] && pad1=$GAP |
|
pad2=$(( edge - LW2 - RW2 )); [ "$pad2" -lt "$GAP" ] && pad2=$GAP |
|
SP1=$(printf "%*s" "$pad1" "") |
|
SP2=$(printf "%*s" "$pad2" "") |
|
|
|
printf "%b%s%b\n%b%s%b\n" "$L1" "$SP1" "$RIGHT1" "$L2" "$SP2" "$RIGHT2" |