@silvery/ui 0.3.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/package.json +71 -0
- package/src/animation/easing.ts +38 -0
- package/src/animation/index.ts +18 -0
- package/src/animation/useAnimation.ts +143 -0
- package/src/animation/useInterval.ts +39 -0
- package/src/animation/useLatest.ts +35 -0
- package/src/animation/useTimeout.ts +65 -0
- package/src/animation/useTransition.ts +110 -0
- package/src/animation.ts +24 -0
- package/src/ansi/index.ts +43 -0
- package/src/canvas/index.ts +169 -0
- package/src/cli/ansi.ts +85 -0
- package/src/cli/index.ts +39 -0
- package/src/cli/multi-progress.ts +340 -0
- package/src/cli/progress-bar.ts +222 -0
- package/src/cli/spinner.ts +275 -0
- package/src/components/Badge.tsx +54 -0
- package/src/components/Breadcrumb.tsx +72 -0
- package/src/components/Button.tsx +73 -0
- package/src/components/CommandPalette.tsx +186 -0
- package/src/components/Console.tsx +79 -0
- package/src/components/CursorLine.tsx +71 -0
- package/src/components/Divider.tsx +67 -0
- package/src/components/EditContextDisplay.tsx +164 -0
- package/src/components/ErrorBoundary.tsx +179 -0
- package/src/components/Form.tsx +86 -0
- package/src/components/GridCell.tsx +42 -0
- package/src/components/HorizontalVirtualList.tsx +375 -0
- package/src/components/ModalDialog.tsx +179 -0
- package/src/components/PickerDialog.tsx +208 -0
- package/src/components/PickerList.tsx +93 -0
- package/src/components/ProgressBar.tsx +126 -0
- package/src/components/Screen.tsx +78 -0
- package/src/components/ScrollbackList.tsx +92 -0
- package/src/components/ScrollbackView.tsx +390 -0
- package/src/components/SelectList.tsx +176 -0
- package/src/components/Skeleton.tsx +87 -0
- package/src/components/Spinner.tsx +64 -0
- package/src/components/SplitView.tsx +199 -0
- package/src/components/Table.tsx +139 -0
- package/src/components/Tabs.tsx +203 -0
- package/src/components/TextArea.tsx +264 -0
- package/src/components/TextInput.tsx +240 -0
- package/src/components/Toast.tsx +216 -0
- package/src/components/Toggle.tsx +73 -0
- package/src/components/Tooltip.tsx +60 -0
- package/src/components/TreeView.tsx +212 -0
- package/src/components/Typography.tsx +233 -0
- package/src/components/VirtualList.tsx +318 -0
- package/src/components/VirtualView.tsx +221 -0
- package/src/components/useReadline.ts +213 -0
- package/src/components/useTextArea.ts +648 -0
- package/src/components.ts +133 -0
- package/src/display/Table.tsx +179 -0
- package/src/display/index.ts +13 -0
- package/src/hooks/useTea.ts +133 -0
- package/src/image/Image.tsx +187 -0
- package/src/image/index.ts +15 -0
- package/src/image/kitty-graphics.ts +161 -0
- package/src/image/sixel-encoder.ts +194 -0
- package/src/images.ts +22 -0
- package/src/index.ts +34 -0
- package/src/input/Select.tsx +155 -0
- package/src/input/TextInput.tsx +227 -0
- package/src/input/index.ts +25 -0
- package/src/progress/als-context.ts +160 -0
- package/src/progress/declarative.ts +519 -0
- package/src/progress/index.ts +54 -0
- package/src/progress/step-node.ts +152 -0
- package/src/progress/steps.ts +425 -0
- package/src/progress/task.ts +138 -0
- package/src/progress/tasks.ts +216 -0
- package/src/react/ProgressBar.tsx +146 -0
- package/src/react/Spinner.tsx +74 -0
- package/src/react/Tasks.tsx +144 -0
- package/src/react/context.tsx +145 -0
- package/src/react/index.ts +30 -0
- package/src/types.ts +252 -0
- package/src/utils/eta.ts +155 -0
- package/src/utils/index.ts +13 -0
- package/src/wrappers/index.ts +36 -0
- package/src/wrappers/with-progress.ts +250 -0
- package/src/wrappers/with-select.ts +194 -0
- package/src/wrappers/with-spinner.ts +108 -0
- package/src/wrappers/with-text-input.ts +388 -0
- package/src/wrappers/wrap-emitter.ts +158 -0
- package/src/wrappers/wrap-generator.ts +143 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VirtualView - App-managed scrolling within a Screen rectangle.
|
|
3
|
+
*
|
|
4
|
+
* A scrollable area where items mount/unmount based on scroll position,
|
|
5
|
+
* managed entirely by the app. Uses the shared useVirtualizer() engine.
|
|
6
|
+
*
|
|
7
|
+
* Unlike ScrollbackView (which uses native terminal scrollback), VirtualView
|
|
8
|
+
* keeps everything in the React tree. Items are simply unmounted when they
|
|
9
|
+
* scroll out of the viewport and remounted when they scroll back in.
|
|
10
|
+
*
|
|
11
|
+
* Trade-offs vs ScrollbackView:
|
|
12
|
+
* - Mouse events work on scrolled-off items (if you scroll back)
|
|
13
|
+
* - App controls scroll position (no snap-to-bottom issue)
|
|
14
|
+
* - Text selection requires Shift+drag (mouse tracking active)
|
|
15
|
+
* - Memory lives in the React tree, not the terminal buffer
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <Screen>
|
|
20
|
+
* <Header />
|
|
21
|
+
* <VirtualView
|
|
22
|
+
* items={logs}
|
|
23
|
+
* height={20}
|
|
24
|
+
* renderItem={(item, index) => <LogEntry key={item.id} data={item} />}
|
|
25
|
+
* estimateHeight={() => 3}
|
|
26
|
+
* />
|
|
27
|
+
* <StatusBar />
|
|
28
|
+
* </Screen>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import React, { forwardRef, useImperativeHandle } from "react"
|
|
33
|
+
import { useVirtualizer } from "@silvery/react/hooks/useVirtualizer"
|
|
34
|
+
import { Box } from "@silvery/react/components/Box"
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Types
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
export interface VirtualViewProps<T> {
|
|
41
|
+
/** Array of items to render */
|
|
42
|
+
items: T[]
|
|
43
|
+
|
|
44
|
+
/** Height of the viewport in rows */
|
|
45
|
+
height: number
|
|
46
|
+
|
|
47
|
+
/** Estimated height of each item in rows (fixed or per-index function). Default: 1 */
|
|
48
|
+
estimateHeight?: number | ((index: number) => number)
|
|
49
|
+
|
|
50
|
+
/** Render function for each item */
|
|
51
|
+
renderItem: (item: T, index: number) => React.ReactNode
|
|
52
|
+
|
|
53
|
+
/** Index to scroll to (declarative). When undefined, scroll state freezes. */
|
|
54
|
+
scrollTo?: number
|
|
55
|
+
|
|
56
|
+
/** Extra items to render beyond viewport for smooth scrolling. Default: 5 */
|
|
57
|
+
overscan?: number
|
|
58
|
+
|
|
59
|
+
/** Maximum items to render at once. Default: 100 */
|
|
60
|
+
maxRendered?: number
|
|
61
|
+
|
|
62
|
+
/** Padding from edge before scrolling (in items). Default: 2 */
|
|
63
|
+
scrollPadding?: number
|
|
64
|
+
|
|
65
|
+
/** Show overflow indicators (▲N/▼N). Default: false */
|
|
66
|
+
overflowIndicator?: boolean
|
|
67
|
+
|
|
68
|
+
/** Optional key extractor (defaults to index) */
|
|
69
|
+
keyExtractor?: (item: T, index: number) => string | number
|
|
70
|
+
|
|
71
|
+
/** Width of the viewport (optional, uses parent width if not specified) */
|
|
72
|
+
width?: number
|
|
73
|
+
|
|
74
|
+
/** Gap between items in rows. Default: 0 */
|
|
75
|
+
gap?: number
|
|
76
|
+
|
|
77
|
+
/** Render separator between items (alternative to gap) */
|
|
78
|
+
renderSeparator?: () => React.ReactNode
|
|
79
|
+
|
|
80
|
+
/** Mouse wheel handler for scrolling */
|
|
81
|
+
onWheel?: (event: { deltaY: number }) => void
|
|
82
|
+
|
|
83
|
+
/** Called when the visible range reaches near the end of the list (infinite scroll). */
|
|
84
|
+
onEndReached?: () => void
|
|
85
|
+
/** How many items from the end to trigger onEndReached. Default: 5 */
|
|
86
|
+
onEndReachedThreshold?: number
|
|
87
|
+
|
|
88
|
+
/** Content rendered after all items inside the scroll container (e.g., hidden count indicator) */
|
|
89
|
+
listFooter?: React.ReactNode
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface VirtualViewHandle {
|
|
93
|
+
/** Imperatively scroll to a specific item index */
|
|
94
|
+
scrollToItem(index: number): void
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// Constants
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
const DEFAULT_ESTIMATE_HEIGHT = 1
|
|
102
|
+
const DEFAULT_OVERSCAN = 5
|
|
103
|
+
const DEFAULT_MAX_RENDERED = 100
|
|
104
|
+
const DEFAULT_SCROLL_PADDING = 2
|
|
105
|
+
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Component
|
|
108
|
+
// =============================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* App-managed scrollable view with virtualization.
|
|
112
|
+
*
|
|
113
|
+
* Items mount/unmount based on scroll position within a fixed-height viewport.
|
|
114
|
+
* Scroll state management:
|
|
115
|
+
* - When scrollTo is defined: actively track and scroll to that index
|
|
116
|
+
* - When scrollTo is undefined: freeze scroll state (critical for multi-column layouts)
|
|
117
|
+
*/
|
|
118
|
+
function VirtualViewInner<T>(
|
|
119
|
+
{
|
|
120
|
+
items,
|
|
121
|
+
height,
|
|
122
|
+
estimateHeight = DEFAULT_ESTIMATE_HEIGHT,
|
|
123
|
+
renderItem,
|
|
124
|
+
scrollTo,
|
|
125
|
+
overscan = DEFAULT_OVERSCAN,
|
|
126
|
+
maxRendered = DEFAULT_MAX_RENDERED,
|
|
127
|
+
scrollPadding = DEFAULT_SCROLL_PADDING,
|
|
128
|
+
overflowIndicator,
|
|
129
|
+
keyExtractor,
|
|
130
|
+
width,
|
|
131
|
+
gap = 0,
|
|
132
|
+
renderSeparator,
|
|
133
|
+
onWheel,
|
|
134
|
+
onEndReached,
|
|
135
|
+
onEndReachedThreshold,
|
|
136
|
+
listFooter,
|
|
137
|
+
}: VirtualViewProps<T>,
|
|
138
|
+
ref: React.ForwardedRef<VirtualViewHandle>,
|
|
139
|
+
): React.ReactElement {
|
|
140
|
+
// Convert item-based estimateHeight to index-based for useVirtualizer
|
|
141
|
+
const indexEstimate = typeof estimateHeight === "function" ? estimateHeight : estimateHeight
|
|
142
|
+
|
|
143
|
+
const { range, leadingHeight, trailingHeight, scrollOffset, scrollToItem } = useVirtualizer({
|
|
144
|
+
count: items.length,
|
|
145
|
+
estimateHeight: indexEstimate,
|
|
146
|
+
viewportHeight: height,
|
|
147
|
+
scrollTo,
|
|
148
|
+
scrollPadding,
|
|
149
|
+
overscan,
|
|
150
|
+
maxRendered,
|
|
151
|
+
gap,
|
|
152
|
+
getItemKey: keyExtractor ? (index) => keyExtractor(items[index]!, index) : undefined,
|
|
153
|
+
onEndReached,
|
|
154
|
+
onEndReachedThreshold,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Expose scrollToItem method via ref
|
|
158
|
+
useImperativeHandle(ref, () => ({ scrollToItem }), [scrollToItem])
|
|
159
|
+
|
|
160
|
+
// Empty state
|
|
161
|
+
if (items.length === 0) {
|
|
162
|
+
return (
|
|
163
|
+
<Box flexDirection="column" height={height} width={width}>
|
|
164
|
+
{/* Empty - nothing to render */}
|
|
165
|
+
</Box>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Get the slice of items to render
|
|
170
|
+
const { startIndex, endIndex } = range
|
|
171
|
+
const visibleItems = items.slice(startIndex, endIndex)
|
|
172
|
+
|
|
173
|
+
// Calculate scrollTo index for silvery Box overflow="scroll"
|
|
174
|
+
const hasTopPlaceholder = leadingHeight > 0
|
|
175
|
+
const currentSelectedIndex = scrollTo !== undefined ? Math.max(0, Math.min(scrollTo, items.length - 1)) : scrollOffset
|
|
176
|
+
const selectedIndexInSlice = currentSelectedIndex - startIndex
|
|
177
|
+
const isSelectedInSlice = selectedIndexInSlice >= 0 && selectedIndexInSlice < visibleItems.length
|
|
178
|
+
const scrollToIndex = hasTopPlaceholder ? selectedIndexInSlice + 1 : selectedIndexInSlice
|
|
179
|
+
const boxScrollTo = isSelectedInSlice ? Math.max(0, scrollToIndex) : undefined
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Box
|
|
183
|
+
flexDirection="column"
|
|
184
|
+
height={height}
|
|
185
|
+
width={width}
|
|
186
|
+
overflow="scroll"
|
|
187
|
+
scrollTo={boxScrollTo}
|
|
188
|
+
overflowIndicator={overflowIndicator}
|
|
189
|
+
onWheel={onWheel}
|
|
190
|
+
>
|
|
191
|
+
{/* Leading placeholder for virtual height */}
|
|
192
|
+
{leadingHeight > 0 && <Box height={leadingHeight} flexShrink={0} />}
|
|
193
|
+
|
|
194
|
+
{/* Render visible items */}
|
|
195
|
+
{visibleItems.map((item, i) => {
|
|
196
|
+
const originalIndex = startIndex + i
|
|
197
|
+
const key = keyExtractor ? keyExtractor(item, originalIndex) : originalIndex
|
|
198
|
+
const isLast = i === visibleItems.length - 1
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<React.Fragment key={key}>
|
|
202
|
+
{renderItem(item, originalIndex)}
|
|
203
|
+
{!isLast && renderSeparator && renderSeparator()}
|
|
204
|
+
{!isLast && gap > 0 && !renderSeparator && <Box height={gap} flexShrink={0} />}
|
|
205
|
+
</React.Fragment>
|
|
206
|
+
)
|
|
207
|
+
})}
|
|
208
|
+
|
|
209
|
+
{/* Footer content (e.g., filter hidden count) */}
|
|
210
|
+
{listFooter}
|
|
211
|
+
|
|
212
|
+
{/* Trailing placeholder for virtual height */}
|
|
213
|
+
{trailingHeight > 0 && <Box height={trailingHeight} flexShrink={0} />}
|
|
214
|
+
</Box>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Export with forwardRef - use type assertion for generic component
|
|
219
|
+
export const VirtualView = forwardRef(VirtualViewInner) as <T>(
|
|
220
|
+
props: VirtualViewProps<T> & { ref?: React.ForwardedRef<VirtualViewHandle> },
|
|
221
|
+
) => React.ReactElement
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useReadline Hook
|
|
3
|
+
*
|
|
4
|
+
* This hook lives in components/ because it's tightly coupled to TextInput.
|
|
5
|
+
* It manages readline state (cursor position, history, kill ring) that TextInput renders.
|
|
6
|
+
*
|
|
7
|
+
* Full readline-style line editing for terminal text input.
|
|
8
|
+
* Supports cursor movement, word operations, kill ring, and all standard shortcuts.
|
|
9
|
+
*
|
|
10
|
+
* Shortcuts:
|
|
11
|
+
* - Ctrl+A: Move to beginning of line
|
|
12
|
+
* - Ctrl+E: Move to end of line
|
|
13
|
+
* - Ctrl+B / Left: Move cursor left
|
|
14
|
+
* - Ctrl+F / Right: Move cursor right
|
|
15
|
+
* - Alt+B: Move cursor back one word
|
|
16
|
+
* - Alt+F: Move cursor forward one word
|
|
17
|
+
* - Ctrl+W / Alt+Backspace: Delete word backwards (adds to kill ring)
|
|
18
|
+
* - Alt+D: Delete word forwards (adds to kill ring)
|
|
19
|
+
* - Ctrl+U: Delete to beginning (adds to kill ring)
|
|
20
|
+
* - Ctrl+K: Delete to end (adds to kill ring)
|
|
21
|
+
* - Ctrl+Y: Yank (paste from kill ring)
|
|
22
|
+
* - Alt+Y: Cycle through kill ring (after Ctrl+Y)
|
|
23
|
+
* - Ctrl+T: Transpose characters
|
|
24
|
+
* - Ctrl+H / Backspace: Delete char before cursor
|
|
25
|
+
* - Ctrl+D / Delete: Delete char at cursor (or exit if empty)
|
|
26
|
+
*
|
|
27
|
+
* Note: Alt key detection requires terminal support. Some terminals send
|
|
28
|
+
* ESC followed by the key instead of a proper alt modifier.
|
|
29
|
+
*/
|
|
30
|
+
import { useCallback, useRef, useState } from "react"
|
|
31
|
+
import { useInput } from "@silvery/react/hooks"
|
|
32
|
+
import { killRing, addToKillRing, handleReadlineKey, type YankState } from "@silvery/react/hooks/readline-ops"
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Types
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
export interface ReadlineState {
|
|
39
|
+
/** Current text value */
|
|
40
|
+
value: string
|
|
41
|
+
/** Cursor position (0 = before first char, value.length = after last char) */
|
|
42
|
+
cursor: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface UseReadlineOptions {
|
|
46
|
+
/** Initial value */
|
|
47
|
+
initialValue?: string
|
|
48
|
+
/** Called when value changes */
|
|
49
|
+
onChange?: (value: string) => void
|
|
50
|
+
/** Whether input is active */
|
|
51
|
+
isActive?: boolean
|
|
52
|
+
/** Handle Enter key (default: false - let parent handle) */
|
|
53
|
+
handleEnter?: boolean
|
|
54
|
+
/** Called when Enter is pressed (requires handleEnter: true) */
|
|
55
|
+
onSubmit?: (value: string) => void
|
|
56
|
+
/** Handle Escape key (default: false - let parent handle) */
|
|
57
|
+
handleEscape?: boolean
|
|
58
|
+
/** Handle Up/Down arrows (default: false - let parent handle for history) */
|
|
59
|
+
handleVerticalArrows?: boolean
|
|
60
|
+
/** Called on Ctrl+D with empty input (default: undefined) */
|
|
61
|
+
onEOF?: () => void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UseReadlineResult {
|
|
65
|
+
/** Current text value */
|
|
66
|
+
value: string
|
|
67
|
+
/** Cursor position */
|
|
68
|
+
cursor: number
|
|
69
|
+
/** Text before cursor (for rendering) */
|
|
70
|
+
beforeCursor: string
|
|
71
|
+
/** Text after cursor (for rendering) */
|
|
72
|
+
afterCursor: string
|
|
73
|
+
/** Clear the input */
|
|
74
|
+
clear: () => void
|
|
75
|
+
/** Set value programmatically (cursor moves to end) */
|
|
76
|
+
setValue: (value: string) => void
|
|
77
|
+
/** Set both value and cursor position */
|
|
78
|
+
setValueWithCursor: (value: string, cursor: number) => void
|
|
79
|
+
/** Kill ring contents (for debugging/display) */
|
|
80
|
+
killRing: string[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Hook
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
export function useReadline({
|
|
88
|
+
initialValue = "",
|
|
89
|
+
onChange,
|
|
90
|
+
isActive = true,
|
|
91
|
+
handleEnter = false,
|
|
92
|
+
handleEscape = false,
|
|
93
|
+
handleVerticalArrows = false,
|
|
94
|
+
onEOF,
|
|
95
|
+
onSubmit,
|
|
96
|
+
}: UseReadlineOptions = {}): UseReadlineResult {
|
|
97
|
+
const [state, setState] = useState<ReadlineState>({
|
|
98
|
+
value: initialValue,
|
|
99
|
+
cursor: initialValue.length,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Mutable ref for synchronous reads in the event handler.
|
|
103
|
+
// Without this, rapid keypresses between React renders all read the same
|
|
104
|
+
// stale closure state and overwrite each other.
|
|
105
|
+
const stateRef = useRef<ReadlineState>({ value: initialValue, cursor: initialValue.length })
|
|
106
|
+
stateRef.current = state
|
|
107
|
+
|
|
108
|
+
const yankStateRef = useRef<YankState | null>(null)
|
|
109
|
+
|
|
110
|
+
/** Apply a ReadlineKeyResult to state */
|
|
111
|
+
const applyResult = useCallback(
|
|
112
|
+
(result: { value: string; cursor: number; yankState: YankState | null }, prevValue: string) => {
|
|
113
|
+
yankStateRef.current = result.yankState
|
|
114
|
+
if (result.value === prevValue && result.cursor === stateRef.current.cursor) return
|
|
115
|
+
const next = { value: result.value, cursor: result.cursor }
|
|
116
|
+
stateRef.current = next
|
|
117
|
+
setState(next)
|
|
118
|
+
if (result.value !== prevValue) onChange?.(result.value)
|
|
119
|
+
},
|
|
120
|
+
[onChange],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const clear = useCallback(() => {
|
|
124
|
+
const next = { value: "", cursor: 0 }
|
|
125
|
+
stateRef.current = next
|
|
126
|
+
setState(next)
|
|
127
|
+
onChange?.("")
|
|
128
|
+
yankStateRef.current = null
|
|
129
|
+
}, [onChange])
|
|
130
|
+
|
|
131
|
+
const setValue = useCallback(
|
|
132
|
+
(value: string) => {
|
|
133
|
+
const next = { value, cursor: value.length }
|
|
134
|
+
stateRef.current = next
|
|
135
|
+
setState(next)
|
|
136
|
+
onChange?.(value)
|
|
137
|
+
yankStateRef.current = null
|
|
138
|
+
},
|
|
139
|
+
[onChange],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const setValueWithCursor = useCallback(
|
|
143
|
+
(value: string, cursor: number) => {
|
|
144
|
+
const next = { value, cursor: Math.max(0, Math.min(cursor, value.length)) }
|
|
145
|
+
stateRef.current = next
|
|
146
|
+
setState(next)
|
|
147
|
+
onChange?.(value)
|
|
148
|
+
yankStateRef.current = null
|
|
149
|
+
},
|
|
150
|
+
[onChange],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
useInput(
|
|
154
|
+
(input, key) => {
|
|
155
|
+
const { value, cursor } = stateRef.current
|
|
156
|
+
|
|
157
|
+
// Let parent handle Enter/Escape/vertical arrows unless explicitly enabled
|
|
158
|
+
if (key.return && !handleEnter) return
|
|
159
|
+
if (key.return && handleEnter) {
|
|
160
|
+
onSubmit?.(value)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (key.escape && !handleEscape) return
|
|
164
|
+
if ((key.upArrow || key.downArrow) && !handleVerticalArrows) return
|
|
165
|
+
|
|
166
|
+
// Single-line specific: Ctrl+D on empty input = EOF
|
|
167
|
+
if (key.ctrl && input === "d" && value.length === 0) {
|
|
168
|
+
onEOF?.()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Single-line specific: Ctrl+A/E move to beginning/end of entire text
|
|
173
|
+
if (key.ctrl && input === "a") {
|
|
174
|
+
applyResult({ value, cursor: 0, yankState: null }, value)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
if (key.ctrl && input === "e") {
|
|
178
|
+
applyResult({ value, cursor: value.length, yankState: null }, value)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Single-line specific: Ctrl+U/K kill to beginning/end of entire text
|
|
183
|
+
if (key.ctrl && input === "u") {
|
|
184
|
+
if (cursor === 0) return
|
|
185
|
+
addToKillRing(value.slice(0, cursor))
|
|
186
|
+
applyResult({ value: value.slice(cursor), cursor: 0, yankState: null }, value)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
if (key.ctrl && input === "k") {
|
|
190
|
+
if (cursor >= value.length) return
|
|
191
|
+
addToKillRing(value.slice(cursor))
|
|
192
|
+
applyResult({ value: value.slice(0, cursor), cursor, yankState: null }, value)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Shared readline operations (cursor movement, word ops, kill ring, yank, etc.)
|
|
197
|
+
const result = handleReadlineKey(input, key, value, cursor, yankStateRef.current)
|
|
198
|
+
if (result) applyResult(result, value)
|
|
199
|
+
},
|
|
200
|
+
{ isActive },
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
value: state.value,
|
|
205
|
+
cursor: state.cursor,
|
|
206
|
+
beforeCursor: state.value.slice(0, state.cursor),
|
|
207
|
+
afterCursor: state.value.slice(state.cursor),
|
|
208
|
+
clear,
|
|
209
|
+
setValue,
|
|
210
|
+
setValueWithCursor,
|
|
211
|
+
killRing: [...killRing],
|
|
212
|
+
}
|
|
213
|
+
}
|