From b836a9d2ee0d8937bfe071677e89b0f09bfc8c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kao=20F=C3=A9lix?= Date: Sun, 4 Jan 2026 20:37:08 +0100 Subject: [PATCH] feat(tui): add symbol key support to keybinding system (#450) - Added SymbolKey type with 32 symbol keys - Added symbol key constants to Key helper (Key.backtick, Key.comma, Key.period, etc.) - Updated matchesKey() and parseKey() to handle symbol key input - Added documentation in coding-agent README with examples --- packages/coding-agent/README.md | 22 ++++++- packages/tui/src/keys.ts | 113 +++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 1858f9d1..2d652613 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -272,7 +272,12 @@ Both modes are configurable via `/settings`: "one-at-a-time" delivers messages o All keyboard shortcuts can be customized via `~/.pi/agent/keybindings.json`. Each action can be bound to one or more keys. -**Key format:** `modifier+key` where modifiers are `ctrl`, `shift`, `alt` and keys are `a-z`, `0-9`, `escape`, `tab`, `enter`, `space`, `backspace`, `delete`, `home`, `end`, `up`, `down`, `left`, `right`. +**Key format:** `modifier+key` where modifiers are `ctrl`, `shift`, `alt` and keys are: + +- Letters: `a-z` +- Numbers: `0-9` +- Special keys: `escape`, `tab`, `enter`, `space`, `backspace`, `delete`, `home`, `end`, `up`, `down`, `left`, `right` +- Symbol keys: `` ` ``, `-`, `=`, `[`, `]`, `\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?` **Configurable actions:** @@ -338,6 +343,21 @@ All keyboard shortcuts can be customized via `~/.pi/agent/keybindings.json`. Eac } ``` +**Example (symbol keys):** + +```json +{ + "submit": ["enter", "ctrl+j"], + "newLine": ["shift+enter", "ctrl+;"], + "toggleThinking": "ctrl+/", + "cycleModelForward": "ctrl+.", + "cycleModelBackward": "ctrl+,", + "interrupt": ["escape", "ctrl+`"] +} +``` + +> **Note:** Some `ctrl+symbol` combinations overlap with ASCII control characters due to terminal legacy behavior (e.g., `ctrl+[` is the same as Escape, `ctrl+M` is the same as Enter). These can still be used with `ctrl+shift+key` (e.g., `ctrl+shift+]`). See [Kitty keyboard protocol: legacy ctrl mapping of ASCII keys](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys) for all unsupported keys. + ### Bash Mode Prefix commands with `!` to execute them and add output to context: diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index 22a36036..70dfeed3 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -4,6 +4,11 @@ * Supports both legacy terminal sequences and Kitty keyboard protocol. * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ * + * Symbol keys are also supported, however some ctrl+symbol combos + * overlap with ASCII codes, e.g. ctrl+[ = ESC. + * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys + * Those can still be * used for ctrl+shift combos + * * API: * - matchesKey(data, keyId) - Check if input matches a key identifier * - parseKey(data) - Parse input and return the key identifier @@ -42,6 +47,39 @@ type Letter = | "y" | "z"; +type SymbolKey = + | "`" + | "-" + | "=" + | "[" + | "]" + | "\\" + | ";" + | "'" + | "," + | "." + | "/" + | "!" + | "@" + | "#" + | "$" + | "%" + | "^" + | "&" + | "*" + | "(" + | ")" + | "_" + | "+" + | "|" + | "~" + | "{" + | "}" + | ":" + | "<" + | ">" + | "?"; + type SpecialKey = | "escape" | "esc" @@ -58,7 +96,7 @@ type SpecialKey = | "left" | "right"; -type BaseKey = Letter | SpecialKey; +type BaseKey = Letter | SymbolKey | SpecialKey; /** * Union type of all valid key identifiers. @@ -87,6 +125,7 @@ export type KeyId = * * Usage: * - Key.escape, Key.enter, Key.tab, etc. for special keys + * - Key.backtick, Key.comma, Key.period, etc. for symbol keys * - Key.ctrl("c"), Key.alt("x") for single modifier * - Key.ctrlShift("p"), Key.ctrlAlt("x") for combined modifiers */ @@ -107,6 +146,39 @@ export const Key = { left: "left" as const, right: "right" as const, + // Symbol keys + backtick: "`" as const, + hyphen: "-" as const, + equals: "=" as const, + leftbracket: "[" as const, + rightbracket: "]" as const, + backslash: "\\" as const, + semicolon: ";" as const, + quote: "'" as const, + comma: "," as const, + period: "." as const, + slash: "/" as const, + exclamation: "!" as const, + at: "@" as const, + hash: "#" as const, + dollar: "$" as const, + percent: "%" as const, + caret: "^" as const, + ampersand: "&" as const, + asterisk: "*" as const, + leftparen: "(" as const, + rightparen: ")" as const, + underscore: "_" as const, + plus: "+" as const, + pipe: "|" as const, + tilde: "~" as const, + leftbrace: "{" as const, + rightbrace: "}" as const, + colon: ":" as const, + lessthan: "<" as const, + greaterthan: ">" as const, + question: "?" as const, + // Single modifiers ctrl: (key: K): `ctrl+${K}` => `ctrl+${key}`, shift: (key: K): `shift+${K}` => `shift+${key}`, @@ -128,6 +200,40 @@ export const Key = { // Constants // ============================================================================= +const SYMBOL_KEYS = new Set([ + "`", + "-", + "=", + "[", + "]", + "\\", + ";", + "'", + ",", + ".", + "/", + "!", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "(", + ")", + "_", + "+", + "|", + "~", + "{", + "}", + ":", + "<", + ">", + "?", +]); + const MODIFIERS = { shift: 1, alt: 2, @@ -403,8 +509,8 @@ export function matchesKey(data: string, keyId: KeyId): boolean { return matchesKittySequence(data, ARROW_CODEPOINTS.right, modifier); } - // Handle single letter keys (a-z) - if (key.length === 1 && key >= "a" && key <= "z") { + // Handle single letter keys (a-z) and some symbols + if (key.length === 1 && ((key >= "a" && key <= "z") || SYMBOL_KEYS.has(key))) { const codepoint = key.charCodeAt(0); if (ctrl && !shift && !alt) { @@ -464,6 +570,7 @@ export function parseKey(data: string): string | undefined { else if (codepoint === ARROW_CODEPOINTS.left) keyName = "left"; else if (codepoint === ARROW_CODEPOINTS.right) keyName = "right"; else if (codepoint >= 97 && codepoint <= 122) keyName = String.fromCharCode(codepoint); + else if (SYMBOL_KEYS.has(String.fromCharCode(codepoint))) keyName = String.fromCharCode(codepoint); if (keyName) { return mods.length > 0 ? `${mods.join("+")}+${keyName}` : keyName;