#!/bin/bash
# Claude Code status line:  <machine>:<session> · <◉|○> [· ⎇ branch] · <model> · <effort> · ctx% · in<tok> · Σtok · 7d% (reset) · 5h% [· $cost]
#
# This is a SANITIZED example, stripped of machine-specific color mapping tied to one specific
# personal setup. Read the guide section this ships with for what each segment means and how to
# customize it. Needs python3; git and tmux are optional (each segment degrades quietly, just
# omitted, when its dependency is absent). The spawn-order number also needs a session manager
# that writes "# registered:" timestamp files under ~/.amux/sessions -- skip that part if you do
# not have one; the square still renders fine without a number in it.
#
# - the focus chip is ALWAYS shown, as a symbol only: ◉ blue = focus/brief view ON (tool detail +
#   diffs collapsed), ○ grey = OFF. It sits next to the session label, not out at the quota end,
#   and survives width-shedding.
# - a leading per-session color SQUARE with the spawn-order NUMBER INSIDE it (bright bg, hash-picked;
#   auto-contrast black/white number so it always reads) makes every Claude session instantly
#   distinguishable; the machine:session text keeps its own per-machine color (see MACHINES below --
#   optional, only useful if you run sessions on multiple computers). <session> is the STABLE
#   session name if you use a session manager like amux (locked tmux window name), else it falls back
#   to the project directory name. The number is the live spawn rank by amux "# registered:", when amux
#   is present.
# - branch is SILENT when the anchor repo sits clean on main/master; it shows on any other branch, or as
#   "main*" when main/master has uncommitted work -> a visible branch ALWAYS means something to act on.
#   Anchored to project_dir (the stable session root), NOT current_dir, which drifts with every Bash cd
#   and can even point at a deleted dir; current_dir is the fallback, and wins when it is the better
#   of the two. Prefixed with the repo name ("nuidsl-testbed:feat/x") when the branch belongs
#   to a repo other than the dir shown at left. * = uncommitted.
# - ctx-left and 7d-quota go green→yellow→red; 7d shows time-until-reset.
# - in<tok> is context_window.total_input_tokens: the raw token count for what actually goes out on the
#   NEXT prompt (system prompt + tool defs + full history + everything else sent, not just what you
#   typed) -- the same underlying number ctx% is a percentage OF. Current window only, same as ctx%.
# - Σtok is CUMULATIVE tokens for the whole session, unlike ctx% which is only the current window and
#   collapses right after /compact. Claude Code exposes no lifetime-token field, so it is banked locally
#   in ~/.claude/.token-state/<session_id>.state: whenever the current window total DROPS versus the last
#   render, that is a compaction, so the pre-drop peak gets banked and counting resumes on top of the
#   smaller post-compact number. Resets naturally on /clear, which starts a fresh session_id.
# - $cost is cost.total_cost_usd, which Claude Code itself already tracks cumulatively across /compact
#   (only /clear resets it) -- shown alongside Σtok since both survive compaction the same way.
# - Responsive: if the line exceeds $COLUMNS (Claude Code sets it), sheds 5h→$cost→effort→7d→branch→Σtok to fit.
# Reads Claude's JSON status context on stdin.
exec python3 -c '
import sys, json, os, time, subprocess, glob, zlib
try:
    d = json.load(sys.stdin)
except Exception:
    d = {}
def g(*ks):
    cur = d
    for k in ks:
        if not isinstance(cur, dict): return None
        cur = cur.get(k)
    return cur
cwd  = g("workspace","current_dir") or d.get("cwd") or os.getcwd()
# Stable session label: prefer the amux session name (the locked tmux window name,
# which survives the agent cd-ing around) over the live cwd, which drifts. Env
# override AMUX_NAME wins if set; falls back to the launch/project dir basename.
def _tmux_w():
    if os.environ.get("TMUX"):
        try:
            return subprocess.run(["tmux","display-message","-p","#W"],
                                  capture_output=True, text=True, timeout=1).stdout.strip()
        except Exception:
            return ""
    return ""
_raw_w = _tmux_w()
def _strip_sfx(s):   # drop a trailing " (…)" so our own "name (M eff)" round-trips to the clean name
    return s[:s.rfind(" (")].rstrip() if (s.endswith(")") and " (" in s) else s
