@a9s/cli 0.0.1 → 1.0.6

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.
Files changed (95) hide show
  1. package/README.md +167 -2
  2. package/dist/scripts/seed.js +310 -0
  3. package/dist/src/App.js +476 -0
  4. package/dist/src/adapters/ServiceAdapter.js +1 -0
  5. package/dist/src/adapters/capabilities/ActionCapability.js +1 -0
  6. package/dist/src/adapters/capabilities/DetailCapability.js +1 -0
  7. package/dist/src/adapters/capabilities/EditCapability.js +1 -0
  8. package/dist/src/adapters/capabilities/YankCapability.js +42 -0
  9. package/dist/src/adapters/capabilities/YankCapability.test.js +29 -0
  10. package/dist/src/components/AdvancedTextInput.js +200 -0
  11. package/dist/src/components/AdvancedTextInput.test.js +190 -0
  12. package/dist/src/components/AutocompleteInput.js +29 -0
  13. package/dist/src/components/DetailPanel.js +12 -0
  14. package/dist/src/components/DiffViewer.js +17 -0
  15. package/dist/src/components/ErrorStatePanel.js +5 -0
  16. package/dist/src/components/HUD.js +31 -0
  17. package/dist/src/components/HelpPanel.js +33 -0
  18. package/dist/src/components/ModeBar.js +43 -0
  19. package/dist/src/components/Table/index.js +109 -0
  20. package/dist/src/components/Table/widths.js +19 -0
  21. package/dist/src/components/TableSkeleton.js +25 -0
  22. package/dist/src/components/YankHelpPanel.js +43 -0
  23. package/dist/src/constants/commands.js +15 -0
  24. package/dist/src/constants/keybindings.js +530 -0
  25. package/dist/src/constants/keys.js +37 -0
  26. package/dist/src/features/AppMainView.integration.test.js +133 -0
  27. package/dist/src/features/AppMainView.js +95 -0
  28. package/dist/src/hooks/inputEvents.js +1 -0
  29. package/dist/src/hooks/mainInputScopes.js +68 -0
  30. package/dist/src/hooks/mainInputScopes.test.js +24 -0
  31. package/dist/src/hooks/useActionController.js +78 -0
  32. package/dist/src/hooks/useAppController.js +102 -0
  33. package/dist/src/hooks/useAppController.test.js +54 -0
  34. package/dist/src/hooks/useAppData.js +48 -0
  35. package/dist/src/hooks/useAwsContext.js +77 -0
  36. package/dist/src/hooks/useAwsProfiles.js +53 -0
  37. package/dist/src/hooks/useAwsRegions.js +105 -0
  38. package/dist/src/hooks/useCommandRouter.js +56 -0
  39. package/dist/src/hooks/useCommandRouter.test.js +27 -0
  40. package/dist/src/hooks/useDetailController.js +57 -0
  41. package/dist/src/hooks/useDetailController.test.js +32 -0
  42. package/dist/src/hooks/useHelpPanel.js +65 -0
  43. package/dist/src/hooks/useHierarchyState.js +39 -0
  44. package/dist/src/hooks/useInputEventProcessor.js +450 -0
  45. package/dist/src/hooks/useInputEventProcessor.test.js +174 -0
  46. package/dist/src/hooks/useKeyChord.js +83 -0
  47. package/dist/src/hooks/useMainInput.js +18 -0
  48. package/dist/src/hooks/useNavigation.js +47 -0
  49. package/dist/src/hooks/usePendingAction.js +8 -0
  50. package/dist/src/hooks/usePickerManager.js +130 -0
  51. package/dist/src/hooks/usePickerState.js +47 -0
  52. package/dist/src/hooks/usePickerTable.js +20 -0
  53. package/dist/src/hooks/useServiceView.js +226 -0
  54. package/dist/src/hooks/useUiHints.js +60 -0
  55. package/dist/src/hooks/useYankMode.js +24 -0
  56. package/dist/src/hooks/yankHeaderMarkers.js +23 -0
  57. package/dist/src/hooks/yankHeaderMarkers.test.js +49 -0
  58. package/dist/src/index.js +30 -0
  59. package/dist/src/services.js +12 -0
  60. package/dist/src/state/atoms.js +27 -0
  61. package/dist/src/types.js +12 -0
  62. package/dist/src/utils/aws.js +39 -0
  63. package/dist/src/utils/debugLogger.js +34 -0
  64. package/dist/src/utils/secretDisplay.js +45 -0
  65. package/dist/src/utils/withFullscreen.js +38 -0
  66. package/dist/src/views/dynamodb/adapter.js +22 -0
  67. package/dist/src/views/iam/adapter.js +258 -0
  68. package/dist/src/views/iam/capabilities/detailCapability.js +93 -0
  69. package/dist/src/views/iam/capabilities/editCapability.js +59 -0
  70. package/dist/src/views/iam/capabilities/yankCapability.js +6 -0
  71. package/dist/src/views/iam/capabilities/yankOptions.js +15 -0
  72. package/dist/src/views/iam/schema.js +7 -0
  73. package/dist/src/views/iam/types.js +1 -0
  74. package/dist/src/views/iam/utils.js +21 -0
  75. package/dist/src/views/route53/adapter.js +22 -0
  76. package/dist/src/views/s3/adapter.js +154 -0
  77. package/dist/src/views/s3/capabilities/actionCapability.js +172 -0
  78. package/dist/src/views/s3/capabilities/detailCapability.js +115 -0
  79. package/dist/src/views/s3/capabilities/editCapability.js +35 -0
  80. package/dist/src/views/s3/capabilities/yankCapability.js +6 -0
  81. package/dist/src/views/s3/capabilities/yankOptions.js +55 -0
  82. package/dist/src/views/s3/client.js +12 -0
  83. package/dist/src/views/s3/fetcher.js +86 -0
  84. package/dist/src/views/s3/schema.js +6 -0
  85. package/dist/src/views/s3/utils.js +19 -0
  86. package/dist/src/views/secretsmanager/adapter.js +188 -0
  87. package/dist/src/views/secretsmanager/capabilities/actionCapability.js +193 -0
  88. package/dist/src/views/secretsmanager/capabilities/detailCapability.js +46 -0
  89. package/dist/src/views/secretsmanager/capabilities/editCapability.js +116 -0
  90. package/dist/src/views/secretsmanager/capabilities/yankCapability.js +7 -0
  91. package/dist/src/views/secretsmanager/capabilities/yankOptions.js +68 -0
  92. package/dist/src/views/secretsmanager/schema.js +28 -0
  93. package/dist/src/views/secretsmanager/types.js +1 -0
  94. package/package.json +72 -5
  95. package/index.js +0 -1
