Last active
January 24, 2026 22:11
-
-
Save benabraham/dfa45406d7472827b75d16fed4d8e39c to your computer and use it in GitHub Desktop.
Simple Claude Code status line with context usage progress bar (dark/light themes, truecolor/256 fallback)
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
| #!/usr/bin/env python3 | |
| """ | |
| Simple Claude Code StatusLine Script | |
| Shows context usage/progress with colored bar | |
| Uses current_usage field for accurate context window calculations | |
| INSTALLATION: | |
| 1. Save this file to ~/.claude/simple-statusline.py | |
| 2. Make it executable: | |
| chmod +x ~/.claude/simple-statusline.py | |
| 3. Add to ~/.claude/settings.json: | |
| { | |
| "statusLine": { | |
| "type": "command", | |
| "command": "~/.claude/simple-statusline.py" | |
| } | |
| } | |
| 4. Set THEME below to 'dark' or 'light' | |
| 5. Restart Claude Code | |
| Note: After initial setup, edits to this script take effect immediately (no restart needed). | |
| Latest version: https://gist.github.com/3728d8efd4dd62ad82b22e0e286e85d0 | |
| """ | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| import time | |
| from datetime import datetime | |
| # ============================================================================= | |
| # CONFIGURATION | |
| # ============================================================================= | |
| # Progress bar width in characters | |
| BAR_WIDTH = 33 | |
| # Active theme: 'dark' or 'light' (must be set) | |
| THEME = "dark" | |
| # Color format: ("#RRGGBB", fallback_256) | |
| # Set hex to None to always use 256 fallback | |
| THEMES = { | |
| "dark": { | |
| # Model badge colors: (bg, fg) | |
| "model_sonnet": (("#A3BE8C", 108), ("#2E3440", 236)), # nord14 bg, nord0 fg | |
| "model_opus": (("#88C0D0", 110), ("#2E3440", 236)), # nord8 bg, nord0 fg | |
| "model_haiku": (("#4C566A", 60), ("#ECEFF4", 255)), # nord3 bg, nord6 fg | |
| "model_default": (("#D8DEE9", 253), ("#2E3440", 236)), # nord4 bg, nord0 fg | |
| # Unused portion of progress bar | |
| "bar_empty": ("#292c33", 234), # darker than nord0 | |
| # Text colors (Nord) | |
| "text_percent": (("#5E81AC", None), 67), # nord10 | |
| "text_numbers": (("#5E81AC", None), 67), # nord10 | |
| "text_cwd": (("#81A1C1", None), 110), # nord9 | |
| "text_git": (("#B48EAD", None), 139), # nord15 purple | |
| "text_na": (("#D08770", None), 173), # nord12 orange | |
| # Usage indicator colors (ratio-based) | |
| "usage_green": ("#A3BE8C", 108), # nord14 - on track | |
| "usage_yellow": ("#EBCB8B", 222), # nord13 - using faster | |
| "usage_red": ("#BF616A", 131), # nord11 - burning through | |
| # Progress bar gradient: (threshold, (hex, fallback_256)) | |
| # Threshold means "use this color if pct < threshold" | |
| "gradient": [ | |
| (10, ("#183522", 22)), # 0-9% dark green | |
| (20, ("#153E21", 22)), # 10-19% | |
| (30, ("#104620", 28)), # 20-29% | |
| (40, ("#0B4E1C", 28)), # 30-39% | |
| (50, ("#065716", 34)), # 40-49% bright green | |
| (60, ("#2E5900", 106)), # 50-59% yellow-green | |
| (70, ("#5D4F00", 136)), # 60-69% olive | |
| (80, ("#833A00", 166)), # 70-79% orange | |
| (90, ("#A10700", 160)), # 80-89% red-orange | |
| (101, ("#B30000", 196)), # 90-100% red | |
| ], | |
| }, | |
| "light": { | |
| # Model badge colors: (bg, fg) | |
| "model_sonnet": ( | |
| ("#8FAA78", 107), | |
| ("#FFFFFF", 231), | |
| ), # muted green bg, white fg | |
| "model_opus": (("#6AA2B2", 73), ("#FFFFFF", 231)), # muted aqua bg, white fg | |
| "model_haiku": (("#8C96AA", 103), ("#FFFFFF", 231)), # muted grey bg, white fg | |
| "model_default": (("#646E82", 66), ("#FFFFFF", 231)), # slate bg, white fg | |
| # Unused portion of progress bar | |
| "bar_empty": ("#D8DEE9", 253), # nord4 | |
| # Text colors | |
| "text_percent": (("#505050", None), 240), # dark grey | |
| "text_numbers": (("#505050", None), 240), # dark grey | |
| "text_cwd": (("#3C465A", None), 238), # dark slate | |
| "text_git": (("#508C50", None), 65), # muted green | |
| "text_na": (("#D08770", None), 173), # nord12 orange | |
| # Usage indicator colors (ratio-based) - darker for light bg | |
| "usage_green": ("#4A7C4A", 65), # dark green - on track | |
| "usage_yellow": ("#9A7B00", 136), # dark yellow/gold - using faster | |
| "usage_red": ("#A03030", 124), # dark red - burning through | |
| # Progress bar gradient | |
| "gradient": [ | |
| (10, ("#22783C", 29)), # 0-9% green | |
| (20, ("#228237", 29)), # 10-19% | |
| (30, ("#228C32", 35)), # 20-29% | |
| (40, ("#329628", 35)), # 30-39% | |
| (50, ("#46A01E", 70)), # 40-49% | |
| (60, ("#828C00", 142)), # 50-59% yellow-green | |
| (70, ("#A08200", 178)), # 60-69% olive/yellow | |
| (80, ("#B46400", 172)), # 70-79% orange | |
| (90, ("#C83C00", 166)), # 80-89% red-orange | |
| (101, ("#D21E1E", 160)), # 90-100% red | |
| ], | |
| }, | |
| } | |
| # ============================================================================= | |
| # COLOR SUPPORT DETECTION & CONVERSION | |
| # ============================================================================= | |
| def hex_to_rgb(hex_color): | |
| """Convert '#RRGGBB' hex string to (R, G, B) tuple""" | |
| if hex_color is None: | |
| return None | |
| h = hex_color.lstrip("#") | |
| return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) | |
| def supports_truecolor(): | |
| """Detect if terminal supports 24-bit true color""" | |
| colorterm = os.environ.get("COLORTERM", "").lower() | |
| return colorterm in ("truecolor", "24bit") | |
| TRUECOLOR = supports_truecolor() | |
| # ============================================================================= | |
| # ANSI ESCAPE HELPERS | |
| # ============================================================================= | |
| RESET = "\033[0m" | |
| BOLD = "\033[1m" | |
| def _color(rgb, fallback_256, is_bg=False): | |
| """Generate ANSI color code with truecolor/256 fallback. RGB can be hex string or tuple.""" | |
| prefix = 48 if is_bg else 38 | |
| if TRUECOLOR and rgb is not None: | |
| if isinstance(rgb, str): | |
| rgb = hex_to_rgb(rgb) | |
| return f"\033[{prefix};2;{rgb[0]};{rgb[1]};{rgb[2]}m" | |
| else: | |
| return f"\033[{prefix};5;{fallback_256}m" | |
| def fg_themed(color_tuple): | |
| """Foreground color from theme tuple ((rgb, _), fallback) or ((rgb, fallback), _)""" | |
| if isinstance(color_tuple[0], tuple): | |
| rgb, fallback = color_tuple[0] | |
| if fallback is None: | |
| fallback = color_tuple[1] | |
| else: | |
| rgb, fallback = color_tuple | |
| return _color(rgb, fallback, is_bg=False) | |
| def bg_themed(color_tuple): | |
| """Background color from theme tuple ((rgb, fallback), _)""" | |
| rgb, fallback = color_tuple[0] | |
| return _color(rgb, fallback, is_bg=True) | |
| def fg_gradient(rgb, fallback_256): | |
| """Foreground from gradient tuple""" | |
| return _color(rgb, fallback_256, is_bg=False) | |
| def fg_empty(): | |
| """Foreground for empty bar portion""" | |
| theme = THEMES[THEME] | |
| rgb, fallback = theme["bar_empty"] | |
| return _color(rgb, fallback, is_bg=False) | |
| # ============================================================================= | |
| # THEME-AWARE COLOR FUNCTIONS | |
| # ============================================================================= | |
| def get_colors_for_percentage(pct): | |
| """Return (rgb, fallback_256) for progress bar fill at given percentage""" | |
| theme = THEMES[THEME] | |
| for threshold, color in theme["gradient"]: | |
| if pct < threshold: | |
| return color | |
| return theme["gradient"][-1][1] | |
| def get_model_colors(model): | |
| """Return (bg_code, fg_code) for model badge""" | |
| theme = THEMES[THEME] | |
| if "Sonnet" in model: | |
| key = "model_sonnet" | |
| elif "Opus" in model: | |
| key = "model_opus" | |
| elif "Haiku" in model: | |
| key = "model_haiku" | |
| else: | |
| key = "model_default" | |
| bg_tuple, fg_tuple = theme[key] | |
| bg_code = _color(bg_tuple[0], bg_tuple[1], is_bg=True) | |
| fg_code = _color(fg_tuple[0], fg_tuple[1], is_bg=False) | |
| return bg_code + BOLD + fg_code | |
| def text_color(key): | |
| """Get text color by key: 'percent', 'numbers', 'cwd', 'git'""" | |
| theme = THEMES[THEME] | |
| color_tuple = theme[f"text_{key}"] | |
| return fg_themed(color_tuple) | |
| # ============================================================================= | |
| # UTILITY FUNCTIONS | |
| # ============================================================================= | |
| def center_text(text, min_width=12): | |
| """Center text with 1-char padding on each side, minimum 12 chars wide""" | |
| width = max(min_width, len(text) + 2) | |
| padding = (width - len(text)) // 2 | |
| right_padding = width - len(text) - padding | |
| return " " * padding + text + " " * right_padding | |
| def get_git_branch(cwd): | |
| """Get current git branch, or None""" | |
| try: | |
| result = subprocess.run( | |
| ["git", "-C", cwd, "branch", "--show-current"], | |
| capture_output=True, | |
| text=True, | |
| timeout=1, | |
| ) | |
| if result.returncode == 0: | |
| return result.stdout.strip() or None | |
| except Exception: | |
| pass | |
| return None | |
| def get_cwd_suffix(cwd): | |
| """Format cwd and git branch for display""" | |
| if not cwd: | |
| return "" | |
| # Shorten home directory to ~ | |
| home = os.path.expanduser("~") | |
| if cwd.startswith(home): | |
| cwd_short = "~" + cwd[len(home) :] | |
| else: | |
| cwd_short = cwd | |
| suffix = f" {text_color('cwd')}{cwd_short}" | |
| git_branch = get_git_branch(cwd) | |
| if git_branch: | |
| suffix += f" {BOLD}{text_color('git')}[{git_branch}]" | |
| return suffix | |
| # ============================================================================= | |
| # TRANSCRIPT PARSING (for comparison with API - remove when bug #13783 is fixed) | |
| # ============================================================================= | |
| def get_tokens_from_transcript(transcript_path): | |
| """Parse JSONL transcript for accurate context tokens.""" | |
| if not transcript_path or not os.path.exists(transcript_path): | |
| return None | |
| latest_usage = None | |
| latest_timestamp = None | |
| total_output_tokens = 0 | |
| try: | |
| with open(transcript_path, 'r') as f: | |
| for line in f: | |
| try: | |
| entry = json.loads(line) | |
| if entry.get('isSidechain') or entry.get('isApiErrorMessage'): | |
| continue | |
| usage = entry.get('message', {}).get('usage') | |
| timestamp = entry.get('timestamp') | |
| if usage and timestamp: | |
| total_output_tokens += usage.get('output_tokens', 0) | |
| if latest_timestamp is None or timestamp > latest_timestamp: | |
| latest_timestamp = timestamp | |
| latest_usage = usage | |
| except json.JSONDecodeError: | |
| continue | |
| except (IOError, OSError): | |
| return None | |
| if latest_usage: | |
| return ( | |
| latest_usage.get('input_tokens', 0) | |
| + latest_usage.get('cache_read_input_tokens', 0) | |
| + latest_usage.get('cache_creation_input_tokens', 0) | |
| + total_output_tokens | |
| ) | |
| return None | |
| # ============================================================================= | |
| # USAGE LIMITS API | |
| # ============================================================================= | |
| USAGE_CACHE_PATH = os.path.expanduser("~/.claude/.usage_cache.json") | |
| USAGE_CACHE_DURATION = 300 # 5 minutes | |
| CREDENTIALS_PATH = os.path.expanduser("~/.claude/.credentials.json") | |
| def get_oauth_token(): | |
| """Get OAuth access token from macOS Keychain or credentials file.""" | |
| # On macOS, try Keychain first | |
| if sys.platform == 'darwin': | |
| try: | |
| result = subprocess.run( | |
| ['security', 'find-generic-password', '-s', 'Claude Code-credentials', '-w'], | |
| capture_output=True, | |
| text=True, | |
| timeout=5, | |
| ) | |
| if result.returncode == 0 and result.stdout.strip(): | |
| creds = json.loads(result.stdout.strip()) | |
| return creds.get('claudeAiOauth', {}).get('accessToken') | |
| except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError): | |
| pass | |
| # Fallback to credentials file (Linux, Windows, or if Keychain fails) | |
| try: | |
| with open(CREDENTIALS_PATH, 'r') as f: | |
| creds = json.load(f) | |
| return creds.get('claudeAiOauth', {}).get('accessToken') | |
| except (IOError, json.JSONDecodeError, KeyError): | |
| return None | |
| def fetch_usage_data(): | |
| """Fetch usage data from Anthropic OAuth API, with caching.""" | |
| # Check cache first | |
| try: | |
| if os.path.exists(USAGE_CACHE_PATH): | |
| with open(USAGE_CACHE_PATH, 'r') as f: | |
| cache = json.load(f) | |
| if time.time() - cache.get('timestamp', 0) < USAGE_CACHE_DURATION: | |
| return cache.get('data') | |
| except (IOError, json.JSONDecodeError): | |
| pass | |
| # Get OAuth token | |
| token = get_oauth_token() | |
| if not token: | |
| return None | |
| # Fetch from API using curl | |
| try: | |
| result = subprocess.run( | |
| [ | |
| 'curl', '-s', '-f', | |
| '-H', 'Accept: application/json', | |
| '-H', 'Content-Type: application/json', | |
| '-H', 'User-Agent: claude-code/2.0.32', | |
| '-H', 'anthropic-beta: oauth-2025-04-20', | |
| '-H', f'Authorization: Bearer {token}', | |
| 'https://api.anthropic.com/api/oauth/usage' | |
| ], | |
| capture_output=True, | |
| text=True, | |
| timeout=5, | |
| ) | |
| if result.returncode != 0: | |
| return None | |
| data = json.loads(result.stdout) | |
| # Cache the result | |
| try: | |
| with open(USAGE_CACHE_PATH, 'w') as f: | |
| json.dump({'timestamp': time.time(), 'data': data}, f) | |
| except IOError: | |
| pass | |
| return data | |
| except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError): | |
| return None | |
| def get_usage_color(ratio): | |
| """Get foreground color for usage indicator based on time/usage ratio.""" | |
| theme = THEMES[THEME] | |
| if ratio >= 1.0: | |
| rgb, fallback = theme["usage_green"] | |
| elif ratio >= 0.5: | |
| rgb, fallback = theme["usage_yellow"] | |
| else: | |
| rgb, fallback = theme["usage_red"] | |
| return _color(rgb, fallback, is_bg=False) | |
| def get_usage_gauge(ratio): | |
| """Get gauge character showing usage vs time ratio. | |
| - ratio > 1 (ahead): fills from TOP with green, using BG/FG trick | |
| - ratio < 1 (behind): fills from BOTTOM with yellow/red, FG only | |
| """ | |
| theme = THEMES[THEME] | |
| gauges = "▁▂▃▄▅▆▇█" # fills from bottom | |
| if ratio >= 1.0: | |
| # Ahead - show how much ahead, filling from top (green) | |
| # Use BG = green, FG = empty to create top-fill illusion | |
| ahead = min(1.0, ratio - 1.0) # 0 = exactly on track, 1 = way ahead | |
| empty_rgb, empty_fb = theme["bar_empty"] | |
| green_rgb, green_fb = theme["usage_green"] | |
| # Invert gauge for top-fill: more ahead = more visible from top | |
| index = int(ahead * 7.99) | |
| index = max(0, min(7, index)) | |
| # Use █ (full FG) when minimal - shows dark (empty), not green | |
| char = gauges[7 - index] if index > 0 else "█" | |
| bg = _color(green_rgb, green_fb, is_bg=True) | |
| fg = _color(empty_rgb, empty_fb, is_bg=False) | |
| return f"{bg}{fg}{char}{RESET}" | |
| else: | |
| # Behind - show how much behind, filling from bottom (yellow/red) | |
| behind = min(1.0, 1.0 - ratio) # 0 = on track, 1 = critical | |
| empty_rgb, empty_fb = theme["bar_empty"] | |
| if ratio >= 0.5: | |
| warn_rgb, warn_fb = theme["usage_yellow"] | |
| else: | |
| warn_rgb, warn_fb = theme["usage_red"] | |
| index = int(behind * 7.99) | |
| index = max(0, min(7, index)) | |
| # Use space if index is 0 (too small to show) | |
| char = gauges[index] if index > 0 else " " | |
| bg = _color(empty_rgb, empty_fb, is_bg=True) | |
| fg = _color(warn_rgb, warn_fb, is_bg=False) | |
| return f"{bg}{fg}{char}{RESET}" | |
| def format_usage_indicator(usage_data): | |
| """Format usage indicator for status line.""" | |
| if usage_data is None: | |
| return f" {text_color('na')}usage: N/A" | |
| if not usage_data: | |
| return "" | |
| indicators = [] | |
| # Define limit types to process: (key, window_hours, time_format) | |
| # Use NBSP (\u00a0) between day and time for weekly | |
| limit_configs = [ | |
| ('five_hour', 5, '%H:%M'), # Session: show time | |
| ('seven_day', 7 * 24, '%a\u00a0%H:%M'), # Weekly: show day+time | |
| ] | |
| for key, window_hours, time_fmt in limit_configs: | |
| limit = usage_data.get(key) | |
| if not limit: | |
| continue | |
| utilization_pct = limit.get('utilization', 0) # 0-100 percentage | |
| resets_at = limit.get('resets_at') | |
| if not resets_at: | |
| continue | |
| # Parse reset time | |
| try: | |
| reset_dt = datetime.fromisoformat(resets_at.replace('Z', '+00:00')) | |
| except ValueError: | |
| continue | |
| now = datetime.now(reset_dt.tzinfo) | |
| remaining_pct = max(0, int(100 - utilization_pct)) | |
| reset_label = reset_dt.astimezone().strftime(time_fmt) | |
| # Calculate time elapsed in window | |
| window_seconds = window_hours * 3600 | |
| window_start = reset_dt.timestamp() - window_seconds | |
| elapsed_seconds = now.timestamp() - window_start | |
| time_elapsed_pct = max(0, min(100, (elapsed_seconds / window_seconds) * 100)) | |
| # Calculate ratio: time_elapsed / utilization | |
| # Higher ratio = good (more time than usage) | |
| # Lower ratio = bad (more usage than time) | |
| if utilization_pct > 0: | |
| ratio = time_elapsed_pct / utilization_pct | |
| else: | |
| ratio = 1.0 # No usage = perfect | |
| color = get_usage_color(ratio) | |
| gauge = get_usage_gauge(ratio) | |
| gauge_part = f"{gauge}\u00a0" if gauge else "" | |
| indicators.append(f"{gauge_part}{color}{remaining_pct}\u00a0%\u00a0→\u00a0{reset_label}") | |
| if not indicators: | |
| return "" | |
| return f" {'\u00a0\u00a0'.join(indicators)}" | |
| # ============================================================================= | |
| # MAIN STATUS LINE BUILDER | |
| # ============================================================================= | |
| def build_progress_bar(pct, model, cwd, total_tokens, context_limit, transcript_tokens=None, calc_pct=None, usage_indicator=""): | |
| """Build the full status line string""" | |
| bar_length = BAR_WIDTH | |
| filled = round(pct * BAR_WIDTH / 100) | |
| empty = bar_length - filled | |
| bar_rgb, bar_256 = get_colors_for_percentage(pct) | |
| model_color = get_model_colors(model) | |
| # Build comparison suffix - only show if values differ by more than 10% | |
| comparisons = [] | |
| show_comparison = False | |
| if transcript_tokens is not None and total_tokens is not None and total_tokens > 0: | |
| diff_pct = abs(transcript_tokens - total_tokens) / total_tokens * 100 | |
| if diff_pct > 10: | |
| comparisons.append(f"{transcript_tokens // 1000}k") | |
| show_comparison = True | |
| if calc_pct is not None and pct > 0: | |
| diff_pct = abs(calc_pct - pct) / pct * 100 | |
| if diff_pct > 10: | |
| comparisons.append(f"{calc_pct}\u00a0%") | |
| show_comparison = True | |
| if show_comparison and comparisons: | |
| theme = THEMES[THEME] | |
| red_rgb, red_fb = theme["usage_red"] | |
| red_color = _color(red_rgb, red_fb, is_bg=False) | |
| comparison = f"{red_color}\u00a0{{{'\u00a0'.join(comparisons)}}}" | |
| else: | |
| comparison = "" | |
| # Token display (may be None if only API percentage available) | |
| numbers_color = text_color('numbers') | |
| if total_tokens is not None: | |
| token_display = f"{numbers_color}\u00a0({total_tokens // 1000}k/{context_limit // 1000}k)" | |
| else: | |
| token_display = f"{numbers_color}\u00a0(--/{context_limit // 1000}k)" | |
| parts = [ | |
| model_color + center_text(model) + RESET, | |
| fg_gradient(bar_rgb, bar_256) + "█" * filled, | |
| fg_empty() + "█" * empty, | |
| RESET + text_color("percent"), | |
| f" {pct}\u00a0%", | |
| token_display, | |
| comparison, | |
| get_cwd_suffix(cwd), | |
| usage_indicator, | |
| RESET, | |
| ] | |
| return "".join(parts) | |
| def build_na_line(model, cwd): | |
| """Build status line when no usage data available""" | |
| model_color = get_model_colors(model) | |
| suffix = get_cwd_suffix(cwd) | |
| return f"{model_color}{center_text(model)}{RESET} {text_color('na')} context size N/A{suffix}{RESET}" | |
| # ============================================================================= | |
| # DEMO MODE | |
| # ============================================================================= | |
| def show_usage_demo(): | |
| """Demo mode to show usage indicator with mock data""" | |
| from datetime import timedelta | |
| now = datetime.now().astimezone() | |
| # Create mock usage data with different scenarios (utilization is 0-100%) | |
| # Color is based on ratio = time_elapsed / utilization | |
| # ratio >= 0.9: green, 0.5-0.9: orange, < 0.5: red | |
| scenarios = [ | |
| ("Green - on track (ratio ~1.0)", { | |
| # 20% used, 1h elapsed of 5h = 20% time -> ratio = 1.0 | |
| "five_hour": {"utilization": 20, "resets_at": (now + timedelta(hours=4)).isoformat()}, | |
| # 14% used, 1d elapsed of 7d = 14% time -> ratio = 1.0 | |
| "seven_day": {"utilization": 14, "resets_at": (now + timedelta(days=6)).isoformat()}, | |
| }), | |
| ("Yellow - using faster (ratio ~0.7)", { | |
| # 50% used, 2h elapsed of 5h = 40% time -> ratio = 0.8 | |
| "five_hour": {"utilization": 50, "resets_at": (now + timedelta(hours=3)).isoformat()}, | |
| "seven_day": {"utilization": 14, "resets_at": (now + timedelta(days=6)).isoformat()}, | |
| }), | |
| ("Red - burning through it (ratio < 0.5)", { | |
| # 80% used, 2h elapsed of 5h = 40% time -> ratio = 0.5 | |
| "five_hour": {"utilization": 80, "resets_at": (now + timedelta(hours=3)).isoformat()}, | |
| # 60% used, 2d elapsed of 7d = 28% time -> ratio = 0.47 | |
| "seven_day": {"utilization": 60, "resets_at": (now + timedelta(days=5)).isoformat()}, | |
| }), | |
| ] | |
| print("Usage Indicator Demo (color based on time/usage ratio):") | |
| print("=" * 60) | |
| print(" ratio >= 1.0: green | 0.5-1.0: yellow | < 0.5: red") | |
| print() | |
| for name, mock_data in scenarios: | |
| indicator = format_usage_indicator(mock_data) | |
| print(f"{name}:") | |
| print(f" {indicator}{RESET}") | |
| print() | |
| def show_scale_demo(mode="animate"): | |
| """Demo mode to show color gradient""" | |
| def show_bar(pct): | |
| bar_length = BAR_WIDTH | |
| filled = round(pct * BAR_WIDTH / 100) | |
| empty = bar_length - filled | |
| bar_rgb, bar_256 = get_colors_for_percentage(pct) | |
| bar = ( | |
| fg_gradient(bar_rgb, bar_256) | |
| + "█" * filled | |
| + fg_empty() | |
| + "█" * empty | |
| + RESET | |
| ) | |
| return bar | |
| if mode == "animate": | |
| try: | |
| while True: | |
| for pct in range(101): | |
| print(f"\r{pct:3d}%: {show_bar(pct)}", end="", flush=True) | |
| time.sleep(0.1) | |
| time.sleep(0.5) | |
| except KeyboardInterrupt: | |
| print() | |
| elif mode in ("min", "max", "mid"): | |
| ranges = [ | |
| (0, 9), | |
| (10, 19), | |
| (20, 29), | |
| (30, 39), | |
| (40, 49), | |
| (50, 59), | |
| (60, 69), | |
| (70, 79), | |
| (80, 89), | |
| (90, 100), | |
| ] | |
| print(f"Color Scale Demo ({mode} value):") | |
| print() | |
| for lo, hi in ranges: | |
| pct = lo if mode == "min" else hi if mode == "max" else (lo + hi) // 2 | |
| print(f"{lo:3d}-{hi:3d}%: {show_bar(pct)}") | |
| else: | |
| print(f"Error: Invalid mode '{mode}'. Use: min, max, mid, or animate") | |
| sys.exit(1) | |
| # ============================================================================= | |
| # MAIN | |
| # ============================================================================= | |
| def main(): | |
| # Check theme is configured | |
| if THEME not in THEMES: | |
| # Yellow text on red bg, then red text on yellow bg | |
| print( | |
| f"\033[48;5;196m\033[38;5;220m\033[1m PLEASE SET THEME to 'dark' or 'light' in simple-statusline.py \033[0m" | |
| ) | |
| print( | |
| f"\033[48;5;220m\033[38;5;196m\033[1m PLEASE SET THEME to 'dark' or 'light' in simple-statusline.py \033[0m" | |
| ) | |
| return | |
| # Handle demo modes | |
| if len(sys.argv) > 1: | |
| if sys.argv[1] == "--show-scale": | |
| show_scale_demo(sys.argv[2] if len(sys.argv) > 2 else "animate") | |
| return | |
| if sys.argv[1] == "--test-usage": | |
| show_usage_demo() | |
| return | |
| # Read and parse JSON input | |
| try: | |
| data = json.load(sys.stdin) | |
| except json.JSONDecodeError: | |
| print("statusline: invalid JSON input", file=sys.stderr) | |
| return | |
| model = data.get("model", {}).get("display_name", "Claude") | |
| cwd = data.get("cwd", "") | |
| # Get context window info | |
| context_window = data.get("context_window", {}) | |
| context_limit = context_window.get("context_window_size", 200000) | |
| used_percentage = context_window.get("used_percentage") | |
| current_usage = context_window.get("current_usage") | |
| # Calculate total tokens for display purposes | |
| if current_usage: | |
| total_tokens = ( | |
| current_usage.get("input_tokens", 0) | |
| + current_usage.get("cache_creation_input_tokens", 0) | |
| + current_usage.get("cache_read_input_tokens", 0) | |
| + current_usage.get("output_tokens", 0) | |
| ) | |
| else: | |
| total_tokens = None | |
| # Calculate percentage from tokens (for comparison) | |
| if total_tokens and total_tokens > 0: | |
| calc_pct = min(100, int(total_tokens * 100 / context_limit)) | |
| else: | |
| calc_pct = None | |
| # Use API percentage, fall back to calculated | |
| if used_percentage is not None: | |
| pct = int(used_percentage) | |
| elif calc_pct is not None: | |
| pct = calc_pct | |
| else: | |
| print(build_na_line(model, cwd)) | |
| return | |
| # Get transcript tokens for comparison | |
| transcript_path = data.get("transcript_path") | |
| transcript_tokens = get_tokens_from_transcript(transcript_path) | |
| # Get usage limits indicator | |
| usage_data = fetch_usage_data() | |
| usage_indicator = format_usage_indicator(usage_data) | |
| print(build_progress_bar(pct, model, cwd, total_tokens, context_limit, transcript_tokens, calc_pct, usage_indicator)) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment