|
#!/bin/bash |
|
# Claude Code Statusline - Multi-line, column-aligned status display |
|
# Lines share a 3-column grid so pipes line up: |
|
# [Col A: 31 chars] | [Col B: 31 chars] | [Col C: variable] |
|
# Line 1: model | context bar | dir branch [wt] |
|
# Line 2: current bar | weekly bar | extra bar |
|
# Line 3: resets time | resets datetime | resets date |
|
# Line 4: think ●●● | $cost |
|
# Receives JSON via stdin from Claude Code |
|
|
|
input=$(cat) |
|
|
|
# ── Extract data ── |
|
CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir // ""') |
|
PROJECT_DIR=$(echo "$input" | jq -r '.workspace.project_dir // ""') |
|
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0') |
|
TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // ""') |
|
|
|
# Get the ACTUAL model from the transcript (last assistant message), |
|
# since the statusline JSON model field reflects the global preference, |
|
# not the per-session model. |
|
MODEL="?" |
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then |
|
ACTUAL_MODEL=$(tail -50 "$TRANSCRIPT_PATH" 2>/dev/null \ |
|
| jq -r 'select(.type == "assistant") | .message.model // empty' 2>/dev/null \ |
|
| tail -1) |
|
if [ -n "$ACTUAL_MODEL" ] && [ "$ACTUAL_MODEL" != "null" ]; then |
|
case "$ACTUAL_MODEL" in |
|
*opus-4-6*) MODEL="Opus 4.6" ;; |
|
*opus-4-5*) MODEL="Opus 4.5" ;; |
|
*opus*) MODEL="Opus" ;; |
|
*sonnet-4-5*) MODEL="Sonnet 4.5" ;; |
|
*sonnet*) MODEL="Sonnet" ;; |
|
*haiku-4-5*) MODEL="Haiku 4.5" ;; |
|
*haiku*) MODEL="Haiku" ;; |
|
*) MODEL="$ACTUAL_MODEL" ;; |
|
esac |
|
fi |
|
fi |
|
|
|
# Context window data |
|
CONTEXT_MAX=$(echo "$input" | jq -r '.context_window.context_window_size // 0') |
|
INPUT_TOKENS=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0') |
|
OUTPUT_TOKENS=$(echo "$input" | jq -r '.context_window.current_usage.output_tokens // 0') |
|
CACHE_READ=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0') |
|
CACHE_CREATION=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0') |
|
USED_PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0') |
|
|
|
CONTENT_TOKENS=$((CACHE_READ + INPUT_TOKENS + CACHE_CREATION + OUTPUT_TOKENS)) |
|
# Format cost |
|
if [ "$COST" != "0" ] && [ "$COST" != "null" ]; then |
|
COST_FMT=$(printf "%.2f" "$COST" 2>/dev/null || echo "$COST") |
|
else |
|
COST_FMT="0.00" |
|
fi |
|
|
|
# Shorten directory |
|
if [ -n "$PROJECT_DIR" ] && [ "$CURRENT_DIR" != "$PROJECT_DIR" ]; then |
|
DIR_DISPLAY="${CURRENT_DIR#"$PROJECT_DIR"/}" |
|
else |
|
DIR_DISPLAY="${CURRENT_DIR##*/}" |
|
fi |
|
|
|
# Git branch + worktree detection |
|
GIT_BRANCH="" |
|
IS_WORKTREE=false |
|
if [ -n "$CURRENT_DIR" ] && [ -d "$CURRENT_DIR/.git" ] || git -C "$CURRENT_DIR" rev-parse --git-dir &>/dev/null 2>&1; then |
|
BRANCH=$(git -C "$CURRENT_DIR" branch --show-current 2>/dev/null) |
|
[ -n "$BRANCH" ] && GIT_BRANCH=" $BRANCH" |
|
# In a worktree, --git-dir and --git-common-dir differ |
|
GIT_DIR=$(git -C "$CURRENT_DIR" rev-parse --git-dir 2>/dev/null) |
|
GIT_COMMON=$(git -C "$CURRENT_DIR" rev-parse --git-common-dir 2>/dev/null) |
|
[ -n "$GIT_DIR" ] && [ -n "$GIT_COMMON" ] && [ "$GIT_DIR" != "$GIT_COMMON" ] && IS_WORKTREE=true |
|
fi |
|
|
|
# ── True-color ANSI codes ── |
|
RST="\033[0m" |
|
BOLD="\033[1m" |
|
DIM="\033[2m" |
|
BLUE="\033[38;2;0;153;255m" |
|
ORANGE="\033[38;2;255;176;85m" |
|
GREEN="\033[38;2;0;160;0m" |
|
CYAN="\033[38;2;100;200;200m" |
|
RED="\033[38;2;255;85;85m" |
|
YELLOW="\033[38;2;230;200;0m" |
|
WHITE="\033[38;2;220;220;220m" |
|
GRAY="\033[38;2;180;180;180m" |
|
MAGENTA="\033[35m" |
|
|
|
# ── Layout constants ── |
|
COL_W=31 # Fixed column width for cols A and B |
|
BAR_WIDTH=15 # Progress bar dot count |
|
SEP=" ${DIM}|${RST} " |
|
|
|
# ── Helpers ── |
|
|
|
# Print spaces to pad from current visible width to COL_W |
|
pad() { |
|
local gap=$(( COL_W - $1 )) |
|
[ "$gap" -gt 0 ] && printf "%*s" "$gap" "" |
|
} |
|
|
|
build_bar() { |
|
local pct=$1 width=$2 |
|
[ "$pct" -lt 0 ] 2>/dev/null && pct=0 |
|
[ "$pct" -gt 100 ] 2>/dev/null && pct=100 |
|
local filled=$(( pct * width / 100 )) |
|
local empty=$(( width - filled )) |
|
local bar_color |
|
if [ "$pct" -ge 90 ]; then bar_color="$RED" |
|
elif [ "$pct" -ge 70 ]; then bar_color="$YELLOW" |
|
elif [ "$pct" -ge 50 ]; then bar_color="$ORANGE" |
|
else bar_color="$GREEN" |
|
fi |
|
local filled_str="" empty_str="" |
|
for ((i=0; i<filled; i++)); do filled_str+="●"; done |
|
for ((i=0; i<empty; i++)); do empty_str+="○"; done |
|
printf "%b%s%b%b%s%b" "$bar_color" "$filled_str" "$RST" "$DIM" "$empty_str" "$RST" |
|
} |
|
|
|
format_tokens() { |
|
local n=$1 |
|
if [ "$n" -ge 1000000 ] 2>/dev/null; then |
|
printf "%s" "$(echo "scale=1; $n / 1000000" | bc)m" |
|
elif [ "$n" -ge 1000 ] 2>/dev/null; then |
|
printf "%s" "$(( n / 1000 ))k" |
|
else |
|
printf "%s" "$n" |
|
fi |
|
} |
|
|
|
format_reset_time() { |
|
local iso="$1" style="$2" |
|
[ -z "$iso" ] && return |
|
local epoch |
|
epoch=$(date -d "$iso" +%s 2>/dev/null) |
|
[ -z "$epoch" ] && return |
|
if [ "$style" = "time" ]; then |
|
date -d "@$epoch" "+%-I:%M%P" 2>/dev/null |
|
else |
|
date -d "@$epoch" "+%b %-d, %-I:%M%P" 2>/dev/null |
|
fi |
|
} |
|
|
|
cached_fetch() { |
|
local cache_file="$1" max_age="$2" |
|
shift 2 |
|
if [ -f "$cache_file" ]; then |
|
local mtime now age |
|
mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0) |
|
now=$(date +%s) |
|
age=$(( now - mtime )) |
|
if [ "$age" -lt "$max_age" ]; then |
|
cat "$cache_file" 2>/dev/null |
|
return |
|
fi |
|
fi |
|
local resp |
|
resp=$(curl -s --max-time 3 "$@" 2>/dev/null) |
|
if [ -n "$resp" ] && [ "$resp" != "null" ]; then |
|
echo "$resp" > "$cache_file" 2>/dev/null |
|
echo "$resp" |
|
elif [ -f "$cache_file" ]; then |
|
cat "$cache_file" 2>/dev/null |
|
fi |
|
} |
|
|
|
# ── Read settings ── |
|
THINKING="off" |
|
EFFORT="" |
|
SETTINGS_FILE="$HOME/.claude/settings.json" |
|
if [ -f "$SETTINGS_FILE" ]; then |
|
THINK_VAL=$(jq -r '.alwaysThinkingEnabled // false' "$SETTINGS_FILE" 2>/dev/null) |
|
[ "$THINK_VAL" = "true" ] && THINKING="on" |
|
EFFORT=$(jq -r '.effortLevel // ""' "$SETTINGS_FILE" 2>/dev/null) |
|
fi |
|
|
|
# ── Context color ── |
|
if [ "$USED_PCT" -lt 50 ]; then CTX_COLOR="$GREEN" |
|
elif [ "$USED_PCT" -lt 75 ]; then CTX_COLOR="$YELLOW" |
|
else CTX_COLOR="$RED" |
|
fi |
|
|
|
# ── Pre-compute display strings ── |
|
TOKENS_USED_STR=$(format_tokens $CONTENT_TOKENS) |
|
TOKENS_MAX_STR=$(format_tokens $CONTEXT_MAX) |
|
|
|
# ── Fetch usage data BEFORE any output (avoid render timeout) ── |
|
CREDS_FILE="$HOME/.claude/.credentials.json" |
|
USAGE_DATA="" |
|
|
|
if [ -f "$CREDS_FILE" ]; then |
|
TOKEN=$(jq -r '.claudeAiOauth.accessToken // ""' "$CREDS_FILE" 2>/dev/null) |
|
if [ -n "$TOKEN" ]; then |
|
RESP=$(cached_fetch "/tmp/claude-statusline-usage-cache.json" 60 \ |
|
-H "Accept: application/json" \ |
|
-H "Authorization: Bearer $TOKEN" \ |
|
-H "anthropic-beta: oauth-2025-04-20" \ |
|
"https://api.anthropic.com/api/oauth/usage") |
|
if [ -n "$RESP" ] && echo "$RESP" | jq -e '.five_hour' &>/dev/null; then |
|
USAGE_DATA="$RESP" |
|
fi |
|
fi |
|
fi |
|
|
|
EXTRA_ENABLED=false |
|
if [ -n "$USAGE_DATA" ]; then |
|
FIVE_PCT=$(echo "$USAGE_DATA" | jq -r '.five_hour.utilization // 0' | xargs printf "%.0f" 2>/dev/null) |
|
FIVE_RESET_ISO=$(echo "$USAGE_DATA" | jq -r '.five_hour.resets_at // ""') |
|
FIVE_RESET=$(format_reset_time "$FIVE_RESET_ISO" "time") |
|
|
|
SEVEN_PCT=$(echo "$USAGE_DATA" | jq -r '.seven_day.utilization // 0' | xargs printf "%.0f" 2>/dev/null) |
|
SEVEN_RESET_ISO=$(echo "$USAGE_DATA" | jq -r '.seven_day.resets_at // ""') |
|
SEVEN_RESET=$(format_reset_time "$SEVEN_RESET_ISO" "datetime") |
|
|
|
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' | xargs printf "%.0f" 2>/dev/null) |
|
EXTRA_USED=$(echo "$USAGE_DATA" | jq -r '.extra_usage.used_credits // 0') |
|
EXTRA_LIMIT=$(echo "$USAGE_DATA" | jq -r '.extra_usage.monthly_limit // 0') |
|
EXTRA_USED_D=$(echo "scale=2; $EXTRA_USED / 100" | bc 2>/dev/null || echo "0") |
|
EXTRA_LIMIT_D=$(echo "scale=2; $EXTRA_LIMIT / 100" | bc 2>/dev/null || echo "0") |
|
EXTRA_RESET=$(date -d "$(date +%Y-%m-01) +1 month" "+%b %-d" 2>/dev/null) |
|
fi |
|
fi |
|
|
|
# ═══════════════════════════════════════════════════════════ |
|
# ALL OUTPUT BELOW — data is ready, print fast |
|
# ═══════════════════════════════════════════════════════════ |
|
|
|
# ── LINE 1: model | context bar | dir branch [wt] ── |
|
# Col A: model |
|
printf "%b%b%s%b" "$BOLD" "$BLUE" "$MODEL" "$RST" |
|
pad ${#MODEL} |
|
printf "%b" "$SEP" |
|
# Col B: context bar |
|
build_bar "$USED_PCT" $BAR_WIDTH |
|
USED_STR="${USED_PCT}%" |
|
TOK_STR="${TOKENS_USED_STR}/${TOKENS_MAX_STR}" |
|
printf " %b%s%b %b%s%b" "$CTX_COLOR" "$USED_STR" "$RST" "$DIM" "$TOK_STR" "$RST" |
|
pad $(( BAR_WIDTH + 1 + ${#USED_STR} + 1 + ${#TOK_STR} )) |
|
printf "%b" "$SEP" |
|
# Col C: dir branch [wt] (free to grow) |
|
printf "%b%s%b" "$MAGENTA" "$DIR_DISPLAY" "$RST" |
|
[ -n "$GIT_BRANCH" ] && printf "%b%b%s%b" "$DIM" "$GREEN" "$GIT_BRANCH" "$RST" |
|
$IS_WORKTREE && printf " %b%bwt%b" "$BOLD" "$ORANGE" "$RST" |
|
echo |
|
|
|
# ── LINE 2: rate limit bars ── |
|
|
|
if [ -n "$USAGE_DATA" ]; then |
|
# Col A: current (5h) bar |
|
printf "%bcurrent:%b " "$WHITE" "$RST" |
|
build_bar "$FIVE_PCT" $BAR_WIDTH |
|
PCT_STR="${FIVE_PCT}%" |
|
printf " %b%s%b" "$CYAN" "$PCT_STR" "$RST" |
|
pad $(( 9 + BAR_WIDTH + 1 + ${#PCT_STR} )) |
|
printf "%b" "$SEP" |
|
|
|
# Col B: weekly (7d) bar |
|
printf "%bweekly:%b " "$WHITE" "$RST" |
|
build_bar "$SEVEN_PCT" $BAR_WIDTH |
|
PCT_STR="${SEVEN_PCT}%" |
|
printf " %b%s%b" "$CYAN" "$PCT_STR" "$RST" |
|
pad $(( 9 + BAR_WIDTH + 1 + ${#PCT_STR} )) |
|
printf "%b" "$SEP" |
|
|
|
# Col C: extra bar (if enabled) |
|
if [ "$EXTRA_ENABLED" = "true" ]; then |
|
printf "%bextra:%b " "$WHITE" "$RST" |
|
build_bar "$EXTRA_PCT" $BAR_WIDTH |
|
printf " %b\$%s/\$%s%b" "$CYAN" "$EXTRA_USED_D" "$EXTRA_LIMIT_D" "$RST" |
|
fi |
|
fi |
|
echo |
|
|
|
# ── LINE 3: all resets (only if usage data) ── |
|
|
|
if [ -n "$USAGE_DATA" ]; then |
|
# Col A: 5h reset (under current bar) |
|
RESET_A="resets ${FIVE_RESET}" |
|
printf "%b%s%b" "$GRAY" "$RESET_A" "$RST" |
|
pad ${#RESET_A} |
|
printf "%b" "$SEP" |
|
|
|
# Col B: 7d reset (under weekly bar) |
|
RESET_B="resets ${SEVEN_RESET}" |
|
printf "%b%s%b" "$GRAY" "$RESET_B" "$RST" |
|
pad ${#RESET_B} |
|
printf "%b" "$SEP" |
|
|
|
# Col C: extra reset (under extra bar) |
|
if [ "$EXTRA_ENABLED" = "true" ]; then |
|
printf "%bresets %s%b" "$GRAY" "$EXTRA_RESET" "$RST" |
|
fi |
|
echo |
|
fi |
|
|
|
# ── LINE 4: think | $cost ── |
|
|
|
# Col A: think effort |
|
if [ "$THINKING" = "on" ]; then |
|
case "$EFFORT" in |
|
high) printf "%bthink %b●●●%b" "$WHITE" "$GREEN" "$RST"; TVLEN=9 ;; |
|
medium) printf "%bthink %b●●%b%b○%b" "$WHITE" "$YELLOW" "$RST" "$DIM" "$RST"; TVLEN=9 ;; |
|
low) printf "%bthink %b●%b%b○○%b" "$WHITE" "$RED" "$RST" "$DIM" "$RST"; TVLEN=9 ;; |
|
*) printf "%bthink %b●●●%b" "$WHITE" "$GREEN" "$RST"; TVLEN=9 ;; |
|
esac |
|
else |
|
printf "%bthink %b○○○%b" "$WHITE" "$DIM" "$RST"; TVLEN=9 |
|
fi |
|
pad $TVLEN |
|
printf "%b" "$SEP" |
|
|
|
# Col B: $cost |
|
COST_STR="\$${COST_FMT}" |
|
printf "%b%s%b" "$DIM" "$COST_STR" "$RST" |
|
echo |