diff --git a/CHANGELOG.md b/CHANGELOG.md index 224ac4e..e61b734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ ## Unreleased +## v0.31.0 — 2026-08-20 — the operator is not a reading + +**The ledger row was printing two different X's and calling them both +`x`.** `×` (U+00D7) has always been a cell — the window the pool will +not cover — and `x` was the ASCII letter standing in for multiplication. +The folded tail put them one column apart in the same red: `...×5hx28`, +where the first mark is a fact about your week and the second is +punctuation. It read like a font failure. The multiplication sign is +now `✕` (U+2715) everywhere the product multiplies: the folded future +`...▯5h✕28`, the pace `0.7✕`, the budget notice `- 19✕5h left`, and +`report`'s `(~4.1 ✕ 5h windows unused)`. Cells are the ink; the +operator is punctuation, and it is deliberately the lighter of the two +marks. + +**Why that codepoint and not a heavier one.** Row 2 right-anchors the +week row to line 1's edge by counting characters, so a glyph that +counts one and draws two overhangs the edge once per pace suffix — the +same constraint that keeps `≡` and `☠` on their single column. `╳` +(U+2573) is ambiguous-width and doubles under a CJK locale; `✖` +(U+2716) is Emoji=Yes and can fall through to a colour font. `✕` is +neutral-width and not an emoji. Both alternatives look stronger and are +one `MULT_GLYPH=╳` away if your terminal disagrees. + ## v0.30.0 — 2026-08-19 — the future folds **The 7d strip stopped drawing the future one cell at a time.** Thirty diff --git a/README.md b/README.md index e2a63b2..0514085 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Every component earns its place: | User tier | Neutral white-weight (MAX bold, PRO normal, dim otherwise) — identity, never a status color. Truncated display name. | | Quota | Integer percentages. The 5h badge always carries its reset time while a window is live — `5h[42%@14:30]` reads "42% used, resets at 14:30" — because on a 5h horizon the reset is the number you plan the current sitting around. Wall-clock, not a countdown, on purpose: Claude Code only re-renders the statusline on activity, so a relative "@1h38m" silently decays into a lie during idle gaps, while "@14:30" stays true in a frozen frame. (The 7d badge is hybrid: day-relative `@5d` while the reset is >= 24h out — decays one day per day, mild and narrow — switching to the same wall-clock `@04:00` inside the last day, where an `@6h`/`@<1h` countdown decayed by the hour exactly when pressure keeps the suffix visible.) When a window's utilization climbs between renders, a reverse-video `+N` token appears right after the badge for ~60s: `5h[44%@14:32]+2` means "you just burned 2%". A drop (window reset) stays quiet — the fresh low number is its own signal. **7d is forecast, not leveled**: a learned per-weekday burn profile (EWMA over your own usage history) plus your recent 24h burn project whether the quota outlasts the window — your heavy Tuesday counts more than a generic average. The verdict is color alone; under pressure the badge shows when relief arrives: `7d[44%@5d]` red means "at your pace, dry days before the reset 5 days from now"; inside the last day it reads `7d[92%@04:00]` — resets at 04:00. Cold start (<14 days history) falls back to window-average pacing. Recovery color when reset is imminent. **Model-scoped weekly quota**: when the usage API carries a per-model weekly limit (`limits[]`, `kind=weekly_scoped`) for the model your session is running, it renders right after the model+context block — `fabl5[1m][12%] fb[67%]` on a Fable 5 session, `op[33%]` on Opus — because the quota is a property of the model you're running, not of the account-wide 5h/7d cluster. It's a weekly number (same reset as the 7d badge), scoped to one model. Other models' scoped quotas stay hidden: only the limit constraining *this* session is signal. Supersedes the legacy `seven_day_opus`/`seven_day_sonnet` fields, which the API now sends as null. | | Extra usage | Monthly spend, limit, prepaid balance. `--extra auto` shows when quota runs out. | -| Week row | **A row of its own, under the badges**: `5h ▅█▃▮▯ 0.9x @23:00 7d ▅▁▂ ▃▅ˍ▃▅ …▮▯▯...▯5hx19 0.7x @Wed 09:00` — this sitting by the hour, the week as its 5h windows (day-gapped, the far future folded to a counted `...▯5hx19`), height = what each cell burned, `▮` now, `×` where the pool runs dry, each strip ending with its pace and reset. Reconstructed from your own usage log; `auto` shows it once there is history to show. See [Week row](#week-row). | +| Week row | **A row of its own, under the badges**: `5h ▅█▃▮▯ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ …▮▯▯...▯5h✕19 0.7✕ @Wed 09:00` — this sitting by the hour, the week as its 5h windows (day-gapped, the far future folded to a counted `...▯5h✕19`), height = what each cell burned, `▮` now, `×` where the pool runs dry, each strip ending with its pace and reset. Reconstructed from your own usage log; `auto` shows it once there is history to show. See [Week row](#week-row). | | Deadman | **Invisible until a switch is armed.** Surfaces [deadman](https://github.com/thevibeworks/deadman) — a dead man's switch that hands the session off when you stop responding. `[☠ armed 42m]` (dim) counts down to the auto-handoff; `[☠ warned 3m]` (yellow) means the phone warning went out; `[☠ due]` means the handoff fires imminently. Sits on the left lane next to the path — it describes this session's lifecycle, not a quota. One `command -v` when the tool is absent, one fast file read when present; nothing armed renders nothing. `--deadman off` disables it. | | Cache health | **Quiet until it bites.** Claude Code never re-renders an idle session, and while you work the prompt cache is always freshly ~1 TTL from expiry — so a proactive "expiring soon" isn't honestly observable, and `auto` spends no width on it. It speaks only when a rewrite actually happens: `≡!419k` the instant you resume onto a dead cache (idle longer than the TTL) or a mid-session prefix collapse — a 419k-token re-cache at ~20x the read rate (and the same burn on your 5h/7d quota on subscriptions). **Bold red past 200k** — the premium-band miss. `≡~` while a large prefix rebuilds. `--cache always` additionally keeps the freeze-safe deadline `≡@15:20` (last request + TTL; a past time in a frozen frame reads "expired at 15:20"). TTL defaults to 1h (claude.ai subscriber sessions) or 5m (API-key auth); an observed usage breakdown overrides it. The `≡` glyph (U+2261) reads as stacked cache layers — one terminal column, quiet and distinct. | @@ -255,7 +255,7 @@ right edge shared with line 1: ```text proj (main*) +84/-14 8m $6.72 fabl5[1m][██░░42%] fb[66%] [MAX|@work] 5h[38%@23:00] 7d[39%] -- 3x5h left · 20%/win 5h ▅█▃▮▯ 0.9x 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7x @Wed 09:00 +- 3✕5h left · 20%/win 5h ▅█▃▮▯ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00 ``` The 5h strip prints no reset: line 1's `5h[38%@23:00]` already carries @@ -272,12 +272,12 @@ edge and the full sentence sits flush-left beneath them: ```text proj (main*) fabl5[1m][██░░42%] fb[66%] [MAX|@work] 5h[38%@23:00] 7d[39%] - 5h ▅█▃▮▯ 0.9x @23:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7x @Wed 09:00 - - budget ~3x5h left · even 20%/win · heading ~52% + 5h ▅█▃▮▯ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00 + - budget ~3✕5h left · even 20%/win · heading ~52% ``` -Each strip ends with its **pace** (used ÷ elapsed: `0.7x` is on track, -`1.6x` caps early — dim below 1x, pressure-tinted from 1x, hidden for +Each strip ends with its **pace** (used ÷ elapsed: `0.7✕` is on track, +`1.6✕` caps early — dim below 1✕, pressure-tinted from 1✕, hidden for the first 15 min of a window) and the **reset** its right edge stands for (`@23:00` inside 24h, `@Wed 09:00` beyond) — axis labels for a timeline, not badges restated. When the row shows, the advisor's calm @@ -292,7 +292,7 @@ fire. left, the last a 3h stub), height = the 7d points that window burned, with a thin gap at each local midnight so days read as clusters — and a day that held five windows shows it — without a ruler. History draws in - full; the future folds: two hollow cells after `▮`, then `...▯5hx28` — + full; the future folds: two hollow cells after `▮`, then `...▯5h✕28` — 28 more 5h slots to the reset, all alike (`×` red when the tail projects dry). The `week` subcommand's wide ledger still draws every slot. @@ -304,7 +304,8 @@ fire. | `▮` | the cell you are in now | | `▯` | a cell still ahead of you — the hollow of `▮`, an empty slot waiting | | `×` | a cell the pool will not cover at the current pace (7d: the learned forecast's dry point, linear when untrained; 5h: linear, the same projection as the badge) | -| `...▯5hx28` | the folded 7d future: 28 more 5h slots to the reset, one token instead of 28 hollow cells (`×` red when the tail ends dry) | +| `...▯5h✕28` | the folded 7d future: 28 more 5h slots to the reset, one token instead of 28 hollow cells (`×` red when the tail ends dry) | +| `✕` | not a cell — the multiplication sign, the row's one operator (`...▯5h✕28`, `0.7✕`, `- 19✕5h left`). Deliberately not `×` (U+00D7), which is already a reading: cells are the ink, the operator is punctuation, and `...×5h✕28` has to say both at once. One terminal column and no emoji fallback, so the row still meets line 1's edge; override with `MULT_GLYPH` (`╳` and `✖` look stronger but are ambiguous-width and emoji-presentation respectively) | Burn cells take their badge's pressure color; everything else is neutral, so the row never adds an alarm channel of its own. Both strips are @@ -331,7 +332,7 @@ long one: ```text proj (main*) +84/-14 8m $6.72 fabl5[1m][██░░42%] fb[91%] [MAX|@work] 5h[38%@23:00] 7d[55%] -+ fb 91% vs 7d 55% · go op 5h ▅█▃▮▯ 0.9x 7d ▅▁▂ ▃▅ˍ▃▅ … ▆▆ˍ▂▮▯▯ 0.6x @Wed 09:00 ++ fb 91% vs 7d 55% · go op 5h ▅█▃▮▯ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ … ▆▆ˍ▂▮▯▯ 0.6✕ @Wed 09:00 + fb weekly 91% against 7d 55% · the model caps first, not the account; op sits at 33%, so run it for the bulk ``` @@ -364,7 +365,7 @@ clauses about one window can never disagree. | `+ 5h ~40m left · 70% unused` | Said only when the *week* is stranding capacity — an unspent 5h window is otherwise headroom, not waste, since the 5h window is a rate limit and not a budget. | | `+ ~62% will expire · go heavier` | On pace to strand a large chunk of the subscription. Speaks only to an engaged, unsqueezed session. | | `+ work 5h[8%] free` | A sibling account in the same shared home is idle while this one is pinned. | -| `- 19x5h left · 1.1%/win` | The calm budget: runway, what even looks like, where you land (long form). | +| `- 19✕5h left · 1.1%/win` | The calm budget: runway, what even looks like, where you land (long form). | `--notice off` keeps row 3 quiet; `--advisor off` silences both. `--check` and `--week` print the long form, since a terminal command has a whole @@ -381,7 +382,7 @@ $ ~/.claude/statusline.sh report # or --days 90 usage report - work (last 28d, 79 samples) 7d windows closed: 1 - Tue 07-28 00:00 used 51% expired 49% (~4.7 x 5h windows unused) + Tue 07-28 00:00 used 51% expired 49% (~4.7 ✕ 5h windows unused) 5h windows closed: 3 avg 95% at close 2 hit the cap exchange rate: one full 5h window = ~10.46% of the week (~9.6 windows/week, learned) @@ -508,7 +509,7 @@ run. Setting only `CLAUDE_CACHE_DIR` keeps the legacy single-dir behavior. npm exec --yes bats -- t/ ``` -417 tests across `t/statusline.bats` (405 statusline + integration) and +418 tests across `t/statusline.bats` (406 statusline + integration) and `t/install.bats` (12 installer). CI runs on push and PR to `main`. ## Project Structure diff --git a/docs/index.html b/docs/index.html index a29ebcf..e2ce771 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Claude Code Statusline — a meter for your Claude Code plan - + @@ -204,7 +204,7 @@

claude-code-statusline — a meter for your Claude Code plan

-
417 tests · jq + curl · no daemon · no telemetry
+
418 tests · jq + curl · no daemon · no telemetry
@@ -247,12 +247,12 @@

Read the meter

The 7-day window, forecast-colored. A learned per-weekday burn profile projects whether the pool outlasts the window — your heavy Tuesday counts more than a generic average. Red answers: at your pace, dry before the reset.
-
5h ▂▅█ 0.8x№ 0001
-
The 5h ledger — this sitting by the hour. Height is what each hour cost; now, ahead, ˍ ran-but-negligible, no sample. Ends with its pace: above 1x you cap before the reset.
+
5h ▂▅█ 0.8✕№ 0001
+
The 5h ledger — this sitting by the hour. Height is what each hour cost; now, ahead, ˍ ran-but-negligible, no sample. Ends with its pace: above 1✕ you cap before the reset.
-
7d ▃▅ˍ▃▅ ▃▃▁▂▁▯▯...▯5hx14 0.7x№ 0001
-
The week ledger — history in full, the future folded. Each cell one 5h window, day-gapped; after the now-marker the empty run folds to a counted tail: ...▯5hx14 answers "14 slots to the reset", and × red answers "the tail runs dry".
+
7d ▃▅ˍ▃▅ ▃▃▁▂▁▯▯...▯5h✕14 0.7✕№ 0001
+
The week ledger — history in full, the future folded. Each cell one 5h window, day-gapped; after the now-marker the empty run folds to a counted tail: ...▯5h✕14 answers "14 slots to the reset", and × red answers "the tail runs dry" — is the operator, × is a reading.
! 7d dry ~Tue 04:00 · hard stop№ 0008
@@ -277,7 +277,7 @@

Built like a meter should be

Numbers that don't lie

Wall-clock resets instead of decaying countdowns. A visible [░░░░░░0%] after /compact instead of a hidden bar. Stale data is badged, never passed off as fresh. Burn is a monotone envelope, never a re-earned dip.

Nothing running in the background

-

No daemon, no telemetry, no npm. It runs when Claude Code renders the prompt, reads stdin plus one shared cached API call, prints its rows, exits. 417 bats tests on the one file.

+

No daemon, no telemetry, no npm. It runs when Claude Code renders the prompt, reads stdin plus one shared cached API call, prints its rows, exits. 418 bats tests on the one file.

@@ -327,14 +327,14 @@

What it is not

// Rows beneath line 1 hang as one block: its right edge meets line 1's // edge, every row shares one left edge — exactly how the script pads them. // 5h ledger: 5 hour cells. 7d ledger: history in full, future folded. - var L5 = s('d','5h ') + s('g','▂▅█') + s('w','▮') + s('d2','▯') + s('d',' 0.8x'); - var L5b = s('d','5h ') + s('g','▂▅█▄') + s('w','▮') + s('d',' 0.9x'); - var L7 = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯...▯5hx14') + s('d',' 0.7x @Wed 09:00'); - var L7x = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯') + s('r','...×5hx14') + s('d',' 0.9x @Wed 09:00'); + var L5 = s('d','5h ') + s('g','▂▅█') + s('w','▮') + s('d2','▯') + s('d',' 0.8✕'); + var L5b = s('d','5h ') + s('g','▂▅█▄') + s('w','▮') + s('d',' 0.9✕'); + var L7 = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯...▯5h✕14') + s('d',' 0.7✕ @Wed 09:00'); + var L7x = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯') + s('r','...×5h✕14') + s('d',' 0.9✕ @Wed 09:00'); // the 5h strip prints no reset: line 1's 5h badge already carries it var LEDGER = L5 + ' ' + L7, LEDGER_B = L5b + ' ' + L7, LEDGER_X = L5b + ' ' + L7x; - var BUDGET = s('d2','- 10x5h left · 4.5%/win'); - var BUDGET_B = s('d2','- 10x5h left · 4.3%/win'); + var BUDGET = s('d2','- 10✕5h left · 4.5%/win'); + var BUDGET_B = s('d2','- 10✕5h left · 4.3%/win'); var PIN = s('c','+ ') + s('c','fb 91%') + s('c',' vs 7d 55% · go op'); var FLASH = s('c','+ fb weekly 91% against 7d 55% · the model caps first, not the account'); var PIN_DRY = s('r','! ') + s('r','7d dry ~Tue 04:00') + s('r',' · hard stop'); diff --git a/llms.txt b/llms.txt index 58f5497..d808009 100644 --- a/llms.txt +++ b/llms.txt @@ -4,11 +4,11 @@ > per-model weekly limits), context pressure, session cost, prompt-cache > health, and git activity in Claude Code's statusLine hook — plus a > week row (this 5h window by the hour, the 7d period as its 5h -> windows with the future folded to a counted `...▯5hx28` tail: where +> windows with the future folded to a counted `...▯5h✕28` tail: where > the points went), an advisor row that interprets the > numbers (cap projections, expiring-surplus and underuse advice, fleet > relief), and a `report` subcommand that ledgers what each closed window -> expired unused. 417 bats tests, deps are jq + curl, no daemon, no +> expired unused. 418 bats tests, deps are jq + curl, no daemon, no > telemetry. Key facts for agents: @@ -40,7 +40,7 @@ Key facts for agents: - [statusline.sh](https://github.com/thevibeworks/claude-code-statusline/blob/main/statusline.sh): the entire product - [install.sh](https://github.com/thevibeworks/claude-code-statusline/blob/main/install.sh): installer (curl | bash entrypoint) -- [t/statusline.bats](https://github.com/thevibeworks/claude-code-statusline/blob/main/t/statusline.bats): 405 statusline tests +- [t/statusline.bats](https://github.com/thevibeworks/claude-code-statusline/blob/main/t/statusline.bats): 406 statusline tests - [t/install.bats](https://github.com/thevibeworks/claude-code-statusline/blob/main/t/install.bats): 12 installer tests ## Optional diff --git a/statusline.sh b/statusline.sh index 55d0f2e..39ee338 100755 --- a/statusline.sh +++ b/statusline.sh @@ -78,6 +78,16 @@ ADVISOR_UNDERUSE_MIN_5H=25 # underuse advice only in an engaged session (5 # font tofus it. CACHE_GLYPH="${CACHE_GLYPH:-≡}" +# Multiplication sign for the ledger rows' two factors: the folded-future +# count (`...▯5h✕28`) and the pace (`0.6✕`). NOT × (U+00D7) — that glyph is +# already a strip cell, the one the pool will not cover, so reusing it would +# print `...×5h×28` and make an operator look like a reading. U+2715 is the +# lighter mark of the pair by design: cells are the ink, the operator is +# punctuation. eaw=N and not an emoji, so it stays one column and the padding +# math holds — ╳ (U+2573) is ambiguous-width (double under a CJK locale) and +# ✖ (U+2716) is Emoji=Yes (colour-font fallback); either would overhang row 2. +MULT_GLYPH="${MULT_GLYPH:-✕}" + # Unobserved-TTL default for the cache expiry deadline. Mirrors the CLI's own # rule (should1hCacheTTL, CC source): claude.ai subscribers in the REPL get # cache_control ttl:"1h" (confirmed on every breakpoint in live traces); @@ -2443,7 +2453,7 @@ run_usage_report() { esac if [ -n "$ppw" ]; then wins=$(awk -v w="$waste" -v p="$ppw" 'BEGIN{printf "%.1f", w / p}') - printf ' %s used %s%% expired %s%% (~%s x 5h windows unused)\n' \ + printf ' %s used %s%% expired %s%% (~%s '"$MULT_GLYPH"' 5h windows unused)\n' \ "$when" "$used_i" "$waste" "$wins" else printf ' %s used %s%% expired %s%%\n' "$when" "$used_i" "$waste" @@ -2623,7 +2633,7 @@ run_session_summary() { # ▮ the window you are in now # ▯ a window still ahead of you # × a window the pool will not cover at the current pace -# Count ▮ and what follows for the budget line's own "~Nx5h left". Past +# Count ▮ and what follows for the budget line's own "~N✕5h left". Past # cells come from usage.jsonl; unknown and idle stay different glyphs # because drawing a gap in the record as an idle session is the one lie # this row must not tell. The prospective glance beside report's @@ -2642,8 +2652,9 @@ run_session_summary() { # ▮ the cell you are in now # ▯ a cell still ahead of you (the hollow of ▮: an empty slot) # × a cell the pool will not cover at the current pace -# ...▯5hx28 the folded future: 28 more 5h slots to the reset, all alike -# (× red when the tail projects dry) — live 7d strip only +# ...▯5h✕28 the folded future: 28 more 5h slots to the reset, all alike +# (× red when the tail projects dry) — live 7d strip only. +# ✕ is the operator, × is a cell: the two never mean the same. # Unknown and idle are deliberately different glyphs: drawing a gap in the # record as an idle session is the one lie this row must not tell. # Shared by the live `--week` row and the `week` subcommand, so the two @@ -2654,7 +2665,7 @@ FIVE_CELLS=5 FIVE_CELL_SECS=3600 WEEK_CACHE_TTL_SECS=300 # The live row compresses the 7d strip's future run: after the now-marker it -# keeps WEEK_FUTURE_KEEP hollow cells, then folds the rest into `...▯5hx28` +# keeps WEEK_FUTURE_KEEP hollow cells, then folds the rest into `...▯5h✕28` # (count = slots to the reset; × red when the tail projects dry). History is # information, the future is all the same cell — but only fold when it hides # enough to matter, so a closing week still draws to its edge. @@ -2799,7 +2810,7 @@ build_ledger_strip() { tzoff_s=$(date +%z | awk '{ s=substr($0,1,1)=="-"?-1:1; h=substr($0,2,2)+0; m=substr($0,4,2)+0; print s*(h*3600+m*60) }') awk -v w="$cells_n" -v cs="$cell_secs" -v ps="$ps" -v now="$now" -v dry="$dry" \ -v gaps="$gaps" -v tz="$tzoff_s" -v fut="$fut" \ - -v minhide="$WEEK_FUTURE_MIN_HIDE" -v unit="$unit" \ + -v minhide="$WEEK_FUTURE_MIN_HIDE" -v unit="$unit" -v mult="$MULT_GLYPH" \ -v lo="${span_lo:--1}" -v hi="${span_hi:--1}" -v cells="$cells" \ -v C_FILL="$fill_color" -v C_DIM="$DIM" -v C_NOW="$BOLD" \ -v C_DRY="$RED" -v C_OFF="$RESET" ' @@ -2848,20 +2859,20 @@ build_ledger_strip() { # dries before the reset), count = slots left to the reset if (dry >= 0 && dry < w) { tg = "×"; tc = C_DRY } else { tg = "▯"; tc = C_DIM } - s = s C_OFF tc "..." tg unit "x" (w - lim) + s = s C_OFF tc "..." tg unit mult (w - lim) } print s C_OFF }' } -# 7d strip: 34 x 5h cells from the period start, day-gapped. $5 is +# 7d strip: 34 ✕ 5h cells from the period start, day-gapped. $5 is # week_history_cells' line; $6 folds the future tail after that many kept # cells (the live row passes WEEK_FUTURE_KEEP; the `week` report draws all). build_week_strip() { build_ledger_strip "$1" "$2" "$3" "$4" "$5" "$WEEK_CELLS" 18000 1 "${6:-0}" 5h } -# 5h strip: 5 x 1h cells of the current window. $5 is +# 5h strip: 5 ✕ 1h cells of the current window. $5 is # five_history_cells' line. build_five_strip() { build_ledger_strip "$1" "$2" "$3" "$4" "$5" "$FIVE_CELLS" "$FIVE_CELL_SECS" 0 @@ -2907,8 +2918,8 @@ five_period_start() { } # Tail of a strip: pace and the axis label of its right end (the reset). -# pace = used / elapsed-fraction; >1x means the pool caps before the reset. -# Dim below 1x, pressure-tinted from 1x (status lane), hidden while the +# pace = used / elapsed-fraction; >1✕ means the pool caps before the reset. +# Dim below 1✕, pressure-tinted from 1✕ (status lane), hidden while the # window is too young to judge (ADVISOR_PACE_MIN_ELAPSED). Reset is wall # clock: `@04:00` inside 24h, `@Wed 09:00` beyond — an axis label for a # timeline that ends there. $5 = "hide" drops it: when the badge above @@ -2922,7 +2933,7 @@ strip_tail() { pace=$(awk -v u="$pct" -v e="$elapsed" -v l="$length" 'BEGIN{ printf "%.1f", (u/100)/(e/l) }') band=$(awk -v p="$pace" 'BEGIN{ print (p>=1.5?2:(p>=1.0?1:0)) }') case "$band" in 2) tint="$RED" ;; 1) tint="$YELLOW" ;; *) tint="$DIM" ;; esac - out=" ${tint}${pace}x${RESET}" + out=" ${tint}${pace}${MULT_GLYPH}${RESET}" fi if [ "$reset_mode" = "hide" ]; then printf '%s' "$out" @@ -2937,7 +2948,7 @@ strip_tail() { printf '%s %s@%s%s' "$out" "$DIM" "$when" "$RESET" } -# The live row: `5h ▂▅█▃▮▯▯▯▯▯ 0.6x @04:00 7d ▅▁▂▃▅ ˍ▃▅▃▃ …▮▯▯ 0.7x @Wed 09:00` under the badges — this +# The live row: `5h ▂▅█▃▮▯▯▯▯▯ 0.6✕ @04:00 7d ▅▁▂▃▅ ˍ▃▅▃▃ …▮▯▯ 0.7✕ @Wed 09:00` under the badges — this # sitting at the left, the week at the right, one grammar. Prints nothing # when there is no live window, or — in auto mode — when the log holds no # sample for either period yet (a row of ░░░▮▯▯ says nothing the badges do @@ -3535,7 +3546,7 @@ build_advisor_fleet_hint() { # mid-week underuse, engaged sessions # only — reaches exactly the users # who can act on it -# budget ~19x5h left · even 1.1%/win · heading ~52% +# budget ~19✕5h left · even 1.1%/win · heading ~52% # always-mode calm line (shared # budget frame with claude.py's # watch advisor); in the last @@ -4061,9 +4072,9 @@ notice_collect() { else local even even=$(awk -v h="$surplus" -v w="$windows" 'BEGIN{printf "%.1f", h/w}') - notice_add 10 '-' acct "acct.budget.${windows}" "${windows}x5h" \ - "${windows}x5h left · ${even}%/win" \ - "budget ~${windows}x5h left · even ${even}%/win${heading_part}" + notice_add 10 '-' acct "acct.budget.${windows}" "${windows}${MULT_GLYPH}5h" \ + "${windows}${MULT_GLYPH}5h left · ${even}%/win" \ + "budget ~${windows}${MULT_GLYPH}5h left · even ${even}%/win${heading_part}" fi fi fi diff --git a/t/helpers.bash b/t/helpers.bash index 1f90c31..0400cfc 100644 --- a/t/helpers.bash +++ b/t/helpers.bash @@ -59,6 +59,7 @@ CACHE_BREAK_DROP_PCT=5 CACHE_TTL_DEFAULT="1h" CACHE_HEAVY_TOKENS=200000 CACHE_GLYPH="≡" +MULT_GLYPH="✕" ADVISOR_PACE_MIN_ELAPSED=900 ADVISOR_FLEET_FRESH_SECS=3600 ADVISOR_FLEET_FREE_PCT=40 diff --git a/t/statusline.bats b/t/statusline.bats index 69dde8b..3ab6c56 100644 --- a/t/statusline.bats +++ b/t/statusline.bats @@ -1013,7 +1013,7 @@ _write_ledger_fixture() { # dir run run_usage_report 28 [ "$status" -eq 0 ] # 38% expired / 10 ppw = 3.8 windows' worth - [[ "$output" == *"(~3.8 x 5h windows unused)"* ]] + [[ "$output" == *"(~3.8 ✕ 5h windows unused)"* ]] [[ "$output" == *"one full 5h window = ~10.00% of the week"* ]] [[ "$output" == *"week in progress: 44% used"* ]] # 44% + 4d x 10%/day: heading ~83-84% @@ -1120,7 +1120,7 @@ _write_ledger_fixture() { # dir # the row carries its own remaining/reset; no ruler, no day labels [[ "$plain" =~ ▯+\ +[0-9]+[dhm].*@[A-Z][a-z][a-z]\ [0-9]{2}:[0-9]{2} ]] [[ "$plain" != *"'-------'"* ]] - [[ "$plain" == *"budget ~24x5h left"* ]] + [[ "$plain" == *"budget ~24✕5h left"* ]] [[ "$plain" != *"stale"* ]] rm -rf "$tmpdir" } @@ -1136,7 +1136,7 @@ _write_ledger_fixture() { # dir [ "$status" -eq 0 ] plain=$(strip_ansi "$output") bar=$(echo "$plain" | head -1 | sed 's/^7d 55% //; s/ .*$//' | tr -d ' ') - stated=$(echo "$plain" | sed -n 's/.*budget ~\([0-9]*\)x5h left.*/\1/p') + stated=$(echo "$plain" | sed -n 's/.*budget ~\([0-9]*\)✕5h left.*/\1/p') counted=$(echo "$bar" | grep -o '▮.*' | grep -o . | wc -l) [ "$counted" -eq "$stated" ] rm -rf "$tmpdir" @@ -1206,7 +1206,7 @@ _seed_week_store() { plain=$(strip_ansi "$row") # no live 5h window in the fixture: the 7d strip alone [[ "$plain" == "7d "* ]] - strip=$(printf '%s' "${plain#7d }" | sed 's/ [0-9.]*x @.*$//; s/ @.*$//') + strip=$(printf '%s' "${plain#7d }" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//') bar=$(printf '%s' "$strip" | tr -d ' ') [ "$(echo "$bar" | grep -o . | wc -l)" -eq 34 ] [[ "$bar" =~ [▁▂▃▄▅▆▇█]ˍ[▁▂▃▄▅▆▇█] ]] @@ -1240,17 +1240,17 @@ _seed_week_store() { rm -rf "$tmpdir" } -@test "strip_tail: pace from 1x tints, below 1x dims, young window hides it; reset is an axis label" { +@test "strip_tail: pace from 1✕ tints, below 1✕ dims, young window hides it; reset is an axis label" { now=$(date +%s) - # 50% used, 2h30 into a 5h window -> 1.0x (yellow); reset inside 24h -> @HH:MM + # 50% used, 2h30 into a 5h window -> 1.0✕ (yellow); reset inside 24h -> @HH:MM t=$(strip_tail 50 9000 18000 "$now") plain=$(strip_ansi "$t") - [[ "$plain" =~ ^\ 1\.0x\ @[0-9]{2}:[0-9]{2}$ ]] - [[ "$t" == *"${YELLOW}1.0x"* ]] - # 10% at 2h30 -> 0.2x dim - t=$(strip_tail 10 9000 18000 "$now"); [[ "$t" == *"${DIM}0.2x"* ]] - # 80% at 2h30 -> 1.6x red - t=$(strip_tail 80 9000 18000 "$now"); [[ "$t" == *"${RED}1.6x"* ]] + [[ "$plain" =~ ^\ 1\.0✕\ @[0-9]{2}:[0-9]{2}$ ]] + [[ "$t" == *"${YELLOW}1.0✕"* ]] + # 10% at 2h30 -> 0.2✕ dim + t=$(strip_tail 10 9000 18000 "$now"); [[ "$t" == *"${DIM}0.2✕"* ]] + # 80% at 2h30 -> 1.6✕ red + t=$(strip_tail 80 9000 18000 "$now"); [[ "$t" == *"${RED}1.6✕"* ]] # 10 minutes in: too young for a pace, the reset still labels the edge plain=$(strip_ansi "$(strip_tail 10 17400 18000 "$now")") [[ "$plain" =~ ^\ @[0-9]{2}:[0-9]{2}$ ]] @@ -1268,7 +1268,7 @@ _seed_week_store() { usage=$(jq -c --arg r "$reset_5h" '.five_hour.resets_at = $r | .five_hour.utilization = 40' "$tmpdir/usage.cache") plain=$(strip_ansi "$(build_week_row "$usage" auto)") # 5h resets inside 24h -> @HH:MM; 7d resets in 31h -> @Ddd HH:MM - [[ "$plain" =~ ^5h\ .*▮.*\ [0-9]\.[0-9]x\ @[0-9]{2}:[0-9]{2}\ \ 7d\ .*▮.*\ [0-9]\.[0-9]x\ @[A-Z][a-z][a-z]\ [0-9]{2}:[0-9]{2}$ ]] + [[ "$plain" =~ ^5h\ .*▮.*\ [0-9]\.[0-9]✕\ @[0-9]{2}:[0-9]{2}\ \ 7d\ .*▮.*\ [0-9]\.[0-9]✕\ @[A-Z][a-z][a-z]\ [0-9]{2}:[0-9]{2}$ ]] rm -rf "$tmpdir" } @@ -1339,10 +1339,10 @@ _seed_week_store() { CLAUDE_ACCOUNT_DIR="$tmpdir" reset_7d=$(date -u -d '+5 days' '+%Y-%m-%dT%H:%M:%SZ') usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":20,"resets_at":"%s"}}' "$(date +%s)" "$reset_7d") - plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*x @.*$//; s/ @.*$//') + plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//') # everything from ▮ to the end is one contiguous run: kept hollow cells, # then the folded tail (no gap ever lands right after the now-marker) - [[ "${plain#*▮}" =~ ^▯+\.\.\.▯5hx[0-9]+$ ]] + [[ "${plain#*▮}" =~ ^▯+\.\.\.▯5h✕[0-9]+$ ]] rm -rf "$tmpdir" } @@ -1351,9 +1351,9 @@ _seed_week_store() { CLAUDE_ACCOUNT_DIR="$tmpdir" reset_7d=$(date -u -d '+5 days' '+%Y-%m-%dT%H:%M:%SZ') usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":20,"resets_at":"%s"}}' "$(date +%s)" "$reset_7d") - plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*x @.*$//; s/ @.*$//' | tr -d ' ') + plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//' | tr -d ' ') strip="${plain#7d}" - [[ "$strip" =~ ^(.*)\.\.\.▯5hx([0-9]+)$ ]] + [[ "$strip" =~ ^(.*)\.\.\.▯5h✕([0-9]+)$ ]] drawn=$(printf '%s' "${BASH_REMATCH[1]}" | grep -o . | wc -l) [ $((drawn + BASH_REMATCH[2])) -eq 34 ] # exactly WEEK_FUTURE_KEEP hollow cells stay visible before the fold @@ -1367,11 +1367,26 @@ _seed_week_store() { _write_profile_cache "$tmpdir" 21 30 0 # 30%/day learned: dries days early reset_7d=$(date -u -d '+5 days' '+%Y-%m-%dT%H:%M:%SZ') usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":50,"resets_at":"%s"}}' "$(date +%s)" "$reset_7d") - plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*x @.*$//; s/ @.*$//' | tr -d ' ') - [[ "$plain" =~ \.\.\.×5hx[0-9]+$ ]] + plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//' | tr -d ' ') + [[ "$plain" =~ \.\.\.×5h✕[0-9]+$ ]] + # the dry cell and the operator sit side by side in this token and must + # never be the same mark: × is a reading, ✕ is punctuation + [[ "$plain" == *"×5h✕"* ]] rm -rf "$tmpdir" } +@test "MULT_GLYPH: one column, distinct from the dry cell, overridable" { + # row 2 right-anchors the week row by ${#week_plain}, a character count — + # a glyph carrying a variation selector counts 1 and draws 2, overhanging + # the edge once per pace suffix. Guard the default, not the override. + [ "${#MULT_GLYPH}" -eq 1 ] + [ "$MULT_GLYPH" = "✕" ] + [ "$MULT_GLYPH" != "×" ] + now=$(date +%s) + t=$(MULT_GLYPH=╳; strip_ansi "$(strip_tail 50 9000 18000 "$now")") + [[ "$t" == *"1.0╳"* ]] +} + @test "build_week_row: the 5h strip credits each hour with the points it added" { tmpdir=$(mktemp -d) CLAUDE_ACCOUNT_DIR="$tmpdir" @@ -1390,7 +1405,7 @@ _seed_week_store() { done plain=$(strip_ansi "$(build_week_row "$usage" auto)") [[ "$plain" == "5h "* ]] - five="${plain#5h }"; five="${five%% 7d*}"; five=$(printf '%s' "$five" | sed 's/ [0-9.]*x @.*$//; s/ @.*$//') + five="${plain#5h }"; five="${five%% 7d*}"; five=$(printf '%s' "$five" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//') [ "$five" = "▃▅ˍ█▮" ] # 30% in 4h+ -> dry past the reset: holds, so no × in this window [[ "$five" != *×* ]] @@ -1403,8 +1418,8 @@ _seed_week_store() { reset_7d=$(date -u -d '+5 days' '+%Y-%m-%dT%H:%M:%SZ') usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":20,"resets_at":"%s"}}' "$(date +%s)" "$reset_7d") [ -z "$(build_week_row "$usage" auto)" ] - plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*x @.*$//; s/ @.*$//' | tr -d ' ') - [[ "$plain" =~ ^7d░+▮▯+\.\.\.▯5hx[0-9]+$ ]] + plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//' | tr -d ' ') + [[ "$plain" =~ ^7d░+▮▯+\.\.\.▯5h✕[0-9]+$ ]] [ -z "$(build_week_row "$usage" off)" ] # no live 7d window: nothing, even in always mode [ -z "$(build_week_row '{"seven_day":{"utilization":20}}' always)" ] @@ -1449,9 +1464,9 @@ _seed_week_store() { [ "$(compact_text "$t" 200)" = "$t" ] [ "$(compact_text "$t" 40)" = "! 5h caps ~05:18, 52m before reset" ] [ "$(compact_text "$t" 20)" = "! 5h caps ~05:18" ] - b="- budget ~3x5h left · even 20%/win · heading ~52%" - [ "$(compact_text "$b" 36)" = "- budget ~3x5h left · even 20%/win" ] - [ "$(compact_text "$b" 19)" = "- budget ~3x5h left" ] + b="- budget ~3✕5h left · even 20%/win · heading ~52%" + [ "$(compact_text "$b" 36)" = "- budget ~3✕5h left · even 20%/win" ] + [ "$(compact_text "$b" 19)" = "- budget ~3✕5h left" ] # no joint left: hard cut with an ellipsis, never a mid-word lie [ "$(compact_text "- budget last window is long" 16)" = "- budget last w…" ] # under 16 columns nothing honest fits @@ -3406,10 +3421,10 @@ JSON reset_7d=$(date -u -d '+4 days 22 hours' '+%Y-%m-%dT%H:%M:%SZ') usage="{\"five_hour\":{\"utilization\":20,\"resets_at\":\"$reset_5h\"},\"seven_day\":{\"utilization\":21,\"resets_at\":\"$reset_7d\"}}" plain=$(strip_ansi "$(build_advisor_line "$usage" always)") - [[ "$plain" =~ ^-\ 24x5h\ left\ ·\ 3\.3%/win$ ]] + [[ "$plain" =~ ^-\ 24✕5h\ left\ ·\ 3\.3%/win$ ]] # the whole sentence is still there for surfaces with a line to spend long=$(strip_ansi "$(notice_long_line "$(notice_collect "$usage" always)")") - [[ "$long" =~ ^-\ budget\ ~24x5h\ left\ ·\ even\ 3\.3%/win\ ·\ heading\ ~70%$ ]] + [[ "$long" =~ ^-\ budget\ ~24✕5h\ left\ ·\ even\ 3\.3%/win\ ·\ heading\ ~70%$ ]] } @test "build_advisor_line: budget degrades to plain headroom in the last window" { @@ -3602,8 +3617,8 @@ JSON now=$(date +%s) show=$(strip_ansi "$(strip_tail 50 3600 18000 "$now" show)") hide=$(strip_ansi "$(strip_tail 50 3600 18000 "$now" hide)") - [[ "$show" =~ ^\ [0-9]\.[0-9]x\ @[0-9]{2}:[0-9]{2}$ ]] - [[ "$hide" =~ ^\ [0-9]\.[0-9]x$ ]] + [[ "$show" =~ ^\ [0-9]\.[0-9]✕\ @[0-9]{2}:[0-9]{2}$ ]] + [[ "$hide" =~ ^\ [0-9]\.[0-9]✕$ ]] # default is still show: `--week` on its own has no badge above it [ "$(strip_ansi "$(strip_tail 50 3600 18000 "$now")")" = "$show" ] } @@ -3621,7 +3636,7 @@ JSON l2=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 2p)") # line 1 carries the 5h reset, so the 5h strip does not repeat it re_badge='5h\[40%@[0-9]{2}:[0-9]{2}\]' - re_strip='5h [^ ]+ [0-9]\.[0-9]x 7d' + re_strip='5h [^ ]+ [0-9]\.[0-9]✕ 7d' re_seven='7d .*[0-9]{2}:[0-9]{2}$' [[ "$l1" =~ $re_badge ]] [[ "$l2" =~ $re_strip ]]