proj = os.environ.get("AMUX_NAME") or _strip_sfx(_raw_w) or os.path.basename((g("workspace","project_dir") or cwd).rstrip("/")) or cwd
node = os.uname().nodename.split(".")[0]
host = node
# Optional per-machine color: if you run Claude Code on multiple computers, name them here so
# each ones session label gets its own color and you can tell them apart at a glance. Add one tuple
# per machine: a substring of its hostname, and an xterm-256 color number (0-255). Unmatched hosts
# (or the only machine you use) fall back to a neutral grey.
MACHINES = [("laptop",81),("desktop",214),("server",108)]   # EXAMPLE -- replace with your own hostnames
def machine_color(n):
    n = n.lower()
    if os.geteuid() == 0: return 196   # root sessions always flag red, regardless of machine
    for sub,c in MACHINES:
        if sub in n: return c
    return 244
mc = machine_color(node)
# Per-session color SQUARE: a bright xterm-256 color chosen by a stable crc32 hash of
# the session name, drawn as a leading solid square. Pure color block (no text on it)
# so it is always readable; the machine:session text keeps the per-machine color -> the line
# carries BOTH machine identity (text color) and session identity (square color).
SQUARE = [160,130,94,58,28,22,23,25,26,27,57,91,127,162,125,161]   # deep jewel tones -> uniform WHITE number, contrast >=4.7
sq = SQUARE[zlib.crc32(proj.encode()) % len(SQUARE)]
# Auto-contrast text color for the number sitting INSIDE the colored square: black on
# light squares, white on dark ones, so the number always reads no matter which color.
def xlum(i):
    if i < 16:
        base = [(0,0,0),(128,0,0),(0,128,0),(128,128,0),(0,0,128),(128,0,128),(0,128,128),
                (192,192,192),(128,128,128),(255,0,0),(0,255,0),(255,255,0),(0,0,255),
                (255,0,255),(0,255,255),(255,255,255)]
        r, gg, b = base[i]
    elif i < 232:
        j = i - 16; s = [0,95,135,175,215,255]
        r, gg, b = s[j//36], s[(j//6)%6], s[j%6]
    else:
        v = 8 + 10*(i-232); r = gg = b = v
    def lin(c):
        c = c / 255.0
        return c/12.92 if c <= 0.03928 else ((c+0.055)/1.055) ** 2.4
    return 0.2126*lin(r) + 0.7152*lin(gg) + 0.0722*lin(b)   # WCAG relative luminance 0..1
# black if the square is light enough that black beats white (crossover at L=0.179);
# use TRUE 256 black/white (16/231) because 30;1 bold-black renders as grey.
tcol = "38;5;16" if xlum(sq) > 0.179 else "38;5;231"
# Live spawn-order number: rank this session among all amux sessions by their
# "# registered:" timestamp (no amux changes needed). None if not an amux session.
def spawn_num(name):
    sdir = os.path.expanduser("~/.amux/sessions")
    try:
        names = os.listdir(sdir)
    except Exception:
        return None
    items = []
    for fn in names:
        if not fn.endswith(".env"):
            continue
        ts = ""
        try:
            for ln in open(os.path.join(sdir, fn)):
                ln = ln.strip()
                if ln.startswith("# registered:"):
                    ts = ln.split(":", 1)[1].strip(); break
        except Exception:
            continue
        items.append((ts or "zzzz", fn[:-4]))
    items.sort()
    for i, it in enumerate(items, 1):
        if it[1] == name:
            return i
    return None
snum = spawn_num(proj)
DEFAULTB = ("main", "master")
def gitbranch(path):   # -> (branch, dirty) for the repo holding path; None if not a repo / detached
    if not path: return None
    try:
        out = subprocess.run(["git","-C",path,"status","-sb"],
                             capture_output=True, text=True, timeout=1).stdout
    except Exception:
        return None
    ls = out.splitlines()
    if not ls or not ls[0].startswith("## "): return None
    hdr = ls[0][3:]
    if "no branch" in hdr: return None
    return (hdr.split("...")[0].split(" ")[0], len(ls) > 1)
def worth(bi):         # spend status-line width only on off-default branches or uncommitted work
    return bool(bi) and (bi[0] not in DEFAULTB or bi[1])
# Anchor on project_dir (the stable session root). current_dir follows whatever a Bash call last cd-ed
# into, so it drifts and can even name a deleted dir -> it is the FALLBACK, and it wins only when it is
# the better of the two (anchor clean on main, but the live cwd is in another repo on a real
# branch). _bsrc remembers which one won so the label can name the repo if it is not the dir at left.
_pdir = g("workspace","project_dir")
_bp = gitbranch(_pdir)
_bc = _bp if cwd == _pdir else gitbranch(cwd)
_bi, _bsrc = (_bp, _pdir) if worth(_bp) else ((_bc, cwd) if worth(_bc) else (None, None))
br = (_bi[0] + ("*" if _bi[1] else "")) if _bi else None
def headbranch(repo):   # cheap branch read, NO subprocess: .git/HEAD -> branch name (None if detached)
    try:
        p = os.path.join(repo, ".git")
        if not os.path.isdir(p): return None             # worktree/submodule .git FILE -> skip, stay cheap
        h = open(os.path.join(p, "HEAD")).read().strip()
    except Exception:
        return None
    return h[16:] if h.startswith("ref: refs/heads/") else None
# CONTAINER dir -- holds sibling repos but is not one itself (e.g. ~/claudecode/nuidsl-mmtestbed, which
# carries VRSketch + nuidsl-mmsketching + open-brush). git only ever walks UP, so such a dir has no branch
# of its own and the segment used to go blank. Surface the children that sit OFF the default branch, read
# straight from .git/HEAD -- a plain file read, because git status on a Unity-sized child can burn the full
# 1s timeout on every render. No dirty check for children for the same reason (no * on a roll-up).
if br is None:
    _kids = []
    try:
        _base = _pdir or cwd
        for nm in sorted(os.listdir(_base))[:24]:
            kb = headbranch(os.path.join(_base, nm))
            if kb and kb not in DEFAULTB: _kids.append((nm, kb))
    except Exception:
        _kids = []
    if _kids:
        br = "%s:%s" % _kids[0]
        if len(_kids) > 1: br += " +%d" % (len(_kids) - 1)
        _bsrc = None                                     # already repo-labeled -> skip the rev-parse prefix
def cdown(ts):
    if not ts: return ""
    s = int(ts - time.time())
    if s <= 0: return ""
    if s >= 86400:
        dd = s // 86400; hh = (s % 86400) // 3600
        return f"{dd}d {hh}h" if hh else f"{dd}d"
    if s >= 3600:
        hh = s // 3600; mm = (s % 3600) // 60
        return f"{hh}h {mm}m" if mm else f"{hh}h"   # hours range -> show minutes too (days range stays d/h)
    return f"{max(1,s//60)}m"
model  = g("model","display_name") or ""
effort = g("effort","level") or ""
ctxL   = g("context_window","remaining_percentage")
d7     = g("rate_limits","seven_day","used_percentage")
d7r    = cdown(g("rate_limits","seven_day","resets_at"))
d5     = g("rate_limits","five_hour","used_percentage")
d5r    = cdown(g("rate_limits","five_hour","resets_at"))
cost_usd = g("cost","total_cost_usd")
sid    = g("session_id") or ""
HOME   = os.path.expanduser("~")
TOKCOL = "38;5;117"; COSTCOL = "38;5;144"
def humantok(n):
    n = int(n)
    if n >= 1000000: return f"{n/1000000:.1f}M"
    if n >= 1000: return f"{n/1000:.0f}k"
    return str(n)
def safe_sid(s):
    return "".join(c for c in s if c.isalnum() or c in "-_") or "nosession"
# Cumulative tokens for the WHOLE session, surviving /compact. context_window.total_input_tokens and
# total_output_tokens only describe the CURRENT window (per Claude Code docs: reflects the most recent
# API response, collapses toward 0 right after /compact until the next call repopulates it) -- there is
# no field for lifetime session tokens, so bank it in a tiny per-session state file. On every render: if
# the current total DROPS versus last time, that is a compaction event -- bank the pre-drop peak and keep
# counting on top of the smaller number. Resets naturally because /clear starts a brand-new session_id.
tokdir = os.path.join(HOME, ".claude", ".token-state")
try: os.makedirs(tokdir, exist_ok=True)
except Exception: pass
tokfile = os.path.join(tokdir, safe_sid(sid) + ".state")
ti = g("context_window","total_input_tokens") or 0
to = g("context_window","total_output_tokens") or 0
raw = int(ti) + int(to)
last_raw, banked = 0, 0
try:
    parts = open(tokfile).read().split()
    if len(parts) == 2: last_raw, banked = int(parts[0]), int(parts[1])
except Exception: pass
if raw > 0:
    if raw < last_raw: banked += last_raw
    last_raw = raw
    try: open(tokfile, "w").write(f"{last_raw} {banked}")
    except Exception: pass
total_tok = banked + last_raw
try:                                                          # best-effort weekly sweep of old session files
    swstamp = os.path.join(tokdir, ".sweep-stamp")
    if not (os.path.exists(swstamp) and time.time() - os.path.getmtime(swstamp) < 86400):
        open(swstamp, "a").close(); os.utime(swstamp, None)
        cutoff = time.time() - 7*86400
        for fn in os.listdir(tokdir):
            fp = os.path.join(tokdir, fn)
            if os.path.isfile(fp) and os.path.getmtime(fp) < cutoff:
                try: os.remove(fp)
                except Exception: pass
except Exception: pass
def col(code, s): return f"\033[{code}m{s}\033[0m"
DIM = "2;37"; SEP = " · "; AMBER = "38;2;245;201;106"   # truecolor amber; terminals without truecolor support degrade this to ~221
segs = []   # [prio, visible, colored] in display order; higher prio = kept longer
def add(prio, vis, code): segs.append([prio, vis, col(code, vis)])
_wd = os.path.basename(cwd.rstrip("/")) or host               # LIVE working dir (drifts; the fixed session name is the top-right label)
_lbl = f"{host}:…/{_wd}"
if snum:
    _bvis = f" {snum} "; _bcol = col(f"48;5;{sq};{tcol};1", _bvis)   # number INSIDE the color square
else:
    _bvis = "■"; _bcol = col(f"1;38;5;{sq}", "■")               # no amux number -> plain square
segs.append([100, f"{_bvis} {_lbl}", _bcol + " " + col(f"1;38;5;{mc}", _lbl)])
# Focus-view chip. SILENT when focus view is off -- same rule as the branch segment: a visible chip
# always means the transcript is hiding tool detail (ctrl+o expands, ctrl+shift+b / focus toggles).
# The status JSON carries NO view-mode field, so resolve it exactly the way Claude Code does:
# settings viewMode wins when set, else the briefTranscript flag in ~/.claude.json. Prio 90 -> it
# outlives every shed except the machine:session label.
def focus_on():
    _h = os.path.expanduser("~")
    _root = _pdir or cwd or _h
    for _p in (os.path.join(_root, ".claude", "settings.local.json"),
               os.path.join(_root, ".claude", "settings.json"),
               _h + "/.claude/settings.local.json",
               _h + "/.claude/settings.json"):
        try:
            _vm = json.load(open(_p)).get("viewMode")
        except Exception:
            continue
        if _vm:
            return _vm == "focus"
    try:
        return bool(json.load(open(_h + "/.claude.json")).get("briefTranscript"))
    except Exception:
        return False
_fc = focus_on()
add(90, "◉" if _fc else "○", "38;5;111" if _fc else "38;5;244")   # filled+blue = focus ON, hollow+grey = OFF
mp = model.rsplit(" ", 1)
tier = mp[0] if len(mp) == 2 and mp[1][:1].isdigit() else model   # "Opus 4.8" -> "Opus" (CC = always latest)
TIERABBR = {"Opus":"Op","Sonnet":"Sn","Haiku":"Hk","Fable":"Fb"}   # short form keeps room for a long session name
me = f"{TIERABBR.get(tier, tier)} {effort}".strip() if (tier or effort) else ""
if me: add(20, me, AMBER)
# Drive the iTerm tab title (via tmux set-titles = #W): "name (M eff!)" — model initial + short effort,
# ! marks Fable. Live (recomputed each render), tracks /model & /effort. Rename only when it changed.
if _raw_w and os.environ.get("TMUX_PANE"):
    _es = {"xhigh":"xh","high":"hi","medium":"md","low":"lo","max":"mx","minimal":"mn","ultra":"ul"}.get(effort.lower(), effort[:2])
    _mi = tier[:1].upper() if tier else ""
    _fab = "fable" in model.lower() or tier.lower().startswith("fable")
    _lab = (f"{_mi} {_es}".strip() + ("!" if _fab else "")).strip()
    _want = f"{proj} ({_lab})" if _lab else proj
    if _want != _raw_w:
        try: subprocess.run(["tmux","rename-window","-t",os.environ["TMUX_PANE"],_want],capture_output=True,timeout=1)
        except Exception: pass
if ctxL is not None:
    c = "38;5;46" if ctxL > 50 else "38;5;226" if ctxL > 20 else "38;5;196"
    add(50, f"ctx {ctxL:.0f}%", c)
if ti > 0:                                                    # raw input tokens for the NEXT prompt (prompt + everything
    add(42, f"in {humantok(ti)}", "38;5;81")                  # else sent) -- current window only, unlike Sigma below
if total_tok > 0:
    add(40, f"Σ{humantok(total_tok)}", TOKCOL)
if br:                                                        # branch sits right before 7d
    if _bsrc:                                                 # name the repo only when it is NOT the dir at left
        _rn = ""
        try:
            _rn = os.path.basename(subprocess.run(["git","-C",_bsrc,"rev-parse","--show-toplevel"],
                                   capture_output=True, text=True, timeout=1).stdout.strip())
        except Exception: pass
        if _rn and _rn != _wd: br = f"{_rn}:{br}"
    add(35, f"⎇ {br}", "38;5;214" if br.endswith("*") else "38;5;109")
if d7 is not None:
    c = "38;5;46" if d7 < 70 else "38;5;226" if d7 < 90 else "38;5;196"
    add(30, f"7d {d7:.0f}%" + (f" {d7r}" if d7r else ""), c)
if d5 is not None:
    if br is None:                                            # no repo -> fill the empty slot with the FULL 5h (reset incl.)
        add(10, f"5h {d5:.0f}%" + (f" {d5r}" if d5r else ""), "1;38;5;196" if d5 > 79 else AMBER)
    elif d5 > 79:                                             # repo present -> 5h shows only as a RED over-79 alarm, %% only
        add(10, f"5h {d5:.0f}%", "1;38;5;196")
if cost_usd is not None and cost_usd > 0:
    add(15, f"${cost_usd:.2f}", COSTCOL)
# Pending-upgrade indicator. native installer self-updates -> a newer build already on disk
# means "restart to apply" (amber up-arrow ver, computed inline, no network). brew/npm do NOT
# auto-update -> a red Brew:U / npm:U from the throttled background check below (overrides).
# NOTE: this whole program is a single-quoted  python3 -c  string — keep it APOSTROPHE-FREE.
HOME = os.path.expanduser("~")
def _vt(s):
    try: return tuple(int(x) for x in s.split("."))
    except Exception: return ()
ver = g("version") or ""
up = ""; upc = "38;5;214"
nv = [b for b in (os.path.basename(p) for p in glob.glob(HOME+"/.local/share/claude/versions/*")) if _vt(b)]
if ver and nv and _vt(max(nv, key=_vt)) > _vt(ver):
    up = "↻" + max(nv, key=_vt)                              # restart to apply downloaded build
try:
    fl = open(HOME+"/.claude/.upgrade-flag").read().strip()
    if fl: up, upc = fl, "1;38;5;196"                        # bold red: needs a manual upgrade
except Exception: pass
if up: add(45, up, upc)
try:                                                          # refresh the brew/npm flag, detached, <=2h
    st = HOME+"/.claude/.upgrade-stamp"
    if not (os.path.exists(st) and time.time()-os.path.getmtime(st) < 7200):
        open(st,"a").close(); os.utime(st, None)
        subprocess.Popen(["bash", HOME+"/.claude/upgrade-check.sh"],
                         stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                         stdin=subprocess.DEVNULL, start_new_session=True)
except Exception: pass
# Responsive: Claude Code sets $COLUMNS (>=2.1.153; tput/TTY do NOT work here). If the
# joined line would exceed it, shed the lowest-priority segment (5h -> effort -> 7d ->
# branch) until it fits; machine:project (prio 100) always survives.
try: cols = int(os.environ.get("COLUMNS", "0"))
except ValueError: cols = 0
def width(ss): return (sum(len(s[1]) for s in ss) + len(SEP)*(len(ss)-1)) if ss else 0
while cols > 0 and len(segs) > 1 and width(segs) > cols:
    segs.pop(max(range(len(segs)), key=lambda i: (-segs[i][0], i)))   # lowest prio, rightmost on ties
sys.stdout.write(col(DIM, SEP).join(s[2] for s in segs))
'
