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▃▅ˍ▃▅ ▃▃▁▂▁▮▯▯...▯5hx140.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✕140.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 ]]