cookbook: add keymap-cheatsheet recipe, a shortcut sheet kept in sync with keymap.conf - #394
Draft
p4elkin wants to merge 1 commit into
Draft
cookbook: add keymap-cheatsheet recipe, a shortcut sheet kept in sync with keymap.conf#394p4elkin wants to merge 1 commit into
p4elkin wants to merge 1 commit into
Conversation
p4elkin
force-pushed
the
cookbook/keymap-cheatsheet
branch
from
August 8, 2026 12:11
a9ee3db to
ea4ecfe
Compare
p4elkin
marked this pull request as ready for review
August 8, 2026 13:01
p4elkin
marked this pull request as draft
August 8, 2026 13:05
p4elkin
marked this pull request as ready for review
August 8, 2026 13:14
p4elkin
marked this pull request as draft
August 8, 2026 13:17
p4elkin
marked this pull request as ready for review
August 8, 2026 13:26
p4elkin
marked this pull request as draft
August 8, 2026 14:03
p4elkin
force-pushed
the
cookbook/keymap-cheatsheet
branch
from
August 8, 2026 14:30
bf6b33b to
c1a9fa7
Compare
p4elkin
marked this pull request as ready for review
August 8, 2026 14:31
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
force-pushed
the
cookbook/keymap-cheatsheet
branch
from
August 8, 2026 20:15
cab7942 to
9b4e301
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_sessionis 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:
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.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=0falls 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+spacedoes not documentcmd+ctrl+shift+s, and a row forctrl+x>ctrl+adoes not documentctrl+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.confallowing 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:
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 overlayrender-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 reasoncheatsheet.py— the gate, the drafting, the starter sheet. Standard library onlyREADME.md, and the index row under Panes, pickers and inputThe model call
claude-haiku-4-5-20251001, written as the full id because the shorthaikualias is not recognized byclaude -pand falls back to the default model — the picker recipe's finding, and its comment is kept.AGTERM_CHEATSHEET_MODELoverrides it.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:
SHORTCUTS.mdyet, is generated mechanically fromkeymap.conf— chord plus action name, underscores turned to spaces. No call.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
claudefound, 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
PATHthe runner hands it — bundledagtermctl,/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
PATHand nothing the opener exported. It can resolve neitherglownorpython3for 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.shellLineevals 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 noglowreachable 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
glowon thePATH, and a round-trip through the app's own wrapper —env -ipluseval "$AGTERM_OVL_CMD"renders the sheet and exits 0.PATHmeans requiring it, so Requirements reads "the first release after 0.21.0". The fix isbabc760and 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
shellcheckon 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 ofclaude: 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.