Skip to content

cookbook: add keymap-cheatsheet recipe, a shortcut sheet kept in sync with keymap.conf - #394

Draft
p4elkin wants to merge 1 commit into
umputun:masterfrom
p4elkin:cookbook/keymap-cheatsheet
Draft

cookbook: add keymap-cheatsheet recipe, a shortcut sheet kept in sync with keymap.conf#394
p4elkin wants to merge 1 commit into
umputun:masterfrom
p4elkin:cookbook/keymap-cheatsheet

Conversation

@p4elkin

@p4elkin p4elkin commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

One chord renders a hand-written shortcut sheet in an overlay. A chord you bound but never wrote down gets a drafted row before the sheet opens.

The sheet is a markdown file the reader writes and keeps, not generated output. That is the premise: the chord is the only half a machine knows, and the half worth documenting is why you reach for it. previous_attention_session is accurate and useless; "previous session wanting you, and a session still working is deliberately not included" is the sentence you actually want three months later. So nothing here rewrites the sheet.

What it does instead, on the press after you bind something new, is ask a model what the binding does and append one row to a holding section at the end. You move it into the right table and put it in your own words. The chord is documented from the moment it is bound rather than whenever you next remember, and the prose you wrote is never touched.

The gate

A cheat sheet you press several times a day cannot afford a model call, so two stages decide:

  1. keymap.conf's modification time against a stamp file. Unchanged and the pass returns — about 40ms including python startup. This is every press but a handful.
  2. Changed, so compare the chord sets. Nothing missing, which is what a comment edit or an already-documented chord looks like, and the stamp moves on with no model call.

Only a chord the sheet has never mentioned reaches the model. Measured on real files: 50ms for no change, 36ms for a comment-only edit, 3.6s to draft two new chords, and AGTERM_CHEATSHEET_DRAFT=0 falls back to a plain drift banner.

The maintenance pass runs from the custom command, before the overlay opens. That is forced rather than chosen: a session has one overlay slot, and the progress panel and the cheat sheet both want it.

Matching a chord

A chord matches as a whole chord, not as a substring, so a row for cmd+ctrl+shift+space does not document cmd+ctrl+shift+s, and a row for ctrl+x>ctrl+a does not document ctrl+a.

Only table rows are searched. A sheet names a chord in two senses — "this is what it does", which is an entry, and "this one is still free", which is a note — and searching the whole file lets the second stand in for the first, so a stale "still free" line hides a chord bound long after it was written. A sheet with no table at all falls back to the whole text.

Chords are read from keymap.conf allowing leading whitespace, since the parser trims the line before reading the verb, and accepting every modifier spelling agterm takes, case-insensitively.

Noticing a row that went stale

A chord set answers "is this chord written down anywhere", which says nothing about a chord that kept its key and got a different command. That leaves a row naming the right chord and describing the wrong thing, and it is the failure the sheet this was written for actually hit — a respawn-pane chord became a conversation picker and the row went on describing the pane.

So a second file beside the stamp records what each chord was bound to as of the last time the sheet was current, and a documented chord whose line has changed since gets a banner of its own:

> Bound to something else now, so the row may be stale: cmd+ctrl+shift+r
>
> Edit the sheet and this clears itself.

Nothing is rewritten. It says the row is suspect and leaves the sentence to the reader, like everything else here. The comparison is against the sheet's own modification time, so there is no acknowledgement to click: edit the sheet and every chord counts as described correctly again; leave it and the same chords are named on every press.

Seeding runs before the modification-time gate. On an existing install the stamp is already current, so anything after the gate would wait for the next keymap edit — and that edit is exactly the one that could rebind a chord with no baseline to notice it against.

Files

  • keymap-cheatsheet.sh — what the keymap line calls: the maintenance pass, then the overlay
  • render-cheatsheet.sh — runs inside the overlay, the sheet piped into the pager. Separate because agterm spawns the overlay program from its own environment, so nothing the opener exports reaches it; it takes python and the pager as arguments for the same reason
  • cheatsheet.py — the gate, the drafting, the starter sheet. Standard library only
  • README.md, and the index row under Panes, pickers and input

The model call

claude-haiku-4-5-20251001, written as the full id because the short haiku alias is not recognized by claude -p and falls back to the default model — the picker recipe's finding, and its comment is kept. AGTERM_CHEATSHEET_MODEL overrides it.

claude -p --model claude-haiku-4-5-20251001 --safe-mode --no-session-persistence
       --tools "" --settings '{"hooks":{}}' --output-format json
       --system-prompt ... --json-schema ...

with MAX_THINKING_TOKENS=0. No tools, no hooks, nothing persisted, and the answer constrained to {chord, does} rows. Three to seven seconds for one or two rows.

It writes one thing: the right-hand column of a drafted row. Two things it does not touch are worth separating out, because "a model keeps my cheat sheet" would be the wrong impression to take from this recipe:

  • The starter sheet, written on a machine that has no SHORTCUTS.md yet, is generated mechanically from keymap.conf — chord plus action name, underscores turned to spaces. No call.
  • The reader's own sheet. Every table and paragraph in it is hand-written and never rewritten. Drafted rows land in a holding section at the end and stay there until the reader files them.

Regenerating the sheet was the other way to do all this, and it is the wrong one: it would replace the tier tables, the explanations and the groupings — everything a model cannot write — to fix a couple of cells, and you would reach for it exactly when the sheet has the most hand-written value to lose.

