This issue was prepared with the help of an LLM (root-cause analysis and drafting), based on debugging a real config that stopped working. Environment/repro details below were confirmed on my own machine.
Environment
- Hunk version: 0.19.0 (
hunk --version)
- Terminal: Windows console, SSH'd into a Linux box (
TERM=xterm-256color on the remote side)
- Config:
~/.config/hunk/config.toml, [keybindings] table
Summary
Any [keybindings] entry written with the alt/option modifier (e.g. "alt+n") is silently unreachable on a normal terminal. The key never fires the bound command, and there's no warning/notice that the binding is dead.
Steps to reproduce
- Add to
~/.config/hunk/config.toml:
[keybindings]
"hunk.review.pageDown" = ["pagedown", "alt+n"]
- Launch
hunk diff over SSH from a Windows console (or really any terminal without Kitty keyboard protocol support).
- Press Alt+N.
Expected: triggers hunk.review.pageDown.
Actual: nothing happens.
Root cause
parseKeyChord (src/extension-api/keys.ts) maps the alt/option token to a distinct option boolean, separate from meta (which cmd/meta sets):
MODIFIER_TOKENS = {
ctrl: "ctrl", control: "ctrl",
meta: "meta", cmd: "meta", command: "meta",
alt: "option", option: "option",
shift: "shift",
};
So "alt+n" parses to { base: "n", option: true, meta: false }.
But the actual terminal key decoder (in the vendored @opentui/core keypress parser) never produces that combination for a physically-pressed Alt key on a plain ANSI terminal. When Alt+letter arrives as the classic 2-byte ESC+letter sequence, it's decoded via metaKeyCodeRe (/^\x1b([a-zA-Z0-9])$/) as:
key.meta = true; // option stays false
And matchesKeyChord requires an exact match on both flags:
if (Boolean(key.ctrl) !== parsed.ctrl || Boolean(key.meta) !== parsed.meta) return false;
if (Boolean(key.option) !== parsed.option) return false;
parsed.option = true, parsed.meta = false from "alt+n" can never equal key.option = false, key.meta = true from the terminal, so the chord is dead.
(If the terminal does speak the Kitty keyboard protocol, Alt is decoded differently again — there it sets meta: true and option: true together, from key2.meta = mods.alt || mods.meta; key2.option = mods.alt;. That combination doesn't match a bare "alt+n" chord either, and doesn't match "meta+n" either — only "alt+meta+n" would.)
Net effect: there is no single documented chord string that reliably maps to "the Alt key" across terminal modes. Users have to guess/reverse-engineer which of alt+x, meta+x, or alt+meta+x will actually fire, depending on their terminal and whether Kitty protocol negotiated.
Suggested fix (one of)
- Make
alt/option in the config grammar match on option || meta (i.e. treat a plain Alt press as satisfying an alt+-bound chord regardless of which single flag the decoder happened to set), rather than requiring an exact boolean match on both option and meta.
- Alternatively, document in
docs/keybindings.md that plain-terminal Alt presses must be bound as meta+x, and Kitty-protocol Alt presses must be bound as alt+meta+x — but this pushes a decoder implementation detail onto every user's config and makes alt/option effectively useless as a config token on its own.
- At minimum, add a startup notice (like the existing "Ignored [keybindings] entries with unsupported values" notice) when a configured chord uses a modifier combination that can never be produced by the active key decoder, so this doesn't fail silently.
Workaround
Binding the same command to meta+x (for non-Kitty terminals) and alt+meta+x (for Kitty-protocol terminals) together works around it:
"hunk.review.pageDown" = ["pagedown", "meta+n", "alt+meta+n"]
Environment
hunk --version)TERM=xterm-256coloron the remote side)~/.config/hunk/config.toml,[keybindings]tableSummary
Any
[keybindings]entry written with thealt/optionmodifier (e.g."alt+n") is silently unreachable on a normal terminal. The key never fires the bound command, and there's no warning/notice that the binding is dead.Steps to reproduce
~/.config/hunk/config.toml:hunk diffover SSH from a Windows console (or really any terminal without Kitty keyboard protocol support).Expected: triggers
hunk.review.pageDown.Actual: nothing happens.
Root cause
parseKeyChord(src/extension-api/keys.ts) maps thealt/optiontoken to a distinctoptionboolean, separate frommeta(whichcmd/metasets):So
"alt+n"parses to{ base: "n", option: true, meta: false }.But the actual terminal key decoder (in the vendored
@opentui/corekeypress parser) never produces that combination for a physically-pressed Alt key on a plain ANSI terminal. When Alt+letter arrives as the classic 2-byteESC+letter sequence, it's decoded viametaKeyCodeRe(/^\x1b([a-zA-Z0-9])$/) as:And
matchesKeyChordrequires an exact match on both flags:parsed.option = true, parsed.meta = falsefrom"alt+n"can never equalkey.option = false, key.meta = truefrom the terminal, so the chord is dead.(If the terminal does speak the Kitty keyboard protocol, Alt is decoded differently again — there it sets
meta: trueandoption: truetogether, fromkey2.meta = mods.alt || mods.meta; key2.option = mods.alt;. That combination doesn't match a bare"alt+n"chord either, and doesn't match"meta+n"either — only"alt+meta+n"would.)Net effect: there is no single documented chord string that reliably maps to "the Alt key" across terminal modes. Users have to guess/reverse-engineer which of
alt+x,meta+x, oralt+meta+xwill actually fire, depending on their terminal and whether Kitty protocol negotiated.Suggested fix (one of)
alt/optionin the config grammar match onoption || meta(i.e. treat a plain Alt press as satisfying analt+-bound chord regardless of which single flag the decoder happened to set), rather than requiring an exact boolean match on bothoptionandmeta.docs/keybindings.mdthat plain-terminal Alt presses must be bound asmeta+x, and Kitty-protocol Alt presses must be bound asalt+meta+x— but this pushes a decoder implementation detail onto every user's config and makesalt/optioneffectively useless as a config token on its own.Workaround
Binding the same command to
meta+x(for non-Kitty terminals) andalt+meta+x(for Kitty-protocol terminals) together works around it: