Skip to content

Instantly share code, notes, and snippets.

@davesque
Last active March 2, 2026 11:48
Show Gist options
  • Select an option

  • Save davesque/6137cd1ba5cd64c0b5ca178569f8e353 to your computer and use it in GitHub Desktop.

Select an option

Save davesque/6137cd1ba5cd64c0b5ca178569f8e353 to your computer and use it in GitHub Desktop.
Claude Code status line — two-line layout with context, quota, and cost bars
#!/usr/bin/env python3
"""Claude Code status line — two-row, three-column box.
Row 1: Model/Dir/Duration │ Cost (burn rate) │ Token velocity
Row 2: Context bar (used/tot) │ 5h quota bar ⟳reset │ 7d quota bar ⟳reset
Rendered inside a box-drawing border (┌─┬─┐ │ └─┴─┘) with centered cells,
color-coded progress bars, and hot pink pacing markers.
"""
import json
import re
import subprocess
import sys
import time
import urllib.request
from datetime import datetime
from pathlib import Path
# ---------------------------------------------------------------------------
# ANSI helpers
# ---------------------------------------------------------------------------
RESET = "\033[0m"
DIM = "\033[2m"
GRAY = "\033[38;5;242m"
WHITE = "\033[38;5;255m"
CYAN = "\033[96m"
HOT_PINK = "\033[38;5;199m"
# Subdued bar colors (non-bold, 256-color)
BAR_GREEN = "\033[38;5;65m"
BAR_YELLOW = "\033[38;5;137m"
BAR_RED = "\033[38;5;131m"
ANSI_RE = re.compile(r"\033\[[0-9;]*m")
def dim(s: str) -> str:
return f"{DIM}{s}{RESET}"
def visible_len(s: str) -> int:
"""Length of a string after stripping ANSI escape codes."""
return len(ANSI_RE.sub("", s))
def render_grid(rows: list[list[str]]) -> str:
"""Render rows inside a box-drawing border with centered cells.
Each row is a list of cell strings (may contain ANSI codes).
Columns are padded to the max visible width across all rows,
then enclosed in a ┌─┬─┐ / │ │ │ / └─┴─┘ box.
"""
if not rows:
return ""
num_cols = max(len(r) for r in rows)
# Find max visible width for each column
col_widths = [0] * num_cols
for row in rows:
for c, cell in enumerate(row):
col_widths[c] = max(col_widths[c], visible_len(cell))
lines: list[str] = []
pipe = f"{DIM}{GRAY}│{RESET}"
sep = f" {pipe} "
def rule(left_corner: str, mid: str, right_corner: str) -> str:
parts = []
for c in range(num_cols):
parts.append("─" * (col_widths[c] + 2))
if c < num_cols - 1:
parts.append(mid)
return f"{DIM}{GRAY}{left_corner}{''.join(parts)}{right_corner}{RESET}"
lines.append(rule("┌", "┬", "┐"))
for row in rows:
parts: list[str] = []
for c in range(num_cols):
cell = row[c] if c < len(row) else ""
pad = col_widths[c] - visible_len(cell)
left = pad // 2
right = pad - left
parts.append(" " * left + cell + " " * right)
lines.append(f"{pipe} " + sep.join(parts) + f" {pipe}")
lines.append(rule("└", "┴", "┘"))
return "\n".join(lines)
def pct_color(
pct: float, green_thresh: int = 50, yellow_thresh: int = 80,
) -> str:
"""Return ANSI color code for a usage percentage."""
p = int(round(pct))
if p < green_thresh:
return BAR_GREEN
elif p < yellow_thresh:
return BAR_YELLOW
else:
return BAR_RED
# ---------------------------------------------------------------------------
# Formatting helpers
# ---------------------------------------------------------------------------
def format_k(val: int) -> str:
return f"{val // 1000}k" if val >= 1000 else str(val)
def format_tok(val: int) -> str:
sign = "+" if val >= 0 else ""
if abs(val) >= 1000:
return f"{sign}{val / 1000:.1f}k"
return f"{sign}{val}"
def format_ema(val: float) -> str:
if abs(val) >= 1000:
return f"{val / 1000:.1f}k"
return f"{int(round(val))}"
def format_duration(ms: float) -> str:
s = int(ms) // 1000
h, s = divmod(s, 3600)
m, s = divmod(s, 60)
if h > 0:
return f"{h}h{m}m"
if m > 0:
return f"{m}m{s}s"
return f"{s}s"
# ---------------------------------------------------------------------------
# Progress bar
# ---------------------------------------------------------------------------
def build_bar(
pct: float,
width: int = 20,
target_pct: float | None = None,
green_thresh: int = 50,
yellow_thresh: int = 80,
) -> str:
"""Build a colored progress bar string.
If target_pct is given, a hot pink │ pacing marker is placed at that
position in the bar.
"""
used_int = int(round(pct))
filled = min(used_int * width // 100, width)
bar_color = pct_color(pct, green_thresh, yellow_thresh)
target_pos = -1
if target_pct is not None:
target_pos = int(round(target_pct)) * width // 100
target_pos = max(0, min(target_pos, width - 1))
# Build in contiguous color segments to minimize ANSI codes.
segments: list[str] = []
i = 0
while i < width:
if i == target_pos:
segments.append(f"{RESET}{HOT_PINK}│")
i += 1
elif i < filled:
# Run of filled chars until target marker or end of filled section
end = target_pos if i < target_pos < filled else filled
segments.append(f"{bar_color}" + "█" * (end - i))
i = end
else:
# Run of empty chars until target marker or end
end = target_pos if target_pos > i else width
segments.append(f"{DIM}{GRAY}" + "░" * (end - i))
i = end
return "".join(segments) + RESET
# ---------------------------------------------------------------------------
# Working directory
# ---------------------------------------------------------------------------
def shorten_dir(path: str) -> str:
home = str(Path.home())
if path.startswith(home):
path = "~" + path[len(home):]
if path.startswith("~/"):
parts = path[2:].split("/")
if len(parts) > 2:
path = "~/…/" + "/".join(parts[-2:])
return path
# ---------------------------------------------------------------------------
# Token velocity (EMA persisted per session)
# ---------------------------------------------------------------------------
EMA_ALPHA = 2 / 9 # N=8 turns
def update_velocity(session_id: str, total_tokens: int) -> tuple[int, float]:
"""Update EMA state and return (delta, ema)."""
state_dir = Path.home() / ".claude"
state_file = state_dir / f"statusline-state-{session_id}.json"
prev_total = 0
prev_ema = 0.0
turn = 0
try:
state = json.loads(state_file.read_text())
prev_total = state.get("total_tokens", 0)
prev_ema = state.get("ema", 0.0)
turn = state.get("turn", 0)
except (FileNotFoundError, json.JSONDecodeError, OSError):
pass
turn += 1
delta = total_tokens - prev_total
ema = float(delta) if turn <= 1 else EMA_ALPHA * delta + (1 - EMA_ALPHA) * prev_ema
try:
state_file.write_text(
json.dumps({"turn": turn, "total_tokens": total_tokens, "ema": round(ema, 1)})
+ "\n"
)
except OSError:
pass
# Prune stale state files (>24h) — only on first turn to avoid repeated scans
if turn == 1:
try:
cutoff = time.time() - 86400
for f in state_dir.glob("statusline-state-*.json"):
if f != state_file and f.stat().st_mtime < cutoff:
f.unlink(missing_ok=True)
except OSError:
pass
return delta, ema
# ---------------------------------------------------------------------------
# Usage quota (5-hour and 7-day rolling windows)
# ---------------------------------------------------------------------------
USAGE_CACHE = Path("/tmp/claude-statusline-usage.json")
USAGE_CACHE_AGE = 60 # seconds
def get_oauth_token() -> str | None:
"""Read OAuth access token from keychain (macOS) or credentials file."""
# macOS keychain
try:
result = subprocess.run(
["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
capture_output=True, text=True, timeout=3,
)
if result.returncode == 0:
creds = json.loads(result.stdout.strip())
token = creds.get("claudeAiOauth", {}).get("accessToken")
if token:
return token
except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError, OSError):
pass
# Linux/WSL credentials file
creds_file = Path.home() / ".claude" / ".credentials.json"
try:
creds = json.loads(creds_file.read_text())
return creds.get("claudeAiOauth", {}).get("accessToken")
except (FileNotFoundError, json.JSONDecodeError, OSError):
pass
return None
def fetch_usage() -> dict | None:
"""Fetch usage from Anthropic API and cache it."""
token = get_oauth_token()
if not token:
return None
try:
req = urllib.request.Request(
"https://api.anthropic.com/api/oauth/usage",
headers={
"Authorization": f"Bearer {token}",
"anthropic-beta": "oauth-2025-04-20",
},
)
with urllib.request.urlopen(req, timeout=3) as resp:
data = json.loads(resp.read())
except Exception:
return None
if "five_hour" not in data or "seven_day" not in data:
return None
try:
USAGE_CACHE.write_text(json.dumps(data))
except OSError:
pass
return data
def get_usage(now: float) -> dict | None:
"""Return cached usage data, refreshing if stale.
Falls back to stale cache if the API call fails.
"""
cached = None
try:
if USAGE_CACHE.exists():
cached = json.loads(USAGE_CACHE.read_text())
age = now - USAGE_CACHE.stat().st_mtime
if age <= USAGE_CACHE_AGE:
return cached
except (json.JSONDecodeError, OSError):
pass
return fetch_usage() or cached
def _reset_epoch(resets_at: str) -> float | None:
"""Parse an ISO timestamp to epoch seconds."""
try:
return datetime.fromisoformat(resets_at).timestamp()
except ValueError:
return None
def time_until_reset(resets_at: str, now: float) -> str | None:
"""Return a human-readable string like '2h13m' or '3d5h' until reset."""
epoch = _reset_epoch(resets_at)
if epoch is None:
return None
remaining = int(epoch - now)
if remaining <= 0:
return None
days, remaining = divmod(remaining, 86400)
hours, remaining = divmod(remaining, 3600)
minutes = remaining // 60
if days > 0:
return f"{days}d{hours}h"
if hours > 0:
return f"{hours}h{minutes}m"
return f"{minutes}m"
def pacing_target(resets_at: str, window_secs: int, now: float) -> float | None:
"""Compute what percentage of the window has elapsed (0-100)."""
epoch = _reset_epoch(resets_at)
if epoch is None:
return None
start_epoch = epoch - window_secs
elapsed = max(0, min(now - start_epoch, window_secs))
return elapsed / window_secs * 100
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
raw = sys.stdin.read().strip()
if not raw:
return
try:
data = json.loads(raw)
except json.JSONDecodeError:
return
now = time.time()
# --- Extract fields ---
model = (data.get("model") or {}).get("display_name", "Unknown")
session_id = data.get("session_id", "unknown")
cw = data.get("context_window") or {}
used_pct = cw.get("used_percentage")
ctx_window_size = cw.get("context_window_size", 200000)
cost_data = data.get("cost") or {}
cost_usd = cost_data.get("total_cost_usd")
duration_ms = cost_data.get("total_duration_ms")
work_dir = (data.get("workspace") or {}).get("current_dir", "")
# --- Context percentage ---
# used_percentage is input-only and undercounts by ~10-15% vs Claude Code's
# internal compaction threshold (which includes system prompts, tool
# definitions, and reserved output buffer not exposed in the JSON).
# current_usage.output_tokens is only the latest response (not cumulative),
# so adding it barely moves the needle. We use used_percentage as-is.
if used_pct is not None:
ctx_pct = used_pct
total_ctx = int(used_pct * ctx_window_size / 100) if ctx_window_size else 0
else:
ctx_pct = None
total_ctx = 0
# --- Token velocity ---
total_tokens = cw.get("total_input_tokens", 0) + cw.get("total_output_tokens", 0)
delta, ema = update_velocity(session_id, total_tokens)
# --- Usage quota ---
usage = get_usage(now)
# === Build 3-column grid ===
# Column 1: model/dir/duration | ctx bar
# Column 2: cost/burn | 5h bar
# Column 3: velocity | 7d bar
# --- Row 1 cells ---
r1c1_parts: list[str] = [f"{WHITE}{model}{RESET}"]
if work_dir:
r1c1_parts.append(shorten_dir(work_dir))
if duration_ms is not None:
r1c1_parts.append(f"{CYAN}{format_duration(duration_ms)}{RESET}")
r1c1 = " ".join(r1c1_parts)
r1c2 = ""
if cost_usd is not None:
r1c2 = f"${cost_usd:.2f}"
if duration_ms is not None and int(duration_ms) // 1000 >= 10:
hrs = duration_ms / 3_600_000
burn_num = f"${cost_usd / hrs:.2f}" if hrs > 0 else "--"
r1c2 += f" {dim('(')}{burn_num}{dim('/hr)')}"
elif duration_ms is not None:
r1c2 += f" {dim('(')}{dim('--')}{dim('/hr)')}"
r1c3 = f"{dim('last')} {format_tok(delta)} {dim('avg')} {format_ema(ema)}{dim('/turn')}"
# --- Row 2 cells ---
if ctx_pct is not None:
ctx_int = int(round(ctx_pct))
bar = build_bar(ctx_pct, 15, green_thresh=60, yellow_thresh=85)
cc = pct_color(ctx_pct, 60, 85)
r2c1 = f"{dim('ctx')} {bar} {cc}{ctx_int}%{RESET} {dim('(')}{format_k(total_ctx)}{dim('/')}{format_k(ctx_window_size)}{dim(')')}"
else:
r2c1 = f"{dim('ctx')} {DIM}{GRAY}no data{RESET}"
r2c2 = ""
if usage and "five_hour" in usage:
fh = usage["five_hour"]
pct_5h = fh.get("utilization", 0)
resets_5h = fh.get("resets_at", "")
target = pacing_target(resets_5h, 5 * 3600, now)
bar_5h = build_bar(pct_5h, 15, target)
cc5 = pct_color(pct_5h)
r2c2 = f"{dim('5h')} {bar_5h} {cc5}{int(round(pct_5h))}%{RESET}"
ttl_5h = time_until_reset(resets_5h, now)
if ttl_5h:
r2c2 += f" {dim('⟳')}{ttl_5h}"
r2c3 = ""
if usage and "seven_day" in usage:
sd = usage["seven_day"]
pct_7d = sd.get("utilization", 0)
resets_7d = sd.get("resets_at", "")
target = pacing_target(resets_7d, 7 * 24 * 3600, now)
bar_7d = build_bar(pct_7d, 15, target)
cc7 = pct_color(pct_7d)
r2c3 = f"{dim('7d')} {bar_7d} {cc7}{int(round(pct_7d))}%{RESET}"
ttl_7d = time_until_reset(resets_7d, now)
if ttl_7d:
r2c3 += f" {dim('⟳')}{ttl_7d}"
sys.stdout.write(render_grid([
[r1c1, r1c2, r1c3],
[r2c1, r2c2, r2c3],
]))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment