├── credentials.json.example ├── .gitignore ├── README.md └── statusline.sh /credentials.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "claudeAiOauth": { 3 | "accessToken": "YOUR_ACCESS_TOKEN_HERE" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Credentials and tokens 2 | .credentials.json 3 | credentials.json 4 | *.token 5 | *.key 6 | 7 | # Cache files 8 | claude-usage-cache.json 9 | /tmp/claude-usage-cache.json 10 | 11 | # Personal test data 12 | test-credentials.json 13 | 14 | # OS files 15 | .DS_Store 16 | Thumbs.db 17 | 18 | # Editor files 19 | *.swp 20 | *.swo 21 | *~ 22 | .vscode/ 23 | .idea/ 24 | 25 | # Test outputs 26 | test-output/ 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poor Man's Statusline 2 | 3 | Lightweight custom statusline for Claude Code with Powerline-style segments, usage limits, reset timers, and context monitoring. 4 | 5 | ## Features 6 | 7 | - **Powerline-style segments** with colored backgrounds and smooth arrow transitions 8 | - **Reset timers** showing countdown AND absolute time until limits reset (e.g., "2h11m @ 10pm") 9 | - Real-time API usage (5-hour and 7-day limits) 10 | - Per-model availability (Opus and Sonnet with distinct colors) 11 | - Context window monitoring with auto-compact warning 12 | - 1-minute smart caching 13 | - Automatically adapts to your Claude Pro tier (basic or higher) 14 | 15 | ## Example Output 16 | 17 | **Claude Pro (higher tiers with 7-day limits):** 18 | ``` 19 | opus-4-5 project main 20 | 5h 27% 2h11m @ 10pm 7d 34% 4d2h @ Dec 2 21 | Opus 34% Sonnet 20% 22 | ``` 23 | 24 | **Claude Pro (basic tier):** 25 | ``` 26 | sonnet-4-5 project main 27 | 5h 45% 3h30m @ 2pm 28 | ``` 29 | 30 | **Color scheme:** 31 | - Magenta: Model name, Opus limit 32 | - Yellow: Project directory 33 | - Green: Git branch 34 | - Cyan: Usage percentages 35 | - Blue: Reset countdowns, Sonnet limit 36 | - Gray: Context info 37 | 38 | ## Prerequisites 39 | 40 | - Claude Code (installed and authenticated) 41 | - `jq` - JSON processor 42 | - `curl` - HTTP client 43 | - Terminal with 256-color support 44 | 45 | ```bash 46 | # macOS 47 | brew install jq 48 | 49 | # Ubuntu/Debian 50 | sudo apt-get install jq 51 | ``` 52 | 53 | ## Installation 54 | 55 | ```bash 56 | # 1. Clone the repository 57 | git clone https://github.com/alexfazio/cc-poor-mans-statusline.git 58 | cd cc-poor-mans-statusline 59 | 60 | # 2. Make the script executable 61 | chmod +x statusline.sh 62 | ``` 63 | 64 | ### 3. Configure Claude Code 65 | 66 | Add the statusline to your `~/.claude/settings.json`: 67 | 68 | ```json 69 | { 70 | "statusLine": { 71 | "type": "command", 72 | "command": "/path/to/cc-poor-mans-statusline/statusline.sh" 73 | } 74 | } 75 | ``` 76 | 77 | Replace `/path/to/` with the actual location where you cloned the repo. For example: 78 | - `~/Documents/GitHub/cc-poor-mans-statusline/statusline.sh` 79 | - `~/projects/cc-poor-mans-statusline/statusline.sh` 80 | 81 | > **Tip:** You can also use `~` for your home directory in the path. 82 | 83 | Your credentials at `~/.claude/.credentials.json` are used automatically. 84 | 85 | ## Customization 86 | 87 | Edit `statusline.sh`: 88 | 89 | ```bash 90 | CACHE_TTL=60 # API cache duration in seconds 91 | CONTEXT_WINDOW=200000 # Model context window size 92 | AUTO_COMPACT_THRESHOLD=160000 # Warning threshold (80%) 93 | ``` 94 | 95 | ## Troubleshooting 96 | 97 | **No usage data showing?** 98 | - Verify credentials exist: `ls ~/.claude/.credentials.json` 99 | - Test API manually: `curl -s -H "Authorization: Bearer $(jq -r '.claudeAiOauth.accessToken' ~/.claude/.credentials.json)" -H "anthropic-beta: oauth-2025-04-20" "https://api.anthropic.com/api/oauth/usage" | jq .` 100 | 101 | **Script running slowly?** 102 | - Increase cache TTL: `CACHE_TTL=300` 103 | 104 | **Context not showing?** 105 | - Context only appears during active conversations 106 | 107 | **Powerline arrows not rendering?** 108 | - Ensure your terminal supports Unicode (U+E0B0) 109 | - Most modern terminals work out of the box 110 | 111 | ## Security 112 | 113 | Never commit: 114 | - `~/.claude/.credentials.json` (your OAuth token) 115 | - `/tmp/claude-usage-cache.json` (usage data) 116 | - Screenshots with personal info 117 | 118 | The included `.gitignore` prevents accidental leaks. 119 | -------------------------------------------------------------------------------- /statusline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Read JSON input from stdin 4 | input=$(cat) 5 | 6 | # Extract model information 7 | model_name=$(echo "$input" | jq -r '.model.id // "unknown"') 8 | 9 | # Extract directory information 10 | cwd=$(echo "$input" | jq -r '.workspace.current_dir') 11 | 12 | # Get git branch if in a git repo (using -C to avoid cd) 13 | branch=$(git -C "$cwd" branch --show-current 2>/dev/null) 14 | 15 | # Get basename of directory 16 | dir_name=$(basename "$cwd") 17 | 18 | # ============================================ 19 | # HELPER FUNCTIONS 20 | # ============================================ 21 | 22 | # Function to calculate visible length (strip ANSI codes) 23 | visible_length() { 24 | local string="$1" 25 | # Remove ANSI escape sequences (both \033 and \x1b formats) and count characters 26 | local clean 27 | clean=$(printf "%b" "$string" | sed $'s/\033\[[0-9;]*m//g') 28 | echo "${#clean}" 29 | } 30 | 31 | # Powerline arrow character (U+E0B0) 32 | PL_ARROW="" 33 | 34 | # Powerline segment: bg color, fg color, text, next segment's bg color 35 | # Uses 256-color ANSI codes 36 | pl_segment() { 37 | local bg=$1 fg=$2 text=$3 next_bg=$4 38 | # Background + foreground for text, then transition arrow 39 | printf "\033[48;5;%dm\033[38;5;%dm %s \033[48;5;%dm\033[38;5;%dm%s" \ 40 | "$bg" "$fg" "$text" "$next_bg" "$bg" "$PL_ARROW" 41 | } 42 | 43 | # Final powerline segment (no arrow, just reset) 44 | pl_segment_end() { 45 | local bg=$1 fg=$2 text=$3 46 | printf "\033[48;5;%dm\033[38;5;%dm %s \033[0m\033[38;5;%dm%s\033[0m" \ 47 | "$bg" "$fg" "$text" "$bg" "$PL_ARROW" 48 | } 49 | 50 | # ============================================ 51 | # TIME FORMATTING FUNCTIONS 52 | # ============================================ 53 | 54 | # Format countdown from ISO timestamp (e.g., "4h23m" or "2d5h") 55 | # Args: $1 = ISO timestamp, $2 = type ("5h" or "7d") 56 | format_countdown() { 57 | local iso_ts=$1 58 | local type=$2 59 | 60 | if [ -z "$iso_ts" ] || [ "$iso_ts" = "null" ]; then 61 | echo "" 62 | return 63 | fi 64 | 65 | # Strip timezone suffix and milliseconds for macOS date parsing 66 | local ts_clean 67 | ts_clean=$(echo "$iso_ts" | sed 's/+00:00$//' | sed 's/Z$//' | sed 's/\.[0-9]*//') 68 | 69 | # Parse ISO timestamp to epoch (macOS format) - use TZ=UTC since API returns UTC times 70 | local reset_epoch 71 | reset_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$ts_clean" "+%s" 2>/dev/null) 72 | 73 | if [ -z "$reset_epoch" ]; then 74 | echo "" 75 | return 76 | fi 77 | 78 | local now_epoch 79 | now_epoch=$(date +%s) 80 | local diff=$((reset_epoch - now_epoch)) 81 | 82 | # If already past, show 0 83 | if [ "$diff" -le 0 ]; then 84 | echo "0m" 85 | return 86 | fi 87 | 88 | # Format based on type 89 | if [ "$type" = "5h" ]; then 90 | # Short format: hours and minutes 91 | local hours=$((diff / 3600)) 92 | local mins=$(((diff % 3600) / 60)) 93 | if [ "$hours" -gt 0 ]; then 94 | echo "${hours}h${mins}m" 95 | else 96 | echo "${mins}m" 97 | fi 98 | else 99 | # Long format: days and hours 100 | local days=$((diff / 86400)) 101 | local hours=$(((diff % 86400) / 3600)) 102 | if [ "$days" -gt 0 ]; then 103 | echo "${days}d${hours}h" 104 | else 105 | echo "${hours}h" 106 | fi 107 | fi 108 | } 109 | 110 | # Format absolute time from ISO timestamp (e.g., "6PM" or "Dec 5") 111 | # Args: $1 = ISO timestamp, $2 = type ("5h" or "7d") 112 | format_absolute_time() { 113 | local iso_ts=$1 114 | local type=$2 115 | 116 | if [ -z "$iso_ts" ] || [ "$iso_ts" = "null" ]; then 117 | echo "" 118 | return 119 | fi 120 | 121 | # Strip timezone suffix and milliseconds for macOS date parsing 122 | local ts_clean 123 | ts_clean=$(echo "$iso_ts" | sed 's/+00:00$//' | sed 's/Z$//' | sed 's/\.[0-9]*//') 124 | 125 | # Parse as UTC to get correct epoch, then format in local time 126 | local epoch 127 | epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$ts_clean" "+%s" 2>/dev/null) 128 | 129 | if [ -z "$epoch" ]; then 130 | echo "" 131 | return 132 | fi 133 | 134 | # Format the epoch in local time 135 | if [ "$type" = "5h" ]; then 136 | # Short-term: show time like "6pm" or "6:30pm" 137 | local mins 138 | mins=$(date -r "$epoch" "+%M" 2>/dev/null) 139 | local result 140 | if [ "$mins" = "00" ]; then 141 | result=$(date -r "$epoch" "+%-I%p" 2>/dev/null) 142 | else 143 | result=$(date -r "$epoch" "+%-I:%M%p" 2>/dev/null) 144 | fi 145 | # Convert to lowercase and remove periods (AM/PM → am/pm) 146 | echo "$result" | tr '[:upper:]' '[:lower:]' | sed 's/\.//g' 147 | else 148 | # Long-term: show date like "Dec 5" 149 | date -r "$epoch" "+%b %-d" 2>/dev/null 150 | fi 151 | } 152 | 153 | # ============================================ 154 | # USAGE LIMITS FUNCTIONS 155 | # ============================================ 156 | 157 | CACHE_FILE="/tmp/claude-usage-cache.json" 158 | CACHE_TTL=60 # 1 minute in seconds 159 | CREDENTIALS_FILE="$HOME/.claude/.credentials.json" 160 | 161 | # Function to check if cache is valid 162 | is_cache_valid() { 163 | if [ ! -f "$CACHE_FILE" ]; then 164 | return 1 165 | fi 166 | 167 | local cache_time 168 | cache_time=$(jq -r '.timestamp // 0' "$CACHE_FILE" 2>/dev/null) 169 | local current_time 170 | current_time=$(date +%s) 171 | local age=$((current_time - cache_time)) 172 | 173 | if [ "$age" -lt "$CACHE_TTL" ]; then 174 | return 0 175 | else 176 | return 1 177 | fi 178 | } 179 | 180 | # Function to fetch usage from API 181 | fetch_usage() { 182 | # Check if credentials file exists 183 | if [ ! -f "$CREDENTIALS_FILE" ]; then 184 | return 1 185 | fi 186 | 187 | # Extract access token 188 | local token 189 | token=$(jq -r '.claudeAiOauth.accessToken // empty' "$CREDENTIALS_FILE" 2>/dev/null) 190 | if [ -z "$token" ]; then 191 | return 1 192 | fi 193 | 194 | # Make API request with 2 second timeout 195 | local response 196 | response=$(curl -s --max-time 2 \ 197 | -H "Authorization: Bearer $token" \ 198 | -H "anthropic-beta: oauth-2025-04-20" \ 199 | -H "Content-Type: application/json" \ 200 | "https://api.anthropic.com/api/oauth/usage" 2>/dev/null) 201 | 202 | if ! [ -n "$response" ]; then 203 | return 1 204 | fi 205 | 206 | # Parse response - utilization values 207 | local five_hour 208 | five_hour=$(echo "$response" | jq -r '.five_hour.utilization // null' 2>/dev/null) 209 | local seven_day 210 | seven_day=$(echo "$response" | jq -r '.seven_day.utilization // null' 2>/dev/null) 211 | local seven_day_opus 212 | seven_day_opus=$(echo "$response" | jq -r '.seven_day_opus.utilization // null' 2>/dev/null) 213 | local seven_day_sonnet 214 | seven_day_sonnet=$(echo "$response" | jq -r '.seven_day_sonnet.utilization // null' 2>/dev/null) 215 | 216 | # Parse response - reset timestamps 217 | local five_hour_resets 218 | five_hour_resets=$(echo "$response" | jq -r '.five_hour.resets_at // null' 2>/dev/null) 219 | local seven_day_resets 220 | seven_day_resets=$(echo "$response" | jq -r '.seven_day.resets_at // null' 2>/dev/null) 221 | 222 | # At least five_hour is required 223 | if [ "$five_hour" = "null" ]; then 224 | return 1 225 | fi 226 | 227 | # Write to cache with model-specific data and reset timestamps 228 | local current_time 229 | current_time=$(date +%s) 230 | jq -n \ 231 | --argjson ts "$current_time" \ 232 | --argjson fh "$five_hour" \ 233 | --arg fhr "$five_hour_resets" \ 234 | --argjson sd "$seven_day" \ 235 | --arg sdr "$seven_day_resets" \ 236 | --argjson sdo "${seven_day_opus:-null}" \ 237 | --argjson sds "${seven_day_sonnet:-null}" \ 238 | '{timestamp: $ts, five_hour: $fh, five_hour_resets: $fhr, seven_day: $sd, seven_day_resets: $sdr, seven_day_opus: $sdo, seven_day_sonnet: $sds}' \ 239 | > "$CACHE_FILE" 240 | 241 | # Output space-separated values (includes reset timestamps) 242 | echo "$five_hour $seven_day $seven_day_opus $seven_day_sonnet $five_hour_resets $seven_day_resets" 243 | return 0 244 | } 245 | 246 | # Function to get usage (from cache or API) 247 | get_usage() { 248 | if is_cache_valid; then 249 | # Read from cache 250 | local five_hour 251 | five_hour=$(jq -r '.five_hour' "$CACHE_FILE" 2>/dev/null) 252 | local seven_day 253 | seven_day=$(jq -r '.seven_day' "$CACHE_FILE" 2>/dev/null) 254 | local seven_day_opus 255 | seven_day_opus=$(jq -r '.seven_day_opus // "null"' "$CACHE_FILE" 2>/dev/null) 256 | local seven_day_sonnet 257 | seven_day_sonnet=$(jq -r '.seven_day_sonnet // "null"' "$CACHE_FILE" 2>/dev/null) 258 | local five_hour_resets 259 | five_hour_resets=$(jq -r '.five_hour_resets // "null"' "$CACHE_FILE" 2>/dev/null) 260 | local seven_day_resets 261 | seven_day_resets=$(jq -r '.seven_day_resets // "null"' "$CACHE_FILE" 2>/dev/null) 262 | echo "$five_hour $seven_day $seven_day_opus $seven_day_sonnet $five_hour_resets $seven_day_resets" 263 | else 264 | # Fetch from API 265 | fetch_usage 266 | fi 267 | } 268 | 269 | # Function to format percentage with color coding 270 | format_percentage() { 271 | local percentage=$1 272 | 273 | # Convert percentage to integer 274 | local pct_int=${percentage%.*} 275 | 276 | # Choose color based on percentage 277 | local color 278 | if [ "$pct_int" -le 50 ]; then 279 | color="\033[1;32m" # Green 280 | elif [ "$pct_int" -le 80 ]; then 281 | color="\033[1;33m" # Yellow 282 | else 283 | color="\033[1;31m" # Red 284 | fi 285 | 286 | # Return colored percentage only (no bar) 287 | printf "${color}%d%%\033[0m" "$pct_int" 288 | } 289 | 290 | # Function to format a model's usage percentage 291 | # Args: $1 = model name (e.g., "Opus"), $2 = percentage (or "null") 292 | format_model_usage() { 293 | local model_name=$1 294 | local percentage=$2 295 | 296 | # Natural formatting without fixed width 297 | local formatted_name="${model_name}:" 298 | 299 | # Handle null values 300 | if [ "$percentage" = "null" ] || [ -z "$percentage" ]; then 301 | # Gray color for null/unavailable 302 | printf "%s \033[1;30m--\033[0m" "$formatted_name" 303 | else 304 | # Normal color-coded percentage 305 | local pct_formatted 306 | pct_formatted=$(format_percentage "$percentage") 307 | printf "%s %s" "$formatted_name" "$pct_formatted" 308 | fi 309 | } 310 | 311 | # Function to get usage data as associative-style output 312 | # Returns: five_hour seven_day seven_day_opus seven_day_sonnet five_hour_resets seven_day_resets 313 | get_usage_data() { 314 | local usage_data 315 | usage_data=$(get_usage) 316 | 317 | if [ -z "$usage_data" ]; then 318 | return 1 319 | fi 320 | 321 | echo "$usage_data" 322 | } 323 | 324 | # ============================================ 325 | # CONTEXT USAGE FUNCTIONS 326 | # ============================================ 327 | 328 | CONTEXT_WINDOW=200000 # Claude Sonnet 4.5 context window 329 | AUTO_COMPACT_THRESHOLD=160000 # 80% of context window 330 | 331 | # Function to format token count (e.g., 35234 → "35.2K") 332 | format_token_count() { 333 | local tokens=$1 334 | 335 | if [ -z "$tokens" ] || [ "$tokens" -eq 0 ]; then 336 | echo "0" 337 | return 338 | fi 339 | 340 | # Convert to K format if >= 1000 341 | if [ "$tokens" -ge 1000 ]; then 342 | # Calculate with one decimal place 343 | local k_value 344 | k_value=$(echo "scale=1; $tokens / 1000" | bc 2>/dev/null) 345 | echo "${k_value}K" 346 | else 347 | echo "$tokens" 348 | fi 349 | } 350 | 351 | # Function to calculate context tokens from transcript 352 | calculate_context_tokens() { 353 | local transcript_path="$1" 354 | 355 | # Check if transcript exists 356 | if [ ! -f "$transcript_path" ]; then 357 | return 1 358 | fi 359 | 360 | # Read last 100 lines in reverse, find first valid usage 361 | local context_tokens=0 362 | while IFS= read -r line; do 363 | # Skip sidechain and error messages 364 | if echo "$line" | grep -q '"isSidechain":true'; then 365 | continue 366 | fi 367 | if echo "$line" | grep -q '"isApiErrorMessage":true'; then 368 | continue 369 | fi 370 | 371 | # Check if line has usage object 372 | if echo "$line" | grep -q '"usage":{'; then 373 | # Extract tokens 374 | local input_tokens 375 | input_tokens=$(echo "$line" | jq -r '.message.usage.input_tokens // 0' 2>/dev/null) 376 | local cache_read 377 | cache_read=$(echo "$line" | jq -r '.message.usage.cache_read_input_tokens // 0' 2>/dev/null) 378 | local cache_create 379 | cache_create=$(echo "$line" | jq -r '.message.usage.cache_creation_input_tokens // 0' 2>/dev/null) 380 | 381 | # Calculate context (input side only) 382 | context_tokens=$((input_tokens + cache_read + cache_create)) 383 | break # Found most recent, stop 384 | fi 385 | done < <(tail -n 100 "$transcript_path" 2>/dev/null | tail -r 2>/dev/null || tail -n 100 "$transcript_path" 2>/dev/null | awk '{lines[NR]=$0} END {for(i=NR;i>0;i--) print lines[i]}') 386 | 387 | echo "$context_tokens" 388 | } 389 | 390 | # Function to format context display (returns string) 391 | format_context() { 392 | # Extract transcript path from input 393 | local transcript_path 394 | transcript_path=$(echo "$input" | jq -r '.transcript_path // empty') 395 | 396 | if [ -z "$transcript_path" ]; then 397 | echo "" # Return empty string if no transcript 398 | return 399 | fi 400 | 401 | # Calculate context tokens 402 | local context_tokens 403 | context_tokens=$(calculate_context_tokens "$transcript_path") 404 | 405 | if [ -z "$context_tokens" ] || [ "$context_tokens" -eq 0 ]; then 406 | echo "" # Return empty string if no context data 407 | return 408 | fi 409 | 410 | # Calculate percentage 411 | local percentage 412 | percentage=$((context_tokens * 100 / CONTEXT_WINDOW)) 413 | 414 | # Format token count 415 | local formatted_tokens 416 | formatted_tokens=$(format_token_count "$context_tokens") 417 | 418 | # Check if approaching auto-compact threshold (> 80%) 419 | local warning="" 420 | if [ "$context_tokens" -gt "$AUTO_COMPACT_THRESHOLD" ]; then 421 | warning=" ⚠️" 422 | fi 423 | 424 | # Build and return context string 425 | local ctx_pct 426 | ctx_pct=$(format_percentage "$percentage") 427 | printf "CTX: %s %s%s" "$formatted_tokens" "$ctx_pct" "$warning" 428 | } 429 | 430 | # ============================================ 431 | # BUILD THE POWERLINE STATUS LINE 432 | # ============================================ 433 | 434 | # Color definitions (256-color palette) 435 | C_MAGENTA=5 # Model, Opus 436 | C_YELLOW=3 # Project 437 | C_GREEN=2 # Branch 438 | C_CYAN=6 # Usage limits 439 | C_BLUE=4 # Reset times, Sonnet 440 | C_GRAY=8 # Context 441 | C_WHITE=15 # Light text 442 | C_BLACK=0 # Dark text 443 | 444 | # Shorten model name (e.g., "claude-sonnet-4-5-20250929" → "sonnet-4-5") 445 | short_model=$(echo "$model_name" | sed 's/^claude-//' | sed 's/-[0-9]\{8\}$//') 446 | 447 | # ============================================ 448 | # ROW 1: Model → Project → Branch 449 | # ============================================ 450 | row1="" 451 | if [ -n "$branch" ]; then 452 | row1=$(pl_segment $C_MAGENTA $C_WHITE "$short_model" $C_YELLOW) 453 | row1="${row1}$(pl_segment $C_YELLOW $C_BLACK "$dir_name" $C_GREEN)" 454 | row1="${row1}$(pl_segment_end $C_GREEN $C_BLACK "$branch")" 455 | else 456 | row1=$(pl_segment $C_MAGENTA $C_WHITE "$short_model" $C_YELLOW) 457 | row1="${row1}$(pl_segment_end $C_YELLOW $C_BLACK "$dir_name")" 458 | fi 459 | printf "%b\n" "$row1" 460 | 461 | # ============================================ 462 | # ROW 2: Usage Limits with Reset Times 463 | # ============================================ 464 | usage_data=$(get_usage_data) 465 | if [ -n "$usage_data" ]; then 466 | read -r five_hour seven_day seven_day_opus seven_day_sonnet five_hour_resets seven_day_resets <<< "$usage_data" 467 | 468 | if [ -n "$five_hour" ] && [ "$five_hour" != "null" ]; then 469 | # Format 5h percentage 470 | five_h_int=${five_hour%.*} 471 | 472 | # Build 5h reset info 473 | five_h_countdown=$(format_countdown "$five_hour_resets" "5h") 474 | five_h_absolute=$(format_absolute_time "$five_hour_resets" "5h") 475 | 476 | row2="" 477 | if [ "$seven_day" != "null" ] && [ -n "$seven_day" ]; then 478 | # Full display: 5h and 7d 479 | seven_d_int=${seven_day%.*} 480 | seven_d_countdown=$(format_countdown "$seven_day_resets" "7d") 481 | seven_d_absolute=$(format_absolute_time "$seven_day_resets" "7d") 482 | 483 | # Build 5h segment 484 | if [ -n "$five_h_countdown" ] && [ -n "$five_h_absolute" ]; then 485 | row2=$(pl_segment $C_CYAN $C_BLACK "5h ${five_h_int}%" $C_BLUE) 486 | row2="${row2}$(pl_segment $C_BLUE $C_WHITE "${five_h_countdown} @ ${five_h_absolute}" $C_CYAN)" 487 | else 488 | row2=$(pl_segment $C_CYAN $C_BLACK "5h ${five_h_int}%" $C_CYAN) 489 | fi 490 | 491 | # Build 7d segment 492 | if [ -n "$seven_d_countdown" ] && [ -n "$seven_d_absolute" ]; then 493 | row2="${row2}$(pl_segment $C_CYAN $C_BLACK "7d ${seven_d_int}%" $C_BLUE)" 494 | row2="${row2}$(pl_segment_end $C_BLUE $C_WHITE "${seven_d_countdown} @ ${seven_d_absolute}")" 495 | else 496 | row2="${row2}$(pl_segment_end $C_CYAN $C_BLACK "7d ${seven_d_int}%")" 497 | fi 498 | else 499 | # Basic tier: only 5h 500 | if [ -n "$five_h_countdown" ] && [ -n "$five_h_absolute" ]; then 501 | row2=$(pl_segment $C_CYAN $C_BLACK "5h ${five_h_int}%" $C_BLUE) 502 | row2="${row2}$(pl_segment_end $C_BLUE $C_WHITE "${five_h_countdown} @ ${five_h_absolute}")" 503 | else 504 | row2=$(pl_segment_end $C_CYAN $C_BLACK "5h ${five_h_int}%") 505 | fi 506 | fi 507 | 508 | printf "%b\n" "$row2" 509 | fi 510 | fi 511 | 512 | # ============================================ 513 | # ROW 3: Model Limits + Context 514 | # ============================================ 515 | row3="" 516 | has_row3=false 517 | 518 | # Model-specific limits (only if 7d data exists) 519 | if [ -n "$usage_data" ]; then 520 | read -r five_hour seven_day seven_day_opus seven_day_sonnet five_hour_resets seven_day_resets <<< "$usage_data" 521 | 522 | if [ "$seven_day" != "null" ] && [ -n "$seven_day" ]; then 523 | has_row3=true 524 | # Opus uses overall 7d 525 | opus_int=${seven_day%.*} 526 | 527 | # Sonnet: use dedicated limit if available 528 | if [ "$seven_day_sonnet" != "null" ] && [ -n "$seven_day_sonnet" ]; then 529 | sonnet_int=${seven_day_sonnet%.*} 530 | else 531 | sonnet_int=${seven_day%.*} 532 | fi 533 | 534 | row3=$(pl_segment $C_MAGENTA $C_WHITE "Opus ${opus_int}%" $C_BLUE) 535 | # Check if context will follow (peek ahead) 536 | ctx_check=$(format_context) 537 | if [ -n "$ctx_check" ]; then 538 | row3="${row3}$(pl_segment $C_BLUE $C_WHITE "Sonnet ${sonnet_int}%" $C_GRAY)" 539 | else 540 | row3="${row3}$(pl_segment_end $C_BLUE $C_WHITE "Sonnet ${sonnet_int}%")" 541 | fi 542 | fi 543 | fi 544 | 545 | # Context usage 546 | context_result=$(format_context) 547 | if [ -n "$context_result" ]; then 548 | # Extract just the values from the formatted context 549 | ctx_tokens=$(echo "$context_result" | sed 's/CTX: //' | awk '{print $1}') 550 | ctx_pct=$(echo "$context_result" | grep -oE '[0-9]+%') 551 | ctx_warning="" 552 | if echo "$context_result" | grep -q "⚠️"; then 553 | ctx_warning=" ⚠️" 554 | fi 555 | 556 | if [ "$has_row3" = true ]; then 557 | row3="${row3}$(pl_segment_end $C_GRAY $C_WHITE "CTX ${ctx_tokens} ${ctx_pct}${ctx_warning}")" 558 | else 559 | has_row3=true 560 | row3=$(pl_segment_end $C_GRAY $C_WHITE "CTX ${ctx_tokens} ${ctx_pct}${ctx_warning}") 561 | fi 562 | fi 563 | 564 | if [ "$has_row3" = true ]; then 565 | printf "%b\n" "$row3" 566 | fi 567 | --------------------------------------------------------------------------------