Everything about the call fails soft: no claude found, a non-zero exit, a timeout, unparseable output, or an answer naming a chord nobody asked about — each returns nothing, the stamp is left alone so the next press tries again, and the sheet opens with the drift banner. Output goes to a temp file rather than a pipe, so a large answer cannot block the child while the poll loop runs. Cells are flattened to one line with | escaped, because a drafted row is written into the reader's own table. The sheet is written through a temp sibling and renamed, carrying the original's permission bits.

PATH

Neither script hardcodes a directory, and the two halves get there differently.

The opener is a custom command, so it takes the PATH the runner hands it — bundled agtermctl, /usr/local/bin, /opt/homebrew/bin.

The overlay half is not covered by that and cannot be: an overlay program is launched by the terminal, not by the custom command, so it gets the app's own PATH and nothing the opener exported. It can resolve neither glow nor python3 for itself.

So the opener resolves both and passes them across as absolute paths on the command line, which is the only channel into an overlay. OverlayCapture.shellLine evals the command string, so each word is single-quoted going in; a path containing a single quote is the one case that does not survive. With no glow reachable the opener sends /usr/bin/less -R, so the reader gets raw markdown rather than an overlay that closes on the spot and looks exactly like a chord that never fired.

Checked three ways: the command string the opener builds with and without glow on the PATH, and a round-trip through the app's own wrapper — env -i plus eval "$AGTERM_OVL_CMD" renders the sheet and exits 0.

⚠️ Relying on the runner's PATH means requiring it, so Requirements reads "the first release after 0.21.0". The fix is babc760 and unreleased as this is written, so the version number is left blank for you to fill when it ships — say the word if you would rather the recipe carried a workaround and stayed usable on 0.21.0.

Checks

shellcheck on both scripts, ruff check --ignore EXE001, the six headings, the shebangs, the index in both directions — all clean locally, and Cookbook checks pass on CI.

Behavior exercised against throwaway files in a temp AGTERM_CONFIG_DIR, with a stub in place of claude: starter sheet on first run, silent second run, a comment-only keymap edit costing no model call, drafting after a new binding, an existing sheet never replaced, a hostile cell escaped, a renamed chord dropped, an oversized answer not stalling the press, file permissions surviving the write, and the stale-row check across its four states: seeded quiet, named after a rebind, still named on the next press, cleared by an edit to the sheet.

@p4elkin
p4elkin force-pushed the cookbook/keymap-cheatsheet branch from a9ee3db to ea4ecfe Compare August 8, 2026 12:11
@p4elkin p4elkin changed the title cookbook: keymap-cheatsheet, a shortcut sheet in an overlay that checks itself against keymap.conf cookbook: keymap-cheatsheet, a shortcut sheet in an overlay that drafts a row for every chord you never wrote down Aug 8, 2026
@p4elkin
p4elkin marked this pull request as ready for review August 8, 2026 13:01
@p4elkin
p4elkin requested a review from umputun as a code owner August 8, 2026 13:01
@p4elkin
p4elkin marked this pull request as draft August 8, 2026 13:05
@p4elkin
p4elkin marked this pull request as ready for review August 8, 2026 13:14
@p4elkin
p4elkin marked this pull request as draft August 8, 2026 13:17
@p4elkin
p4elkin marked this pull request as ready for review August 8, 2026 13:26
@p4elkin p4elkin changed the title cookbook: keymap-cheatsheet, a shortcut sheet in an overlay that drafts a row for every chord you never wrote down cookbook: add keymap-cheatsheet recipe, a shortcut sheet kept in sync with keymap.conf Aug 8, 2026
@p4elkin
p4elkin marked this pull request as draft August 8, 2026 14:03
@p4elkin
p4elkin force-pushed the cookbook/keymap-cheatsheet branch from bf6b33b to c1a9fa7 Compare August 8, 2026 14:30
@p4elkin
p4elkin marked this pull request as ready for review August 8, 2026 14:31
@p4elkin
p4elkin marked this pull request as draft August 8, 2026 15:03
One chord renders a hand-written shortcut sheet in an overlay, and the sheet is
kept level with keymap.conf without being taken over.

The chord is the only half a machine knows; the half worth documenting is why
you reach for it, and an action name never says that. So a sentence the reader
wrote is never rewritten. A chord the sheet has no line about at all gets a
drafted row appended to a holding area, and a machine with no sheet yet gets a
whole one written for it, grouped into sections — the one file the model writes,
because there is nothing to preserve on a first run.

Two stages gate the work, since a sheet pressed several times a day cannot
afford a model call. keymap.conf's modification time against a stamp decides
whether to look, and the chord set decides whether to act. An ordinary press
costs one stat.

Three things are checked. A chord must appear in a table row, matched as a whole
chord: prose does not count, because "this one is still free" names a chord in
the wrong sense and would hide one bound long after that line was written. A
chord that stayed bound while its binding changed underneath is caught against a
baseline recorded beside the stamp, comparing what a binding does rather than
how its line is spelled. And such a row is rewritten in place only where the
words are the model's, proved by the holding heading or by the text recorded
when it wrote them; a cell the reader has reworded is named instead, and asking
that before calling the model is what stops a refused row costing a call on
every press.

The maintenance pass runs from the custom command rather than from inside the
overlay, because a session has one overlay slot and the progress panel needs it.
The overlay half takes python, the pager and the path settings as arguments: an
overlay program is launched by the terminal and inherits neither the opener's
PATH nor anything it exported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@p4elkin
p4elkin force-pushed the cookbook/keymap-cheatsheet branch from cab7942 to 9b4e301 Compare August 8, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants