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
This commit is contained in:
Kao Félix 2026-01-04 20:37:08 +01:00 committed by GitHub
parent 61cadc226c
commit b836a9d2ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 131 additions and 4 deletions

View file

@ -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:

View file

@ -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: <K extends BaseKey>(key: K): `ctrl+${K}` => `ctrl+${key}`,
shift: <K extends BaseKey>(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;