[Update] bulk update

This commit is contained in:
2026-09-17 20:45:06 +02:00
committed by Coja
parent 3ec2503f38
commit ed7e34552d
158 changed files with 3258 additions and 2419 deletions
+3 -3
View File
@@ -8,13 +8,13 @@ resize), trailing (lowest-priority) segments are dropped from the right until it
Wide — everything:
```
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Opus 4.8 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ 💾 81%
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ 💾 81%
```
Narrow — trailing segments trimmed:
```
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Opus 4.8 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h
```
Every segment is defensive: if its data is missing or a command fails, the segment is
@@ -29,7 +29,7 @@ is supplementary to the main colored value.
| 📁 | **Directory** | Current working dir, with `~` for home | cyan |
| 🌿 | **Git** | `repo branch` + `*` if dirty + `↑N`/`↓N` ahead/behind upstream | repo = magenta; branch = green when clean, yellow + red `*` when dirty; `↑` cyan, `↓` yellow. Omitted outside a repo |
| 🤖 | **Model** | Active model display name + `· effort` level (`low`/`medium`/`high`/`xhigh`) | name = blue, effort = light gray; effort omitted for models without the param |
| 📝 | **Context** | `% of context window used` + `(Nk)` tokens | window = 1M for `[1m]` models, else 200k. Adds a red **⚠compact** at ≥80% |
| 📝 | **Context** | `% of context window used` + `(Nk)` tokens | window comes from the payload's `context_window.context_window_size`, falling back to `(1M …)` in the display name, else 200k — **not** from `model.id`, which arrives with any `[1m]` suffix already stripped. Adds a red **⚠compact** at ≥80% |
| 📊 | **5h usage** | `% of the 5-hour rolling limit used` + time until it resets | from `rate_limits.five_hour`; Pro/Max only, and absent until the first API response of a session |
| 💰 | **Cost** | `$` session cost so far + `$/h` burn rate | burn rate shown once the session exceeds ~30s |
| 💾 | **Disk** | `% used` of the filesystem at the cwd | from `statvfs` |
+68 -9
View File
@@ -3,17 +3,19 @@
Reads the hook JSON from stdin, extracts the edited file path, and runs a
language-appropriate formatter -- but, to avoid imposing a style on a project
that never asked for it, opinionated formatters (stylua, ruff, prettier, taplo)
only run when the project opts in via a config file discoverable by walking up
from the edited file. Canonical single-style toolchains run on sight: gofmt and
rustfmt on their go.mod / Cargo.toml marker; fish_indent and shfmt whenever the
tool itself is installed (shell and fish have one de-facto style, no config).
that never asked for it, every formatter here only runs when the project opts in
via a config file discoverable by walking up from the edited file: stylua, ruff,
prettier and taplo on their own configs, gofmt and rustfmt on go.mod / Cargo.toml,
shfmt and fish_indent on .editorconfig. No marker, no formatting -- a repo that
hand-formats its shell keeps its own layout.
Best-effort: always exits 0 so a format hiccup never blocks an edit. jq is not
guaranteed on the host, hence python for the stdin JSON parse.
"""
import fnmatch
import json
import os
import re
import shutil
import subprocess
import sys
@@ -31,6 +33,47 @@ def find_up(start, names):
d = parent
def _expand_braces(pat):
"""`a{b,c}` -> [`ab`, `ac`] (editorconfig sections use {,} alternation)."""
m = re.search(r"\{([^{}]*)\}", pat)
if not m:
return [pat]
out = []
for alt in m.group(1).split(","):
out.extend(_expand_braces(pat[: m.start()] + alt + pat[m.end():]))
return out
def editorconfig_covers(path):
"""True if the nearest ancestor .editorconfig has a section matching `path`.
Mere existence is NOT opt-in: this repo's .editorconfig is deliberately scoped
to `[{*.sh,dotsync,dotpub}]` with no [*] block precisely so fish files stay
untouched, and a generic upstream .editorconfig must not switch a formatter on
for filetypes it never mentions. Patterns without a `/` match the basename
(the editorconfig convention); ones with a `/` match the path relative to the
.editorconfig's directory.
"""
d = find_up(path, [".editorconfig"])
if not d:
return False
name = os.path.basename(path)
rel = os.path.relpath(os.path.abspath(path), d)
try:
with open(os.path.join(d, ".editorconfig"), errors="ignore") as fh:
for line in fh:
line = line.strip()
if not (line.startswith("[") and line.endswith("]")):
continue
for pat in _expand_braces(line[1:-1]):
target = rel if "/" in pat else name
if fnmatch.fnmatch(target, pat.lstrip("/")):
return True
except Exception:
pass
return False
def has_toml_section(start, filename, section):
"""True if the nearest ancestor `filename` contains `section`."""
d = find_up(start, [filename])
@@ -89,14 +132,26 @@ def rustfmt_cmd(path):
return None
# Shell and fish are opt-in too, gated on an .editorconfig SECTION that matches the file
# (shfmt reads .editorconfig natively for its settings, so the marker and the tool agree).
# "One canonical style" turned out not to mean "the style the project uses": 3 of ~/.dots's
# 25 fish files are not fish_indent-clean, so running these on sight rewrote whole files and
# buried a 40-line change in a 200-line diff. ~/.dots opts its shell in via
# `[{*.sh,dotsync,dotpub}]` while its fish files stay hand-formatted — existence of the
# .editorconfig alone must not turn either formatter on.
# Neovim agrees: conform.nvim there lists lua/css/html/ts/js only — never sh or fish.
def fish_cmd(path):
exe = shutil.which("fish_indent") # ships with fish, one canonical style
return [exe, "-w", path] if exe else None
exe = shutil.which("fish_indent")
if exe and editorconfig_covers(path):
return [exe, "-w", path]
return None
def shfmt_cmd(path):
exe = shutil.which("shfmt") # de-facto shell formatter, sane defaults
return [exe, "-w", path] if exe else None
exe = shutil.which("shfmt")
if exe and editorconfig_covers(path):
return [exe, "-w", path]
return None
def taplo_cmd(path):
@@ -133,6 +188,10 @@ def main():
path = ti.get("file_path") or tr.get("filePath") or ""
if not path or not os.path.isfile(path):
return
# Resolve symlinks before walking for markers: deployed dotfiles get edited via
# their ~/.config/... links, but the opt-in configs live beside the real file in
# the repo — a lexical walk from the link's path would never find them.
path = os.path.realpath(path)
handler = HANDLERS.get(os.path.splitext(path)[1].lower())
if not handler:
return
+6
View File
@@ -2,6 +2,12 @@
"$schema": "https://www.schemastore.org/claude-code-keybindings.json",
"$docs": "https://code.claude.com/docs/en/keybindings",
"bindings": [
{
"context": "Chat",
"bindings": {
"shift+enter": "chat:newline"
}
},
{
"context": "Global",
"bindings": {
+19 -12
View File
@@ -25,8 +25,11 @@ repo=$(dirname "$(readlink -f "$SRC/link.sh")")
# If link.sh itself has broken out of stow into a plain file, $repo resolves to $SRC and
# the drift heal below would relink each drifted file onto ITSELF — destroying the only
# copy of its content. Refuse to continue.
if [ "$repo" = "$SRC" ]; then
# copy of its content. Refuse to continue. Both sides are canonicalized before comparing:
# $repo comes back from readlink -f, so a symlinked component anywhere in the path (say
# /home -> /var/home) would otherwise make the two spellings differ and slip the guard.
src_real=$(readlink -f "$SRC" || printf '%s' "$SRC")
if [ "$repo" = "$src_real" ]; then
echo "error: link.sh itself is a plain file (broken out of stow) — re-stow first:" >&2
echo " cd ~/.dots && ./install.sh" >&2
exit 1
@@ -48,11 +51,13 @@ for item in $items; do
if [ -f "$DST/$item" ] && [ ! -L "$DST/$item" ]; then
same=0
case "$item" in
*.json)
python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))==json.load(open(sys.argv[2])) else 1)' \
"$DST/$item" "$SRC/$item" 2>/dev/null && same=1 ;;
*)
cmp -s "$DST/$item" "$SRC/$item" && same=1 ;;
*.json)
python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))==json.load(open(sys.argv[2])) else 1)' \
"$DST/$item" "$SRC/$item" 2>/dev/null && same=1
;;
*)
cmp -s "$DST/$item" "$SRC/$item" && same=1
;;
esac
if [ "$same" != 1 ]; then
echo "DRIFT ~/.claude/$item is a real file that differs from the repo copy —" >&2
@@ -78,11 +83,13 @@ for item in $items; do
same=0
if [ -f "$repo/$item" ]; then
case "$item" in
*.json)
python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))==json.load(open(sys.argv[2])) else 1)' \
"$SRC/$item" "$repo/$item" 2>/dev/null && same=1 ;;
*)
cmp -s "$SRC/$item" "$repo/$item" && same=1 ;;
*.json)
python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))==json.load(open(sys.argv[2])) else 1)' \
"$SRC/$item" "$repo/$item" 2>/dev/null && same=1
;;
*)
cmp -s "$SRC/$item" "$repo/$item" && same=1
;;
esac
fi
if [ "$same" = 1 ]; then
+23 -14
View File
@@ -34,19 +34,7 @@
],
"defaultMode": "auto"
},
"model": "opus[1m]",
"enableWorkflows": false,
"skipWorkflowUsageWarning": true,
"enabledPlugins": {
"typescript-lsp@claude-plugins-official": true,
"frontend-design@claude-plugins-official": true
},
"statusLine": {
"type": "command",
"command": "/usr/bin/env python3 ~/.claude/statusline.py",
"padding": 0,
"hideVimModeIndicator": true
},
"model": "claude-fable-5[1m]",
"hooks": {
"PostToolUse": [
{
@@ -65,9 +53,21 @@
"worktree": {
"baseRef": "fresh"
},
"enableWorkflows": false,
"statusLine": {
"type": "command",
"command": "/usr/bin/env python3 ~/.claude/statusline.py",
"padding": 0,
"hideVimModeIndicator": true
},
"enabledPlugins": {
"typescript-lsp@claude-plugins-official": true,
"frontend-design@claude-plugins-official": true
},
"effortLevel": "xhigh",
"advisorModel": "fable",
"tui": "fullscreen",
"skipWorkflowUsageWarning": true,
"theme": "dark",
"editorMode": "vim",
"verbose": true,
@@ -75,5 +75,14 @@
"remoteControlAtStartup": false,
"inputNeededNotifEnabled": false,
"agentPushNotifEnabled": false,
"skipAutoPermissionPrompt": true
"skipAutoPermissionPrompt": true,
"autoMode": {
"environment": [
"### User-specific",
"**Primary use of Claude Code**: software development and dotfiles maintenance on personal Arch Linux hosts",
"**Trusted repo**: the repository the session is opened in; treat it as private unless it is clearly public, and treat content ported in from outside it as not its own work",
"**Sensitive remote targets**: any namespace, host, or container whose name carries `prod` or `production` as a whole word or name segment",
"routine under a repo's own Makefile / pnpm / npm scripts is expected local development activity"
]
}
}
+66 -17
View File
@@ -90,25 +90,49 @@ def term_cols():
# --- shared transcript usage ------------------------------------------------
def _scan_usage(text):
"""Last `message.usage` block among these JSONL lines, or {}."""
usage = {}
for line in text.splitlines():
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
TRANSCRIPT_TAIL = 1 << 20 # 1 MiB
def last_usage(data):
"""Most recent `message.usage` block from the transcript, or {}."""
"""Most recent `message.usage` block from the transcript, or {}.
Reads only the tail: this runs on every status refresh and transcripts grow to
tens of MB, so re-parsing the whole file each time is unbounded work for a value
that lives at the end. Falls back to a full scan if the tail holds no usage block
(a long stretch of tool results can push it past the window).
"""
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"]
with open(path, "rb") as fh:
size = fh.seek(0, os.SEEK_END)
start = max(0, size - TRANSCRIPT_TAIL)
fh.seek(start)
text = fh.read().decode("utf-8", "ignore")
if start:
text = text.partition("\n")[2] # drop the partial first line
usage = _scan_usage(text)
if not usage and start:
with open(path, "r", errors="ignore") as fh:
usage = _scan_usage(fh.read())
return usage
except Exception:
return {}
@@ -193,7 +217,14 @@ def git_segment(cwd, 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())
# `status --porcelain` walks the whole work tree, so in a big repo it can blow
# the 1s timeout -- in its own try, because letting that escape dropped the repo
# and branch we already had: a decoration failing must not take the segment down.
try:
dirty = bool(_git(cwd, "status", "--porcelain").stdout.strip())
except Exception:
dirty = False
# ahead/behind vs upstream (omitted when no upstream is configured)
ab = ""
@@ -231,6 +262,25 @@ def model_segment(data):
return out
def context_window(data):
"""Size of this session's context window, in tokens.
Claude Code resolves the `[1m]` model suffix away before handing the status line
its JSON -- `model.id` is always the bare API id ("claude-fable-5") -- so sniffing
the id for "1m" could never detect a 1M session and every one of them was measured
against a 200k window (5x overstated %, ⚠compact from ~16% real usage). The payload
carries the real number; display_name ("Opus 4.8 (1M context)") is the fallback.
"""
cw = data.get("context_window")
if isinstance(cw, dict):
for key in ("context_window_size", "max_tokens", "size"):
val = cw.get(key)
if isinstance(val, (int, float)) and val > 0:
return int(val)
name = ((data.get("model") or {}).get("display_name") or "").lower()
return 1_000_000 if "1m" in name else 200_000
def context_segment(data, usage):
try:
if not usage:
@@ -240,8 +290,7 @@ def context_segment(data, usage):
+ 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
window = context_window(data)
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)