@mdxui/terminal 2.0.0
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.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Keyboard Simulator
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for simulating keyboard events in Storybook stories
|
|
5
|
+
* for testing interactive terminal components.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { createKeyboardSimulator, KeySequence } from '@mdxui/terminal/storybook'
|
|
12
|
+
*
|
|
13
|
+
* const simulator = createKeyboardSimulator({ renderer })
|
|
14
|
+
*
|
|
15
|
+
* // Single key press
|
|
16
|
+
* simulator.press('enter')
|
|
17
|
+
*
|
|
18
|
+
* // Key with modifiers
|
|
19
|
+
* simulator.press('s', { ctrl: true })
|
|
20
|
+
*
|
|
21
|
+
* // Predefined sequences
|
|
22
|
+
* simulator.sequence(KeySequence.NAVIGATE_DOWN_3)
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { InteractiveRenderer } from '../renderers/interactive'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Types
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Key modifiers for keyboard events
|
|
34
|
+
*/
|
|
35
|
+
export interface KeyModifiers {
|
|
36
|
+
/** Ctrl key pressed */
|
|
37
|
+
ctrl?: boolean
|
|
38
|
+
/** Alt/Option key pressed */
|
|
39
|
+
alt?: boolean
|
|
40
|
+
/** Shift key pressed */
|
|
41
|
+
shift?: boolean
|
|
42
|
+
/** Meta/Command key pressed */
|
|
43
|
+
meta?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Single key press event
|
|
48
|
+
*/
|
|
49
|
+
export interface KeyPress {
|
|
50
|
+
/** Key name (e.g., 'enter', 'tab', 'a', 'escape') */
|
|
51
|
+
key: string
|
|
52
|
+
/** Key modifiers */
|
|
53
|
+
modifiers?: KeyModifiers
|
|
54
|
+
/** Delay in ms before this key press (default: 0) */
|
|
55
|
+
delay?: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Configuration for the keyboard simulator
|
|
60
|
+
*/
|
|
61
|
+
export interface KeyboardSimulatorConfig {
|
|
62
|
+
/** Interactive renderer instance */
|
|
63
|
+
renderer: InteractiveRenderer
|
|
64
|
+
/** Default delay between key presses in ms (default: 0) */
|
|
65
|
+
defaultDelay?: number
|
|
66
|
+
/** Callback when key is pressed */
|
|
67
|
+
onKeyPress?: (key: string, modifiers: KeyModifiers) => void
|
|
68
|
+
/** Callback when sequence completes */
|
|
69
|
+
onSequenceComplete?: (sequence: KeyPress[]) => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Keyboard simulator instance
|
|
74
|
+
*/
|
|
75
|
+
export interface KeyboardSimulator {
|
|
76
|
+
/** Press a single key */
|
|
77
|
+
press(key: string, modifiers?: KeyModifiers): void
|
|
78
|
+
/** Press a key and return a promise that resolves after the default delay */
|
|
79
|
+
pressAsync(key: string, modifiers?: KeyModifiers): Promise<void>
|
|
80
|
+
/** Execute a sequence of key presses */
|
|
81
|
+
sequence(keys: KeyPress[]): void
|
|
82
|
+
/** Execute a sequence of key presses with delays */
|
|
83
|
+
sequenceAsync(keys: KeyPress[]): Promise<void>
|
|
84
|
+
/** Type a string character by character */
|
|
85
|
+
type(text: string): void
|
|
86
|
+
/** Type a string character by character with delays */
|
|
87
|
+
typeAsync(text: string, charDelay?: number): Promise<void>
|
|
88
|
+
/** Reset simulator state */
|
|
89
|
+
reset(): void
|
|
90
|
+
/** Get list of pressed keys (for assertions) */
|
|
91
|
+
getPressedKeys(): string[]
|
|
92
|
+
/** Clear pressed keys history */
|
|
93
|
+
clearHistory(): void
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Predefined Key Sequences
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Common key sequences for testing navigation patterns
|
|
102
|
+
*/
|
|
103
|
+
export const KeySequence = {
|
|
104
|
+
/** Navigate down one item */
|
|
105
|
+
NAVIGATE_DOWN: [{ key: 'down' }] as KeyPress[],
|
|
106
|
+
|
|
107
|
+
/** Navigate down three items */
|
|
108
|
+
NAVIGATE_DOWN_3: [
|
|
109
|
+
{ key: 'down' },
|
|
110
|
+
{ key: 'down' },
|
|
111
|
+
{ key: 'down' },
|
|
112
|
+
] as KeyPress[],
|
|
113
|
+
|
|
114
|
+
/** Navigate up one item */
|
|
115
|
+
NAVIGATE_UP: [{ key: 'up' }] as KeyPress[],
|
|
116
|
+
|
|
117
|
+
/** Navigate up three items */
|
|
118
|
+
NAVIGATE_UP_3: [
|
|
119
|
+
{ key: 'up' },
|
|
120
|
+
{ key: 'up' },
|
|
121
|
+
{ key: 'up' },
|
|
122
|
+
] as KeyPress[],
|
|
123
|
+
|
|
124
|
+
/** Tab forward through focusables */
|
|
125
|
+
TAB_FORWARD: [{ key: 'tab' }] as KeyPress[],
|
|
126
|
+
|
|
127
|
+
/** Tab forward three times */
|
|
128
|
+
TAB_FORWARD_3: [
|
|
129
|
+
{ key: 'tab' },
|
|
130
|
+
{ key: 'tab' },
|
|
131
|
+
{ key: 'tab' },
|
|
132
|
+
] as KeyPress[],
|
|
133
|
+
|
|
134
|
+
/** Tab backward through focusables */
|
|
135
|
+
TAB_BACKWARD: [{ key: 'tab', modifiers: { shift: true } }] as KeyPress[],
|
|
136
|
+
|
|
137
|
+
/** Tab backward three times */
|
|
138
|
+
TAB_BACKWARD_3: [
|
|
139
|
+
{ key: 'tab', modifiers: { shift: true } },
|
|
140
|
+
{ key: 'tab', modifiers: { shift: true } },
|
|
141
|
+
{ key: 'tab', modifiers: { shift: true } },
|
|
142
|
+
] as KeyPress[],
|
|
143
|
+
|
|
144
|
+
/** Select/activate focused element */
|
|
145
|
+
SELECT: [{ key: 'enter' }] as KeyPress[],
|
|
146
|
+
|
|
147
|
+
/** Toggle focused element (e.g., checkbox) */
|
|
148
|
+
TOGGLE: [{ key: 'space' }] as KeyPress[],
|
|
149
|
+
|
|
150
|
+
/** Cancel/escape current action */
|
|
151
|
+
CANCEL: [{ key: 'escape' }] as KeyPress[],
|
|
152
|
+
|
|
153
|
+
/** Navigate to first item (vim: gg) */
|
|
154
|
+
GOTO_FIRST: [{ key: 'g' }, { key: 'g' }] as KeyPress[],
|
|
155
|
+
|
|
156
|
+
/** Navigate to last item (vim: G) */
|
|
157
|
+
GOTO_LAST: [{ key: 'G', modifiers: { shift: true } }] as KeyPress[],
|
|
158
|
+
|
|
159
|
+
/** Open search mode (vim: /) */
|
|
160
|
+
SEARCH: [{ key: '/' }] as KeyPress[],
|
|
161
|
+
|
|
162
|
+
/** Navigate down and select */
|
|
163
|
+
NAVIGATE_DOWN_SELECT: [{ key: 'down' }, { key: 'enter' }] as KeyPress[],
|
|
164
|
+
|
|
165
|
+
/** Navigate to next item using j (vim) */
|
|
166
|
+
VIM_DOWN: [{ key: 'j' }] as KeyPress[],
|
|
167
|
+
|
|
168
|
+
/** Navigate to previous item using k (vim) */
|
|
169
|
+
VIM_UP: [{ key: 'k' }] as KeyPress[],
|
|
170
|
+
|
|
171
|
+
/** Navigate left using h (vim) */
|
|
172
|
+
VIM_LEFT: [{ key: 'h' }] as KeyPress[],
|
|
173
|
+
|
|
174
|
+
/** Navigate right using l (vim) */
|
|
175
|
+
VIM_RIGHT: [{ key: 'l' }] as KeyPress[],
|
|
176
|
+
|
|
177
|
+
/** Delete sequence (vim: dd) */
|
|
178
|
+
VIM_DELETE: [{ key: 'd' }, { key: 'd' }] as KeyPress[],
|
|
179
|
+
|
|
180
|
+
/** Home key - jump to beginning */
|
|
181
|
+
HOME: [{ key: 'home' }] as KeyPress[],
|
|
182
|
+
|
|
183
|
+
/** End key - jump to end */
|
|
184
|
+
END: [{ key: 'end' }] as KeyPress[],
|
|
185
|
+
|
|
186
|
+
/** Page down */
|
|
187
|
+
PAGE_DOWN: [{ key: 'pagedown' }] as KeyPress[],
|
|
188
|
+
|
|
189
|
+
/** Page up */
|
|
190
|
+
PAGE_UP: [{ key: 'pageup' }] as KeyPress[],
|
|
191
|
+
|
|
192
|
+
/** Ctrl+C - cancel/interrupt */
|
|
193
|
+
INTERRUPT: [{ key: 'c', modifiers: { ctrl: true } }] as KeyPress[],
|
|
194
|
+
|
|
195
|
+
/** Ctrl+S - save */
|
|
196
|
+
SAVE: [{ key: 's', modifiers: { ctrl: true } }] as KeyPress[],
|
|
197
|
+
} as const
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Implementation
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Convert key and modifiers to binding string format
|
|
205
|
+
*/
|
|
206
|
+
function toBindingString(key: string, modifiers?: KeyModifiers): string {
|
|
207
|
+
const parts: string[] = []
|
|
208
|
+
|
|
209
|
+
if (modifiers?.ctrl) parts.push('ctrl')
|
|
210
|
+
if (modifiers?.alt) parts.push('alt')
|
|
211
|
+
if (modifiers?.shift) parts.push('shift')
|
|
212
|
+
if (modifiers?.meta) parts.push('meta')
|
|
213
|
+
|
|
214
|
+
parts.push(key.toLowerCase())
|
|
215
|
+
|
|
216
|
+
return parts.join('+')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Sleep utility for async operations
|
|
221
|
+
*/
|
|
222
|
+
function sleep(ms: number): Promise<void> {
|
|
223
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Creates a keyboard simulator for testing interactive terminal components.
|
|
228
|
+
*
|
|
229
|
+
* The simulator wraps the interactive renderer's `emitKey` method and provides
|
|
230
|
+
* higher-level abstractions for common keyboard testing patterns.
|
|
231
|
+
*
|
|
232
|
+
* @param config - Simulator configuration
|
|
233
|
+
* @returns Keyboard simulator instance
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```tsx
|
|
237
|
+
* const renderer = await createInteractiveRenderer({ vimBindings: true })
|
|
238
|
+
* const simulator = createKeyboardSimulator({ renderer })
|
|
239
|
+
*
|
|
240
|
+
* // Register some focusable elements
|
|
241
|
+
* renderer.registerFocusable('item-1', { tabIndex: 0 })
|
|
242
|
+
* renderer.registerFocusable('item-2', { tabIndex: 0 })
|
|
243
|
+
* renderer.registerFocusable('item-3', { tabIndex: 0 })
|
|
244
|
+
*
|
|
245
|
+
* // Focus first item
|
|
246
|
+
* renderer.focusById('item-1')
|
|
247
|
+
*
|
|
248
|
+
* // Navigate down using simulator
|
|
249
|
+
* simulator.press('j') // vim down
|
|
250
|
+
* expect(renderer.getFocusedId()).toBe('item-2')
|
|
251
|
+
*
|
|
252
|
+
* // Or use predefined sequences
|
|
253
|
+
* simulator.sequence(KeySequence.NAVIGATE_DOWN)
|
|
254
|
+
* expect(renderer.getFocusedId()).toBe('item-3')
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export function createKeyboardSimulator(
|
|
258
|
+
config: KeyboardSimulatorConfig
|
|
259
|
+
): KeyboardSimulator {
|
|
260
|
+
const { renderer, defaultDelay = 0, onKeyPress, onSequenceComplete } = config
|
|
261
|
+
|
|
262
|
+
// Track pressed keys for assertions
|
|
263
|
+
const pressedKeys: string[] = []
|
|
264
|
+
|
|
265
|
+
const simulator: KeyboardSimulator = {
|
|
266
|
+
press(key: string, modifiers?: KeyModifiers): void {
|
|
267
|
+
const bindingString = toBindingString(key, modifiers)
|
|
268
|
+
pressedKeys.push(bindingString)
|
|
269
|
+
onKeyPress?.(key, modifiers ?? {})
|
|
270
|
+
renderer.emitKey(bindingString)
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
async pressAsync(key: string, modifiers?: KeyModifiers): Promise<void> {
|
|
274
|
+
this.press(key, modifiers)
|
|
275
|
+
if (defaultDelay > 0) {
|
|
276
|
+
await sleep(defaultDelay)
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
sequence(keys: KeyPress[]): void {
|
|
281
|
+
for (const keyPress of keys) {
|
|
282
|
+
this.press(keyPress.key, keyPress.modifiers)
|
|
283
|
+
}
|
|
284
|
+
onSequenceComplete?.(keys)
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
async sequenceAsync(keys: KeyPress[]): Promise<void> {
|
|
288
|
+
for (const keyPress of keys) {
|
|
289
|
+
const delay = keyPress.delay ?? defaultDelay
|
|
290
|
+
if (delay > 0) {
|
|
291
|
+
await sleep(delay)
|
|
292
|
+
}
|
|
293
|
+
this.press(keyPress.key, keyPress.modifiers)
|
|
294
|
+
}
|
|
295
|
+
onSequenceComplete?.(keys)
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
type(text: string): void {
|
|
299
|
+
for (const char of text) {
|
|
300
|
+
// Handle special characters
|
|
301
|
+
if (char === '\n') {
|
|
302
|
+
this.press('enter')
|
|
303
|
+
} else if (char === '\t') {
|
|
304
|
+
this.press('tab')
|
|
305
|
+
} else if (char === ' ') {
|
|
306
|
+
this.press('space')
|
|
307
|
+
} else {
|
|
308
|
+
// Check if uppercase (implies shift)
|
|
309
|
+
const isUpperCase = char === char.toUpperCase() && char !== char.toLowerCase()
|
|
310
|
+
this.press(char, isUpperCase ? { shift: true } : undefined)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
async typeAsync(text: string, charDelay = 50): Promise<void> {
|
|
316
|
+
for (const char of text) {
|
|
317
|
+
if (charDelay > 0) {
|
|
318
|
+
await sleep(charDelay)
|
|
319
|
+
}
|
|
320
|
+
if (char === '\n') {
|
|
321
|
+
this.press('enter')
|
|
322
|
+
} else if (char === '\t') {
|
|
323
|
+
this.press('tab')
|
|
324
|
+
} else if (char === ' ') {
|
|
325
|
+
this.press('space')
|
|
326
|
+
} else {
|
|
327
|
+
const isUpperCase = char === char.toUpperCase() && char !== char.toLowerCase()
|
|
328
|
+
this.press(char, isUpperCase ? { shift: true } : undefined)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
reset(): void {
|
|
334
|
+
this.clearHistory()
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
getPressedKeys(): string[] {
|
|
338
|
+
return [...pressedKeys]
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
clearHistory(): void {
|
|
342
|
+
pressedKeys.length = 0
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return simulator
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// Assertion Helpers
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Assert that a specific key was pressed
|
|
355
|
+
*/
|
|
356
|
+
export function expectKeyPressed(
|
|
357
|
+
simulator: KeyboardSimulator,
|
|
358
|
+
key: string,
|
|
359
|
+
modifiers?: KeyModifiers
|
|
360
|
+
): void {
|
|
361
|
+
const bindingString = toBindingString(key, modifiers)
|
|
362
|
+
const pressedKeys = simulator.getPressedKeys()
|
|
363
|
+
|
|
364
|
+
if (!pressedKeys.includes(bindingString)) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Expected key "${bindingString}" to be pressed, but it was not. Pressed keys: [${pressedKeys.join(', ')}]`
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Assert that a sequence of keys was pressed in order
|
|
373
|
+
*/
|
|
374
|
+
export function expectSequencePressed(
|
|
375
|
+
simulator: KeyboardSimulator,
|
|
376
|
+
sequence: KeyPress[]
|
|
377
|
+
): void {
|
|
378
|
+
const pressedKeys = simulator.getPressedKeys()
|
|
379
|
+
const expectedKeys = sequence.map((kp) => toBindingString(kp.key, kp.modifiers))
|
|
380
|
+
|
|
381
|
+
// Find the sequence in pressed keys
|
|
382
|
+
let found = false
|
|
383
|
+
for (let i = 0; i <= pressedKeys.length - expectedKeys.length; i++) {
|
|
384
|
+
let match = true
|
|
385
|
+
for (let j = 0; j < expectedKeys.length; j++) {
|
|
386
|
+
if (pressedKeys[i + j] !== expectedKeys[j]) {
|
|
387
|
+
match = false
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (match) {
|
|
392
|
+
found = true
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!found) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`Expected sequence [${expectedKeys.join(', ')}] to be pressed in order, but it was not. Pressed keys: [${pressedKeys.join(', ')}]`
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Assert that no keys were pressed
|
|
406
|
+
*/
|
|
407
|
+
export function expectNoKeysPressed(simulator: KeyboardSimulator): void {
|
|
408
|
+
const pressedKeys = simulator.getPressedKeys()
|
|
409
|
+
|
|
410
|
+
if (pressedKeys.length > 0) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Expected no keys to be pressed, but found: [${pressedKeys.join(', ')}]`
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Assert total number of key presses
|
|
419
|
+
*/
|
|
420
|
+
export function expectKeyPressCount(
|
|
421
|
+
simulator: KeyboardSimulator,
|
|
422
|
+
count: number
|
|
423
|
+
): void {
|
|
424
|
+
const pressedKeys = simulator.getPressedKeys()
|
|
425
|
+
|
|
426
|
+
if (pressedKeys.length !== count) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`Expected ${count} key press(es), but found ${pressedKeys.length}. Pressed keys: [${pressedKeys.join(', ')}]`
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============================================================================
|
|
434
|
+
// Browser/DOM Key Event Integration (for Storybook)
|
|
435
|
+
// ============================================================================
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Map DOM KeyboardEvent to our key format
|
|
439
|
+
*/
|
|
440
|
+
export function mapDOMKeyEvent(event: KeyboardEvent): KeyPress {
|
|
441
|
+
// Normalize key names
|
|
442
|
+
let key = event.key.toLowerCase()
|
|
443
|
+
|
|
444
|
+
// Map special keys
|
|
445
|
+
const keyMap: Record<string, string> = {
|
|
446
|
+
arrowup: 'up',
|
|
447
|
+
arrowdown: 'down',
|
|
448
|
+
arrowleft: 'left',
|
|
449
|
+
arrowright: 'right',
|
|
450
|
+
' ': 'space',
|
|
451
|
+
return: 'enter',
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
key = keyMap[key] ?? key
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
key,
|
|
458
|
+
modifiers: {
|
|
459
|
+
ctrl: event.ctrlKey,
|
|
460
|
+
alt: event.altKey,
|
|
461
|
+
shift: event.shiftKey,
|
|
462
|
+
meta: event.metaKey,
|
|
463
|
+
},
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Create a keyboard event handler for DOM events that forwards to simulator
|
|
469
|
+
*/
|
|
470
|
+
export function createDOMKeyHandler(
|
|
471
|
+
simulator: KeyboardSimulator,
|
|
472
|
+
options?: {
|
|
473
|
+
/** Prevent default browser behavior */
|
|
474
|
+
preventDefault?: boolean
|
|
475
|
+
/** Stop event propagation */
|
|
476
|
+
stopPropagation?: boolean
|
|
477
|
+
/** Keys to ignore (don't forward to simulator) */
|
|
478
|
+
ignoreKeys?: string[]
|
|
479
|
+
}
|
|
480
|
+
): (event: KeyboardEvent) => void {
|
|
481
|
+
const { preventDefault = true, stopPropagation = true, ignoreKeys = [] } = options ?? {}
|
|
482
|
+
|
|
483
|
+
return (event: KeyboardEvent) => {
|
|
484
|
+
const keyPress = mapDOMKeyEvent(event)
|
|
485
|
+
|
|
486
|
+
// Check if key should be ignored
|
|
487
|
+
if (ignoreKeys.includes(keyPress.key)) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (preventDefault) {
|
|
492
|
+
event.preventDefault()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (stopPropagation) {
|
|
496
|
+
event.stopPropagation()
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
simulator.press(keyPress.key, keyPress.modifiers)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI Escape Code Constants
|
|
3
|
+
*
|
|
4
|
+
* Standard ANSI escape sequences for terminal text styling.
|
|
5
|
+
* These codes work in all terminals that support ANSI escape codes.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ANSI escape code constants for terminal styling.
|
|
12
|
+
*
|
|
13
|
+
* Includes:
|
|
14
|
+
* - Reset code to clear all styles
|
|
15
|
+
* - Text styling (bold, dim, italic, underline, strikethrough)
|
|
16
|
+
* - Standard 8 foreground colors (black through white)
|
|
17
|
+
* - Bright variants of all 8 foreground colors
|
|
18
|
+
* - Standard 8 background colors
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* // Apply bold cyan text
|
|
23
|
+
* console.log(`${ANSI.bold}${ANSI.cyan}Hello${ANSI.reset}`)
|
|
24
|
+
*
|
|
25
|
+
* // Use with styled helper
|
|
26
|
+
* console.log(styled('Error!', ANSI.bold, ANSI.red))
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const ANSI = {
|
|
30
|
+
// Reset
|
|
31
|
+
reset: '\x1b[0m',
|
|
32
|
+
|
|
33
|
+
// Styles
|
|
34
|
+
bold: '\x1b[1m',
|
|
35
|
+
dim: '\x1b[2m',
|
|
36
|
+
italic: '\x1b[3m',
|
|
37
|
+
underline: '\x1b[4m',
|
|
38
|
+
inverse: '\x1b[7m',
|
|
39
|
+
strikethrough: '\x1b[9m',
|
|
40
|
+
|
|
41
|
+
// Foreground colors (basic)
|
|
42
|
+
black: '\x1b[30m',
|
|
43
|
+
red: '\x1b[31m',
|
|
44
|
+
green: '\x1b[32m',
|
|
45
|
+
yellow: '\x1b[33m',
|
|
46
|
+
blue: '\x1b[34m',
|
|
47
|
+
magenta: '\x1b[35m',
|
|
48
|
+
cyan: '\x1b[36m',
|
|
49
|
+
white: '\x1b[37m',
|
|
50
|
+
|
|
51
|
+
// Bright foreground colors
|
|
52
|
+
brightBlack: '\x1b[90m',
|
|
53
|
+
brightRed: '\x1b[91m',
|
|
54
|
+
brightGreen: '\x1b[92m',
|
|
55
|
+
brightYellow: '\x1b[93m',
|
|
56
|
+
brightBlue: '\x1b[94m',
|
|
57
|
+
brightMagenta: '\x1b[95m',
|
|
58
|
+
brightCyan: '\x1b[96m',
|
|
59
|
+
brightWhite: '\x1b[97m',
|
|
60
|
+
|
|
61
|
+
// Background colors
|
|
62
|
+
bgBlack: '\x1b[40m',
|
|
63
|
+
bgRed: '\x1b[41m',
|
|
64
|
+
bgGreen: '\x1b[42m',
|
|
65
|
+
bgYellow: '\x1b[43m',
|
|
66
|
+
bgBlue: '\x1b[44m',
|
|
67
|
+
bgMagenta: '\x1b[45m',
|
|
68
|
+
bgCyan: '\x1b[46m',
|
|
69
|
+
bgWhite: '\x1b[47m',
|
|
70
|
+
|
|
71
|
+
// Bright background colors
|
|
72
|
+
bgBrightBlack: '\x1b[100m',
|
|
73
|
+
bgBrightRed: '\x1b[101m',
|
|
74
|
+
bgBrightGreen: '\x1b[102m',
|
|
75
|
+
bgBrightYellow: '\x1b[103m',
|
|
76
|
+
bgBrightBlue: '\x1b[104m',
|
|
77
|
+
bgBrightMagenta: '\x1b[105m',
|
|
78
|
+
bgBrightCyan: '\x1b[106m',
|
|
79
|
+
bgBrightWhite: '\x1b[107m',
|
|
80
|
+
} as const
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box Drawing Characters
|
|
3
|
+
*
|
|
4
|
+
* Unicode box-drawing character sets for terminal UI borders
|
|
5
|
+
* and utility functions for drawing boxes.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unicode box-drawing character sets for terminal UI borders.
|
|
12
|
+
*
|
|
13
|
+
* Provides three styles:
|
|
14
|
+
* - `single` - Standard single-line borders (┌─┐│└┘)
|
|
15
|
+
* - `double` - Double-line borders (╔═╗║╚╝)
|
|
16
|
+
* - `rounded` - Single-line with rounded corners (╭─╮│╰╯)
|
|
17
|
+
*
|
|
18
|
+
* Each set includes:
|
|
19
|
+
* - Corner characters (topLeft, topRight, bottomLeft, bottomRight)
|
|
20
|
+
* - Line characters (horizontal, vertical)
|
|
21
|
+
* - T-junction characters (teeLeft, teeRight, teeTop, teeBottom)
|
|
22
|
+
* - Cross character (cross)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* const chars = boxChars.single
|
|
27
|
+
* const top = chars.topLeft + chars.horizontal.repeat(10) + chars.topRight
|
|
28
|
+
* // Outputs: ┌──────────┐
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const boxChars = {
|
|
32
|
+
single: {
|
|
33
|
+
topLeft: '┌',
|
|
34
|
+
topRight: '┐',
|
|
35
|
+
bottomLeft: '└',
|
|
36
|
+
bottomRight: '┘',
|
|
37
|
+
horizontal: '─',
|
|
38
|
+
vertical: '│',
|
|
39
|
+
teeLeft: '├',
|
|
40
|
+
teeRight: '┤',
|
|
41
|
+
teeTop: '┬',
|
|
42
|
+
teeBottom: '┴',
|
|
43
|
+
cross: '┼',
|
|
44
|
+
},
|
|
45
|
+
double: {
|
|
46
|
+
topLeft: '╔',
|
|
47
|
+
topRight: '╗',
|
|
48
|
+
bottomLeft: '╚',
|
|
49
|
+
bottomRight: '╝',
|
|
50
|
+
horizontal: '═',
|
|
51
|
+
vertical: '║',
|
|
52
|
+
teeLeft: '╠',
|
|
53
|
+
teeRight: '╣',
|
|
54
|
+
teeTop: '╦',
|
|
55
|
+
teeBottom: '╩',
|
|
56
|
+
cross: '╬',
|
|
57
|
+
},
|
|
58
|
+
rounded: {
|
|
59
|
+
topLeft: '╭',
|
|
60
|
+
topRight: '╮',
|
|
61
|
+
bottomLeft: '╰',
|
|
62
|
+
bottomRight: '╯',
|
|
63
|
+
horizontal: '─',
|
|
64
|
+
vertical: '│',
|
|
65
|
+
teeLeft: '├',
|
|
66
|
+
teeRight: '┤',
|
|
67
|
+
teeTop: '┬',
|
|
68
|
+
teeBottom: '┴',
|
|
69
|
+
cross: '┼',
|
|
70
|
+
},
|
|
71
|
+
} as const
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Available box drawing styles.
|
|
75
|
+
* - `'single'` - Standard single-line borders
|
|
76
|
+
* - `'double'` - Double-line borders
|
|
77
|
+
* - `'rounded'` - Single-line with rounded corners
|
|
78
|
+
*/
|
|
79
|
+
export type BoxStyle = keyof typeof boxChars
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Draws a box with specified style and dimensions.
|
|
83
|
+
*
|
|
84
|
+
* Creates an array of strings representing each line of the box.
|
|
85
|
+
* The box is hollow (empty inside) and can be used as a container
|
|
86
|
+
* for content in terminal UIs.
|
|
87
|
+
*
|
|
88
|
+
* @param width - Total width of the box including borders
|
|
89
|
+
* @param height - Total height of the box including borders
|
|
90
|
+
* @param style - Box style ('single', 'double', or 'rounded')
|
|
91
|
+
* @returns Array of strings, one per line of the box
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* const box = drawBox(20, 5, 'rounded')
|
|
96
|
+
* box.forEach(line => console.log(line))
|
|
97
|
+
* // ╭──────────────────╮
|
|
98
|
+
* // │ │
|
|
99
|
+
* // │ │
|
|
100
|
+
* // │ │
|
|
101
|
+
* // ╰──────────────────╯
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function drawBox(
|
|
105
|
+
width: number,
|
|
106
|
+
height: number,
|
|
107
|
+
style: BoxStyle = 'single'
|
|
108
|
+
): string[] {
|
|
109
|
+
// Handle edge cases for dimensions
|
|
110
|
+
if (width < 2) width = 2
|
|
111
|
+
if (height < 2) height = 2
|
|
112
|
+
|
|
113
|
+
// Ensure integers
|
|
114
|
+
width = Math.floor(width)
|
|
115
|
+
height = Math.floor(height)
|
|
116
|
+
|
|
117
|
+
const chars = boxChars[style]
|
|
118
|
+
const lines: string[] = []
|
|
119
|
+
|
|
120
|
+
// Top border
|
|
121
|
+
lines.push(chars.topLeft + chars.horizontal.repeat(Math.max(0, width - 2)) + chars.topRight)
|
|
122
|
+
|
|
123
|
+
// Middle rows
|
|
124
|
+
for (let i = 0; i < height - 2; i++) {
|
|
125
|
+
lines.push(chars.vertical + ' '.repeat(Math.max(0, width - 2)) + chars.vertical)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Bottom border
|
|
129
|
+
lines.push(chars.bottomLeft + chars.horizontal.repeat(Math.max(0, width - 2)) + chars.bottomRight)
|
|
130
|
+
|
|
131
|
+
return lines
|
|
132
|
+
}
|