@@ -0,0 +1,450 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { KB } from "../constants/keys.js";
3
+ import { KEYBINDINGS } from "../constants/keybindings.js";
4
+ import { useKeyChord, matchesTrigger } from "./useKeyChord.js";
5
+ import { resolveHelpScopeAction, resolveNavigateScopeAction, resolvePickerScopeAction, } from "./mainInputScopes.js";
6
+ export function translateRawInputEvent(input, key, runtime, deps) {
7
+ if (runtime.helpOpen) {
8
+ const action = resolveHelpScopeAction(input, deps.resolve(input, key, "help"));
9
+ switch (action.type) {
10
+ case "close":
11
+ return { event: { scope: "help", type: "close" }, resetChord: true };
12
+ case "prevTab":
13
+ return { event: { scope: "help", type: "prevTab" }, resetChord: true };
14
+ case "nextTab":
15
+ return { event: { scope: "help", type: "nextTab" }, resetChord: true };
16
+ case "scrollUp":
17
+ return { event: { scope: "help", type: "scrollUp" }, resetChord: true };
18
+ case "scrollDown":
19
+ return { event: { scope: "help", type: "scrollDown" }, resetChord: true };
20
+ case "goToTab":
21
+ return {
22
+ event: { scope: "help", type: "goToTab", input: action.input },
23
+ resetChord: true,
24
+ };
25
+ case "none":
26
+ return { event: null, resetChord: false };
27
+ }
28
+ }
29
+ if (runtime.pickerMode) {
30
+ const action = resolvePickerScopeAction(key, runtime.pickerMode, deps.resolve(input, key, "picker"));
31
+ switch (action.type) {
32
+ case "consume":
33
+ return { event: null, resetChord: false };
34
+ case "close":
35
+ return {
36
+ event: runtime.pickerMode === "search"
37
+ ? { scope: "picker", type: "cancelSearch" }
38
+ : { scope: "picker", type: "close" },
39
+ resetChord: true,
40
+ };
41
+ case "search":
42
+ return { event: { scope: "picker", type: "startSearch" }, resetChord: true };
43
+ case "down":
44
+ return { event: { scope: "picker", type: "down" }, resetChord: true };
45
+ case "up":
46
+ return { event: { scope: "picker", type: "up" }, resetChord: true };
47
+ case "top":
48
+ return { event: { scope: "picker", type: "top" }, resetChord: true };
49
+ case "bottom":
50
+ return { event: { scope: "picker", type: "bottom" }, resetChord: true };
51
+ case "confirm":
52
+ return { event: { scope: "picker", type: "confirm" }, resetChord: true };
53
+ case "none":
54
+ return { event: null, resetChord: false };
55
+ }
56
+ }
57
+ if (input === "?" &&
58
+ runtime.mode === "navigate" &&
59
+ !runtime.uploadPending &&
60
+ !runtime.describeOpen &&
61
+ !runtime.yankMode &&
62
+ !runtime.pendingActionType) {
63
+ return { event: { scope: "modal", type: "openHelp" }, resetChord: true };
64
+ }
65
+ if (runtime.pendingActionType) {
66
+ if (runtime.pendingActionType === "prompt" && key.escape) {
67
+ return { event: { scope: "modal", type: "cancelPendingPrompt" }, resetChord: true };
68
+ }
69
+ if (runtime.pendingActionType === "confirm") {
70
+ if (input === "y" || input === "Y") {
71
+ return { event: { scope: "pending", type: "submit", confirmed: true }, resetChord: true };
72
+ }
73
+ if (input === "n" || input === "N" || key.escape) {
74
+ return {
75
+ event: { scope: "pending", type: "submit", confirmed: false },
76
+ resetChord: true,
77
+ };
78
+ }
79
+ }
80
+ return { event: null, resetChord: true };
81
+ }
82
+ if (runtime.uploadPending) {
83
+ const scrollAction = deps.resolve(input, key, "navigate");
84
+ if (scrollAction === KB.MOVE_DOWN) {
85
+ return { event: { scope: "scroll", type: "down" }, resetChord: true };
86
+ }
87
+ if (scrollAction === KB.MOVE_UP) {
88
+ return { event: { scope: "scroll", type: "up" }, resetChord: true };
89
+ }
90
+ if (input === "y" || input === "Y") {
91
+ return { event: { scope: "upload", type: "decision", confirmed: true }, resetChord: true };
92
+ }
93
+ if (input === "n" || input === "N" || key.escape) {
94
+ return {
95
+ event: { scope: "upload", type: "decision", confirmed: false },
96
+ resetChord: true,
97
+ };
98
+ }
99
+ return { event: null, resetChord: true };
100
+ }
101
+ if (runtime.describeOpen) {
102
+ const scrollAction = deps.resolve(input, key, "navigate");
103
+ if (scrollAction === KB.MOVE_DOWN) {
104
+ return { event: { scope: "scroll", type: "down" }, resetChord: true };
105
+ }
106
+ if (scrollAction === KB.MOVE_UP) {
107
+ return { event: { scope: "scroll", type: "up" }, resetChord: true };
108
+ }
109
+ if (key.escape) {
110
+ return { event: { scope: "modal", type: "closeDetails" }, resetChord: true };
111
+ }
112
+ return { event: null, resetChord: true };
113
+ }
114
+ if (runtime.yankMode) {
115
+ if (!runtime.selectedRow) {
116
+ return { event: null, resetChord: true };
117
+ }
118
+ if (input === "?") {
119
+ return { event: { scope: "modal", type: "openYankHelp" }, resetChord: true };
120
+ }
121
+ if (key.escape) {
122
+ return { event: { scope: "modal", type: "cancelYank" }, resetChord: true };
123
+ }
124
+ return { event: null, resetChord: true };
125
+ }
126
+ if (key.escape) {
127
+ if (runtime.mode === "search" || runtime.mode === "command") {
128
+ return { event: { scope: "mode", type: "cancelSearchOrCommand" }, resetChord: true };
129
+ }
130
+ return { event: { scope: "mode", type: "clearFilterOrNavigateBack" }, resetChord: true };
131
+ }
132
+ if (key.tab) {
133
+ const canAutocomplete = runtime.mode === "command" &&
134
+ runtime.commandText.length > 0 &&
135
+ deps.hasCommandAutocomplete(runtime.commandText);
136
+ return {
137
+ event: canAutocomplete ? { scope: "mode", type: "commandAutocomplete" } : null,
138
+ resetChord: true,
139
+ };
140
+ }
141
+ if (runtime.mode === "search" || runtime.mode === "command") {
142
+ return { event: null, resetChord: false };
143
+ }
144
+ const baseAction = deps.resolve(input, key, "navigate");
145
+ // Check for scroll actions (j/k) first before other navigate actions
146
+ if (baseAction === KB.MOVE_DOWN) {
147
+ return { event: { scope: "scroll", type: "down" }, resetChord: false };
148
+ }
149
+ if (baseAction === KB.MOVE_UP) {
150
+ return { event: { scope: "scroll", type: "up" }, resetChord: false };
151
+ }
152
+ const navAction = resolveNavigateScopeAction(baseAction);
153
+ switch (navAction.type) {
154
+ case "search":
155
+ return { event: { scope: "mode", type: "startSearch" }, resetChord: false };
156
+ case "command":
157
+ return { event: { scope: "mode", type: "startCommand" }, resetChord: false };
158
+ case "quit":
159
+ return { event: { scope: "navigation", type: "quit" }, resetChord: false };
160
+ case "refresh":
161
+ return { event: { scope: "navigation", type: "refresh" }, resetChord: false };
162
+ case "reveal":
163
+ return { event: { scope: "navigation", type: "reveal" }, resetChord: false };
164
+ case "yank":
165
+ return { event: { scope: "navigation", type: "enterYank" }, resetChord: false };
166
+ case "details":
167
+ return { event: { scope: "navigation", type: "showDetails" }, resetChord: false };
168
+ case "edit":
169
+ return { event: { scope: "navigation", type: "edit" }, resetChord: false };
170
+ case "bottom":
171
+ return { event: { scope: "navigation", type: "bottom" }, resetChord: false };
172
+ case "top":
173
+ return { event: { scope: "navigation", type: "top" }, resetChord: false };
174
+ case "enter":
175
+ return { event: { scope: "navigation", type: "enter" }, resetChord: false };
176
+ case "none":
177
+ return { event: null, resetChord: false };
178
+ }
179
+ }
180
+ export function resolveAdapterBindingEvent(input, key, bindings, pending, row) {
181
+ const scoped = bindings.filter((binding) => (binding.scope ?? "navigate") === "navigate");
182
+ const next = pending.length > 0 ? [...pending, input] : [input];
183
+ const chordHit = scoped.find((binding) => binding.trigger.type === "chord" &&
184
+ binding.trigger.keys.length === next.length &&
185
+ binding.trigger.keys.every((k, i) => k === next[i]));
186
+ if (chordHit) {
187
+ return {
188
+ event: { scope: "adapterAction", type: "run", actionId: chordHit.actionId, row },
189
+ nextPending: [],
190
+ consumed: true,
191
+ };
192
+ }
193
+ const isChordPrefix = scoped.some((binding) => binding.trigger.type === "chord" &&
194
+ binding.trigger.keys.length > next.length &&
195
+ binding.trigger.keys.slice(0, next.length).every((k, i) => k === next[i]));
196
+ if (isChordPrefix) {
197
+ return { event: null, nextPending: next, consumed: true };
198
+ }
199
+ const directHit = scoped.find((binding) => binding.trigger.type !== "chord" && matchesTrigger(input, key, binding.trigger));
200
+ if (directHit) {
201
+ return {
202
+ event: { scope: "adapterAction", type: "run", actionId: directHit.actionId, row },
203
+ nextPending: [],
204
+ consumed: true,
205
+ };
206
+ }
207
+ return { event: null, nextPending: [], consumed: false };
208
+ }
209
+ export function applyInputEvent(event, actions) {
210
+ switch (event.scope) {
211
+ case "system":
212
+ actions.app.exit();
213
+ return;
214
+ case "raw":
215
+ return;
216
+ case "help":
217
+ switch (event.type) {
218
+ case "close":
219
+ actions.help.close();
220
+ return;
221
+ case "prevTab":
222
+ actions.help.prevTab();
223
+ return;
224
+ case "nextTab":
225
+ actions.help.nextTab();
226
+ return;
227
+ case "scrollUp":
228
+ actions.help.scrollUp();
229
+ return;
230
+ case "scrollDown":
231
+ actions.help.scrollDown();
232
+ return;
233
+ case "goToTab":
234
+ actions.help.goToTab(event.input);
235
+ return;
236
+ }
237
+ return;
238
+ case "picker":
239
+ switch (event.type) {
240
+ case "close":
241
+ actions.picker.close();
242
+ return;
243
+ case "cancelSearch":
244
+ actions.picker.cancelSearch();
245
+ return;
246
+ case "startSearch":
247
+ actions.picker.startSearch();
248
+ return;
249
+ case "down":
250
+ actions.picker.moveDown();
251
+ return;
252
+ case "up":
253
+ actions.picker.moveUp();
254
+ return;
255
+ case "top":
256
+ actions.picker.top();
257
+ return;
258
+ case "bottom":
259
+ actions.picker.bottom();
260
+ return;
261
+ case "confirm":
262
+ actions.picker.confirm();
263
+ return;
264
+ }
265
+ return;
266
+ case "modal":
267
+ switch (event.type) {
268
+ case "openHelp":
269
+ actions.help.open();
270
+ return;
271
+ case "openYankHelp":
272
+ actions.yank.openHelp();
273
+ return;
274
+ case "closeYankHelp":
275
+ actions.yank.closeHelp();
276
+ return;
277
+ case "closeDetails":
278
+ actions.details.close();
279
+ return;
280
+ case "cancelYank":
281
+ actions.yank.cancel();
282
+ return;
283
+ case "cancelPendingPrompt":
284
+ actions.pending.cancelPrompt();
285
+ return;
286
+ }
287
+ return;
288
+ case "pending":
289
+ actions.pending.submit(event.confirmed);
290
+ return;
291
+ case "upload":
292
+ actions.upload.decide(event.confirmed);
293
+ return;
294
+ case "mode":
295
+ switch (event.type) {
296
+ case "cancelSearchOrCommand":
297
+ actions.mode.cancelSearchOrCommand();
298
+ return;
299
+ case "clearFilterOrNavigateBack":
300
+ actions.mode.clearFilterOrNavigateBack();
301
+ return;
302
+ case "startSearch":
303
+ actions.mode.startSearch();
304
+ return;
305
+ case "startCommand":
306
+ actions.mode.startCommand();
307
+ return;
308
+ case "commandAutocomplete":
309
+ actions.mode.commandAutocomplete();
310
+ return;
311
+ }
312
+ return;
313
+ case "navigation":
314
+ switch (event.type) {
315
+ case "quit":
316
+ actions.app.exit();
317
+ return;
318
+ case "refresh":
319
+ actions.navigation.refresh();
320
+ return;
321
+ case "reveal":
322
+ actions.navigation.revealToggle();
323
+ return;
324
+ case "enterYank":
325
+ actions.yank.enter();
326
+ return;
327
+ case "showDetails":
328
+ actions.navigation.showDetails();
329
+ return;
330
+ case "edit":
331
+ actions.navigation.editSelection();
332
+ return;
333
+ case "bottom":
334
+ actions.navigation.bottom();
335
+ return;
336
+ case "top":
337
+ actions.navigation.top();
338
+ return;
339
+ case "enter":
340
+ actions.navigation.enter();
341
+ return;
342
+ }
343
+ return;
344
+ case "scroll":
345
+ switch (event.type) {
346
+ case "up":
347
+ actions.scroll.up();
348
+ return;
349
+ case "down":
350
+ actions.scroll.down();
351
+ return;
352
+ }
353
+ return;
354
+ case "adapterAction":
355
+ actions.adapterAction.run(event.actionId, event.row);
356
+ return;
357
+ }
358
+ }
359
+ export function useInputEventProcessor({ runtime, actions, yankOptions, pushYankFeedback, writeClipboard, hasCommandAutocomplete, }) {
360
+ const { resolve, reset } = useKeyChord(KEYBINDINGS);
361
+ const adapterPendingRef = useRef([]);
362
+ const pendingYankHelpRef = useRef(false);
363
+ const resetAllChords = useCallback(() => {
364
+ reset();
365
+ adapterPendingRef.current = [];
366
+ }, [reset]);
367
+ return useCallback((event) => {
368
+ if (event.scope === "raw") {
369
+ // Support fast "y ?" combo even before yankMode state is committed.
370
+ if (pendingYankHelpRef.current) {
371
+ if (event.input === "?") {
372
+ pendingYankHelpRef.current = false;
373
+ resetAllChords();
374
+ actions.yank.openHelp();
375
+ return;
376
+ }
377
+ pendingYankHelpRef.current = false;
378
+ }
379
+ if (runtime.yankHelpOpen) {
380
+ if (event.input === "?" || event.key.escape) {
381
+ resetAllChords();
382
+ actions.yank.closeHelp();
383
+ }
384
+ return;
385
+ }
386
+ if (runtime.yankMode) {
387
+ if (!runtime.selectedRow) {
388
+ resetAllChords();
389
+ return;
390
+ }
391
+ if (event.input === "?") {
392
+ resetAllChords();
393
+ actions.yank.openHelp();
394
+ return;
395
+ }
396
+ if (event.key.escape) {
397
+ resetAllChords();
398
+ actions.yank.cancel();
399
+ return;
400
+ }
401
+ const option = yankOptions.find((o) => matchesTrigger(event.input, event.key, o.trigger));
402
+ resetAllChords();
403
+ if (!option)
404
+ return;
405
+ actions.yank.cancel();
406
+ void option.resolve(runtime.selectedRow).then((value) => {
407
+ if (!value)
408
+ return;
409
+ void writeClipboard(value).then(() => pushYankFeedback(option.feedback));
410
+ });
411
+ return;
412
+ }
413
+ const translated = translateRawInputEvent(event.input, event.key, runtime, {
414
+ resolve: (input, key, scope) => resolve(input, key, scope),
415
+ hasCommandAutocomplete,
416
+ });
417
+ if (translated.resetChord) {
418
+ resetAllChords();
419
+ }
420
+ if (translated.event) {
421
+ if (translated.event.scope === "navigation" && translated.event.type === "enterYank") {
422
+ pendingYankHelpRef.current = true;
423
+ }
424
+ applyInputEvent(translated.event, actions);
425
+ return;
426
+ }
427
+ const adapterResolved = resolveAdapterBindingEvent(event.input, event.key, actions.adapterAction.bindings, adapterPendingRef.current, runtime.selectedRow);
428
+ adapterPendingRef.current = adapterResolved.nextPending;
429
+ if (adapterResolved.event) {
430
+ reset();
431
+ applyInputEvent(adapterResolved.event, actions);
432
+ return;
433
+ }
434
+ return;
435
+ }
436
+ if (event.scope === "system") {
437
+ applyInputEvent(event, actions);
438
+ }
439
+ }, [
440
+ actions,
441
+ hasCommandAutocomplete,
442
+ pushYankFeedback,
443
+ reset,
444
+ resetAllChords,
445
+ resolve,
446
+ runtime,
447
+ writeClipboard,
448
+ yankOptions,
449
+ ]);
450
+ }
@@ -0,0 +1,174 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { KB } from "../constants/keys.js";
3
+ import { applyInputEvent, resolveAdapterBindingEvent, translateRawInputEvent, } from "./useInputEventProcessor.js";
4
+ import { textCell } from "../types.js";
5
+ const key = (patch = {}) => ({
6
+ upArrow: false,
7
+ downArrow: false,
8
+ leftArrow: false,
9
+ rightArrow: false,
10
+ pageDown: false,
11
+ pageUp: false,
12
+ home: false,
13
+ end: false,
14
+ return: false,
15
+ escape: false,
16
+ ctrl: false,
17
+ shift: false,
18
+ tab: false,
19
+ backspace: false,
20
+ delete: false,
21
+ meta: false,
22
+ ...patch,
23
+ });
24
+ const baseRuntime = {
25
+ mode: "navigate",
26
+ filterText: "",
27
+ commandText: "",
28
+ searchEntryFilter: null,
29
+ yankMode: false,
30
+ yankHelpOpen: false,
31
+ selectedRow: null,
32
+ helpOpen: false,
33
+ pickerMode: null,
34
+ describeOpen: false,
35
+ uploadPending: false,
36
+ pendingActionType: null,
37
+ };
38
+ describe("translateRawInputEvent", () => {
39
+ it("maps help scope actions first", () => {
40
+ const result = translateRawInputEvent("", key(), { ...baseRuntime, helpOpen: true }, {
41
+ resolve: () => KB.HELP_CLOSE,
42
+ hasCommandAutocomplete: () => false,
43
+ });
44
+ expect(result).toEqual({
45
+ event: { scope: "help", type: "close" },
46
+ resetChord: true,
47
+ });
48
+ });
49
+ it("consumes picker search typing", () => {
50
+ const result = translateRawInputEvent("a", key(), { ...baseRuntime, pickerMode: "search" }, {
51
+ resolve: () => null,
52
+ hasCommandAutocomplete: () => false,
53
+ });
54
+ expect(result).toEqual({ event: null, resetChord: false });
55
+ });
56
+ it("maps pending confirm y/n", () => {
57
+ const yes = translateRawInputEvent("y", key(), { ...baseRuntime, pendingActionType: "confirm" }, {
58
+ resolve: () => null,
59
+ hasCommandAutocomplete: () => false,
60
+ });
61
+ expect(yes.event).toEqual({ scope: "pending", type: "submit", confirmed: true });
62
+ });
63
+ it("maps y ? to yank help when yank mode is active", () => {
64
+ const result = translateRawInputEvent("?", key(), { ...baseRuntime, yankMode: true, selectedRow: { id: "r1", cells: { name: textCell("x") } } }, {
65
+ resolve: () => null,
66
+ hasCommandAutocomplete: () => false,
67
+ });
68
+ expect(result).toEqual({
69
+ event: { scope: "modal", type: "openYankHelp" },
70
+ resetChord: true,
71
+ });
72
+ });
73
+ });
74
+ describe("resolveAdapterBindingEvent", () => {
75
+ it("resolves adapter chord keybindings like g p", () => {
76
+ const row = { id: "row-1", cells: { name: textCell("n") } };
77
+ const first = resolveAdapterBindingEvent("g", key(), [
78
+ {
79
+ trigger: { type: "chord", keys: ["g", "p"] },
80
+ actionId: "jump-to-path",
81
+ label: "jump",
82
+ adapterId: "s3",
83
+ },
84
+ ], [], row);
85
+ expect(first.event).toBeNull();
86
+ expect(first.nextPending).toEqual(["g"]);
87
+ const second = resolveAdapterBindingEvent("p", key(), [
88
+ {
89
+ trigger: { type: "chord", keys: ["g", "p"] },
90
+ actionId: "jump-to-path",
91
+ label: "jump",
92
+ adapterId: "s3",
93
+ },
94
+ ], first.nextPending, row);
95
+ expect(second.event).toEqual({
96
+ scope: "adapterAction",
97
+ type: "run",
98
+ actionId: "jump-to-path",
99
+ row,
100
+ });
101
+ });
102
+ });
103
+ describe("applyInputEvent", () => {
104
+ it("routes event to grouped action handlers", () => {
105
+ const actions = {
106
+ app: { exit: vi.fn() },
107
+ help: {
108
+ open: vi.fn(),
109
+ close: vi.fn(),
110
+ prevTab: vi.fn(),
111
+ nextTab: vi.fn(),
112
+ scrollUp: vi.fn(),
113
+ scrollDown: vi.fn(),
114
+ goToTab: vi.fn(),
115
+ },
116
+ picker: {
117
+ close: vi.fn(),
118
+ cancelSearch: vi.fn(),
119
+ startSearch: vi.fn(),
120
+ moveDown: vi.fn(),
121
+ moveUp: vi.fn(),
122
+ top: vi.fn(),
123
+ bottom: vi.fn(),
124
+ confirm: vi.fn(),
125
+ },
126
+ mode: {
127
+ cancelSearchOrCommand: vi.fn(),
128
+ clearFilterOrNavigateBack: vi.fn(),
129
+ startSearch: vi.fn(),
130
+ startCommand: vi.fn(),
131
+ commandAutocomplete: vi.fn(),
132
+ },
133
+ navigation: {
134
+ refresh: vi.fn(),
135
+ revealToggle: vi.fn(),
136
+ showDetails: vi.fn(),
137
+ editSelection: vi.fn(),
138
+ top: vi.fn(),
139
+ bottom: vi.fn(),
140
+ enter: vi.fn(),
141
+ },
142
+ scroll: {
143
+ up: vi.fn(),
144
+ down: vi.fn(),
145
+ },
146
+ yank: {
147
+ enter: vi.fn(),
148
+ cancel: vi.fn(),
149
+ openHelp: vi.fn(),
150
+ closeHelp: vi.fn(),
151
+ },
152
+ details: {
153
+ close: vi.fn(),
154
+ },
155
+ pending: {
156
+ cancelPrompt: vi.fn(),
157
+ submit: vi.fn(),
158
+ },
159
+ upload: {
160
+ decide: vi.fn(),
161
+ },
162
+ adapterAction: {
163
+ run: vi.fn(),
164
+ bindings: [],
165
+ },
166
+ };
167
+ applyInputEvent({ scope: "navigation", type: "refresh" }, actions);
168
+ expect(actions.navigation.refresh).toHaveBeenCalledTimes(1);
169
+ applyInputEvent({ scope: "modal", type: "openHelp" }, actions);
170
+ expect(actions.help.open).toHaveBeenCalledTimes(1);
171
+ applyInputEvent({ scope: "modal", type: "openYankHelp" }, actions);
172
+ expect(actions.yank.openHelp).toHaveBeenCalledTimes(1);
173
+ });
174
+ });