|
1 | 1 | /** |
2 | | - * OpenTUI shell keybinding catalog (pure data) — the help overlay's source |
3 | | - * of truth. Rows with `command` are application chords registered on |
4 | | - * `@opentui/keymap`. Rows without one stay prompt-local: textarea defaults, |
5 | | - * the kill ring, or a chord whose meaning depends on context keys.ts still |
6 | | - * owns. `keybindings.test.ts` drives every row through a live shell. |
| 2 | + * Help rows. Application lines come from the live `@opentui/keymap` |
| 3 | + * registration (`bindAppHelp`). Widget-default rows are the Textarea chords |
| 4 | + * `defaultTextareaKeyBindings` / `PROMPT_KEY_BINDINGS` already perform, plus |
| 5 | + * the kill-ring yank chords the textarea does not implement. |
| 6 | + * There is no second copy of the application chord table here. |
7 | 7 | */ |
8 | 8 |
|
9 | 9 | export interface ShellShortcut { |
10 | 10 | readonly keys: string; |
11 | 11 | readonly description: string; |
12 | | - /** `@opentui/keymap` command name. Absent for prompt-local chords. */ |
13 | | - readonly command?: string; |
14 | | - /** Binding strings for `command`. Absent for prompt-local chords. */ |
15 | | - readonly bindings?: readonly string[]; |
16 | 12 | } |
17 | 13 |
|
18 | | -export const SHELL_SHORTCUTS: readonly ShellShortcut[] = [ |
| 14 | +/** |
| 15 | + * Prompt-local rows. Cursor, delete, word motion, and newline/submit are |
| 16 | + * Textarea defaults (`renderables/Textarea.d.ts` `TextareaAction`). Yank is |
| 17 | + * not a Textarea action; it stays listed because the kill ring records it. |
| 18 | + */ |
| 19 | +export const WIDGET_DEFAULT_SHORTCUTS: readonly ShellShortcut[] = [ |
19 | 20 | { |
20 | 21 | keys: "Enter", |
21 | 22 | description: |
22 | 23 | "soft-steer at the next tool boundary while busy (held above the prompt); send straight through when idle", |
23 | 24 | }, |
24 | | - { |
25 | | - keys: "Alt+Enter", |
26 | | - description: |
27 | | - "queue a follow-up delivered only when the run goes idle; does nothing unless a run is busy", |
28 | | - command: "shell.queue", |
29 | | - bindings: ["alt+return"], |
30 | | - }, |
31 | | - { |
32 | | - keys: "Up / Down / Enter / Ctrl+X", |
33 | | - description: |
34 | | - "on held items above the prompt: select, send now, drop (Esc backs out)", |
35 | | - // Up, Down, and Enter also move the caret, recall history, or send. |
36 | | - // Only Ctrl+X is unambiguous; the others stay in keys.ts. |
37 | | - command: "shell.held.drop", |
38 | | - bindings: ["ctrl+x"], |
39 | | - }, |
40 | | - { |
41 | | - keys: "Ctrl+C", |
42 | | - description: |
43 | | - "pause a busy run and hold the queue, or clear the prompt and attachments when idle; Ctrl+C again to exit — with live sub-agents, a 2nd press stops them, a 3rd quits", |
44 | | - command: "shell.interrupt", |
45 | | - bindings: ["ctrl+c"], |
46 | | - }, |
47 | | - { |
48 | | - keys: "Ctrl+G", |
49 | | - description: |
50 | | - "pop the most recently queued or steered message back into the prompt for editing", |
51 | | - command: "shell.cancel", |
52 | | - bindings: ["ctrl+g"], |
53 | | - }, |
54 | | - { |
55 | | - keys: "Alt+C", |
56 | | - description: |
57 | | - "copy mode: pick a message, tool output, or diff; press again to close it", |
58 | | - command: "shell.copy", |
59 | | - bindings: ["alt+c"], |
60 | | - }, |
61 | | - { |
62 | | - keys: "Alt+M", |
63 | | - description: |
64 | | - "toggle DEC mouse capture (on by default: wheel scroll, click-to-expand, drag-to-copy); off restores native terminal drag-select", |
65 | | - command: "shell.mouse", |
66 | | - bindings: ["alt+m"], |
67 | | - }, |
68 | | - { |
69 | | - keys: "Alt+E", |
70 | | - description: |
71 | | - "expand or collapse every collapsible row (tool call, diff, skill, reasoning)", |
72 | | - command: "shell.expand", |
73 | | - bindings: ["alt+e"], |
74 | | - }, |
75 | | - { |
76 | | - keys: "Alt+T", |
77 | | - description: |
78 | | - "show or hide the task list above the prompt (hidden by default)", |
79 | | - command: "shell.tasks", |
80 | | - bindings: ["alt+t"], |
81 | | - }, |
82 | | - { |
83 | | - keys: "Alt+O", |
84 | | - description: |
85 | | - "observe a live subagent session; a system row says so when there is none", |
86 | | - command: "shell.observe", |
87 | | - bindings: ["alt+o"], |
88 | | - }, |
89 | | - { |
90 | | - keys: "Tab", |
91 | | - description: "move focus between the prompt and the transcript", |
92 | | - command: "shell.focus", |
93 | | - bindings: ["tab"], |
94 | | - }, |
95 | | - { |
96 | | - keys: "Shift+Tab", |
97 | | - description: "cycle reasoning effort for the current model", |
98 | | - command: "shell.effort", |
99 | | - bindings: ["shift+tab"], |
100 | | - }, |
101 | | - { |
102 | | - keys: "Esc", |
103 | | - description: "close the open overlay, or leave subagent observe", |
104 | | - command: "shell.dismiss", |
105 | | - bindings: ["escape"], |
106 | | - }, |
107 | 25 | { |
108 | 26 | keys: "Ctrl+B / Ctrl+F", |
109 | 27 | description: "move the cursor back / forward one character", |
@@ -156,36 +74,39 @@ export const SHELL_SHORTCUTS: readonly ShellShortcut[] = [ |
156 | 74 | description: |
157 | 75 | "insert a newline instead of sending (Shift+Enter also works on terminals that report the modifier)", |
158 | 76 | }, |
159 | | -] as const; |
| 77 | +]; |
160 | 78 |
|
161 | | -/** Application chords the keymap registers. */ |
162 | | -export function keymapShortcuts(): readonly ShellShortcut[] { |
163 | | - return SHELL_SHORTCUTS.filter((row) => row.command !== undefined); |
| 79 | +let appRows: readonly ShellShortcut[] = []; |
| 80 | +let readAppHelp: () => readonly string[] = () => |
| 81 | + appRows.map((row) => `${row.keys} — ${row.description}`); |
| 82 | + |
| 83 | +/** Called when the keymap module loads, and again from the live keymap. */ |
| 84 | +export function setAppShortcutRows(rows: readonly ShellShortcut[]): void { |
| 85 | + appRows = rows; |
164 | 86 | } |
165 | 87 |
|
166 | | -/** |
167 | | - * Prompt-local rows: textarea defaults, the kill ring, paste, and chords |
168 | | - * whose meaning depends on context the keymap does not own. |
169 | | - */ |
170 | | -export function promptLocalShortcuts(): readonly ShellShortcut[] { |
171 | | - return SHELL_SHORTCUTS.filter((row) => row.command === undefined); |
| 88 | +export function bindAppHelp(read: () => readonly string[]): () => void { |
| 89 | + const previous = readAppHelp; |
| 90 | + readAppHelp = read; |
| 91 | + return () => { |
| 92 | + readAppHelp = previous; |
| 93 | + }; |
| 94 | +} |
| 95 | + |
| 96 | +/** Catalog the live tests drive. Application rows are whatever the keymap registered. */ |
| 97 | +export function shellShortcuts(): readonly ShellShortcut[] { |
| 98 | + return [...appRows, ...WIDGET_DEFAULT_SHORTCUTS]; |
172 | 99 | } |
173 | 100 |
|
174 | 101 | function formatShortcut(row: ShellShortcut): string { |
175 | 102 | return `${row.keys} — ${row.description}`; |
176 | 103 | } |
177 | 104 |
|
178 | | -/** |
179 | | - * Help rows from the keymap catalog plus prompt-local rows, in catalog |
180 | | - * order, so the overlay cannot drift from either list. |
181 | | - */ |
| 105 | +/** Keymap command lines, then widget-default rows. */ |
182 | 106 | export function helpItems(): readonly string[] { |
183 | | - const owned = new Set<string>([ |
184 | | - ...keymapShortcuts().map((row) => row.keys), |
185 | | - ...promptLocalShortcuts().map((row) => row.keys), |
186 | | - ]); |
187 | 107 | return [ |
188 | | - ...SHELL_SHORTCUTS.filter((row) => owned.has(row.keys)).map(formatShortcut), |
| 108 | + ...readAppHelp(), |
| 109 | + ...WIDGET_DEFAULT_SHORTCUTS.map(formatShortcut), |
189 | 110 | "Close help", |
190 | 111 | ]; |
191 | 112 | } |
0 commit comments