Show the per-model usage limit, and size the status line by measurement

/usage reports a weekly limit scoped to a single model (Fable) that the
status line did not surface. It is absent from both the status line's stdin
JSON and the legacy seven_day_opus/seven_day_sonnet fields, which are always
null now; it lives in the usage API's .limits[] under kind "weekly_scoped".
Render it labelled by scope.model.display_name so it follows whichever model
the limit applies to, inside the 7d segment since both are weekly limits
sharing one reset time.

Fitting it exposed a problem with sizing by width tier. Tiers only know the
terminal width, so content that varies with session state — branch name, cwd,
model display name — could push a line past the edge and be clipped by the
renderer. Measure the assembled line instead (vis_len) and emit the richest of
four rate-limit variants that fits, on one line or two: with reset times, with
extra-usage credits, bars only, or bars without the per-model segment. Every
branch is fit-checked, including with wrapping disabled. Two lines render
correctly in the status line.

Cap the cwd basename as well: nothing else shortened line one, so a long
project directory could overflow it on its own.

Correct the usable width. Claude Code exports COLUMNS but applies the `padding`
setting on top of it rather than deducting it first, so a line sized to COLUMNS
is clipped; deduct padding on both sides plus a column of margin.

Sanitise payload data before rendering. printf %b interprets backslash escapes,
which is how the colour variables work, so a cwd or model name containing a
literal \n — or a real newline, which jq decodes from the JSON — would split
the line and break both the width measurement and the two-line guarantee.

Parse the usage payload in one jq pass rather than ten. The status line runs on
every redraw and per-field parsing had become the dominant cost; this brings a
render back to roughly what it was before (~155ms vs ~115ms parent), the
remainder being the reset times the tiered version did not show at this width.
Fields are read one per line rather than via @tsv: tab is IFS whitespace, so
`read` collapses runs of it and an empty field — no scoped limit, which is the
common case — silently shifted every later field along by one.

Tests cover the API payload shapes including malformed and hostile input, the
width invariants (never exceed the usable width, never more than two lines,
never wrap unnecessarily), and a sweep over widths, branch lengths, model
names, cwd lengths and extra-usage. They restore the live usage cache on exit,
and remove the fixture outright when there was no cache to restore — otherwise
a test run would leave fabricated usage figures live for an hour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonny Barnes 2026-08-27 17:55:35 +01:00
commit 697ad5055f
No known key found for this signature in database
5 changed files with 629 additions and 49 deletions

View file

@ -10,17 +10,39 @@ SHOW_GIT=true # git branch, dirty status, ahead/behind
SHOW_TOKENS=true # token usage bar
SHOW_THINKING=true # extended thinking indicator
SHOW_RATE_LIMITS=true # 5h / 7d rate limit bars
SHOW_MODEL_LIMIT=true # per-model weekly limit bar (e.g. Fable), when the API reports one
WRAP_NARROW=true # wrap onto a second line rather than dropping segments or being clipped
WRAP_MIN_WIDTH=100 # below this, keep wide-tier line-one content (it can wrap)
BRANCH_MAX_LEN=28 # truncate branch names longer than this
CWD_MAX_LEN=20 # truncate the cwd basename longer than this
GIT_CACHE_SECS=10 # seconds to cache git status (git diff is slow on large repos)
TOKEN_BAR_WIDTH=8 # width of token progress bar
# Terminal width detection.
# Claude Code runs this script in a subprocess — COLUMNS is 0 and tput/stty
# may not see the real TTY. When detection fails we default to 80 (safe/compact)
# rather than wide, so the line never wraps.
# Claude Code exports COLUMNS for this subprocess. There is no controlling
# terminal, so the stty fallback fails; when both fail we default to 80
# (safe/compact) rather than wide.
#
# To show more on a wider terminal, set TERM_WIDTH in settings.json:
# The `padding` setting indents the status line on both sides, and Claude Code
# applies it on top of COLUMNS rather than deducting it first — measured: with
# COLUMNS=121 and padding 2, a 121-column line is clipped with an ellipsis. So
# deduct it here, plus a column of margin.
#
# To override detection entirely, set TERM_WIDTH in settings.json:
# "command": "TERM_WIDTH=160 ~/.claude/statusline.sh"
# Read both settings this script needs in one jq pass.
# Only the user-level file: Claude Code also merges project .claude/settings.json,
# .claude/settings.local.json and managed policy, so a statusLine.padding set at
# project level would be applied by the renderer but missed here.
sl_padding=0
sl_thinking=false
if [ -f "$HOME/.claude/settings.json" ]; then
{ IFS= read -r sl_padding; IFS= read -r sl_thinking; } <<EOF
$(jq -r '[(.statusLine.padding // 0), (.alwaysThinkingEnabled // false)] | .[] | tostring' "$HOME/.claude/settings.json" 2>/dev/null)
EOF
[ "${sl_padding:-0}" -ge 0 ] 2>/dev/null || sl_padding=0
fi
if [ "${TERM_WIDTH:-0}" -le 0 ] 2>/dev/null; then
if [ "${COLUMNS:-0}" -gt 0 ] 2>/dev/null; then
TERM_WIDTH=$COLUMNS
@ -31,6 +53,27 @@ if [ "${TERM_WIDTH:-0}" -le 0 ] 2>/dev/null; then
fi
fi
# Usable width: minus padding on both sides, minus a column of margin.
USABLE_WIDTH=$(( TERM_WIDTH - 2 * sl_padding - 1 ))
[ "$USABLE_WIDTH" -lt 20 ] && USABLE_WIDTH=20
# ${#str} counts characters only under a UTF-8 locale; under LC_ALL=C it counts
# bytes, which would mis-measure the box/block glyphs and break line wrapping.
case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
*UTF-8*|*utf8*|*UTF8*) ;;
*)
# C.UTF-8 on glibc, en_US.UTF-8 on macOS (which has no C.UTF-8).
# Picking one that does not exist makes bash warn on every render and
# silently fall back to byte counting, so verify before committing.
for _loc in C.UTF-8 en_US.UTF-8; do
if LC_ALL="$_loc" locale charmap 2>/dev/null | grep -qi utf; then
export LC_ALL="$_loc"; break
fi
done
unset _loc
;;
esac
input=$(cat)
[ -z "$input" ] && printf "Claude" && exit 0
@ -71,6 +114,31 @@ format_tokens() {
fi
}
# Display width of a string, ignoring ANSI colour escapes. Every glyph the
# statusline uses is single-width, so a character count is the display width.
#
# The colour vars hold the escapes as literal backslash-033 text (they are
# single-quoted, and printf %b only interprets them when the line is finally
# emitted), so strip that literal form as well as a real ESC byte.
vis_len() {
local plain
plain=$(printf '%s' "$1" | sed -e 's/\\033\[[0-9;]*m//g' -e $'s/\033\[[0-9;]*m//g' -e 's/\\\\/\\/g')
printf '%d' "${#plain}"
}
# printf %b interprets backslash escapes -- that is how the colour variables
# above are applied. Data from the API or the filesystem must NOT be
# interpreted: a directory or model name containing a literal \n would split
# the line, breaking both the width measurement and the two-line guarantee.
# Double the backslashes so %b renders them literally.
esc_data() {
local v="$1"
# Real control characters (jq decodes \n in the JSON payload into an actual
# newline) would split the line regardless of escaping, so drop them first.
v="${v//$'\n'/ }"; v="${v//$'\r'/ }"; v="${v//$'\t'/ }"; v="${v//$'\033'/}"
printf '%s' "${v//\\/\\\\}"
}
truncate_str() {
local str="$1" max="$2"
[ "${#str}" -gt "$max" ] \
@ -242,27 +310,42 @@ total_tokens=$(format_tokens $size)
pct_used=$(( size > 0 ? current * 100 / size : 0 ))
thinking_on=false
settings_path="$HOME/.claude/settings.json"
if [ -f "$settings_path" ]; then
thinking_val=$(jq -r '.alwaysThinkingEnabled // false' "$settings_path" 2>/dev/null)
[ "$thinking_val" = "true" ] && thinking_on=true
fi
[ "$sl_thinking" = "true" ] && thinking_on=true
# ===== Adaptive width tiers =====
#
# Tiers (tuned so each tier's max output fits within its min width):
# Tiers now choose only how much of LINE ONE to show. What the rate-limit
# group contains, and whether it wraps, is decided by measurement at the end of
# the script, not here -- tiers cannot see how long the branch, cwd or model
# name is, which is how content used to end up clipped.
#
# full (≥150): CWD, ahead/behind, "◆ thinking", 5h+7d+resets, cost
# wide (100149): ahead/behind, "◆ thinking", 5h+7d bars, cost
# split (7099): short model, ahead/behind, "◆" symbol, 5h+7d bars
# narrow (<70): short model + branch + token only
# full (≥150): CWD, ahead/behind, "◆ thinking", cost
# wide (100149): ahead/behind, "◆ thinking", cost
# split (7699): short model, ahead/behind, "◆" symbol
# (reachable only with WRAP_NARROW=false; otherwise the wrap
# band below overrides this range to wide)
# narrow (<76): short model + branch + token only; no rate limits at all
#
if [ "$TERM_WIDTH" -ge 150 ] 2>/dev/null; then width_tier="full"
elif [ "$TERM_WIDTH" -ge 100 ] 2>/dev/null; then width_tier="wide"
elif [ "$TERM_WIDTH" -ge 76 ] 2>/dev/null; then width_tier="split"
if [ "$USABLE_WIDTH" -ge 150 ] 2>/dev/null; then width_tier="full"
elif [ "$USABLE_WIDTH" -ge 100 ] 2>/dev/null; then width_tier="wide"
elif [ "$USABLE_WIDTH" -ge 76 ] 2>/dev/null; then width_tier="split"
else width_tier="narrow"
fi
# Two-line mode. Between WRAP_FLOOR and WRAP_MIN_WIDTH there isn't room for
# everything on one line, but there IS room across two — so instead of dropping
# the rate-limit group we break before it and render the wide-tier content.
# Below WRAP_FLOOR the narrow tier applies instead, which drops the rate-limit
# group entirely -- so there is nothing to wrap and no second line to put it on.
WRAP_FLOOR=68
wrap_mode=false
if $WRAP_NARROW \
&& [ "$USABLE_WIDTH" -lt "$WRAP_MIN_WIDTH" ] 2>/dev/null \
&& [ "$USABLE_WIDTH" -ge "$WRAP_FLOOR" ] 2>/dev/null; then
wrap_mode=true
width_tier="wide"
fi
# Shorten model name for tight spaces
short_model() {
case "$1" in
@ -274,7 +357,13 @@ short_model() {
}
# ===== Build output =====
# Explicitly empty: bash imports same-named environment variables, and a
# stray $rl_lean would otherwise leak into the rendered line.
out=""
rl_bare=""
rl_lean=""
rl_mid=""
rl_rich=""
# Model — color by family
model_color="$blue"
@ -284,14 +373,20 @@ case "$model_name" in
esac
display_model="$model_name"
# split/narrow: shorten so the 5h + 7d bars still fit on one line
[ "$width_tier" = "split" -o "$width_tier" = "narrow" ] && display_model=$(short_model "$model_name")
out+="${model_color}${display_model}${reset}"
# split/narrow: shorten so the 5h + 7d bars still fit on one line.
# wrap mode too: it renders wide-tier content at a split-tier width, and a long
# display name ("Opus 5 (1M context)" is 19 cols) would overflow line one.
if [ "$width_tier" = "split" -o "$width_tier" = "narrow" ] || $wrap_mode; then
display_model=$(short_model "$model_name")
fi
out+="${model_color}$(esc_data "$display_model")${reset}"
# CWD — full tier only (branch name gives enough context below that)
if [ "$width_tier" = "full" ] && [ -n "$cwd" ]; then
display_dir="${cwd##*/}"
out+="${sep}${dim}${display_dir}${reset}"
# Capped like the branch name: nothing else shortens line one, so an
# unusually long project directory would push it past the terminal edge.
display_dir=$(truncate_str "${cwd##*/}" "$CWD_MAX_LEN")
out+="${sep}${dim}$(esc_data "$display_dir")${reset}"
fi
# Git branch + dirty + ahead/behind
@ -305,9 +400,17 @@ if $SHOW_GIT && [ -n "$cwd" ]; then
[ "$width_tier" = "wide" ] && local_max=24
[ "$width_tier" = "split" ] && local_max=18
[ "$width_tier" = "narrow" ] && local_max=12
# In wrap mode line one is model+branch+tokens+thinking+cost; with the
# shortened model name everything but the branch is ~56 cols, so give
# the branch whatever is left.
if $wrap_mode; then
local_max=$(( USABLE_WIDTH - 56 ))
[ "$local_max" -lt 8 ] && local_max=8
[ "$local_max" -gt 24 ] && local_max=24
fi
g_branch_display=$(truncate_str "$g_branch" "$local_max")
out+="${sep}${dim}${reset} ${magenta}${g_branch_display}${reset}"
out+="${sep}${dim}${reset} ${magenta}$(esc_data "$g_branch_display")${reset}"
if [ "$g_dirty" = "dirty" ]; then
out+=" ${red}${reset}"
@ -355,7 +458,8 @@ if [ -n "$cost_usd" ] && [ "$width_tier" = "wide" -o "$width_tier" = "full" ]; t
fi
# ===== Rate limits (API, cached 60s) =====
# shown in full/wide/split; hidden only in narrow
# Built at every tier except narrow. Which variant is actually emitted,
# and on how many lines, is decided by the measured ladder at the end.
if $SHOW_RATE_LIMITS && [ "$width_tier" != "narrow" ]; then
api_cache="/tmp/claude/statusline-usage-cache.json"
api_cache_max=3600 # 1 hour — rate limit data changes slowly
@ -405,37 +509,121 @@ if $SHOW_RATE_LIMITS && [ "$width_tier" != "narrow" ]; then
bar_width=6
[ "$width_tier" = "split" ] && bar_width=4
five_hour_pct=$(echo "$usage_data" | jq -r '.five_hour.utilization // 0' | awk '{printf "%.0f", $1}')
five_hour_reset_iso=$(echo "$usage_data" | jq -r '.five_hour.resets_at // empty')
five_hour_bar=$(build_bar "$five_hour_pct" "$bar_width")
out+="${sep}${dim}5h${reset} ${five_hour_bar} ${cyan}${five_hour_pct}%${reset}"
# Reset time: full only — below that the 7d bar uses the space instead
if [ "$width_tier" = "full" ]; then
# One jq pass for the whole payload. Parsing it field by field meant
# ~10 jq processes per render on the same JSON; the status line runs on
# every redraw, so that was the dominant cost. Rounding happens in jq
# too, which removes the per-field awk calls.
#
# The per-model weekly limit (e.g. Fable) lives in .limits[] under kind
# "weekly_scoped" -- the legacy seven_day_opus / seven_day_sonnet fields
# are always null now. Prefer an is_active entry, else take the first.
# `.limits[]?` tolerates limits being absent or not an array, and
# `tonumber? // 0` tolerates a percentage arriving as a string.
# One field per line, not @tsv: tab is IFS whitespace, so `read` collapses
# runs of it and an empty field (no scoped limit -> empty display name)
# would silently shift every later field along by one.
{
IFS= read -r five_hour_pct
IFS= read -r seven_day_pct
IFS= read -r five_hour_reset_iso
IFS= read -r seven_day_reset_iso
IFS= read -r scoped_name
IFS= read -r scoped_pct
IFS= read -r extra_enabled
IFS= read -r extra_pct
IFS= read -r extra_used
IFS= read -r extra_limit
} <<EOF
$(echo "$usage_data" | jq -r '
def num: (tonumber? // 0);
([.limits[]? | select(.kind == "weekly_scoped")]
| (map(select(.is_active)) + .) | first) as $scoped
| [ (.five_hour.utilization | num | round),
(.seven_day.utilization | num | round),
(.five_hour.resets_at // ""),
(.seven_day.resets_at // ""),
(($scoped.scope.model.display_name // "") | gsub("[\\n\\r\\t]"; " ")),
($scoped.percent | num | round),
(.extra_usage.is_enabled // false),
(.extra_usage.utilization | num | round),
((.extra_usage.used_credits | num) / 100 * 100 | round / 100),
((.extra_usage.monthly_limit | num) / 100 * 100 | round / 100)
] | .[] | tostring' 2>/dev/null)
EOF
# Three variants of the rate-limit group, richest first:
# rl_rich bars + reset times + extra usage
# rl_mid bars + extra usage
# rl_lean bars only
# The ladder at the end emits the richest one that fits the space it
# has. Keeping a lean variant matters: extra-usage credits add ~30
# columns, which can overflow line two on its own.
seg_5h="${dim}5h${reset} $(build_bar "${five_hour_pct:-0}" "$bar_width") ${cyan}${five_hour_pct:-0}%${reset}"
seg_7d="${sep}${dim}7d${reset} $(build_bar "${seven_day_pct:-0}" "$bar_width") ${cyan}${seven_day_pct:-0}%${reset}"
# Rendered inside the 7d segment rather than as its own: both are weekly
# limits resetting at the same time, so one shared reset label covers
# the pair and saves a separator plus a second timestamp.
seg_scoped=""
if $SHOW_MODEL_LIMIT && [ -n "$scoped_name" ]; then
seg_scoped=" ${dim}$(esc_data "$(truncate_str "$scoped_name" 8)")${reset} $(build_bar "${scoped_pct:-0}" "$bar_width") ${cyan}${scoped_pct:-0}%${reset}"
fi
# Extra usage, when the account has it enabled.
seg_extra=""
if [ "$extra_enabled" = "true" ]; then
seg_extra="${sep}${dim}extra${reset} $(build_bar "${extra_pct:-0}" "$bar_width") ${cyan}\$$(printf '%.2f' "${extra_used:-0}")${dim}/\$$(printf '%.2f' "${extra_limit:-0}")${reset}"
fi
# rl_bare drops the per-model bar too: the last thing worth giving up,
# and the only way to fit at all when wrapping is disabled and the
# terminal is narrow.
rl_bare="${seg_5h}${seg_7d}"
rl_lean="${rl_bare}${seg_scoped}"
rl_mid="${rl_lean}${seg_extra}"
rl_rich="$rl_mid"
# Formatting the two reset timestamps costs ~10 subprocesses (date has
# no portable one-shot form here), so only pay for it when there is a
# chance they will be shown: appended to line one, or alone on line two.
# RESET_COST is the combined width of " reset 4:40p.m." and
# " reset aug 28, 3:00a.m.".
line1_len=$(vis_len "$out")
mid_len=$(vis_len "$rl_mid")
RESET_COST=30
if [ $(( line1_len + 3 + mid_len + RESET_COST )) -le "$USABLE_WIDTH" ] \
|| { $WRAP_NARROW && [ $(( mid_len + RESET_COST )) -le "$USABLE_WIDTH" ]; }; then
five_hour_reset=$(format_reset_time "$five_hour_reset_iso" "time")
[ -n "$five_hour_reset" ] && out+=" ${dim}${five_hour_reset}${reset}"
fi
# 7d bar: all tiers (mirrors 5h); reset time: full only
seven_day_pct=$(echo "$usage_data" | jq -r '.seven_day.utilization // 0' | awk '{printf "%.0f", $1}')
seven_day_bar=$(build_bar "$seven_day_pct" "$bar_width")
out+="${sep}${dim}7d${reset} ${seven_day_bar} ${cyan}${seven_day_pct}%${reset}"
if [ "$width_tier" = "full" ]; then
seven_day_reset_iso=$(echo "$usage_data" | jq -r '.seven_day.resets_at // empty')
seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime")
[ -n "$seven_day_reset" ] && out+=" ${dim}${seven_day_reset}${reset}"
r5=""; [ -n "$five_hour_reset" ] && r5=" ${dim}${five_hour_reset}${reset}"
r7=""; [ -n "$seven_day_reset" ] && r7=" ${dim}${seven_day_reset}${reset}"
rl_rich="${seg_5h}${r5}${seg_7d}${seg_scoped}${r7}${seg_extra}"
fi
fi
fi
# Extra usage: full only
if [ "$width_tier" = "full" ]; then
extra_enabled=$(echo "$usage_data" | jq -r '.extra_usage.is_enabled // false')
if [ "$extra_enabled" = "true" ]; then
extra_pct=$(echo "$usage_data" | jq -r '.extra_usage.utilization // 0' | awk '{printf "%.0f", $1}')
extra_used=$(echo "$usage_data" | jq -r '.extra_usage.used_credits // 0' | awk '{printf "%.2f", $1/100}')
extra_limit=$(echo "$usage_data" | jq -r '.extra_usage.monthly_limit // 0' | awk '{printf "%.2f", $1/100}')
extra_bar=$(build_bar "$extra_pct" "$bar_width")
out+="${sep}${dim}extra${reset} ${extra_bar} ${cyan}\$${extra_used}${dim}/\$${extra_limit}${reset}"
fi
# Attach the rate-limit group, emitting the richest layout that fits:
# 1-3. one line: with resets / with extra usage / bars only
# 4-6. two lines: same order, line two having room the single line lacked
# Every branch is fit-checked, so no layout is chosen that would be clipped by
# the renderer, whatever the branch name, cwd, model name or extra-usage width.
if [ -n "$rl_bare" ]; then
rich_len=$(vis_len "$rl_rich")
lean_len=$(vis_len "$rl_lean")
bare_len=$(vis_len "$rl_bare")
if [ $(( line1_len + 3 + rich_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_rich}"
elif [ $(( line1_len + 3 + mid_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_mid}"
elif [ $(( line1_len + 3 + lean_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_lean}"
elif [ $(( line1_len + 3 + bare_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_bare}"
elif $WRAP_NARROW; then
if [ "$rich_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_rich"
elif [ "$mid_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_mid"
elif [ "$lean_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_lean"
else out+=$'\n'"$rl_bare"
fi
else
# Wrapping disabled: the barest group is the least-bad single line.
out+="${sep}${rl_bare}"
fi
fi

24
claude/tests/run.sh Executable file
View file

@ -0,0 +1,24 @@
#!/bin/bash
# Run every test for claude/statusline.isaacaudet.sh.
#
# Usage: claude/tests/run.sh
#
# The tests render the status line directly, so they need the same tools it
# does (bash, jq, git, python3). They briefly replace the cached usage-API
# response in /tmp/claude with fixtures and restore it on exit.
cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1
status=0
for t in statusline.isaacaudet.payload_test.sh \
statusline.isaacaudet.width_test.sh; do
echo "== $t"
bash "$t" || status=1
echo
done
echo "== statusline.isaacaudet.overflow_test.py"
python3 statusline.isaacaudet.overflow_test.py || status=1
echo
[ "$status" -eq 0 ] && echo "ALL TESTS PASSED" || echo "SOME TESTS FAILED"
exit "$status"

View file

@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Exhaustive overflow sweep for claude/statusline.isaacaudet.sh.
Covers the Isaac Audet-inspired status line (the variant symlinked from
~/.claude/statusline.sh); the sibling statusline.burnrate.sh and
statusline.original.sh are not covered.
Asserts the two hard invariants across a grid of terminal widths, branch-name
lengths and model-name lengths:
1. no rendered line exceeds the usable width
2. never more than two lines
Catches the case tier-based sizing missed: content whose width depends on
session state (branch, cwd, model display name) rather than on the terminal.
"""
import subprocess, re, unicodedata, sys, os, itertools, tempfile, shutil
SCRIPT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"statusline.isaacaudet.sh")
BRANCHES = ["main", "feature/some-longer-branch-name",
"feature/an-extremely-long-branch-name-that-keeps-going-and-going"]
MODELS = ["Opus 5", "Opus 5 (1M context)"]
WIDTHS = range(64, 245, 10)
# The cwd basename is rendered at the widest layouts and is capped by
# CWD_MAX_LEN; vary it, since a long project directory was one of the ways
# line one used to overflow.
CWDS = ["proj", "a-really-quite-long-project-directory-name-that-someone-might-have"]
# Extra-usage credits add ~30 columns and used to overflow line two on their own.
EXTRA = {
"off": '"extra_usage":{"is_enabled":false}',
"on": '"extra_usage":{"is_enabled":true,"monthly_limit":200000,'
'"used_credits":123456,"utilization":62.0}',
}
CACHE = "/tmp/claude/statusline-usage-cache.json"
FIXTURE = ('{"five_hour":{"utilization":5.0,"resets_at":"2026-08-27T15:40:00+00:00"},'
'"seven_day":{"utilization":7.0,"resets_at":"2026-08-28T02:00:00+00:00"},%s,'
'"limits":[{"kind":"weekly_scoped","percent":10,'
'"scope":{"model":{"display_name":"Fable"}},"is_active":true}]}')
def width_of(s):
return sum(2 if unicodedata.east_asian_width(c) in "WF" else 1
for c in re.sub(r"\033\[[0-9;]*m", "", s))
def main():
pad = int(subprocess.run(
["jq", "-r", ".statusLine.padding // 0", os.path.expanduser("~/.claude/settings.json")],
capture_output=True, text=True).stdout.strip() or 0)
root = tempfile.mkdtemp()
# Back up the live usage cache. If there wasn't one, remove the fixture at
# the end rather than leaving it: the status line would treat it as a fresh
# response for up to an hour and report fabricated usage.
had_cache = os.path.exists(CACHE)
saved = open(CACHE, "rb").read() if had_cache else None
fails, checked = [], 0
try:
for cwd_name, extra_name in itertools.product(CWDS, EXTRA):
with open(CACHE, "w") as fh:
fh.write(FIXTURE % EXTRA[extra_name])
repo = os.path.join(root, cwd_name)
os.makedirs(repo, exist_ok=True)
subprocess.run(["git", "-C", repo, "init", "-q"], check=True)
subprocess.run(["git", "-C", repo, "-c", "user.email=t@t", "-c", "user.name=t",
"commit", "-q", "--allow-empty", "-m", "i"], check=True)
for br in BRANCHES:
subprocess.run(["git", "-C", repo, "checkout", "-q", "-B", br], capture_output=True)
for f in os.listdir("/tmp/claude"):
if f.startswith("git-"):
os.remove("/tmp/claude/" + f)
for model, cols in itertools.product(MODELS, WIDTHS):
stdin = ('{"model":{"display_name":"%s"},"cwd":"%s",'
'"cost":{"total_cost_usd":8.31},"context_window":'
'{"context_window_size":1000000,"current_usage":'
'{"input_tokens":137000,"cache_read_input_tokens":34000}}}' % (model, repo))
out = subprocess.run(["bash", SCRIPT], input=stdin, capture_output=True,
text=True, env=dict(os.environ, TERM_WIDTH=str(cols))).stdout
usable = max(20, cols - 2 * pad - 1)
lines = out.rstrip("\n").split("\n")
checked += 1
tag = (br, model, cols, cwd_name, extra_name)
if len(lines) > 2:
fails.append(tag + ("more than two lines",))
continue
if not any(l.strip() for l in lines):
fails.append(tag + ("rendered nothing",))
continue
mx = max(width_of(l) for l in lines)
if mx > usable:
fails.append(tag + (f"max {mx} > usable {usable}",))
finally:
shutil.rmtree(root, ignore_errors=True)
if saved is not None:
with open(CACHE, "wb") as fh:
fh.write(saved)
elif os.path.exists(CACHE):
os.remove(CACHE)
print(f"checked {checked} combinations "
f"(widths {WIDTHS.start}-{WIDTHS.stop - 1}, {len(BRANCHES)} branches, "
f"{len(MODELS)} models, {len(CWDS)} cwds, extra-usage on and off)")
if fails:
print(f"FAIL: {len(fails)} overflow(s)")
for f in fails[:10]:
print(" ", f)
return 1
print("PASS: no line exceeded the usable width")
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,165 @@
#!/bin/bash
# Tests for claude/statusline.isaacaudet.sh — the Isaac Audet-inspired Claude
# Code status line (the variant symlinked from ~/.claude/statusline.sh).
#
# Sibling variants in claude/ (statusline.burnrate.sh, statusline.original.sh)
# are NOT covered by these tests.
#
# Payload shapes: how the usage API's rate-limit JSON is rendered, in
# particular the per-model ("weekly_scoped") limit such as Fable, and how the
# renderer degrades when the group will not fit.
SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/statusline.isaacaudet.sh"
CACHE="/tmp/claude/statusline-usage-cache.json"
BACKUP="$(mktemp)"
REPO="$(mktemp -d)"
[ -f "$CACHE" ] && cp "$CACHE" "$BACKUP"
# Restore the real cache. If there was no cache to begin with, REMOVE the
# fixture rather than leaving it: the status line treats a fixture as a fresh
# response for api_cache_max (1h) and would report fabricated usage.
cleanup() {
if [ -s "$BACKUP" ]; then cp "$BACKUP" "$CACHE"; else rm -f "$CACHE"; fi
rm -f "$BACKUP"; rm -rf "$REPO"
}
trap cleanup EXIT
git -C "$REPO" init -q 2>/dev/null
git -C "$REPO" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init 2>/dev/null
pass=0; fail=0
ok() { echo " PASS $1"; pass=$((pass+1)); }
bad() { echo " FAIL $1"; [ -n "$2" ] && printf '%s\n' "$2" | sed 's/^/ /'; fail=$((fail+1)); }
# Render, with ANSI stripped. $1 = TERM_WIDTH, $2 = cwd, $3 = model name.
render() {
local w="$1" cwd="${2:-$REPO}" model="${3:-Opus 5}"
stdin_json "$cwd" "$model" \
| TERM_WIDTH="$w" bash "$SCRIPT" 2>&1 | sed $'s/\033\[[0-9;]*m//g'
}
# Build the payload in python: a backslash written into shell-constructed JSON
# is a JSON escape, so "pro\nj" would reach the script as a REAL newline rather
# than the literal characters the hostile-string tests mean to exercise.
stdin_json() {
CWD="$1" MODEL="$2" python3 -c '
import json, os
print(json.dumps({"model": {"display_name": os.environ["MODEL"]},
"cwd": os.environ["CWD"],
"cost": {"total_cost_usd": 0.5},
"context_window": {"context_window_size": 200000,
"current_usage": {"input_tokens": 1000}}}))'
}
nth() { printf '%s\n' "$1" | sed -n "${2}p"; }
count() { printf '%s\n' "$1" | wc -l | tr -d ' '; }
vis() { printf '%s' "$1" | python3 -c "
import sys,unicodedata
s=sys.stdin.read(); print(sum(2 if unicodedata.east_asian_width(c) in 'WF' else 1 for c in s))"; }
fixture() { cat > "$CACHE"; }
BASE='"five_hour":{"utilization":5.0,"resets_at":"2026-08-27T15:40:00+00:00"},
"seven_day":{"utilization":7.0,"resets_at":"2026-08-28T02:00:00+00:00"}'
# ---------------------------------------------------------------- scoped limit
echo "Per-model (weekly_scoped) limit:"
fixture <<JSON
{$BASE,"extra_usage":{"is_enabled":false},
"limits":[{"kind":"weekly_scoped","percent":10,"scope":{"model":{"display_name":"Fable"}},"is_active":true}]}
JSON
out=$(render 250)
# Assert the label AND its percentage together, so a bar rendered with the
# wrong number cannot pass.
case "$out" in *"Fable"*"10%"*) ok "wide: renders 'Fable' with its percentage" ;;
*) bad "wide: renders 'Fable' with its percentage" "$out" ;; esac
# Wrapped: assert Fable is on line TWO specifically, not merely present.
out=$(render 90)
[ "$(count "$out")" = "2" ] && ok "narrow: wraps to two lines" || bad "narrow: wraps to two lines" "$out"
case "$(nth "$out" 2)" in *"Fable"*"10%"*) ok "narrow: Fable is on line two" ;;
*) bad "narrow: Fable is on line two" "$out" ;; esac
case "$(nth "$out" 1)" in *"Fable"*) bad "narrow: Fable not duplicated on line one" "$out" ;;
*) ok "narrow: Fable not duplicated on line one" ;; esac
# The label must come from the payload, not be hardcoded.
fixture <<JSON
{$BASE,"extra_usage":{"is_enabled":false},
"limits":[{"kind":"weekly_scoped","percent":42,"scope":{"model":{"display_name":"Nimbus"}},"is_active":false}]}
JSON
out=$(render 250)
case "$out" in *"Nimbus"*"42%"*) ok "label comes from scope.model.display_name" ;;
*) bad "label comes from scope.model.display_name" "$out" ;; esac
case "$out" in *Fable*) bad "no hardcoded 'Fable'" "$out" ;; *) ok "no hardcoded 'Fable'" ;; esac
# ------------------------------------------------------------- malformed input
echo "Malformed / legacy payloads still render the 5h and 7d bars:"
for desc in "no limits key:{$BASE,\"extra_usage\":{\"is_enabled\":false}}" \
"limits is a string:{$BASE,\"extra_usage\":{\"is_enabled\":false},\"limits\":\"nope\"}" \
"limits has no scoped:{$BASE,\"extra_usage\":{\"is_enabled\":false},\"limits\":[{\"kind\":\"weekly_all\",\"percent\":7}]}" \
"percent as string:{$BASE,\"extra_usage\":{\"is_enabled\":false},\"limits\":[{\"kind\":\"weekly_scoped\",\"percent\":\"42.7\",\"scope\":{\"model\":{\"display_name\":\"Str\"}},\"is_active\":true}]}"; do
name="${desc%%:*}"; json="${desc#*:}"
printf '%s' "$json" | fixture
out=$(render 250)
if [ "$(count "$out")" = "1" ] && case "$out" in *"5h"*"7d"*) true;; *) false;; esac \
&& case "$out" in *error*|*null*|*"jq:"*|*"line "*) false;; *) true;; esac; then
ok "$name"
else bad "$name" "$out"; fi
done
# a string percentage should round, not become 0
case "$out" in *"Str"*"43%"*) ok "percent given as a string rounds to 43%" ;;
*) bad "percent given as a string rounds to 43%" "$out" ;; esac
# ------------------------------------------------- extra usage (regression §1)
echo "Extra-usage credits must not overflow line two:"
fixture <<JSON
{$BASE,
"extra_usage":{"is_enabled":true,"monthly_limit":200000,"used_credits":123456,"utilization":62.0},
"limits":[{"kind":"weekly_scoped","percent":10,"scope":{"model":{"display_name":"Fable"}},"is_active":true}]}
JSON
out=$(render 250)
case "$out" in *"extra"*) ok "wide: extra-usage segment is shown" ;;
*) bad "wide: extra-usage segment is shown" "$out" ;; esac
for w in 73 80 95; do
out=$(render "$w"); u=$(( w - 5 )); worst=0
while IFS= read -r l; do c=$(vis "$l"); [ "$c" -gt "$worst" ] && worst=$c; done <<< "$out"
[ "$worst" -le "$u" ] \
&& ok "width $w: no line exceeds usable $u (worst $worst)" \
|| bad "width $w: no line exceeds usable $u (worst $worst)" "$out"
done
# ------------------------------------------ hostile strings (regression §9)
echo "Control characters and backslashes in payload data:"
fixture <<JSON
{$BASE,"extra_usage":{"is_enabled":false},
"limits":[{"kind":"weekly_scoped","percent":10,"scope":{"model":{"display_name":"A\nB"}},"is_active":true}]}
JSON
out=$(render 250)
[ "$(count "$out")" = "1" ] && ok "newline in display_name does not split the line" \
|| bad "newline in display_name does not split the line" "$out"
fixture <<JSON
{$BASE,"extra_usage":{"is_enabled":false},"limits":[]}
JSON
mkdir -p "$REPO/sub"
out=$(render 250 "$REPO/pro\\nj")
[ "$(count "$out")" = "1" ] && ok "literal backslash-n in cwd does not split the line" \
|| bad "literal backslash-n in cwd does not split the line" "$out"
out=$(render 250 "$REPO" 'Opus\n5')
[ "$(count "$out")" = "1" ] && ok "literal backslash-n in model name does not split the line" \
|| bad "literal backslash-n in model name does not split the line" "$out"
# --------------------------------------------- WRAP_NARROW=false (regression §2)
echo "With WRAP_NARROW=false the single line must still be fit-checked:"
NOWRAP=$(mktemp); sed 's/^WRAP_NARROW=true/WRAP_NARROW=false/' "$SCRIPT" > "$NOWRAP"
fixture <<JSON
{$BASE,"extra_usage":{"is_enabled":false},
"limits":[{"kind":"weekly_scoped","percent":10,"scope":{"model":{"display_name":"Fable"}},"is_active":true}]}
JSON
for w in 81 105 130; do
out=$(stdin_json "$REPO" "Opus 5" | TERM_WIDTH=$w bash "$NOWRAP" 2>&1 | sed $'s/\033\[[0-9;]*m//g')
u=$(( w - 5 )); c=$(vis "$out")
[ "$(count "$out")" = "1" ] && [ "$c" -le "$u" ] \
&& ok "width $w: one line, $c <= usable $u" \
|| bad "width $w: one line within usable $u (got $(count "$out") line/s, $c cols)" "$out"
done
rm -f "$NOWRAP"
echo; echo "pass=$pass fail=$fail"; [ "$fail" -eq 0 ]

View file

@ -0,0 +1,93 @@
#!/bin/bash
# Tests for claude/statusline.isaacaudet.sh — the Isaac Audet-inspired Claude
# Code status line (the variant symlinked from ~/.claude/statusline.sh).
#
# Sibling variants in claude/ (statusline.burnrate.sh, statusline.original.sh)
# are NOT covered by these tests.
#
# Width invariants.
#
# Expressed in USABLE width (what the renderer actually has: COLUMNS minus
# padding on both sides minus a margin), since that is what the script wraps on.
# Asserting invariants rather than fixed line counts keeps these from rotting
# every time a segment's content changes.
SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/statusline.isaacaudet.sh"
CACHE="/tmp/claude/statusline-usage-cache.json"
PAD=$(jq -r '.statusLine.padding // 0' "$HOME/.claude/settings.json" 2>/dev/null || echo 0)
OVERHEAD=$(( 2 * PAD + 1 ))
BACKUP=$(mktemp); [ -f "$CACHE" ] && cp "$CACHE" "$BACKUP"
REPO=$(mktemp -d)
# If there was no cache to begin with, REMOVE the fixture rather than leaving
# it: the status line would treat it as a fresh response for up to an hour.
cleanup() {
if [ -s "$BACKUP" ]; then cp "$BACKUP" "$CACHE"; else rm -f "$CACHE"; fi
rm -f "$BACKUP"; rm -rf "$REPO"
}
trap cleanup EXIT
cat > "$CACHE" <<'JSON'
{"five_hour":{"utilization":5.0,"resets_at":"2026-08-27T15:40:00+00:00"},
"seven_day":{"utilization":7.0,"resets_at":"2026-08-28T02:00:00+00:00"},
"extra_usage":{"is_enabled":false},
"limits":[{"kind":"weekly_scoped","group":"weekly","percent":10,"resets_at":"2026-08-28T02:00:00+00:00","scope":{"model":{"display_name":"Fable"}},"is_active":true}]}
JSON
git -C "$REPO" init -q 2>/dev/null
git -C "$REPO" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init 2>/dev/null
widths() { python3 -c "
import sys,re,unicodedata
for ln in sys.stdin.read().rstrip('\n').split('\n'):
p=re.sub(r'\033\[[0-9;]*m','',ln)
print(sum(2 if unicodedata.east_asian_width(c) in 'WF' else 1 for c in p))"; }
pass=0; fail=0
check() { # usable, branch, model, cost, [xfail-reason]
local u="$1" branch="$2" model="$3" cost="$4" xfail="${5:-}"
local w=$(( u + OVERHEAD ))
git -C "$REPO" checkout -q -B "$branch" 2>/dev/null; rm -f /tmp/claude/git-*
local stdin="{\"model\":{\"display_name\":\"$model\"},\"cwd\":\"$REPO\",\"cost\":{\"total_cost_usd\":$cost},\"context_window\":{\"context_window_size\":200000,\"current_usage\":{\"input_tokens\":45000,\"cache_read_input_tokens\":30000}}}"
local out ws n mx=0 over=0
out=$(TERM_WIDTH=$w bash "$SCRIPT" <<<"$stdin")
ws=$(printf '%s' "$out" | widths)
n=$(printf '%s\n' "$ws" | wc -l | tr -d ' ')
while read -r c; do [ "$c" -gt "$u" ] && over=1; [ "$c" -gt "$mx" ] && mx=$c; done <<< "$ws"
local why=""
# INVARIANT 1: never exceed the usable width
[ "$over" = "1" ] && why="line exceeds usable width"
# INVARIANT 2: at most two lines
[ "$n" -gt 2 ] && why="${why:-more than two lines}"
# INVARIANT 3: only wrap when it was actually necessary
if [ -z "$why" ] && [ "$n" = "2" ]; then
local l1 l2; l1=$(printf '%s\n' "$ws" | sed -n 1p); l2=$(printf '%s\n' "$ws" | sed -n 2p)
[ $(( l1 + 3 + l2 )) -le "$u" ] && why="wrapped unnecessarily (would have fit on one line)"
fi
local label="usable=$u branch=${#branch}ch model='${model:0:12}' lines=$n max=$mx"
if [ -n "$xfail" ] && [ -n "$why" ]; then
echo " XFAIL $label -- $why (known, pre-existing: $xfail)"; pass=$((pass+1)); return
fi
if [ -z "$why" ]; then echo " PASS $label"; pass=$((pass+1))
else echo " FAIL $label -- $why"
printf '%s\n' "$out" | sed $'s/\033\[[0-9;]*m//g' | sed 's/^/ /'; fail=$((fail+1)); fi
}
SHORT=main
LONG=feature/some-really-long-branch-name-here
M1="Opus 5"
M2="Opus 5 (1M context)"
echo "Sweep of usable widths, short branch:"
for u in 200 155 150 130 116 110 100 95 85 75 68 67 60 40; do check "$u" "$SHORT" "$M1" 0.50; done
# Narrow tier has a ~35-col floor; nothing can fit below that. Verified byte-identical
# on the pre-change script, so not a regression.
check 25 "$SHORT" "$M1" 0.50 "narrow tier floor ~35 cols"
echo "Sweep with the real long model name:"
for u in 200 150 116 100 85 68 40; do check "$u" "$SHORT" "$M2" 4.61; done
echo "Sweep with a long branch name:"
for u in 200 150 116 100 85 68; do check "$u" "$LONG" "$M2" 4.61; done
check 40 "$LONG" "$M2" 4.61 "narrow tier floor ~43 cols with a long branch"
echo "Large cost figure:"
for u in 116 85 68; do check "$u" "$SHORT" "$M1" 1234.56; done
echo; echo "pass=$pass fail=$fail"; [ "$fail" -eq 0 ]