[Sync] Lua Hyprland chain, licence cleanup

This commit is contained in:
Coja committed 2026-11-09 18:25:35 +01:00
1 parent 93b4f695ce
commit 47166d877f
183 files changed
+5618 -5839

No files matched your search

+218 -31
View File
@@ -2,13 +2,15 @@
"""Claude Code status line.
One line: vim mode | dir (repo-relative, hidden at the root) |
git(repo/branch +dirty +ahead/behind) | model (+effort) |
git(repo/branch +dirty +ahead/behind) | PR review threads (open count, Codex's share) | model (+effort) |
context% (+compact warn) | 5h usage (+reset eta) | cost (+burn rate) |
session tokens (cumulative in/out) | prompt-cache hit% (+ttl, cold marker) | disk. If the
session tokens (cumulative in/out) | decode speed t/s (thinking blocks) |
prompt-cache hit% (+ttl, cold marker) | 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;
status line then only refreshed on message events, so system metrics sat visibly stale
(settings.json's 60 s refreshInterval, added for the PR count, is still too coarse for them);
tmux's status bar owns those now -- plus velocity, cache hit %, api ratio, version,
style.)
@@ -16,6 +18,7 @@ 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 hashlib
import json
import os
import re
@@ -159,6 +162,9 @@ def last_usage(data):
# --- session token totals (transcript, incremental) ---------------------------
TOKENS_STATE_DIR = os.path.expanduser("~/.claude/cache")
TOKENS_RECENT_IDS = 20 # message ids remembered across runs for de-duplication
TPS_SAMPLES = 8 # thinking blocks the t/s readout is averaged over
TPS_MIN_MS, TPS_MIN_TOKENS = 1000, 50 # shorter blocks are first-token latency, not decode speed
TOKENS_SCAN_BUDGET = 8 << 20 # bytes parsed per refresh (~0.1 s); see session_tokens
TOKENS_STATE_MAX_AGE = 7 * 86400 # prune per-session state files older than this
@@ -183,9 +189,10 @@ def _tokens_state_path(data, path):
def _prune_tokens_state(keep):
"""Drop stale per-session state files (one scandir, only when the dir has piled up)."""
"""Drop stale per-session token and per-branch PR state files (one scandir, only when
the dir has piled up)."""
try:
entries = [e for e in os.scandir(TOKENS_STATE_DIR) if e.name.startswith("statusline.tokens.")]
entries = [e for e in os.scandir(TOKENS_STATE_DIR) if e.name.startswith(("statusline.tokens.", "statusline.pr."))]
if len(entries) < 50:
return
cutoff = time.time() - TOKENS_STATE_MAX_AGE
@@ -197,12 +204,21 @@ def _prune_tokens_state(keep):
def session_tokens(data):
"""Cumulative (input, output) tokens this session, summed over the transcript's
assistant `message.usage` blocks -- or None when there is no transcript.
"""Cumulative session token totals plus decode-speed samples, from the transcript --
`{"in", "out", "tps"}` or None when there is no transcript.
Input = input + cache_creation + cache_read (everything the API billed as input), so
on a 1M session it dwarfs output: every call re-reads the whole cached prefix.
Only the main transcript is counted; subagent usage lives in separate sidechain files.
in/out: summed over assistant `message.usage` blocks. Input = input + cache_creation +
cache_read (everything the API billed as input), so on a 1M session it dwarfs output:
every call re-reads the whole cached prefix. Only the main transcript is counted;
subagent usage lives in separate sidechain files.
tps: the last TPS_SAMPLES `[thinking_tokens, thinkingDurationMs]` pairs -- the only
clean per-call timing Claude Code records. The status payload's cost.total_api_duration_ms
is useless for a rate: subagent and advisor calls land in the same counter (it grew 28s
in a 12s wall interval with no main-conversation completion), and the payload's
output_tokens is sometimes a mid-stream partial. thinkingDurationMs is main-conversation
only; it includes first-token latency, so short blocks read low and long ones converge
on the real decode speed -- hence the duration-weighted mean and the minimum-size filter.
Incremental: the status line runs on every refresh and transcripts grow to tens of
MB, so a per-session state file (~/.claude/cache/statusline.tokens.<session>.json)
@@ -210,12 +226,18 @@ def session_tokens(data):
de-duplicated by message id: the transcript writes one line per content block of a
turn, all carrying the same id and the same usage block. Only complete lines are
consumed (the writer may be mid-line); a shrunken file (truncate/rotate) resets.
A cold scan is spread over refreshes, TOKENS_SCAN_BUDGET bytes each: Claude Code kills
a status script when the next refresh fires, and a multi-second scan of a 100 MB
transcript that never gets to write its state would start from zero every time.
Totals climb for a few refreshes on a huge resumed transcript, then settle.
"""
path = data.get("transcript_path")
if not path or not os.path.exists(path):
return None
state_path = _tokens_state_path(data, path)
state = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": []}
fresh = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": [], "tps": [], "tps_ids": []}
state = dict(fresh)
try:
with open(state_path) as fh:
s = json.load(fh)
@@ -225,27 +247,36 @@ def session_tokens(data):
and all(isinstance(s.get(k), int) and s[k] >= 0 for k in ("offset", "in", "out"))
and isinstance(s.get("ids"), list)
):
state = s
state = s # "tps"/"tps_ids" are optional: older state files predate them
except Exception:
pass
def result(st):
return {"in": st["in"], "out": st["out"], "tps": st.get("tps") or []}
size = os.path.getsize(path)
if size < state["offset"]: # truncated or rotated under us: start over
state = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": []}
state = dict(fresh)
if size == state["offset"]:
return state["in"], state["out"]
return result(state)
with open(path, "rb") as fh:
fh.seek(state["offset"])
chunk = fh.read()
end = chunk.rfind(b"\n")
chunk = fh.read(TOKENS_SCAN_BUDGET)
end = chunk.rfind(b"\n")
if end < 0 and len(chunk) == TOKENS_SCAN_BUDGET: # one line bigger than the budget: finish it
chunk += fh.readline()
end = chunk.rfind(b"\n")
if end < 0: # no complete new line yet
return state["in"], state["out"]
return result(state)
text = chunk[: end + 1].decode("utf-8", "ignore")
tin, tout = state["in"], state["out"]
ids = [i for i in state["ids"] if isinstance(i, str)]
seen = set(ids)
tps = [s for s in (state.get("tps") or []) if isinstance(s, list) and len(s) == 2]
tps_ids = [i for i in (state.get("tps_ids") or []) if isinstance(i, str)]
tps_seen = set(tps_ids)
for line in text.splitlines():
if '"usage"' not in line:
continue
@@ -258,19 +289,38 @@ def session_tokens(data):
if not isinstance(usage, dict):
continue
mid = msg.get("id") or obj.get("requestId") or obj.get("uuid")
if not isinstance(mid, str) or mid in seen:
if not isinstance(mid, str):
continue
def num(d, k):
v = d.get(k, 0) if isinstance(d, dict) else 0
return int(v) if isinstance(v, (int, float)) else 0
# Decode-speed sample: checked on every line (not just the first-wins one below) so
# it does not depend on the thinking block being the message's first line.
ms = num(obj, "thinkingDurationMs")
toks = num(usage.get("output_tokens_details"), "thinking_tokens")
if ms >= TPS_MIN_MS and toks >= TPS_MIN_TOKENS and mid not in tps_seen:
tps_seen.add(mid)
tps_ids.append(mid)
tps.append([toks, ms])
if mid in seen:
continue
seen.add(mid)
ids.append(mid)
tin += num(usage, "input_tokens") + num(usage, "cache_creation_input_tokens") + num(usage, "cache_read_input_tokens")
tout += num(usage, "output_tokens")
def num(k):
v = usage.get(k, 0)
return int(v) if isinstance(v, (int, float)) else 0
tin += num("input_tokens") + num("cache_creation_input_tokens") + num("cache_read_input_tokens")
tout += num("output_tokens")
state = {"path": path, "offset": state["offset"] + end + 1, "in": tin, "out": tout, "ids": ids[-TOKENS_RECENT_IDS:]}
state = {
"path": path,
"offset": state["offset"] + end + 1,
"in": tin,
"out": tout,
"ids": ids[-TOKENS_RECENT_IDS:],
"tps": tps[-TPS_SAMPLES:],
"tps_ids": tps_ids[-TOKENS_RECENT_IDS:],
}
try:
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
tmp = f"{state_path}.{os.getpid()}.tmp"
@@ -280,7 +330,7 @@ def session_tokens(data):
_prune_tokens_state(state_path)
except Exception:
pass
return tin, tout
return result(state)
# --- row 1: work ------------------------------------------------------------
@@ -395,6 +445,122 @@ def git_segment(cwd, top):
return ""
# --- PR review threads (gh, cached, fetched in the background) ------------------
PR_TTL = 90 # seconds a fetched count stays fresh
PR_ERR_TTL = 600 # back-off after a failed fetch (no GitHub remote, gh not authed, offline)
PR_CLAIM = 30 # a refresher spawned this recently is still running: don't start another
PR_CODEX = re.compile(r"codex", re.I) # chatgpt-codex-connector; same match as /pr-loop
PR_QUERY = """query($owner:String!,$repo:String!,$branch:String!){
repository(owner:$owner,name:$repo){
pullRequests(headRefName:$branch,states:OPEN,first:1,orderBy:{field:UPDATED_AT,direction:DESC}){
nodes{ number reviewThreads(first:100){ pageInfo{hasNextPage}
nodes{ isResolved comments(first:1){ nodes{ author{login} } } } } } } } }"""
def _pr_cache_path(top, branch):
key = hashlib.sha1(f"{top}\0{branch}".encode()).hexdigest()[:16]
return os.path.join(TOKENS_STATE_DIR, f"statusline.pr.{key}.json")
def _read_json(path):
try:
with open(path) as fh:
d = json.load(fh)
return d if isinstance(d, dict) else {}
except Exception:
return {}
def pr_refresh(cwd, branch, path):
"""Background half of pr_segment (`statusline.py --pr-refresh CWD BRANCH CACHE`): fetch the
branch's open PR and write its unresolved review-thread counts to the cache. `{owner}` /
`{repo}` are gh placeholders resolved from the repo at `cwd`. A failed fetch keeps the last
counts and flags them stale; a branch without an open PR clears them."""
state = _read_json(path)
state["ts"] = time.time()
try:
r = subprocess.run(
["gh", "api", "graphql", "-F", "owner={owner}", "-F", "repo={repo}",
"-f", f"branch={branch}", "-f", f"query={PR_QUERY}"],
cwd=cwd, capture_output=True, text=True, timeout=20,
)
if r.returncode != 0:
raise RuntimeError(r.stderr)
nodes = json.loads(r.stdout)["data"]["repository"]["pullRequests"]["nodes"]
state = {"ts": state["ts"]}
if nodes:
threads = nodes[0]["reviewThreads"]
# a thread belongs to whoever opened it: its first comment's author
authors = [((t["comments"]["nodes"] or [{}])[0].get("author") or {}).get("login") or ""
for t in threads["nodes"] if not t["isResolved"]]
state.update(number=nodes[0]["number"], open=len(authors),
codex=sum(1 for a in authors if PR_CODEX.search(a)),
more=threads["pageInfo"]["hasNextPage"])
except Exception:
state["error"] = True
try:
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
tmp = f"{path}.{os.getpid()}.tmp"
with open(tmp, "w") as fh:
json.dump(state, fh)
os.replace(tmp, path)
except Exception:
pass
def pr_segment(cwd, top):
"""Unresolved review threads (any author, outdated ones included) on the current branch's
open PR: `💬 #N ✓` when none are left, else the count with Codex's share broken out.
A GitHub round trip takes ~0.7 s, too slow for a render, so this only reads a per-branch
cache (~/.claude/cache/statusline.pr.<hash>.json) and, once it is older than PR_TTL,
spawns a detached `--pr-refresh` child to update it -- the new count shows on the next
render. PR-conversation comments are not counted: they have no resolved state, so a
summary or an LGTM would keep the count from ever reaching zero.
"""
try:
if not top or not shutil.which("gh"):
return ""
branch = _git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
if not branch or branch == "HEAD":
return ""
path = _pr_cache_path(top, branch)
state = _read_json(path)
if time.time() - (state.get("ts") or 0) > (PR_ERR_TTL if state.get("error") else PR_TTL):
claim = path + ".lock"
try:
claimed = time.time() - os.path.getmtime(claim) < PR_CLAIM
except OSError:
claimed = False
if not claimed:
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
with open(claim, "w"):
pass
# own session + no inherited stdout: Claude Code reads ours to EOF, and
# cancels an in-flight render when the next one triggers
subprocess.Popen(
[sys.executable, os.path.abspath(__file__), "--pr-refresh", cwd, branch, path],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True,
)
num = state.get("number")
if not num:
return ""
n, codex = state.get("open") or 0, state.get("codex") or 0
out = "💬 " + color(f"#{num} ", "gray")
if not n:
out += color("✓", "green")
else:
out += color(f"{n}{'+' if state.get('more') else ''}", "yellow", bold=True)
if codex:
out += color(" · ", "gray") + color(f"codex {codex}", "red")
if state.get("error"):
out += color(" ?", "gray") # last fetch failed: these counts are stale
return out
except Exception:
return ""
def model_segment(data):
name = (data.get("model") or {}).get("display_name")
if not name:
@@ -503,13 +669,12 @@ def velocity_segment(data):
return ""
def tokens_segment(data):
def tokens_segment(totals):
"""Cumulative session tokens: ↑ input (incl. cache reads/writes) and ↓ output."""
try:
totals = session_tokens(data)
if not totals:
return ""
tin, tout = totals
tin, tout = totals["in"], totals["out"]
if not (tin or tout):
return ""
return "🔢 " + color(f"↑{fmt_tokens(tin)}", "cyan") + " " + color(f"↓{fmt_tokens(tout)}", "green")
@@ -517,6 +682,22 @@ def tokens_segment(data):
return ""
def tps_segment(totals):
"""Decode speed in tokens/s: duration-weighted mean over the last few thinking blocks
(see session_tokens). Omitted until a thinking block ≥1s has completed -- so never
shown with thinking disabled."""
try:
samples = (totals or {}).get("tps") or []
toks = sum(s[0] for s in samples)
ms = sum(s[1] for s in samples)
if toks <= 0 or ms <= 0:
return ""
rate = toks / ms * 1000
return "⚡ " + color(f"{rate:.0f} t/s", bucket(rate, 20, 40, invert=True))
except Exception:
return ""
def cache_segment(usage, data=None):
"""Prompt-cache hit % (higher is better) + the cache TTL, `·cold` when the cache has
expired. Hit % from the 2.1.260+ payload `prompt_cache.hit_ratio` (a 0..1 fraction, null
@@ -697,6 +878,9 @@ def style_segment(data):
def main():
if sys.argv[1:2] == ["--pr-refresh"] and len(sys.argv) == 5:
pr_refresh(*sys.argv[2:])
return
try:
data = json.load(sys.stdin)
except Exception:
@@ -708,6 +892,7 @@ def main():
or os.getcwd()
)
usage = payload_usage(data) or last_usage(data)
totals = session_tokens(data)
top = git_top(cwd)
# One row: work (left) then system (right), joined. If it would overflow the
@@ -720,11 +905,13 @@ def main():
vim_segment(data),
dir_segment(cwd, top),
git_segment(cwd, top),
pr_segment(cwd, top),
model_segment(data),
context_segment(data, usage),
usage_segment(data),
cost_segment(data),
tokens_segment(data),
tokens_segment(totals),
tps_segment(totals),
cache_segment(usage, data),
) if s]
system = [s for s in (