Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions statusline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,23 @@ fi
[ "${#BRANCH}" -gt 30 ] && BRANCH="${BRANCH:0:27}..."

# ── Refresh usage via Anthropic OAuth API ────────────────────────────────────
# Reads the subscription OAuth token from either the legacy credentials file
# (Linux / older Claude Code installs) or the macOS Keychain (current Darwin
# installs store the token there instead of on disk).
read_oauth_token() {
if [ -f "$CREDENTIALS_FILE" ]; then
jq -r '.claudeAiOauth.accessToken // empty' "$CREDENTIALS_FILE" 2>/dev/null
return
fi
if [ "$(uname)" = "Darwin" ] && command -v security >/dev/null 2>&1; then
security find-generic-password -s "Claude Code-credentials" -a "$(whoami)" -w 2>/dev/null \
| jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null
fi
}

refresh_usage_api() {
[ ! -f "$CREDENTIALS_FILE" ] && return 1
local token
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$CREDENTIALS_FILE" 2>/dev/null)
token=$(read_oauth_token)
[ -z "$token" ] && return 1
local resp
resp=$(curl -s --max-time 3 \
Expand Down Expand Up @@ -183,7 +196,15 @@ refresh_usage_api() {

LOCK_FILE="/tmp/statusline-refresh.lock"
if [ "$(cache_age_sec)" -gt "$REFRESH_INTERVAL" ]; then
( flock -n 9 || exit 0; refresh_usage_api ) 9>"$LOCK_FILE"
if command -v flock >/dev/null 2>&1; then
( flock -n 9 || exit 0; refresh_usage_api ) 9>"$LOCK_FILE"
else
# macOS does not ship `flock` by default. The cache_age_sec gate above
# already throttles to one refresh per REFRESH_INTERVAL; concurrent
# renders racing through that gate would cause at most 1-2 duplicate
# API calls per interval for a single user, which is acceptable.
refresh_usage_api
fi
fi

# ── Read cached usage metrics ─────────────────────────────────────────────────
Expand Down Expand Up @@ -275,7 +296,7 @@ if [ -f "$USAGE_FILE" ] && [ "$REFRESH_INTERVAL" -gt 0 ] 2>/dev/null; then
[ "$(cache_age_sec)" -gt $(( REFRESH_INTERVAL * 3 )) ] && IS_STALE=1
fi
[ "$IS_STALE" = 1 ] && [ -n "$BLOCK_DISPLAY" ] && \
BLOCK_DISPLAY=$(echo "$BLOCK_DISPLAY" | sed 's/🟒\|🟑\|πŸ”΄/⚠/')
BLOCK_DISPLAY=$(echo "$BLOCK_DISPLAY" | sed -E 's/(🟒|🟑|πŸ”΄)/⚠/')

# ── Assemble ──────────────────────────────────────────────────────────────────
PARTS=()
Expand Down
18 changes: 15 additions & 3 deletions test_statusline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ assert_not_contains() {
fi
}

# Portable "touch to N minutes ago" β€” `touch -d '30 minutes ago'` works on GNU
# coreutils but not on BSD (macOS), which only accepts a fixed timestamp format
# via `-t`. Compute the timestamp with whichever `date` dialect is available.
touch_minutes_ago() {
local minutes="$1" file="$2" ts
ts=$(date -d "$minutes minutes ago" +%Y%m%d%H%M.%S 2>/dev/null) \
|| ts=$(date -v "-${minutes}M" +%Y%m%d%H%M.%S)
touch -t "$ts" "$file"
}

# ── Unit tests: make_bar ──────────────────────────────────────────────────────
echo ""
echo "=== Unit tests: make_bar ==="
Expand All @@ -61,7 +71,9 @@ run_make_bar() {

count_char() {
local char="$1" str="$2"
echo -n "$str" | grep -o "$char" | wc -l
# BSD `wc -l` right-pads the count with whitespace; GNU does not. Strip it
# so downstream string comparisons work on both.
echo -n "$str" | grep -o "$char" | wc -l | tr -d ' '
}

# pct=0 β†’ 6 empty blocks
Expand Down Expand Up @@ -164,7 +176,7 @@ echo ""
echo "-- Test 5: stale cache --"
USAGE_STALE=$(mktemp /tmp/test-usage-stale-XXXX.json); TMPFILES+=("$USAGE_STALE")
echo '{"timestamp":"2026-02-21T09:00:00+00:00","source":"api","metrics":{"session":{"percent_used":30.0,"percent_remaining":70.0,"resets_at":null}}}' > "$USAGE_STALE"
touch -d '30 minutes ago' "$USAGE_STALE"
touch_minutes_ago 30 "$USAGE_STALE"
OUT=$(run_statusline '{"model":"claude-sonnet-4-6","context_window":{"used_percentage":0}}' \
USAGE_FILE="$USAGE_STALE" REFRESH_INTERVAL=300)
assert_contains "stale cache shows ⚠" "⚠" "$OUT"
Expand All @@ -181,7 +193,7 @@ assert_not_contains "fresh cache no ⚠" "⚠" "$OUT"
# Test 7 β€” REFRESH_INTERVAL=0 never shows ⚠
echo ""
echo "-- Test 7: REFRESH_INTERVAL=0 no stale indicator --"
touch -d '30 minutes ago' "$USAGE_STALE"
touch_minutes_ago 30 "$USAGE_STALE"
OUT=$(run_statusline '{"model":"claude-sonnet-4-6","context_window":{"used_percentage":0}}' \
USAGE_FILE="$USAGE_STALE" REFRESH_INTERVAL=0)
assert_not_contains "interval=0 no ⚠" "⚠" "$OUT"
Expand Down