#!/usr/bin/env python3 """Claude Code status line. One line: vim mode | dir (repo-relative, hidden at the root) | git(repo/branch +dirty +ahead/behind) | model (+effort) | context% (+compact warn) | 5h usage (+reset eta) | cost (+burn rate) | disk. If the rendered line would overflow the terminal, trailing (lowest-priority) segments are dropped until it fits. (Dormant, re-addable in main(): ram, cpu%, temp -- retired 2026-07-26 because the status line only refreshes on message events, so system metrics sat visibly stale; tmux's status bar owns those now -- plus velocity, cache hit %, api ratio, version, style.) Reads the status JSON from stdin (Claude Code statusLine command). Every segment is wrapped defensively so the status line can never crash the UI -- on any error a segment is simply omitted. """ import json import os import re import shutil import subprocess import sys import time import unicodedata # --- ANSI helpers ----------------------------------------------------------- RESET = "\033[0m" DIM = "" # disabled: "\033[2m" blended the gray detail text into dark backgrounds BOLD = "\033[1m" C = { "cyan": "\033[36m", "green": "\033[32m", "yellow": "\033[33m", "red": "\033[31m", "magenta": "\033[35m", "blue": "\033[34m", "gray": "\033[37m", # light gray (was "\033[90m" bright-black, too dark to read) } def color(text, name="", dim=False, bold=False): pre = "" if dim: pre += DIM if bold: pre += BOLD pre += C.get(name, "") return f"{pre}{text}{RESET}" if pre else str(text) def bucket(value, lo, hi, invert=False): """green/yellow/red by threshold. invert=True -> high is good.""" if invert: return "green" if value >= hi else ("yellow" if value >= lo else "red") return "green" if value < lo else ("yellow" if value < hi else "red") DIV = color(" │ ", "gray", dim=True) def join_line(segs): return DIV.join(s for s in segs if s) _ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") def vlen(text): """Visible width of a rendered string: ANSI stripped, emoji counted as 2.""" text = _ANSI_RE.sub("", text) w = 0 for ch in text: if ch in ("\u200d", "\ufe0f", "\ufe0e") or unicodedata.combining(ch): continue # ZWJ / variation selectors / combining marks: zero width o = ord(ch) if o >= 0x1F000 or 0x2600 <= o <= 0x27BF or unicodedata.east_asian_width(ch) in ("W", "F"): w += 2 else: w += 1 return w def term_cols(): """Terminal width. Claude Code exports COLUMNS; 0 if unknown -> stay one line.""" try: return shutil.get_terminal_size(fallback=(0, 0)).columns except Exception: return 0 # --- shared transcript usage ------------------------------------------------ def last_usage(data): """Most recent `message.usage` block from the transcript, or {}.""" try: path = data.get("transcript_path") if not path or not os.path.exists(path): return {} usage = {} with open(path, "r", errors="ignore") as fh: for line in fh: line = line.strip() if not line or '"usage"' not in line: continue try: obj = json.loads(line) except Exception: continue msg = obj.get("message") if isinstance(msg, dict) and isinstance(msg.get("usage"), dict): usage = msg["usage"] return usage except Exception: return {} # --- row 1: work ------------------------------------------------------------ def vim_segment(data): """Vim editor mode as a compact [I]/[N]/[V] (only present with editorMode: vim). Pair with `"hideVimModeIndicator": true` in the statusLine settings block so Claude's own built-in indicator doesn't duplicate this one. """ mode = (data.get("vim") or {}).get("mode") if not mode: return "" m = mode.upper() if m.startswith("INSERT"): return color("[I]", "green", bold=True) if m.startswith("VISUAL"): return color("[V]", "magenta", bold=True) if m.startswith("NORMAL"): return color("[N]", "blue", bold=True) return color(f"[{m[:1]}]", "gray") def abbrev(path): """fish-style prompt_pwd: shrink every component but the last to one char, keeping a leading dot (`.config` -> `.c`).""" parts = path.split(os.sep) out = [] for i, p in enumerate(parts): if i == len(parts) - 1 or p in ("", "~"): out.append(p) elif p.startswith("."): out.append(p[:2]) else: out.append(p[:1]) return os.sep.join(out) def dir_segment(cwd, top=None): """Path *inside* the repo -- git_segment already names the repo itself, so repeating the full ~-path here was pure duplication. Empty at the repo root; a fish-shortened ~-path when we're not in a repo at all.""" try: rel = os.path.relpath(cwd, top) if top else None if rel and not rel.startswith(os.pardir): # `..` -> cwd is outside `top` (symlinked in); use the ~-path if rel == os.curdir: return "" shown = rel if len(rel) <= 28 else abbrev(rel) else: home = os.path.expanduser("~") shown = cwd if cwd == home: shown = "~" elif cwd.startswith(home + os.sep): shown = "~" + cwd[len(home):] shown = abbrev(shown) return "📁 " + color(shown, "cyan") except Exception: return "" def _git(cwd, *args): return subprocess.run( ["git", "-C", cwd, *args], capture_output=True, text=True, timeout=1 ) def git_top(cwd): """Absolute path of the enclosing work tree, or None.""" try: r = _git(cwd, "rev-parse", "--show-toplevel") return r.stdout.strip() or None if r.returncode == 0 else None except Exception: return None def git_segment(cwd, top): try: if not top: return "" repo = os.path.basename(top) or "repo" branch = _git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip() or "?" dirty = bool(_git(cwd, "status", "--porcelain").stdout.strip()) # ahead/behind vs upstream (omitted when no upstream is configured) ab = "" rl = _git(cwd, "rev-list", "--left-right", "--count", "@{upstream}...HEAD") if rl.returncode == 0 and rl.stdout.strip(): try: behind, ahead = (int(x) for x in rl.stdout.split()) bits = [] if ahead: bits.append(color(f"↑{ahead}", "cyan")) if behind: bits.append(color(f"↓{behind}", "yellow")) if bits: ab = " " + "".join(bits) except Exception: ab = "" mark = color("*", "red") if dirty else "" label = color(repo, "magenta", bold=True) br = color(branch, "yellow" if dirty else "green") return f"🌿 {label} {br}{mark}{ab}" except Exception: return "" def model_segment(data): name = (data.get("model") or {}).get("display_name") if not name: return "" name = name.replace(" context)", ")") # "Opus 4.8 (1M context)" -> "Opus 4.8 (1M)" out = "🤖 " + color(name, "blue") level = (data.get("effort") or {}).get("level") if level: out += color(f" · {level}", "gray") return out def context_segment(data, usage): try: if not usage: return "" used = ( usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + usage.get("cache_read_input_tokens", 0) ) model_id = (data.get("model") or {}).get("id", "") window = 1_000_000 if "1m" in model_id.lower() else 200_000 pct = used / window * 100 if window else 0 col = bucket(pct, 50, 80) out = "📝 " + color(f"{pct:.0f}%", col) + color(f" ({used / 1000:.0f}k)", "gray", dim=True) if pct >= 80: out += color(" ⚠compact", "red", bold=True) return out except Exception: return "" def usage_segment(data): """5-hour rolling usage window: used % (+ time until it resets).""" try: five = (data.get("rate_limits") or {}).get("five_hour") or {} pct = five.get("used_percentage") if pct is None: return "" out = "📊 " + color(f"{pct:.0f}%", bucket(pct, 50, 80)) resets = five.get("resets_at") if resets: secs = int(resets) - int(time.time()) if secs > 0: h, m = divmod(secs // 60, 60) out += color(f" {h}h{m:02d}m" if h else f" {m}m", "gray") return out except Exception: return "" def cost_segment(data): try: cost = data.get("cost") or {} usd = cost.get("total_cost_usd") if usd is None: return "" out = "💰 " + color(f"${usd:.2f}", "yellow") dur_ms = cost.get("total_duration_ms") or 0 if dur_ms > 30_000: # need a meaningful window before extrapolating per_hr = usd / (dur_ms / 3_600_000) out += color(f" ${per_hr:.2f}/h", "gray", dim=True) return out except Exception: return "" def velocity_segment(data): try: cost = data.get("cost") or {} added = cost.get("total_lines_added", 0) removed = cost.get("total_lines_removed", 0) if not (added or removed): return "" out = "✏️ " + color(f"+{added}", "green") + color("/", "gray", dim=True) + color(f"-{removed}", "red") dur_ms = cost.get("total_duration_ms") or 0 if dur_ms > 30_000: per_min = added / (dur_ms / 60_000) out += color(f" {per_min:.0f}/m", "gray", dim=True) return out except Exception: return "" def cache_segment(usage): try: if not usage: return "" read = usage.get("cache_read_input_tokens", 0) total = ( usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + read ) if total <= 0: return "" pct = read / total * 100 return "♻️ " + color(f"{pct:.0f}%", bucket(pct, 50, 80, invert=True)) except Exception: return "" def api_segment(data): """Share of wall-clock time spent in API/inference (rough 'busy' ratio).""" try: cost = data.get("cost") or {} wall = cost.get("total_duration_ms") or 0 api = cost.get("total_api_duration_ms") or 0 if wall <= 0 or api <= 0: return "" pct = min(api / wall * 100, 100) return "⚙️ " + color(f"{pct:.0f}%", "blue") except Exception: return "" # --- row 2: system ---------------------------------------------------------- def ram_segment(): try: info = {} with open("/proc/meminfo") as fh: for line in fh: k, _, v = line.partition(":") info[k.strip()] = int(v.split()[0]) # kB total = info.get("MemTotal", 0) avail = info.get("MemAvailable", info.get("MemFree", 0)) if total <= 0: return "" used = total - avail pct = used / total * 100 g = 1024 * 1024 return ( "🧠 " + color(f"{pct:.0f}%", bucket(pct, 70, 85)) + color(f" {used / g:.1f}/{total / g:.0f}G", "gray", dim=True) ) except Exception: return "" def disk_segment(cwd): try: st = os.statvfs(cwd) total = st.f_blocks if total <= 0: return "" used = total - st.f_bfree pct = used / total * 100 return "💾 " + color(f"{pct:.0f}%", bucket(pct, 75, 90)) except Exception: return "" def _cpu_pct(): """Live CPU% diffed against a cached /proc/stat snapshot (no sleeping).""" try: with open("/proc/stat") as fh: parts = fh.readline().split() if not parts or parts[0] != "cpu": return None vals = [int(x) for x in parts[1:]] idle = vals[3] + (vals[4] if len(vals) > 4 else 0) # idle + iowait total = sum(vals) state = os.path.expanduser("~/.claude/cache/statusline.cpu") prev = None try: with open(state) as fh: pt, pi = fh.read().split() prev = (int(pt), int(pi)) except Exception: prev = None try: os.makedirs(os.path.dirname(state), exist_ok=True) with open(state, "w") as fh: fh.write(f"{total} {idle}") except Exception: pass if not prev: return None dt = total - prev[0] di = idle - prev[1] if dt <= 0 or di < 0: return None return max(0.0, min(100.0, (1 - di / dt) * 100)) except Exception: return None def cpu_segment(): try: load1 = os.getloadavg()[0] cores = os.cpu_count() or 1 pct = _cpu_pct() if pct is not None: head = color(f"{pct:.0f}%", bucket(pct, 60, 85)) tail = color(f" {load1:.1f} {cores}c", "gray", dim=True) else: head = color(f"{load1:.2f}", bucket(load1 / cores, 0.7, 1.0)) tail = color(f" {cores}c", "gray", dim=True) return "🖥️ " + head + tail except Exception: return "" def temp_segment(): """Hottest CPU-ish thermal zone in °C, falling back to the max zone.""" try: base = "/sys/class/thermal" if not os.path.isdir(base): return "" prefer = ("x86_pkg_temp", "coretemp", "cpu", "k10temp", "tctl") chosen = None fallback = None for name in os.listdir(base): if not name.startswith("thermal_zone"): continue d = os.path.join(base, name) try: with open(os.path.join(d, "temp")) as fh: milli = int(fh.read().strip()) except Exception: continue ztype = "" try: with open(os.path.join(d, "type")) as fh: ztype = fh.read().strip().lower() except Exception: pass if fallback is None or milli > fallback: fallback = milli if any(p in ztype for p in prefer) and (chosen is None or milli > chosen): chosen = milli milli = chosen if chosen is not None else fallback if milli is None: return "" c = milli / 1000.0 return "🌡️ " + color(f"{c:.0f}°C", bucket(c, 60, 80)) except Exception: return "" def version_segment(data): v = data.get("version") return color(f"v{v}", "gray", dim=True) if v else "" def style_segment(data): name = (data.get("output_style") or {}).get("name") return color(f"🎨{name}", "gray", dim=True) if name else "" def main(): try: data = json.load(sys.stdin) except Exception: data = {} cwd = ( (data.get("workspace") or {}).get("current_dir") or data.get("cwd") or os.getcwd() ) usage = last_usage(data) top = git_top(cwd) # One row: work (left) then system (right), joined. If it would overflow the # terminal, trailing (lowest-priority) segments are dropped so it stays one line. # Dormant helpers kept above for easy re-add: ram_segment, cpu_segment, # temp_segment (retired -- they only refresh on message events, so they sat # stale; the tmux status bar owns system metrics now), velocity_segment, # cache_segment, api_segment, version_segment, style_segment. work = [s for s in ( vim_segment(data), dir_segment(cwd, top), git_segment(cwd, top), model_segment(data), context_segment(data, usage), usage_segment(data), cost_segment(data), ) if s] system = [s for s in ( disk_segment(cwd), ) if s] segs = work + system cols = term_cols() if cols: # trim from the right until it fits one line (cols=0 -> width unknown, keep all) while len(segs) > 1 and vlen(join_line(segs)) > cols: segs.pop() sys.stdout.write(join_line(segs)) if __name__ == "__main__": main()