@f5-sales-demo/pi-tui 19.51.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,340 @@
1
+ import { type ChordBinding, parseBinding } from "./chord-parser";
2
+ import { type KeyId, matchesKey, parseKey } from "./keys";
3
+
4
+ /**
5
+ * Global keybinding registry.
6
+ * Downstream packages can add keybindings via declaration merging.
7
+ */
8
+ export interface Keybindings {
9
+ // Editor navigation and editing
10
+ "tui.editor.cursorUp": true;
11
+ "tui.editor.cursorDown": true;
12
+ "tui.editor.cursorLeft": true;
13
+ "tui.editor.cursorRight": true;
14
+ "tui.editor.cursorWordLeft": true;
15
+ "tui.editor.cursorWordRight": true;
16
+ "tui.editor.cursorLineStart": true;
17
+ "tui.editor.cursorLineEnd": true;
18
+ "tui.editor.jumpForward": true;
19
+ "tui.editor.jumpBackward": true;
20
+ "tui.editor.pageUp": true;
21
+ "tui.editor.pageDown": true;
22
+ "tui.editor.deleteCharBackward": true;
23
+ "tui.editor.deleteCharForward": true;
24
+ "tui.editor.deleteWordBackward": true;
25
+ "tui.editor.deleteWordForward": true;
26
+ "tui.editor.deleteToLineStart": true;
27
+ "tui.editor.deleteToLineEnd": true;
28
+ "tui.editor.yank": true;
29
+ "tui.editor.yankPop": true;
30
+ "tui.editor.undo": true;
31
+ // Generic input actions
32
+ "tui.input.newLine": true;
33
+ "tui.input.submit": true;
34
+ "tui.input.tab": true;
35
+ "tui.input.copy": true;
36
+ // Generic selection actions
37
+ "tui.select.up": true;
38
+ "tui.select.down": true;
39
+ "tui.select.pageUp": true;
40
+ "tui.select.pageDown": true;
41
+ "tui.select.confirm": true;
42
+ "tui.select.cancel": true;
43
+ }
44
+
45
+ export type Keybinding = keyof Keybindings;
46
+
47
+ // Re-export KeyId from keys.ts
48
+ export type { KeyId };
49
+
50
+ /**
51
+ * Binding string accepted by the config layer. Single-stroke values are
52
+ * KeyId (strict template-literal union); chord-syntax values (e.g.
53
+ * "ctrl+x b") are plain strings validated at runtime by parseBinding.
54
+ */
55
+ export type BindingString = KeyId | string;
56
+
57
+ export interface KeybindingDefinition {
58
+ defaultKeys: BindingString | BindingString[];
59
+ description?: string;
60
+ }
61
+
62
+ export type KeybindingDefinitions = Record<string, KeybindingDefinition>;
63
+ export type KeybindingsConfig = Record<string, BindingString | BindingString[] | undefined>;
64
+
65
+ export const TUI_KEYBINDINGS = {
66
+ "tui.editor.cursorUp": { defaultKeys: "up", description: "Move cursor up" },
67
+ "tui.editor.cursorDown": { defaultKeys: "down", description: "Move cursor down" },
68
+ "tui.editor.cursorLeft": {
69
+ defaultKeys: ["left", "ctrl+b"],
70
+ description: "Move cursor left",
71
+ },
72
+ "tui.editor.cursorRight": {
73
+ defaultKeys: ["right", "ctrl+f"],
74
+ description: "Move cursor right",
75
+ },
76
+ "tui.editor.cursorWordLeft": {
77
+ defaultKeys: ["alt+left", "ctrl+left", "alt+b"],
78
+ description: "Move cursor word left",
79
+ },
80
+ "tui.editor.cursorWordRight": {
81
+ defaultKeys: ["alt+right", "ctrl+right", "alt+f"],
82
+ description: "Move cursor word right",
83
+ },
84
+ "tui.editor.cursorLineStart": {
85
+ defaultKeys: ["home", "ctrl+a"],
86
+ description: "Move to line start",
87
+ },
88
+ "tui.editor.cursorLineEnd": {
89
+ defaultKeys: ["end", "ctrl+e"],
90
+ description: "Move to line end",
91
+ },
92
+ "tui.editor.jumpForward": {
93
+ defaultKeys: "ctrl+]",
94
+ description: "Jump forward to character",
95
+ },
96
+ "tui.editor.jumpBackward": {
97
+ defaultKeys: "ctrl+alt+]",
98
+ description: "Jump backward to character",
99
+ },
100
+ "tui.editor.pageUp": { defaultKeys: "pageUp", description: "Page up" },
101
+ "tui.editor.pageDown": { defaultKeys: "pageDown", description: "Page down" },
102
+ "tui.editor.deleteCharBackward": {
103
+ defaultKeys: "backspace",
104
+ description: "Delete character backward",
105
+ },
106
+ "tui.editor.deleteCharForward": {
107
+ defaultKeys: ["delete", "ctrl+d"],
108
+ description: "Delete character forward",
109
+ },
110
+ "tui.editor.deleteWordBackward": {
111
+ defaultKeys: ["ctrl+w", "alt+backspace", "ctrl+backspace"],
112
+ description: "Delete word backward",
113
+ },
114
+ "tui.editor.deleteWordForward": {
115
+ defaultKeys: ["alt+delete", "alt+d"],
116
+ description: "Delete word forward",
117
+ },
118
+ "tui.editor.deleteToLineStart": {
119
+ defaultKeys: "ctrl+u",
120
+ description: "Delete to line start",
121
+ },
122
+ "tui.editor.deleteToLineEnd": {
123
+ defaultKeys: "ctrl+k",
124
+ description: "Delete to line end",
125
+ },
126
+ "tui.editor.yank": { defaultKeys: "ctrl+y", description: "Yank" },
127
+ "tui.editor.yankPop": { defaultKeys: "alt+y", description: "Yank pop" },
128
+ "tui.editor.undo": { defaultKeys: ["ctrl+-", "ctrl+_"], description: "Undo" },
129
+ "tui.input.newLine": { defaultKeys: "shift+enter", description: "Insert newline" },
130
+ "tui.input.submit": { defaultKeys: "enter", description: "Submit input" },
131
+ "tui.input.tab": { defaultKeys: "tab", description: "Tab / autocomplete" },
132
+ "tui.input.copy": { defaultKeys: "ctrl+c", description: "Copy selection" },
133
+ "tui.select.up": { defaultKeys: "up", description: "Move selection up" },
134
+ "tui.select.down": { defaultKeys: "down", description: "Move selection down" },
135
+ "tui.select.pageUp": { defaultKeys: "pageUp", description: "Selection page up" },
136
+ "tui.select.pageDown": {
137
+ defaultKeys: "pageDown",
138
+ description: "Selection page down",
139
+ },
140
+ "tui.select.confirm": { defaultKeys: "enter", description: "Confirm selection" },
141
+ "tui.select.cancel": {
142
+ defaultKeys: ["escape", "ctrl+c"],
143
+ description: "Cancel selection",
144
+ },
145
+ } as const satisfies KeybindingDefinitions;
146
+
147
+ export interface KeybindingConflict {
148
+ key: KeyId;
149
+ keybindings: string[];
150
+ }
151
+
152
+ const SHIFTED_SYMBOL_KEYS = new Set<string>([
153
+ "!",
154
+ "@",
155
+ "#",
156
+ "$",
157
+ "%",
158
+ "^",
159
+ "&",
160
+ "*",
161
+ "(",
162
+ ")",
163
+ "_",
164
+ "+",
165
+ "{",
166
+ "}",
167
+ "|",
168
+ ":",
169
+ "<",
170
+ ">",
171
+ "?",
172
+ "~",
173
+ ]);
174
+
175
+ const normalizeKeyId = (key: BindingString): KeyId => key.toLowerCase() as KeyId;
176
+
177
+ function normalizeKeys(keys: BindingString | BindingString[] | undefined): KeyId[] {
178
+ if (keys === undefined) return [];
179
+ const keyList = Array.isArray(keys) ? keys : [keys];
180
+ const seen = new Set<KeyId>();
181
+ const result: KeyId[] = [];
182
+ for (const key of keyList) {
183
+ const normalized = normalizeKeyId(key);
184
+ if (!seen.has(normalized)) {
185
+ seen.add(normalized);
186
+ result.push(normalized);
187
+ }
188
+ }
189
+ return result;
190
+ }
191
+
192
+ export class KeybindingsManager {
193
+ #definitions: KeybindingDefinitions;
194
+ #userBindings: KeybindingsConfig;
195
+ #keysById = new Map<Keybinding, KeyId[]>();
196
+ #conflicts: KeybindingConflict[] = [];
197
+
198
+ constructor(definitions: KeybindingDefinitions, userBindings: KeybindingsConfig = {}) {
199
+ this.#definitions = definitions;
200
+ this.#userBindings = userBindings;
201
+ this.#rebuild();
202
+ }
203
+
204
+ #rebuild(): void {
205
+ this.#keysById.clear();
206
+ this.#conflicts = [];
207
+
208
+ const userClaims = new Map<KeyId, Set<Keybinding>>();
209
+ for (const [keybinding, keys] of Object.entries(this.#userBindings)) {
210
+ if (!(keybinding in this.#definitions)) continue;
211
+ for (const key of normalizeKeys(keys)) {
212
+ const claimants = userClaims.get(key) ?? new Set<Keybinding>();
213
+ claimants.add(keybinding as Keybinding);
214
+ userClaims.set(key, claimants);
215
+ }
216
+ }
217
+
218
+ for (const [key, keybindings] of userClaims) {
219
+ if (keybindings.size > 1) {
220
+ this.#conflicts.push({ key, keybindings: [...keybindings] });
221
+ }
222
+ }
223
+
224
+ for (const [id, definition] of Object.entries(this.#definitions)) {
225
+ const userKeys = this.#userBindings[id];
226
+ const keys = userKeys === undefined ? normalizeKeys(definition.defaultKeys) : normalizeKeys(userKeys);
227
+ this.#keysById.set(id as Keybinding, keys);
228
+ }
229
+ }
230
+
231
+ matches(data: string, keybinding: Keybinding): boolean {
232
+ const keys = this.#keysById.get(keybinding) ?? [];
233
+ for (const key of keys) {
234
+ if (matchesKey(data, key)) return true;
235
+ }
236
+
237
+ // Handle shifted symbol keys (e.g., shift+- produces _ on US layout)
238
+ const parsed = parseKey(data);
239
+ if (!parsed?.startsWith("shift+")) return false;
240
+ const keyName = parsed.slice("shift+".length);
241
+ if (!SHIFTED_SYMBOL_KEYS.has(keyName)) return false;
242
+ return keys.includes(keyName as KeyId);
243
+ }
244
+
245
+ getKeys(keybinding: Keybinding): KeyId[] {
246
+ return [...(this.#keysById.get(keybinding) ?? [])];
247
+ }
248
+
249
+ getDefinition(keybinding: Keybinding): KeybindingDefinition {
250
+ return this.#definitions[keybinding];
251
+ }
252
+
253
+ getConflicts(): KeybindingConflict[] {
254
+ return this.#conflicts.map(conflict => ({ ...conflict, keybindings: [...conflict.keybindings] }));
255
+ }
256
+
257
+ setUserBindings(userBindings: KeybindingsConfig): void {
258
+ this.#userBindings = userBindings;
259
+ this.#rebuild();
260
+ }
261
+
262
+ getUserBindings(): KeybindingsConfig {
263
+ return { ...this.#userBindings };
264
+ }
265
+
266
+ getResolvedBindings(): KeybindingsConfig {
267
+ const resolved: KeybindingsConfig = {};
268
+ for (const id of Object.keys(this.#definitions)) {
269
+ const keys = this.#keysById.get(id as Keybinding) ?? [];
270
+ resolved[id] = keys.length === 1 ? keys[0]! : [...keys];
271
+ }
272
+ return resolved;
273
+ }
274
+
275
+ /**
276
+ * Return all bindings (standalone + chord) with their action names, with
277
+ * user overrides already applied (same rules as getResolvedBindings).
278
+ */
279
+ getChordBindings(): ChordBinding[] {
280
+ const result: ChordBinding[] = [];
281
+ for (const [action, keys] of this.#keysById) {
282
+ for (const key of keys) {
283
+ const parsed = parseBinding(key);
284
+ if (!parsed.ok) continue;
285
+ result.push({ action, sequence: parsed.sequence });
286
+ }
287
+ }
288
+ return result;
289
+ }
290
+
291
+ /**
292
+ * Return conflicts where a key is used BOTH as a chord leader AND as a
293
+ * standalone binding for some other action. Consumers (InputController)
294
+ * should refuse to initialize the dispatcher if any are reported.
295
+ */
296
+ getChordConflicts(): Array<{
297
+ key: KeyId;
298
+ standaloneActions: string[];
299
+ chordActions: string[];
300
+ }> {
301
+ const standaloneByKey = new Map<KeyId, Set<string>>();
302
+ const leaderByKey = new Map<KeyId, Set<string>>();
303
+ for (const binding of this.getChordBindings()) {
304
+ const [first, second] = binding.sequence;
305
+ if (second === undefined) {
306
+ const set = standaloneByKey.get(first!) ?? new Set<string>();
307
+ set.add(binding.action);
308
+ standaloneByKey.set(first!, set);
309
+ } else {
310
+ const set = leaderByKey.get(first!) ?? new Set<string>();
311
+ set.add(binding.action);
312
+ leaderByKey.set(first!, set);
313
+ }
314
+ }
315
+ const conflicts: Array<{ key: KeyId; standaloneActions: string[]; chordActions: string[] }> = [];
316
+ for (const [key, leaders] of leaderByKey) {
317
+ const standalones = standaloneByKey.get(key);
318
+ if (!standalones || standalones.size === 0) continue;
319
+ conflicts.push({
320
+ key,
321
+ standaloneActions: [...standalones].sort(),
322
+ chordActions: [...leaders].sort(),
323
+ });
324
+ }
325
+ return conflicts;
326
+ }
327
+ }
328
+
329
+ let globalKeybindings: KeybindingsManager | null = null;
330
+
331
+ export function setKeybindings(keybindings: KeybindingsManager): void {
332
+ globalKeybindings = keybindings;
333
+ }
334
+
335
+ export function getKeybindings(): KeybindingsManager {
336
+ if (!globalKeybindings) {
337
+ globalKeybindings = new KeybindingsManager(TUI_KEYBINDINGS);
338
+ }
339
+ return globalKeybindings;
340
+ }