Last active
May 8, 2026 18:25
-
-
Save ericraymond/9423944ea21093b7821cb3f834a8a303 to your computer and use it in GitHub Desktop.
Claude Code status line — forked from daniel3303/ClaudeCodeStatusLine with cost tracking, effort inline, and security fixes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Derived from: https://github.com/daniel3303/ClaudeCodeStatusLine | |
| # Single line: Model (effort) | tokens/total (%) | dir@branch | cost (+delta) | 5h | 7d | |
| # | |
| # Install: | |
| # mkdir -p ~/.claude/statusline | |
| # curl -o ~/.claude/statusline/statusline.sh https://gist.githubusercontent.com/ericraymond/9423944ea21093b7821cb3f834a8a303/raw/statusline.sh | |
| # chmod +x ~/.claude/statusline/statusline.sh | |
| # | |
| # Add to ~/.claude/settings.json: | |
| # jq '.statusLine = {"type":"command","command":"~/.claude/statusline/statusline.sh"}' \ | |
| # ~/.claude/settings.json > /tmp/sl.json && mv /tmp/sl.json ~/.claude/settings.json | |
| # | |
| # Then restart Claude Code. | |
| # | |
| # Changes from upstream: | |
| # - Layout: Model (effort) | context% | dir@branch | cost (+delta) | 5h | 7d | |
| # - Added session cost display with per-message delta | |
| # - Effort level moved inline next to model name | |
| # - 5h/7d hidden when no data (no placeholder dashes) | |
| # - awk injection fixed: shell vars passed via -v instead of string interpolation | |
| set -f # disable globbing | |
| VERSION="1.4.2" | |
| input=$(cat) | |
| if [ -z "$input" ]; then | |
| printf "Claude" | |
| exit 0 | |
| fi | |
| # ANSI colors matching oh-my-posh theme | |
| 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;46;149;153m' | |
| red='\033[38;2;255;85;85m' | |
| yellow='\033[38;2;230;200;0m' | |
| purple='\033[38;2;167;139;250m' | |
| white='\033[38;2;220;220;220m' | |
| dim='\033[2m' | |
| reset='\033[0m' | |
| # Format token counts (e.g., 50k / 200k) | |
| format_tokens() { | |
| local num=$1 | |
| if [ "$num" -ge 1000000 ]; then | |
| awk -v n="$num" 'BEGIN {v=sprintf("%.1f",n/1000000)+0; if(v==int(v)) printf "%dm",v; else printf "%.1fm",v}' | |
| elif [ "$num" -ge 1000 ]; then | |
| awk -v n="$num" 'BEGIN {printf "%.0fk", n / 1000}' | |
| else | |
| printf "%d" "$num" | |
| fi | |
| } | |
| # Format number with commas (e.g., 134,938) | |
| format_commas() { | |
| printf "%'d" "$1" | |
| } | |
| # Return color escape based on usage percentage | |
| # Usage: usage_color <pct> | |
| usage_color() { | |
| local pct=$1 | |
| if [ "$pct" -ge 90 ]; then echo "$red" | |
| elif [ "$pct" -ge 70 ]; then echo "$orange" | |
| elif [ "$pct" -ge 50 ]; then echo "$yellow" | |
| else echo "$green" | |
| fi | |
| } | |
| # Resolve config directory: CLAUDE_CONFIG_DIR (set by alias) or default ~/.claude | |
| claude_config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" | |
| # Return 0 (true) if $1 > $2 using semantic versioning | |
| version_gt() { | |
| local a="${1#v}" b="${2#v}" | |
| local IFS='.' | |
| read -r a1 a2 a3 <<< "$a" | |
| read -r b1 b2 b3 <<< "$b" | |
| a1=${a1:-0}; a2=${a2:-0}; a3=${a3:-0} | |
| b1=${b1:-0}; b2=${b2:-0}; b3=${b3:-0} | |
| [ "$a1" -gt "$b1" ] 2>/dev/null && return 0 | |
| [ "$a1" -lt "$b1" ] 2>/dev/null && return 1 | |
| [ "$a2" -gt "$b2" ] 2>/dev/null && return 0 | |
| [ "$a2" -lt "$b2" ] 2>/dev/null && return 1 | |
| [ "$a3" -gt "$b3" ] 2>/dev/null && return 0 | |
| return 1 | |
| } | |
| # ===== Extract data from JSON ===== | |
| model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"') | |
| model_name=$(echo "$model_name" | sed 's/ *(\([0-9.]*[kKmM]*\) context)/ \1/') # "(1M context)" → "1M" | |
| # Context window | |
| size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000') | |
| [ "$size" -eq 0 ] 2>/dev/null && size=200000 | |
| # Token usage | |
| input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0') | |
| cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0') | |
| cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0') | |
| current=$(( input_tokens + cache_create + cache_read )) | |
| used_tokens=$(format_tokens $current) | |
| total_tokens=$(format_tokens $size) | |
| if [ "$size" -gt 0 ]; then | |
| pct_used=$(( current * 100 / size )) | |
| else | |
| pct_used=0 | |
| fi | |
| pct_remain=$(( 100 - pct_used )) | |
| used_comma=$(format_commas $current) | |
| remain_comma=$(format_commas $(( size - current ))) | |
| # Check reasoning effort | |
| settings_path="$claude_config_dir/settings.json" | |
| effort_level="medium" | |
| if [ -n "$CLAUDE_CODE_EFFORT_LEVEL" ]; then | |
| effort_level="$CLAUDE_CODE_EFFORT_LEVEL" | |
| elif [ -f "$settings_path" ]; then | |
| effort_val=$(jq -r '.effortLevel // empty' "$settings_path" 2>/dev/null) | |
| [ -n "$effort_val" ] && effort_level="$effort_val" | |
| fi | |
| # ===== Build single-line output ===== | |
| out="" | |
| effort_colored="" | |
| case "$effort_level" in | |
| low) effort_colored="${dim}${effort_level}${reset}" ;; | |
| medium) effort_colored="${orange}med${reset}" ;; | |
| high) effort_colored="${green}${effort_level}${reset}" ;; | |
| xhigh) effort_colored="${purple}${effort_level}${reset}" ;; | |
| max) effort_colored="${red}${effort_level}${reset}" ;; | |
| *) effort_colored="${green}${effort_level}${reset}" ;; | |
| esac | |
| out+="${blue}${model_name}${reset} ${dim}(${reset}${effort_colored}${dim})${reset}" | |
| out+=" ${dim}|${reset} " | |
| out+="${orange}${used_tokens}/${total_tokens}${reset} ${dim}(${reset}${green}${pct_used}%${reset}${dim})${reset}" | |
| # Current working directory | |
| cwd=$(echo "$input" | jq -r '.cwd // empty') | |
| if [ -n "$cwd" ]; then | |
| display_dir="${cwd##*/}" | |
| git_branch=$(git -C "${cwd}" rev-parse --abbrev-ref HEAD 2>/dev/null) | |
| out+=" ${dim}|${reset} " | |
| out+="${cyan}${display_dir}${reset}" | |
| if [ -n "$git_branch" ]; then | |
| out+="${dim}@${reset}${green}${git_branch}${reset}" | |
| git_stat=$(git -C "${cwd}" diff --numstat 2>/dev/null | awk '{a+=$1; d+=$2} END {if (a+d>0) printf "+%d -%d", a, d}') | |
| [ -n "$git_stat" ] && out+=" ${dim}(${reset}${green}${git_stat%% *}${reset} ${red}${git_stat##* }${reset}${dim})${reset}" | |
| fi | |
| fi | |
| # ===== Cross-platform OAuth token resolution (from statusline.sh) ===== | |
| # Tries credential sources in order: env var → macOS Keychain → Linux creds file → GNOME Keyring | |
| get_oauth_token() { | |
| local token="" | |
| # 1. Explicit env var override | |
| if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then | |
| echo "$CLAUDE_CODE_OAUTH_TOKEN" | |
| return 0 | |
| fi | |
| # 2. macOS Keychain (Claude Code appends a SHA256 hash of CLAUDE_CONFIG_DIR to the service name) | |
| if command -v security >/dev/null 2>&1; then | |
| local keychain_svc="Claude Code-credentials" | |
| if [ -n "$CLAUDE_CONFIG_DIR" ]; then | |
| local dir_hash | |
| dir_hash=$(echo -n "$CLAUDE_CONFIG_DIR" | shasum -a 256 | cut -c1-8) | |
| keychain_svc="Claude Code-credentials-${dir_hash}" | |
| fi | |
| local blob | |
| blob=$(security find-generic-password -s "$keychain_svc" -w 2>/dev/null) | |
| if [ -n "$blob" ]; then | |
| token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) | |
| if [ -n "$token" ] && [ "$token" != "null" ]; then | |
| echo "$token" | |
| return 0 | |
| fi | |
| fi | |
| fi | |
| # 3. Linux credentials file | |
| local creds_file="${claude_config_dir}/.credentials.json" | |
| if [ -f "$creds_file" ]; then | |
| token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds_file" 2>/dev/null) | |
| if [ -n "$token" ] && [ "$token" != "null" ]; then | |
| echo "$token" | |
| return 0 | |
| fi | |
| fi | |
| # 4. GNOME Keyring via secret-tool | |
| if command -v secret-tool >/dev/null 2>&1; then | |
| local blob | |
| blob=$(timeout 2 secret-tool lookup service "Claude Code-credentials" 2>/dev/null) | |
| if [ -n "$blob" ]; then | |
| token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) | |
| if [ -n "$token" ] && [ "$token" != "null" ]; then | |
| echo "$token" | |
| return 0 | |
| fi | |
| fi | |
| fi | |
| echo "" | |
| } | |
| # ===== LINE 2 & 3: Usage limits with progress bars ===== | |
| # First, try to use rate_limits data provided directly by Claude Code in the JSON input. | |
| # This is the most reliable source — no OAuth token or API call required. | |
| builtin_five_hour_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') | |
| builtin_five_hour_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty') | |
| builtin_seven_day_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') | |
| builtin_seven_day_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty') | |
| use_builtin=false | |
| if [ -n "$builtin_five_hour_pct" ] || [ -n "$builtin_seven_day_pct" ]; then | |
| use_builtin=true | |
| fi | |
| # Cache setup — shared across all Claude Code instances to avoid rate limits | |
| claude_config_dir_hash=$(echo -n "$claude_config_dir" | shasum -a 256 2>/dev/null || echo -n "$claude_config_dir" | sha256sum 2>/dev/null) | |
| claude_config_dir_hash=$(echo "$claude_config_dir_hash" | cut -c1-8) | |
| cache_file="/tmp/claude/statusline-usage-cache-${claude_config_dir_hash}.json" | |
| cache_max_age=60 # seconds between API calls | |
| mkdir -p /tmp/claude | |
| needs_refresh=true | |
| usage_data="" | |
| # Always load cache — used as primary source for API path, and as fallback when builtin reports zero | |
| if [ -f "$cache_file" ] && [ -s "$cache_file" ]; then | |
| cache_mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null) | |
| now=$(date +%s) | |
| cache_age=$(( now - cache_mtime )) | |
| if [ "$cache_age" -lt "$cache_max_age" ]; then | |
| needs_refresh=false | |
| fi | |
| usage_data=$(cat "$cache_file" 2>/dev/null) | |
| fi | |
| # When builtin values are all zero AND reset timestamps are missing, it likely indicates | |
| # an API failure on Claude's side — fall through to cached data instead of displaying | |
| # misleading 0%. Genuine zero responses (after a billing reset) still include valid | |
| # resets_at timestamps, so we trust those. | |
| effective_builtin=false | |
| if $use_builtin; then | |
| # Trust builtin if any percentage is non-zero | |
| if { [ -n "$builtin_five_hour_pct" ] && [ "$(printf '%.0f' "$builtin_five_hour_pct" 2>/dev/null)" != "0" ]; } || \ | |
| { [ -n "$builtin_seven_day_pct" ] && [ "$(printf '%.0f' "$builtin_seven_day_pct" 2>/dev/null)" != "0" ]; }; then | |
| effective_builtin=true | |
| fi | |
| # Also trust if reset timestamps are present — genuine zero responses include valid reset times | |
| if ! $effective_builtin; then | |
| if { [ -n "$builtin_five_hour_reset" ] && [ "$builtin_five_hour_reset" != "null" ] && [ "$builtin_five_hour_reset" != "0" ]; } || \ | |
| { [ -n "$builtin_seven_day_reset" ] && [ "$builtin_seven_day_reset" != "null" ] && [ "$builtin_seven_day_reset" != "0" ]; }; then | |
| effective_builtin=true | |
| fi | |
| fi | |
| fi | |
| # Refresh API cache when stale — runs regardless of builtin rate_limits because | |
| # extra_usage is only exposed through the OAuth usage endpoint (not stdin JSON). | |
| # Throttled to cache_max_age and stampede-locked via touch for shared panes. | |
| if $needs_refresh; then | |
| touch "$cache_file" # stampede lock: prevent parallel panes from fetching simultaneously | |
| token=$(get_oauth_token) | |
| if [ -n "$token" ] && [ "$token" != "null" ]; then | |
| response=$(curl -s --max-time 10 \ | |
| -H "Accept: application/json" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "anthropic-beta: oauth-2025-04-20" \ | |
| -H "User-Agent: claude-code/2.1.34" \ | |
| "https://api.anthropic.com/api/oauth/usage" 2>/dev/null) | |
| # Only cache valid usage responses (not error/rate-limit JSON) | |
| if [ -n "$response" ] && echo "$response" | jq -e '.five_hour' >/dev/null 2>&1; then | |
| usage_data="$response" | |
| echo "$response" > "$cache_file" | |
| fi | |
| fi | |
| # Remove the stampede sentinel if the fetch failed to produce valid JSON — | |
| # otherwise an empty cache file would suppress retries for a full cache_max_age window. | |
| [ -f "$cache_file" ] && [ ! -s "$cache_file" ] && rm -f "$cache_file" | |
| fi | |
| # Cross-platform ISO to epoch conversion | |
| # Converts ISO 8601 timestamp (e.g. "2025-06-15T12:30:00Z" or "2025-06-15T12:30:00.123+00:00") to epoch seconds. | |
| # Properly handles UTC timestamps and converts to local time. | |
| iso_to_epoch() { | |
| local iso_str="$1" | |
| # Try GNU date first (Linux) — handles ISO 8601 format automatically | |
| local epoch | |
| epoch=$(date -d "${iso_str}" +%s 2>/dev/null) | |
| if [ -n "$epoch" ]; then | |
| echo "$epoch" | |
| return 0 | |
| fi | |
| # BSD date (macOS) - handle various ISO 8601 formats | |
| local stripped="${iso_str%%.*}" # Remove fractional seconds (.123456) | |
| stripped="${stripped%%Z}" # Remove trailing Z | |
| stripped="${stripped%%+*}" # Remove timezone offset (+00:00) | |
| stripped="${stripped%%-[0-9][0-9]:[0-9][0-9]}" # Remove negative timezone offset | |
| # Check if timestamp is UTC (has Z or +00:00 or -00:00) | |
| if [[ "$iso_str" == *"Z"* ]] || [[ "$iso_str" == *"+00:00"* ]] || [[ "$iso_str" == *"-00:00"* ]]; then | |
| # For UTC timestamps, parse with timezone set to UTC | |
| epoch=$(env TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null) | |
| else | |
| epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null) | |
| fi | |
| if [ -n "$epoch" ]; then | |
| echo "$epoch" | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| # Format ISO reset time to compact local time | |
| # Usage: format_reset_time <iso_string> <style: time|datetime|date> | |
| format_reset_time() { | |
| local iso_str="$1" | |
| local style="$2" | |
| { [ -z "$iso_str" ] || [ "$iso_str" = "null" ]; } && return | |
| # Parse ISO datetime and convert to local time (cross-platform) | |
| local epoch | |
| epoch=$(iso_to_epoch "$iso_str") | |
| [ -z "$epoch" ] && return | |
| # Format based on style | |
| # Try GNU date first (Linux), then BSD date (macOS) | |
| # Previous implementation piped BSD date through sed/tr, which always returned | |
| # exit code 0 from the last pipe stage, preventing the GNU date fallback from | |
| # ever executing on Linux. | |
| local formatted="" | |
| case "$style" in | |
| time) | |
| formatted=$(date -d "@$epoch" +"%H:%M" 2>/dev/null) || \ | |
| formatted=$(date -j -r "$epoch" +"%H:%M" 2>/dev/null) | |
| ;; | |
| datetime) | |
| formatted=$(date -d "@$epoch" +"%b %-d, %H:%M" 2>/dev/null) || \ | |
| formatted=$(date -j -r "$epoch" +"%b %-d, %H:%M" 2>/dev/null) | |
| ;; | |
| *) | |
| formatted=$(date -d "@$epoch" +"%b %-d" 2>/dev/null) || \ | |
| formatted=$(date -j -r "$epoch" +"%b %-d" 2>/dev/null) | |
| ;; | |
| esac | |
| [ -n "$formatted" ] && echo "$formatted" | |
| } | |
| # Session cost with per-message delta | |
| session_cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0') | |
| if [ -n "$session_cost" ] && [ "$session_cost" != "0" ]; then | |
| cost_fmt=$(LC_NUMERIC=C awk -v c="$session_cost" 'BEGIN {printf "$%.2f", c}') | |
| out+=" ${dim}|${reset} ${purple}${cost_fmt}${reset}" | |
| cost_prev_file="/tmp/claude/statusline-cost-prev-${claude_config_dir_hash}.txt" | |
| prev_cost=0 | |
| [ -f "$cost_prev_file" ] && prev_cost=$(cat "$cost_prev_file" 2>/dev/null) | |
| prev_cost=${prev_cost:-0} | |
| delta=$(LC_NUMERIC=C awk -v c="$session_cost" -v p="$prev_cost" 'BEGIN {d=c-p; printf "%.4f", (d<0)?0:d}') | |
| show_delta=$(LC_NUMERIC=C awk -v p="$prev_cost" -v d="$delta" 'BEGIN {print (p > 0 && d >= 0.005) ? 1 : 0}') | |
| if [ "$show_delta" = "1" ]; then | |
| delta_fmt=$(LC_NUMERIC=C awk -v d="$delta" 'BEGIN {printf "$%.2f", d}') | |
| out+=" ${dim}(+${delta_fmt})${reset}" | |
| fi | |
| echo "$session_cost" > "$cost_prev_file" | |
| fi | |
| sep=" ${dim}|${reset} " | |
| # Render extra_usage segment from API usage data (not available via stdin rate_limits). | |
| # Appends to the global $out. No-op when data is missing or is_enabled is false. | |
| render_extra_usage() { | |
| local data="$1" | |
| [ -z "$data" ] && return | |
| local enabled | |
| enabled=$(echo "$data" | jq -r '.extra_usage.is_enabled // false' 2>/dev/null) | |
| [ "$enabled" != "true" ] && return | |
| local pct used limit | |
| pct=$(echo "$data" | jq -r '.extra_usage.utilization // 0' | awk '{printf "%.0f", $1}') | |
| used=$(echo "$data" | jq -r '.extra_usage.used_credits // 0' | LC_NUMERIC=C awk '{printf "%.2f", $1/100}') | |
| limit=$(echo "$data" | jq -r '.extra_usage.monthly_limit // 0' | LC_NUMERIC=C awk '{printf "%.2f", $1/100}') | |
| if [ -n "$used" ] && [ -n "$limit" ] && [[ "$used" != *'$'* ]] && [[ "$limit" != *'$'* ]]; then | |
| local color | |
| color=$(usage_color "$pct") | |
| out+="${sep}${white}extra${reset} ${color}\$${used}/\$${limit}${reset}" | |
| else | |
| out+="${sep}${white}extra${reset} ${green}enabled${reset}" | |
| fi | |
| } | |
| if $effective_builtin; then | |
| # ---- Use rate_limits data provided directly by Claude Code in JSON input ---- | |
| # resets_at values are Unix epoch integers in this source | |
| if [ -n "$builtin_five_hour_pct" ]; then | |
| five_hour_pct=$(printf "%.0f" "$builtin_five_hour_pct") | |
| five_hour_color=$(usage_color "$five_hour_pct") | |
| out+="${sep}${white}5h${reset} ${five_hour_color}${five_hour_pct}%${reset}" | |
| if [ -n "$builtin_five_hour_reset" ] && [ "$builtin_five_hour_reset" != "null" ]; then | |
| five_hour_reset=$(date -j -r "$builtin_five_hour_reset" +"%H:%M" 2>/dev/null || date -d "@$builtin_five_hour_reset" +"%H:%M" 2>/dev/null) | |
| [ -n "$five_hour_reset" ] && out+=" ${dim}@${five_hour_reset}${reset}" | |
| fi | |
| fi | |
| if [ -n "$builtin_seven_day_pct" ]; then | |
| seven_day_pct=$(printf "%.0f" "$builtin_seven_day_pct") | |
| seven_day_color=$(usage_color "$seven_day_pct") | |
| out+="${sep}${white}7d${reset} ${seven_day_color}${seven_day_pct}%${reset}" | |
| if [ -n "$builtin_seven_day_reset" ] && [ "$builtin_seven_day_reset" != "null" ]; then | |
| seven_day_reset=$(date -j -r "$builtin_seven_day_reset" +"%b %-d, %H:%M" 2>/dev/null || date -d "@$builtin_seven_day_reset" +"%b %-d, %H:%M" 2>/dev/null) | |
| [ -n "$seven_day_reset" ] && out+=" ${dim}@${seven_day_reset}${reset}" | |
| fi | |
| fi | |
| # Render extra_usage from API cache (stdin rate_limits doesn't expose it) | |
| render_extra_usage "$usage_data" | |
| # Cache builtin values so they're available as fallback when API is unavailable. | |
| # Convert epoch resets_at to ISO 8601 for compatibility with the API-format cache parser. | |
| # Preserve extra_usage from prior API response so we don't clobber it. | |
| _fh_reset_json="null" | |
| if [ -n "$builtin_five_hour_reset" ] && [ "$builtin_five_hour_reset" != "null" ] && [ "$builtin_five_hour_reset" != "0" ]; then | |
| _fh_iso=$(date -u -r "$builtin_five_hour_reset" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \ | |
| date -u -d "@$builtin_five_hour_reset" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null) | |
| [ -n "$_fh_iso" ] && _fh_reset_json="\"$_fh_iso\"" | |
| fi | |
| _sd_reset_json="null" | |
| if [ -n "$builtin_seven_day_reset" ] && [ "$builtin_seven_day_reset" != "null" ] && [ "$builtin_seven_day_reset" != "0" ]; then | |
| _sd_iso=$(date -u -r "$builtin_seven_day_reset" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \ | |
| date -u -d "@$builtin_seven_day_reset" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null) | |
| [ -n "$_sd_iso" ] && _sd_reset_json="\"$_sd_iso\"" | |
| fi | |
| _extra_json=$(echo "$usage_data" | jq -c '.extra_usage // null' 2>/dev/null) | |
| [ -z "$_extra_json" ] && _extra_json="null" | |
| printf '{"five_hour":{"utilization":%s,"resets_at":%s},"seven_day":{"utilization":%s,"resets_at":%s},"extra_usage":%s}' \ | |
| "${builtin_five_hour_pct:-0}" "$_fh_reset_json" \ | |
| "${builtin_seven_day_pct:-0}" "$_sd_reset_json" \ | |
| "$_extra_json" > "$cache_file" 2>/dev/null | |
| elif [ -n "$usage_data" ] && echo "$usage_data" | jq -e '.five_hour' >/dev/null 2>&1; then | |
| # ---- Fall back: API-fetched usage data ---- | |
| # ---- 5-hour (current) ---- | |
| 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_reset=$(format_reset_time "$five_hour_reset_iso" "time") | |
| five_hour_color=$(usage_color "$five_hour_pct") | |
| out+="${sep}${white}5h${reset} ${five_hour_color}${five_hour_pct}%${reset}" | |
| [ -n "$five_hour_reset" ] && out+=" ${dim}@${five_hour_reset}${reset}" | |
| # ---- 7-day (weekly) ---- | |
| seven_day_pct=$(echo "$usage_data" | jq -r '.seven_day.utilization // 0' | awk '{printf "%.0f", $1}') | |
| 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") | |
| seven_day_color=$(usage_color "$seven_day_pct") | |
| out+="${sep}${white}7d${reset} ${seven_day_color}${seven_day_pct}%${reset}" | |
| [ -n "$seven_day_reset" ] && out+=" ${dim}@${seven_day_reset}${reset}" | |
| render_extra_usage "$usage_data" | |
| fi | |
| # ===== Update check (cached, 24h TTL) ===== | |
| version_cache_file="/tmp/claude/statusline-version-cache.json" | |
| version_cache_max_age=86400 # 24 hours | |
| version_needs_refresh=true | |
| version_data="" | |
| if [ -f "$version_cache_file" ]; then | |
| vc_mtime=$(stat -c %Y "$version_cache_file" 2>/dev/null || stat -f %m "$version_cache_file" 2>/dev/null) | |
| vc_now=$(date +%s) | |
| vc_age=$(( vc_now - vc_mtime )) | |
| if [ "$vc_age" -lt "$version_cache_max_age" ]; then | |
| version_needs_refresh=false | |
| fi | |
| version_data=$(cat "$version_cache_file" 2>/dev/null) | |
| fi | |
| if $version_needs_refresh; then | |
| touch "$version_cache_file" 2>/dev/null | |
| vc_response=$(curl -s --max-time 5 \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/repos/daniel3303/ClaudeCodeStatusLine/releases/latest" 2>/dev/null) | |
| if [ -n "$vc_response" ] && echo "$vc_response" | jq -e '.tag_name' >/dev/null 2>&1; then | |
| version_data="$vc_response" | |
| echo "$vc_response" > "$version_cache_file" | |
| elif [ ! -s "$version_cache_file" ]; then | |
| # Fetch failed and the cache has no usable content — drop the empty | |
| # stampede lock so the next render retries instead of the fresh mtime | |
| # suppressing update checks for the full 24h TTL. | |
| rm -f "$version_cache_file" 2>/dev/null | |
| fi | |
| fi | |
| update_line="" | |
| if [ -n "$version_data" ]; then | |
| latest_tag=$(echo "$version_data" | jq -r '.tag_name // empty') | |
| if [ -n "$latest_tag" ] && version_gt "$latest_tag" "$VERSION"; then | |
| update_line="\n${dim}Update available: ${latest_tag} → Tell Claude: \"Find my installed status bar and update it\"${reset}" | |
| fi | |
| fi | |
| # Output | |
| printf "%b" "$out$update_line" | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment