Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y jq
- name: Run tests
run: npm exec --yes bats -- t/
- name: Shellcheck (error severity; warnings are tracked, not gated)
run: shellcheck -S error statusline.sh install.sh
# `make check` = shellcheck (error severity; warnings are tracked, not
# gated) then the bats suite — the same two commands a contributor runs.
- name: make check
run: make check
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,77 @@

## Unreleased

## v0.34.0 — 2026-08-24 — five slots, and a window you are in is not one you have left

**The 5h strip is five cells again — but the future in them is empty, not
judged.** v0.33.0 was right that `5h ▮▯×××` was unreadable and wrong about
which half to cut. The `×` was the problem: a linear projection dressed as
ink, saying a third time what the badge (`5h[38%@23:00]`) and the notice
(`5h caps ~14:20`) already say with better gates and an exact time. The
hollow cells were never the problem — they were the axis. Ending the strip
at `▮` took the ruler away with the forecast, and left a bar that grew an
hour at a time and answered "how long have I got" with nothing.

So: five slots, always, one per hour, no dry cell in any of them.
`5h ▃▃▮▯▯` is two whole hours after this one, read off the row without
arithmetic and without a second glance at the clock. Fixed width is the
other half of it — the row holds its shape for the life of the window
instead of reflowing every hour, which is the difference between an axis
and a bar that grows at you. An empty cell is a fact; a `×` is a guess, and
only one of those belongs in a ledger.

`▮` now rides the real clock rather than the grid. `five_period_start`
rounds to five minutes so `week_scan`'s cache key holds still across renders
— a `resets_at` that jitters by a second would re-run a whole-log `jq` pass
every render — and that rounding offsets every hour boundary by up to 2½
minutes. Invisible in a bar height; wrong exactly where this strip is read.
With the marker at `4 - floor(left / 1h)` the hollow count is the whole hours
remaining to the second: at the 119-minute mark, three hours and one minute
left drew as two, and now does not.

**`N✕5h left` no longer counts the window you are standing in.** The row
draws it as `▮` and line 1 prices it as `5h[38%]`, so counting it again
made `▮ + 11` read as twelve, and the budget sentence beside it agreed with
the miscount. "Left" now means still to come: what remains after this
window closes, `(7d left - 5h left)`, divided into windows — a stub at the
end of the week is still a window you can spend, so that rounds up.

The arithmetic has a property the old one did not: both clocks tick down
together, so the difference does not move. The count holds steady for the
life of a window and steps down by exactly one at each rollover. It was a
reading that drifted; it is a countdown now. `windows_ahead` is that
definition in one place, and the folded `...▯(✕N)` prints what the budget
line computed rather than re-deriving it off a 34-cell grid that spans 170h
against a 168h period.

`last window` now means the week ends inside the one you are in — nothing
ahead of it, nothing to divide the surplus across. It used to fire at one
window ahead too, to skip a `/win` clause that would just restate the
headroom; calling two windows the last one to save a redundant clause is the
wrong trade. At one ahead the line keeps the grammar: `1✕5h left · 25.0%/win`.

**`make install`.** The one-liner installed from GitHub and there was no
way to install the tree in front of you, so a working copy got there by
hand — and a stale hand-copy is how v0.28.0 once faked a red "7d dry" at
2%. `make install` runs the same `install.sh` with `STATUSLINE_SRC` set:
one installer, two entrypoints, no drift. It also refuses a
`statusline.sh` that does not parse — a broken statusline is not a worse
render, it is no statusline.

Three things the installer should have been doing all along, now on both
paths: it **keeps the flags** already on `statusLine.command` (rewriting
the whole command silently reverted `--order` and `--debug` on every
update), it writes through a temp file and renames (the script runs on
every render; a half-written one is a broken prompt), and it installs the
`usage-insight` skill beside it (`STATUSLINE_SKILL=0` opts out).

`make status` reports installed-vs-tree drift, settings command and skill
state; `make check` is shellcheck + bats; `make install-check` gates the
install on both. `make help` lists the rest.

435 tests (was 427).


## v0.33.0 — 2026-08-23 — the strip is a record, not a forecast

**The 5h strip stops at `▮`.** It used to draw the rest of the window as
Expand Down
44 changes: 25 additions & 19 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,28 @@ own signal.
## The ledgers (row 2)

```
5h ▃▄▮ 0.6x @04:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▮▯▯...▯(x14) 0.7x @Wed 09:00
```

One grammar, two scales. `5h` = the hours of this window that have
happened; `7d` = the period as 34 five-hour slots, oldest left, a gap at
each local midnight *in history only* — the run from `▮` on is
contiguous. Only the 7d strip draws a future at all: the 5h one stops at
`▮`, because when this window ends is the badge's job (`5h[38%@23:00]`)
and whether it caps is the notice's (`5h caps ~14:20`). Strips carry
history, badges carry state, notices do the warning. History draws in
full; the live row folds the 7d future after two kept cells into
`...▯(x14)` — the 5h windows left before the reset, all alike, `×` red
when the tail projects dry (the `week` report still draws every slot). The
count is the budget line's own, priced from the same instant, and it folds
as soon as folding hides two cells: the future is one fact, and the ink
belongs to history. Each strip ends
5h ▃▄▮▯▯ 0.6x @04:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▮▯▯...▯(x14) 0.7x @Wed 09:00
```

One grammar, two scales. `5h` = this window as five hour cells; `7d` =
the period as 34 five-hour slots, oldest left, a gap at each local
midnight *in history only* — the run from `▮` on is contiguous. Both
strips draw their whole grid, so a strip is an axis and not a bar that
grows at you: it holds its width for the life of the window and `▮` walks
it. On the 5h strip that makes the hollow run the answer to *how long
have I got* — `▃▄▮▯▯` is two whole hours after this one.

What neither strip draws on 5h is a *forecast*. An empty cell is a fact;
a `×` is a guess, and on a 5h window the guess is owned twice already —
the badge states the end (`5h[38%@23:00]`) and the notice names the wall
(`5h caps ~14:20`). Cells carry the shape, badges carry state, notices do
the warning. History draws in full; the live row folds the 7d future
after two kept cells into `...▯(x14)` — the 5h windows *after* the one
you are in, all alike, `×` red when the tail projects dry (the `week`
report still draws every slot). The count is the budget line's own,
priced from the same instant, and it folds as soon as folding hides two
cells: the future is one fact, and the ink belongs to history. Each strip
ends
with its pace (used ÷ elapsed; dim <1x, pressure ≥1x, hidden until 5% of the
window has run) and the reset its right edge is — axis labels, not restated
badges.
Expand All @@ -106,9 +112,9 @@ badges.
▁▂▃▄▅▆▇█ burned; height = points that cell cost (▁ ≤2 … ▅ ≤11 … █ >20)
ˍ ran, negligible — a bar of height zero, on the baseline
░ unknown — no sample; never drawn as idle
▮ now — and the last cell of the 5h strip
▯ ahead — the hollow of ▮ (7d only)
...▯(x14) the folded future: 14 more 5h windows before the reset (live 7d row)
▮ now
▯ ahead — the hollow of ▮; on 5h, the hours left in the window
...▯(x14) the folded future: 14 more 5h windows after this one (live 7d row)
× pace won't cover it (7d only: learned forecast, linear when cold)
```

Expand Down
89 changes: 89 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# claude-code-statusline — dev tasks.
#
# make install put THIS tree's statusline.sh (+ the usage-insight skill)
# into ~/.claude and point settings.json at it, keeping the
# flags already on the command
# make status what is installed vs what is in the tree
# make check shellcheck + the bats suite
#
# install shells out to install.sh with STATUSLINE_SRC set, so the local path
# and the public curl one-liner share one installer and cannot drift.

SHELL := /bin/bash
.DEFAULT_GOAL := help

CLAUDE_CONFIG_DIR ?= $(HOME)/.claude
DEST := $(CLAUDE_CONFIG_DIR)/statusline.sh
SKILL := $(CLAUDE_CONFIG_DIR)/skills/usage-insight/SKILL.md
SETTINGS := $(CLAUDE_CONFIG_DIR)/settings.json
BATS := npm exec --yes bats --

.PHONY: help install install-check uninstall status diff test lint check

help:
@echo "claude-code-statusline"
@echo
@echo " make install install this tree into $(CLAUDE_CONFIG_DIR)"
@echo " make install-check run check first, then install"
@echo " make status installed vs tree, settings, skill"
@echo " make diff diff the installed copy against this tree"
@echo " make uninstall remove the script, the skill and the setting"
@echo " make test bats t/"
@echo " make lint shellcheck -S error"
@echo " make check lint + test"
@echo
@echo " CLAUDE_CONFIG_DIR=... to target another config dir"
@echo " STATUSLINE_SKILL=0 to skip the usage-insight skill"

install:
@STATUSLINE_SRC="$(CURDIR)" CLAUDE_CONFIG_DIR="$(CLAUDE_CONFIG_DIR)" ./install.sh

# The one you want when the tree has uncommitted work: a statusline that
# renders wrong is worse than one that is a version behind.
install-check: check install

uninstall:
@rm -f "$(DEST)"; echo "removed $(DEST)"
@rm -rf "$(dir $(SKILL))"; echo "removed $(dir $(SKILL))"
@if [ -f "$(SETTINGS)" ] && jq -e '.statusLine' "$(SETTINGS)" >/dev/null 2>&1; then \
tmp="$(SETTINGS).tmp.$$$$"; \
jq 'del(.statusLine)' "$(SETTINGS)" > "$$tmp" && mv -f "$$tmp" "$(SETTINGS)"; \
echo "removed statusLine from $(SETTINGS)"; \
fi

status:
@src=$$(sha256sum statusline.sh | cut -c1-12); \
printf 'tree statusline.sh %s bytes %s (%s%s)\n' \
"$$(wc -c < statusline.sh)" "$$src" \
"$$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')" \
"$$(git diff --quiet -- statusline.sh 2>/dev/null || echo ', dirty')"; \
if [ -f "$(DEST)" ]; then \
dst=$$(sha256sum "$(DEST)" | cut -c1-12); \
printf 'installed %s %s bytes %s %s\n' "$(DEST)" \
"$$(wc -c < "$(DEST)")" "$$dst" \
"$$([ "$$src" = "$$dst" ] && echo 'in sync' || echo 'STALE — make install')"; \
else \
printf 'installed %s MISSING — make install\n' "$(DEST)"; \
fi; \
if [ -f "$(SETTINGS)" ]; then \
printf 'settings %s\n' "$$(jq -r '.statusLine.command // "(no statusLine — make install)"' "$(SETTINGS)")"; \
else \
printf 'settings %s MISSING\n' "$(SETTINGS)"; \
fi; \
if [ -f "$(SKILL)" ]; then \
printf 'skill %s %s\n' "$(SKILL)" \
"$$(cmp -s skills/usage-insight/SKILL.md "$(SKILL)" && echo 'in sync' || echo 'STALE — make install')"; \
else \
printf 'skill %s not installed\n' "$(SKILL)"; \
fi

diff:
@diff -u "$(DEST)" statusline.sh && echo "installed copy matches the tree"

test:
@$(BATS) t/

lint:
@shellcheck -S error statusline.sh install.sh

check: lint test
Loading
Loading