@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Keyboard Navigation
|
|
3
|
+
*
|
|
4
|
+
* Keyboard binding manager and React hooks for terminal keyboard navigation.
|
|
5
|
+
* Supports Vim-style bindings, modifier keys, and key sequences.
|
|
6
|
+
*
|
|
7
|
+
* This module provides:
|
|
8
|
+
* - Keyboard manager for handling key bindings and sequences
|
|
9
|
+
* - React hooks for keyboard input, focus management, and navigation
|
|
10
|
+
* - Preset bindings for Vim-style and arrow key navigation
|
|
11
|
+
* - Focus provider for managing focus across components
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { createKeyboardManager, VIM_BINDINGS, useFocus } from '@mdxui/terminal'
|
|
16
|
+
*
|
|
17
|
+
* // Create a keyboard manager with Vim bindings
|
|
18
|
+
* const keyboard = createKeyboardManager({
|
|
19
|
+
* bindings: VIM_BINDINGS,
|
|
20
|
+
* onAction: (action) => console.log('Action:', action)
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* // Handle key presses
|
|
24
|
+
* keyboard.handleKey('j') // triggers 'move-down'
|
|
25
|
+
* keyboard.handleKey('g')
|
|
26
|
+
* keyboard.handleKey('g') // triggers 'move-first' (sequence: gg)
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// Manager exports
|
|
33
|
+
export {
|
|
34
|
+
// Types
|
|
35
|
+
type KeyModifiers,
|
|
36
|
+
type KeyboardAction,
|
|
37
|
+
type KeyBindings,
|
|
38
|
+
type ActionContext,
|
|
39
|
+
type ActionHandler,
|
|
40
|
+
type KeyboardManagerOptions,
|
|
41
|
+
type KeyboardManager,
|
|
42
|
+
// Constants
|
|
43
|
+
KEY,
|
|
44
|
+
// Functions
|
|
45
|
+
matchKey,
|
|
46
|
+
createKeyboardManager,
|
|
47
|
+
} from './manager'
|
|
48
|
+
|
|
49
|
+
// Preset exports
|
|
50
|
+
export {
|
|
51
|
+
VIM_BINDINGS,
|
|
52
|
+
ARROW_BINDINGS,
|
|
53
|
+
COMMON_BINDINGS,
|
|
54
|
+
EMACS_BINDINGS,
|
|
55
|
+
} from './presets'
|
|
56
|
+
|
|
57
|
+
// Focus exports
|
|
58
|
+
export {
|
|
59
|
+
// Types
|
|
60
|
+
type UseKeyboardOptions,
|
|
61
|
+
type UseKeyboardResult,
|
|
62
|
+
type UseFocusOptions,
|
|
63
|
+
type FocusableElement,
|
|
64
|
+
type FocusManagerState,
|
|
65
|
+
type UseNavigableListOptions,
|
|
66
|
+
type UseNavigableListResult,
|
|
67
|
+
type UseNavigableGridOptions,
|
|
68
|
+
type UseNavigableGridResult,
|
|
69
|
+
type FocusProviderProps,
|
|
70
|
+
// Context
|
|
71
|
+
FocusContext,
|
|
72
|
+
// Hooks
|
|
73
|
+
useKeyboard,
|
|
74
|
+
useFocus,
|
|
75
|
+
useFocusManager,
|
|
76
|
+
useNavigableList,
|
|
77
|
+
useNavigableGrid,
|
|
78
|
+
// Components
|
|
79
|
+
FocusProvider,
|
|
80
|
+
} from './focus'
|
|
81
|
+
|
|
82
|
+
// Integration exports
|
|
83
|
+
export {
|
|
84
|
+
// Types
|
|
85
|
+
type NormalizedKey,
|
|
86
|
+
type ReadlineKey,
|
|
87
|
+
type AttachKeyboardOptions,
|
|
88
|
+
type DetachKeyboard,
|
|
89
|
+
type OpenTUIKeyEvent,
|
|
90
|
+
// Functions
|
|
91
|
+
normalizeReadlineKey,
|
|
92
|
+
keyToBindingString,
|
|
93
|
+
attachKeyboardManager,
|
|
94
|
+
normalizeOpenTUIKey,
|
|
95
|
+
createOpenTUIKeyHandler,
|
|
96
|
+
} from './integration'
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Input Integration
|
|
3
|
+
*
|
|
4
|
+
* Integration helpers for connecting keyboard managers to terminal input sources.
|
|
5
|
+
* Supports Node.js readline and OpenTUI key events.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { KeyboardManager, ActionHandler } from './manager'
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Normalized Key Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalized key event structure for keyboard handling.
|
|
18
|
+
*
|
|
19
|
+
* This is the standardized format used internally by the keyboard manager.
|
|
20
|
+
* Input from various sources (readline, OpenTUI) is normalized to this format.
|
|
21
|
+
*/
|
|
22
|
+
export interface NormalizedKey {
|
|
23
|
+
/** The key name (e.g., 'a', 'enter', 'up', 'escape') */
|
|
24
|
+
name: string
|
|
25
|
+
/** Whether Ctrl was held */
|
|
26
|
+
ctrl: boolean
|
|
27
|
+
/** Whether Alt/Option was held */
|
|
28
|
+
alt: boolean
|
|
29
|
+
/** Whether Shift was held */
|
|
30
|
+
shift: boolean
|
|
31
|
+
/** Whether Meta/Cmd was held */
|
|
32
|
+
meta: boolean
|
|
33
|
+
/** Raw character sequence if available */
|
|
34
|
+
sequence?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Readline key event structure from Node.js readline module.
|
|
39
|
+
* @see https://nodejs.org/api/readline.html#event-keypress
|
|
40
|
+
*/
|
|
41
|
+
export interface ReadlineKey {
|
|
42
|
+
/** Key name */
|
|
43
|
+
name?: string
|
|
44
|
+
/** Whether Ctrl was held */
|
|
45
|
+
ctrl?: boolean
|
|
46
|
+
/** Whether Meta/Cmd was held */
|
|
47
|
+
meta?: boolean
|
|
48
|
+
/** Whether Shift was held */
|
|
49
|
+
shift?: boolean
|
|
50
|
+
/** Raw sequence */
|
|
51
|
+
sequence?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Readline Integration
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Normalizes a readline key event to our standard NormalizedKey format.
|
|
60
|
+
*
|
|
61
|
+
* @param str - The character string from the keypress event
|
|
62
|
+
* @param key - The readline key object
|
|
63
|
+
* @returns Normalized key event
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* import { emitKeypressEvents } from 'readline'
|
|
68
|
+
*
|
|
69
|
+
* emitKeypressEvents(process.stdin)
|
|
70
|
+
* process.stdin.on('keypress', (str, key) => {
|
|
71
|
+
* const normalized = normalizeReadlineKey(str, key)
|
|
72
|
+
* console.log(normalized) // { name: 'a', ctrl: false, ... }
|
|
73
|
+
* })
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeReadlineKey(str: string | undefined, key: ReadlineKey | undefined): NormalizedKey {
|
|
77
|
+
// Handle special characters by their sequence
|
|
78
|
+
if (!key && str) {
|
|
79
|
+
// Single character input
|
|
80
|
+
return {
|
|
81
|
+
name: str,
|
|
82
|
+
ctrl: false,
|
|
83
|
+
alt: false,
|
|
84
|
+
shift: str !== str.toLowerCase() && str === str.toUpperCase(),
|
|
85
|
+
meta: false,
|
|
86
|
+
sequence: str,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Map readline key names to our standard names
|
|
91
|
+
const nameMap: Record<string, string> = {
|
|
92
|
+
return: 'enter',
|
|
93
|
+
escape: 'escape',
|
|
94
|
+
up: 'up',
|
|
95
|
+
down: 'down',
|
|
96
|
+
left: 'left',
|
|
97
|
+
right: 'right',
|
|
98
|
+
tab: 'tab',
|
|
99
|
+
backspace: 'backspace',
|
|
100
|
+
delete: 'delete',
|
|
101
|
+
space: 'space',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const keyName = key?.name ?? str ?? ''
|
|
105
|
+
const normalizedName = nameMap[keyName.toLowerCase()] ?? keyName.toLowerCase()
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
name: normalizedName,
|
|
109
|
+
ctrl: key?.ctrl ?? false,
|
|
110
|
+
alt: false, // readline doesn't distinguish alt reliably
|
|
111
|
+
shift: key?.shift ?? false,
|
|
112
|
+
meta: key?.meta ?? false,
|
|
113
|
+
sequence: key?.sequence ?? str,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Converts a NormalizedKey to a string format for binding lookup.
|
|
119
|
+
*
|
|
120
|
+
* Produces strings like:
|
|
121
|
+
* - 'a' (plain key)
|
|
122
|
+
* - 'ctrl+c' (with ctrl modifier)
|
|
123
|
+
* - 'shift+tab' (with shift modifier)
|
|
124
|
+
* - 'ctrl+shift+s' (combined modifiers)
|
|
125
|
+
*
|
|
126
|
+
* @param key - The normalized key event
|
|
127
|
+
* @returns String representation for binding lookup
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const key: NormalizedKey = { name: 'c', ctrl: true, alt: false, shift: false, meta: false }
|
|
132
|
+
* console.log(keyToBindingString(key)) // 'ctrl+c'
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function keyToBindingString(key: NormalizedKey): string {
|
|
136
|
+
const modifiers: string[] = []
|
|
137
|
+
|
|
138
|
+
if (key.ctrl) modifiers.push('ctrl')
|
|
139
|
+
if (key.alt) modifiers.push('alt')
|
|
140
|
+
if (key.shift) modifiers.push('shift')
|
|
141
|
+
if (key.meta) modifiers.push('meta')
|
|
142
|
+
|
|
143
|
+
if (modifiers.length > 0) {
|
|
144
|
+
return `${modifiers.join('+')}+${key.name}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return key.name
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Configuration for attaching a keyboard manager to terminal input.
|
|
152
|
+
*/
|
|
153
|
+
export interface AttachKeyboardOptions {
|
|
154
|
+
/** Custom input stream (defaults to process.stdin) */
|
|
155
|
+
input?: NodeJS.ReadStream
|
|
156
|
+
/** Whether to exit on Ctrl+C (defaults to true) */
|
|
157
|
+
exitOnCtrlC?: boolean
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Cleanup function returned by attachKeyboardManager.
|
|
162
|
+
* Call this to detach the keyboard manager from input.
|
|
163
|
+
*/
|
|
164
|
+
export type DetachKeyboard = () => void
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Attaches a keyboard manager to terminal stdin for receiving key events.
|
|
168
|
+
*
|
|
169
|
+
* This function sets up raw mode on stdin and processes keypress events,
|
|
170
|
+
* normalizing them and passing them to the keyboard manager.
|
|
171
|
+
*
|
|
172
|
+
* **Important:** This function requires a TTY stdin. It will throw if
|
|
173
|
+
* stdin is not a TTY (e.g., when input is piped).
|
|
174
|
+
*
|
|
175
|
+
* @param manager - The keyboard manager to receive key events
|
|
176
|
+
* @param options - Configuration options
|
|
177
|
+
* @returns A cleanup function to detach the keyboard manager
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* import { createKeyboardManager, attachKeyboardManager, VIM_BINDINGS } from '@mdxui/terminal'
|
|
182
|
+
*
|
|
183
|
+
* const manager = createKeyboardManager({
|
|
184
|
+
* bindings: VIM_BINDINGS,
|
|
185
|
+
* onAction: (action) => console.log('Action:', action),
|
|
186
|
+
* })
|
|
187
|
+
*
|
|
188
|
+
* const detach = attachKeyboardManager(manager)
|
|
189
|
+
*
|
|
190
|
+
* // Later, to clean up:
|
|
191
|
+
* detach()
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* // With custom options
|
|
197
|
+
* const detach = attachKeyboardManager(manager, {
|
|
198
|
+
* exitOnCtrlC: false, // Don't exit on Ctrl+C, let manager handle it
|
|
199
|
+
* })
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export function attachKeyboardManager(
|
|
203
|
+
manager: KeyboardManager,
|
|
204
|
+
options: AttachKeyboardOptions = {}
|
|
205
|
+
): DetachKeyboard {
|
|
206
|
+
const { input = process.stdin, exitOnCtrlC = true } = options
|
|
207
|
+
|
|
208
|
+
// Check if we have a TTY
|
|
209
|
+
if (!input.isTTY) {
|
|
210
|
+
throw new Error('attachKeyboardManager requires a TTY stdin. Cannot attach to non-TTY input.')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Dynamic import readline to emit keypress events
|
|
214
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
215
|
+
const readline = require('readline')
|
|
216
|
+
readline.emitKeypressEvents(input)
|
|
217
|
+
|
|
218
|
+
// Store original raw mode state
|
|
219
|
+
const wasRaw = input.isRaw
|
|
220
|
+
|
|
221
|
+
// Enable raw mode to receive individual keypresses
|
|
222
|
+
input.setRawMode(true)
|
|
223
|
+
|
|
224
|
+
// Keypress handler
|
|
225
|
+
const handleKeypress = (str: string | undefined, key: ReadlineKey | undefined) => {
|
|
226
|
+
// Handle Ctrl+C exit if enabled
|
|
227
|
+
if (exitOnCtrlC && key?.ctrl && key?.name === 'c') {
|
|
228
|
+
// Restore terminal state before exiting
|
|
229
|
+
input.setRawMode(wasRaw ?? false)
|
|
230
|
+
process.exit(0)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Normalize the key event
|
|
234
|
+
const normalized = normalizeReadlineKey(str, key)
|
|
235
|
+
|
|
236
|
+
// Convert to binding string format
|
|
237
|
+
const bindingKey = keyToBindingString(normalized)
|
|
238
|
+
|
|
239
|
+
// Pass to manager
|
|
240
|
+
manager.handleKey(bindingKey)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Attach the handler
|
|
244
|
+
input.on('keypress', handleKeypress)
|
|
245
|
+
|
|
246
|
+
// Resume stdin (required when raw mode is enabled)
|
|
247
|
+
input.resume()
|
|
248
|
+
|
|
249
|
+
// Return cleanup function
|
|
250
|
+
return () => {
|
|
251
|
+
input.off('keypress', handleKeypress)
|
|
252
|
+
input.setRawMode(wasRaw ?? false)
|
|
253
|
+
// Note: We don't pause stdin as other handlers may still need it
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// OpenTUI Integration
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* OpenTUI key event structure.
|
|
263
|
+
* Matches the KeyEvent interface from @opentui/core.
|
|
264
|
+
*/
|
|
265
|
+
export interface OpenTUIKeyEvent {
|
|
266
|
+
/** Key name (e.g., 'a', 'enter', 'escape') */
|
|
267
|
+
name: string
|
|
268
|
+
/** Ctrl modifier */
|
|
269
|
+
ctrl: boolean
|
|
270
|
+
/** Meta/Cmd modifier */
|
|
271
|
+
meta: boolean
|
|
272
|
+
/** Shift modifier */
|
|
273
|
+
shift: boolean
|
|
274
|
+
/** Option/Alt modifier */
|
|
275
|
+
option: boolean
|
|
276
|
+
/** Raw sequence */
|
|
277
|
+
sequence: string
|
|
278
|
+
/** Event type: 'press', 'repeat', or 'release' */
|
|
279
|
+
eventType?: 'press' | 'repeat' | 'release'
|
|
280
|
+
/** Whether this is a repeated key */
|
|
281
|
+
repeated?: boolean
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Normalizes an OpenTUI KeyEvent to our standard NormalizedKey format.
|
|
286
|
+
*
|
|
287
|
+
* @param event - The OpenTUI KeyEvent
|
|
288
|
+
* @returns Normalized key event
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```typescript
|
|
292
|
+
* import { useKeyboard } from '@opentui/react'
|
|
293
|
+
*
|
|
294
|
+
* useKeyboard((event) => {
|
|
295
|
+
* const normalized = normalizeOpenTUIKey(event)
|
|
296
|
+
* manager.handleKey(keyToBindingString(normalized))
|
|
297
|
+
* })
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
export function normalizeOpenTUIKey(event: OpenTUIKeyEvent): NormalizedKey {
|
|
301
|
+
// Map OpenTUI key names to our standard names
|
|
302
|
+
const nameMap: Record<string, string> = {
|
|
303
|
+
return: 'enter',
|
|
304
|
+
escape: 'escape',
|
|
305
|
+
up: 'up',
|
|
306
|
+
down: 'down',
|
|
307
|
+
left: 'left',
|
|
308
|
+
right: 'right',
|
|
309
|
+
tab: 'tab',
|
|
310
|
+
backspace: 'backspace',
|
|
311
|
+
delete: 'delete',
|
|
312
|
+
space: 'space',
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const normalizedName = nameMap[event.name.toLowerCase()] ?? event.name.toLowerCase()
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
name: normalizedName,
|
|
319
|
+
ctrl: event.ctrl,
|
|
320
|
+
alt: event.option, // OpenTUI uses 'option' for Alt
|
|
321
|
+
shift: event.shift,
|
|
322
|
+
meta: event.meta,
|
|
323
|
+
sequence: event.sequence,
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Creates a keyboard event handler for use with OpenTUI's useKeyboard hook.
|
|
329
|
+
*
|
|
330
|
+
* This function returns a handler that can be passed directly to OpenTUI's
|
|
331
|
+
* useKeyboard hook to connect it to a KeyboardManager.
|
|
332
|
+
*
|
|
333
|
+
* @param manager - The keyboard manager to receive key events
|
|
334
|
+
* @returns A handler function for OpenTUI's useKeyboard hook
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```tsx
|
|
338
|
+
* import { useKeyboard } from '@opentui/react'
|
|
339
|
+
* import { createKeyboardManager, createOpenTUIKeyHandler, VIM_BINDINGS } from '@mdxui/terminal'
|
|
340
|
+
*
|
|
341
|
+
* function MyComponent() {
|
|
342
|
+
* const manager = useMemo(() => createKeyboardManager({
|
|
343
|
+
* bindings: VIM_BINDINGS,
|
|
344
|
+
* onAction: (action) => console.log('Action:', action),
|
|
345
|
+
* }), [])
|
|
346
|
+
*
|
|
347
|
+
* const handler = useMemo(() => createOpenTUIKeyHandler(manager), [manager])
|
|
348
|
+
*
|
|
349
|
+
* useKeyboard(handler)
|
|
350
|
+
*
|
|
351
|
+
* return <div>...</div>
|
|
352
|
+
* }
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
export function createOpenTUIKeyHandler(manager: KeyboardManager): (event: OpenTUIKeyEvent) => void {
|
|
356
|
+
return (event: OpenTUIKeyEvent) => {
|
|
357
|
+
// Skip release events - only handle press/repeat
|
|
358
|
+
if (event.eventType === 'release') {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Normalize the key event
|
|
363
|
+
const normalized = normalizeOpenTUIKey(event)
|
|
364
|
+
|
|
365
|
+
// Convert to binding string format
|
|
366
|
+
const bindingKey = keyToBindingString(normalized)
|
|
367
|
+
|
|
368
|
+
// Pass to manager
|
|
369
|
+
manager.handleKey(bindingKey)
|
|
370
|
+
}
|
|
371
|
+
}
|