@silvery/tea 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 +54 -0
- package/src/core/index.ts +225 -0
- package/src/core/slice.ts +69 -0
- package/src/create-command-registry.ts +106 -0
- package/src/effects.ts +145 -0
- package/src/focus-events.ts +243 -0
- package/src/focus-manager.ts +491 -0
- package/src/focus-queries.ts +241 -0
- package/src/index.ts +213 -0
- package/src/keys.ts +1382 -0
- package/src/pipe.ts +110 -0
- package/src/plugins.ts +119 -0
- package/src/store/index.ts +306 -0
- package/src/streams/index.ts +405 -0
- package/src/tea/README.md +208 -0
- package/src/tea/index.ts +174 -0
- package/src/text-cursor.ts +206 -0
- package/src/text-decorations.ts +253 -0
- package/src/text-ops.ts +150 -0
- package/src/tree-utils.ts +27 -0
- package/src/types.ts +670 -0
- package/src/with-commands.ts +337 -0
- package/src/with-diagnostics.ts +955 -0
- package/src/with-dom-events.ts +168 -0
- package/src/with-focus.ts +162 -0
- package/src/with-keybindings.ts +180 -0
- package/src/with-react.ts +92 -0
- package/src/with-render.ts +92 -0
- package/src/with-terminal.ts +219 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silvery Types
|
|
3
|
+
*
|
|
4
|
+
* Core types for the Silvery renderer architecture.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FocusEventProps } from "./focus-events"
|
|
8
|
+
import type { LayoutNode } from "@silvery/term/layout-engine"
|
|
9
|
+
import type { MouseEventProps } from "@silvery/term/mouse-events"
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Layout Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A rectangle with position and size.
|
|
17
|
+
* All values are in terminal columns/rows (integers).
|
|
18
|
+
*/
|
|
19
|
+
export interface Rect {
|
|
20
|
+
/** X position (0-indexed terminal column) */
|
|
21
|
+
x: number
|
|
22
|
+
/** Y position (0-indexed terminal row) */
|
|
23
|
+
y: number
|
|
24
|
+
/** Width in terminal columns */
|
|
25
|
+
width: number
|
|
26
|
+
/** Height in terminal rows */
|
|
27
|
+
height: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if two rects are equal (same position and size).
|
|
32
|
+
*/
|
|
33
|
+
export function rectEqual(a: Rect | null, b: Rect | null): boolean {
|
|
34
|
+
if (a === b) return true
|
|
35
|
+
if (!a || !b) return false
|
|
36
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Node Types
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Silvery node types - the primitive elements in the render tree.
|
|
45
|
+
*/
|
|
46
|
+
export type TeaNodeType = "silvery-root" | "silvery-box" | "silvery-text"
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Flexbox properties that can be applied to Box nodes.
|
|
50
|
+
*/
|
|
51
|
+
export interface FlexboxProps {
|
|
52
|
+
// Size
|
|
53
|
+
width?: number | string
|
|
54
|
+
height?: number | string
|
|
55
|
+
minWidth?: number | string
|
|
56
|
+
minHeight?: number | string
|
|
57
|
+
maxWidth?: number | string
|
|
58
|
+
maxHeight?: number | string
|
|
59
|
+
|
|
60
|
+
// Flex
|
|
61
|
+
flexGrow?: number
|
|
62
|
+
flexShrink?: number
|
|
63
|
+
flexBasis?: number | string
|
|
64
|
+
flexDirection?: "row" | "column" | "row-reverse" | "column-reverse"
|
|
65
|
+
flexWrap?: "nowrap" | "wrap" | "wrap-reverse"
|
|
66
|
+
|
|
67
|
+
// Alignment
|
|
68
|
+
alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
|
|
69
|
+
alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
|
|
70
|
+
alignContent?: "flex-start" | "flex-end" | "center" | "stretch" | "space-between" | "space-around" | "space-evenly"
|
|
71
|
+
justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly"
|
|
72
|
+
|
|
73
|
+
// Spacing
|
|
74
|
+
padding?: number
|
|
75
|
+
paddingTop?: number
|
|
76
|
+
paddingBottom?: number
|
|
77
|
+
paddingLeft?: number
|
|
78
|
+
paddingRight?: number
|
|
79
|
+
paddingX?: number
|
|
80
|
+
paddingY?: number
|
|
81
|
+
margin?: number
|
|
82
|
+
marginTop?: number
|
|
83
|
+
marginBottom?: number
|
|
84
|
+
marginLeft?: number
|
|
85
|
+
marginRight?: number
|
|
86
|
+
marginX?: number
|
|
87
|
+
marginY?: number
|
|
88
|
+
gap?: number
|
|
89
|
+
columnGap?: number
|
|
90
|
+
rowGap?: number
|
|
91
|
+
|
|
92
|
+
// Position
|
|
93
|
+
position?: "relative" | "absolute" | "sticky" | "static"
|
|
94
|
+
|
|
95
|
+
// Position offsets (used with position='absolute' or position='relative')
|
|
96
|
+
top?: number | string
|
|
97
|
+
left?: number | string
|
|
98
|
+
bottom?: number | string
|
|
99
|
+
right?: number | string
|
|
100
|
+
|
|
101
|
+
// Sticky offsets (only used when position='sticky')
|
|
102
|
+
// The element will "stick" when it reaches this offset from the container edge
|
|
103
|
+
stickyTop?: number
|
|
104
|
+
stickyBottom?: number
|
|
105
|
+
|
|
106
|
+
// Aspect ratio
|
|
107
|
+
aspectRatio?: number
|
|
108
|
+
|
|
109
|
+
// Display
|
|
110
|
+
display?: "flex" | "none"
|
|
111
|
+
|
|
112
|
+
// Overflow
|
|
113
|
+
overflow?: "visible" | "hidden" | "scroll"
|
|
114
|
+
overflowX?: "visible" | "hidden"
|
|
115
|
+
overflowY?: "visible" | "hidden"
|
|
116
|
+
|
|
117
|
+
// Scroll control (only used when overflow='scroll')
|
|
118
|
+
/** Child index to ensure visible (edge-based: only scrolls if off-screen) */
|
|
119
|
+
scrollTo?: number
|
|
120
|
+
/** Explicit scroll offset in rows (used when scrollTo is undefined for frozen scroll state) */
|
|
121
|
+
scrollOffset?: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Props for testing and identification.
|
|
126
|
+
* These props are stored in the node for DOM query access.
|
|
127
|
+
*/
|
|
128
|
+
export interface TestProps {
|
|
129
|
+
/** Element ID for DOM queries and visual debugging */
|
|
130
|
+
id?: string
|
|
131
|
+
/** Test ID for querying nodes (like Playwright's data-testid) */
|
|
132
|
+
testID?: string
|
|
133
|
+
/** Allow arbitrary data-* attributes for testing */
|
|
134
|
+
[key: `data-${string}`]: unknown
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Underline style variants (SGR 4:x codes).
|
|
139
|
+
* - false: no underline
|
|
140
|
+
* - 'single': standard underline (SGR 4 or 4:1)
|
|
141
|
+
* - 'double': double underline (SGR 4:2)
|
|
142
|
+
* - 'curly': curly/wavy underline (SGR 4:3)
|
|
143
|
+
* - 'dotted': dotted underline (SGR 4:4)
|
|
144
|
+
* - 'dashed': dashed underline (SGR 4:5)
|
|
145
|
+
*/
|
|
146
|
+
export type UnderlineStyle = false | "single" | "double" | "curly" | "dotted" | "dashed"
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Style properties for text rendering.
|
|
150
|
+
*/
|
|
151
|
+
export interface StyleProps {
|
|
152
|
+
color?: string
|
|
153
|
+
backgroundColor?: string
|
|
154
|
+
bold?: boolean
|
|
155
|
+
dim?: boolean
|
|
156
|
+
/** Alias for dim (Ink compatibility) */
|
|
157
|
+
dimColor?: boolean
|
|
158
|
+
italic?: boolean
|
|
159
|
+
/** Enable underline. Use underlineStyle for style variants. */
|
|
160
|
+
underline?: boolean
|
|
161
|
+
/**
|
|
162
|
+
* Underline style variant: 'single' | 'double' | 'curly' | 'dotted' | 'dashed'.
|
|
163
|
+
* Setting this implies underline=true. Takes precedence over underline prop.
|
|
164
|
+
*/
|
|
165
|
+
underlineStyle?: UnderlineStyle
|
|
166
|
+
/**
|
|
167
|
+
* Underline color (independent of text color).
|
|
168
|
+
* Uses SGR 58 (underline color). Falls back to text color if not specified.
|
|
169
|
+
*/
|
|
170
|
+
underlineColor?: string
|
|
171
|
+
strikethrough?: boolean
|
|
172
|
+
inverse?: boolean
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Props for Box component.
|
|
177
|
+
*/
|
|
178
|
+
export interface BoxProps extends FlexboxProps, StyleProps, TestProps, MouseEventProps, FocusEventProps {
|
|
179
|
+
borderStyle?: "single" | "double" | "round" | "bold" | "singleDouble" | "doubleSingle" | "classic"
|
|
180
|
+
borderColor?: string
|
|
181
|
+
borderTop?: boolean
|
|
182
|
+
borderBottom?: boolean
|
|
183
|
+
borderLeft?: boolean
|
|
184
|
+
borderRight?: boolean
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Outline style — renders border characters at the box edges without affecting layout.
|
|
188
|
+
*
|
|
189
|
+
* Unlike `borderStyle` which adds border dimensions to the layout (making the content
|
|
190
|
+
* area smaller), `outlineStyle` draws border characters that OVERLAP the content area.
|
|
191
|
+
* The layout engine sees no border at all — outline is purely visual.
|
|
192
|
+
*
|
|
193
|
+
* Use cases: selection indicators, hover highlights, focus rings — anything that
|
|
194
|
+
* should visually frame a box without shifting content.
|
|
195
|
+
*/
|
|
196
|
+
outlineStyle?: "single" | "double" | "round" | "bold" | "singleDouble" | "doubleSingle" | "classic"
|
|
197
|
+
/** Foreground color for the outline */
|
|
198
|
+
outlineColor?: string
|
|
199
|
+
/** Apply dim styling to the outline */
|
|
200
|
+
outlineDimColor?: boolean
|
|
201
|
+
/** Show top outline edge (default: true) */
|
|
202
|
+
outlineTop?: boolean
|
|
203
|
+
/** Show bottom outline edge (default: true) */
|
|
204
|
+
outlineBottom?: boolean
|
|
205
|
+
/** Show left outline edge (default: true) */
|
|
206
|
+
outlineLeft?: boolean
|
|
207
|
+
/** Show right outline edge (default: true) */
|
|
208
|
+
outlineRight?: boolean
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Override theme for this subtree — $token colors resolve against this theme.
|
|
212
|
+
* Pushed onto the context theme stack during content phase tree walk.
|
|
213
|
+
*/
|
|
214
|
+
theme?: import("@silvery/theme").Theme
|
|
215
|
+
|
|
216
|
+
/** CSS pointer-events equivalent. "none" makes this node and its subtree invisible to hit testing. */
|
|
217
|
+
pointerEvents?: "auto" | "none"
|
|
218
|
+
|
|
219
|
+
onLayout?: (layout: Rect) => void
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Show scroll overflow indicators (▲N / ▼N) for scrollable containers.
|
|
223
|
+
*
|
|
224
|
+
* For bordered containers, indicators appear on the border.
|
|
225
|
+
* For borderless containers, indicators overlay the content at top-right/bottom-right.
|
|
226
|
+
*
|
|
227
|
+
* Only applies when overflow='scroll'.
|
|
228
|
+
*/
|
|
229
|
+
overflowIndicator?: boolean
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Props for Text component.
|
|
234
|
+
*/
|
|
235
|
+
export interface TextProps extends StyleProps, TestProps, MouseEventProps {
|
|
236
|
+
children?: React.ReactNode
|
|
237
|
+
wrap?: "wrap" | "truncate" | "truncate-start" | "truncate-middle" | "truncate-end" | "clip" | boolean
|
|
238
|
+
/** Internal transform function applied to each rendered line. Used by Transform component. */
|
|
239
|
+
internal_transform?: (line: string, index: number) => string
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The core Silvery node - represents an element in the render tree.
|
|
244
|
+
*
|
|
245
|
+
* Each node has:
|
|
246
|
+
* - A Yoga node for layout calculation
|
|
247
|
+
* - Computed layout after Yoga runs
|
|
248
|
+
* - Subscribers that get notified when layout changes
|
|
249
|
+
* - Dirty flags for incremental updates
|
|
250
|
+
*/
|
|
251
|
+
export interface TeaNode {
|
|
252
|
+
/** Node type */
|
|
253
|
+
type: TeaNodeType
|
|
254
|
+
|
|
255
|
+
/** Props passed to this node */
|
|
256
|
+
props: BoxProps | TextProps | Record<string, unknown>
|
|
257
|
+
|
|
258
|
+
/** Child nodes */
|
|
259
|
+
children: TeaNode[]
|
|
260
|
+
|
|
261
|
+
/** Parent node (null for root) */
|
|
262
|
+
parent: TeaNode | null
|
|
263
|
+
|
|
264
|
+
/** The layout node for layout calculation (null for raw text nodes) */
|
|
265
|
+
layoutNode: LayoutNode | null
|
|
266
|
+
|
|
267
|
+
/** Computed layout from previous render (for change detection) */
|
|
268
|
+
prevLayout: Rect | null
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Content-relative position (like CSS offsetTop/offsetLeft).
|
|
272
|
+
* Position within the scrollable content, ignoring scroll offsets.
|
|
273
|
+
* Set after layout phase.
|
|
274
|
+
*/
|
|
275
|
+
contentRect: Rect | null
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Screen-relative position (like CSS getBoundingClientRect).
|
|
279
|
+
* Actual position on the terminal screen, accounting for scroll offsets.
|
|
280
|
+
* Set after screen rect phase.
|
|
281
|
+
*
|
|
282
|
+
* Note: For sticky children, this reflects the node's layout position
|
|
283
|
+
* adjusted for scroll offsets, NOT the actual render position. Use
|
|
284
|
+
* `renderRect` for the actual pixel position on screen.
|
|
285
|
+
*/
|
|
286
|
+
screenRect: Rect | null
|
|
287
|
+
|
|
288
|
+
/** Previous screen rect (for change detection in notifyLayoutSubscribers) */
|
|
289
|
+
prevScreenRect: Rect | null
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Actual render position on the terminal screen.
|
|
293
|
+
* For non-sticky nodes, this equals `screenRect`.
|
|
294
|
+
* For sticky nodes (position="sticky"), this accounts for sticky render
|
|
295
|
+
* offsets — the position where pixels are actually painted.
|
|
296
|
+
*
|
|
297
|
+
* Use this for hit testing, cursor positioning, and any feature that
|
|
298
|
+
* needs to know where a node visually appears on screen.
|
|
299
|
+
* Set after screen rect phase.
|
|
300
|
+
*/
|
|
301
|
+
renderRect: Rect | null
|
|
302
|
+
|
|
303
|
+
/** Previous render rect (for change detection) */
|
|
304
|
+
prevRenderRect: Rect | null
|
|
305
|
+
|
|
306
|
+
/** True if layout changed THIS frame (position or size).
|
|
307
|
+
* Set by propagateLayout in layout phase. Cleared by content phase.
|
|
308
|
+
* This is the authoritative signal for "did layout change?" — unlike
|
|
309
|
+
* !rectEqual(prevLayout, contentRect) which becomes stale when layout
|
|
310
|
+
* phase skips (no dirty nodes). */
|
|
311
|
+
layoutChangedThisFrame: boolean
|
|
312
|
+
|
|
313
|
+
/** True if layout-affecting props changed and Yoga needs recalculation.
|
|
314
|
+
* Set by reconciler on prop changes. Cleared after layout phase. */
|
|
315
|
+
layoutDirty: boolean
|
|
316
|
+
|
|
317
|
+
/** True if content changed but layout didn't (e.g., text content update).
|
|
318
|
+
* Set by reconciler. Cleared by content phase after rendering.
|
|
319
|
+
* NOTE: measure phase may clear this for its text-collection cache —
|
|
320
|
+
* paintDirty acts as the surviving witness for style changes. */
|
|
321
|
+
contentDirty: boolean
|
|
322
|
+
|
|
323
|
+
/** True if visual props changed (color, backgroundColor, borderStyle, etc.).
|
|
324
|
+
* Set by reconciler alongside contentDirty. Survives measure phase clearing
|
|
325
|
+
* of contentDirty, ensuring content phase still detects style changes.
|
|
326
|
+
* Cleared by content phase after rendering. */
|
|
327
|
+
paintDirty: boolean
|
|
328
|
+
|
|
329
|
+
/** True if backgroundColor specifically changed (added, modified, or removed).
|
|
330
|
+
* Set by reconciler when backgroundColor prop changes. Used by content phase
|
|
331
|
+
* to avoid cascading re-renders for border-only paint changes (borderColor
|
|
332
|
+
* doesn't affect the content area). Cleared by content phase. */
|
|
333
|
+
bgDirty: boolean
|
|
334
|
+
|
|
335
|
+
/** True if this node or any descendant has dirty content/layout.
|
|
336
|
+
* Propagated upward by reconciler when any descendant is dirtied.
|
|
337
|
+
* When only subtreeDirty (no other flags), the node's OWN rendering is
|
|
338
|
+
* skipped — only descendants are traversed. Cleared by content phase. */
|
|
339
|
+
subtreeDirty: boolean
|
|
340
|
+
|
|
341
|
+
/** True if direct children were added, removed, or reordered.
|
|
342
|
+
* Set by reconciler on child list changes. Triggers own repaint
|
|
343
|
+
* (gap regions may need clearing) and forces child re-render.
|
|
344
|
+
* Cleared by content phase. */
|
|
345
|
+
childrenDirty: boolean
|
|
346
|
+
|
|
347
|
+
/** Callbacks subscribed to layout changes (used by useContentRect) */
|
|
348
|
+
layoutSubscribers: Set<() => void>
|
|
349
|
+
|
|
350
|
+
/** Text content for text nodes */
|
|
351
|
+
textContent?: string
|
|
352
|
+
|
|
353
|
+
/** True if this is a raw text node (created by createTextInstance) */
|
|
354
|
+
isRawText?: boolean
|
|
355
|
+
|
|
356
|
+
/** True if this node is hidden (for Suspense support) */
|
|
357
|
+
hidden?: boolean
|
|
358
|
+
|
|
359
|
+
/** Sticky children with computed render positions (for non-scroll containers).
|
|
360
|
+
* When a parent has sticky children but is NOT a scroll container, this array
|
|
361
|
+
* holds the computed render offsets. Same shape as scrollState.stickyChildren. */
|
|
362
|
+
stickyChildren?: Array<{
|
|
363
|
+
/** Index of the sticky child */
|
|
364
|
+
index: number
|
|
365
|
+
/** Computed Y offset to render at (relative to parent content area) */
|
|
366
|
+
renderOffset: number
|
|
367
|
+
/** Original natural Y position (relative to parent content area) */
|
|
368
|
+
naturalTop: number
|
|
369
|
+
/** Height of the sticky element */
|
|
370
|
+
height: number
|
|
371
|
+
}>
|
|
372
|
+
|
|
373
|
+
/** Scroll state for overflow='scroll' containers */
|
|
374
|
+
scrollState?: {
|
|
375
|
+
/** Current scroll offset (in terminal rows) */
|
|
376
|
+
offset: number
|
|
377
|
+
/** Previous scroll offset from last render (for incremental rendering) */
|
|
378
|
+
prevOffset: number
|
|
379
|
+
/** Total content height (all children) */
|
|
380
|
+
contentHeight: number
|
|
381
|
+
/** Visible height (container height minus borders/padding) */
|
|
382
|
+
viewportHeight: number
|
|
383
|
+
/** Index of first visible child */
|
|
384
|
+
firstVisibleChild: number
|
|
385
|
+
/** Index of last visible child */
|
|
386
|
+
lastVisibleChild: number
|
|
387
|
+
/** Previous first visible child from last render (for incremental rendering) */
|
|
388
|
+
prevFirstVisibleChild: number
|
|
389
|
+
/** Previous last visible child from last render (for incremental rendering) */
|
|
390
|
+
prevLastVisibleChild: number
|
|
391
|
+
/** Count of items hidden above viewport */
|
|
392
|
+
hiddenAbove: number
|
|
393
|
+
/** Count of items hidden below viewport */
|
|
394
|
+
hiddenBelow: number
|
|
395
|
+
/** Sticky children with their computed render positions */
|
|
396
|
+
stickyChildren?: Array<{
|
|
397
|
+
/** Index of the sticky child */
|
|
398
|
+
index: number
|
|
399
|
+
/** Computed Y offset to render at (relative to viewport, not content) */
|
|
400
|
+
renderOffset: number
|
|
401
|
+
/** Original natural Y position (before sticky adjustment) */
|
|
402
|
+
naturalTop: number
|
|
403
|
+
/** Height of the sticky element */
|
|
404
|
+
height: number
|
|
405
|
+
}>
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// Terminal Buffer Types
|
|
411
|
+
// ============================================================================
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Text attributes that can be applied to a cell.
|
|
415
|
+
*/
|
|
416
|
+
export interface CellAttrs {
|
|
417
|
+
bold?: boolean
|
|
418
|
+
dim?: boolean
|
|
419
|
+
italic?: boolean
|
|
420
|
+
/** Simple underline flag (for backwards compatibility) */
|
|
421
|
+
underline?: boolean
|
|
422
|
+
/**
|
|
423
|
+
* Underline style: 'single' | 'double' | 'curly' | 'dotted' | 'dashed'.
|
|
424
|
+
* When set, takes precedence over the underline boolean.
|
|
425
|
+
*/
|
|
426
|
+
underlineStyle?: UnderlineStyle
|
|
427
|
+
strikethrough?: boolean
|
|
428
|
+
inverse?: boolean
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A single cell in the terminal buffer.
|
|
433
|
+
*/
|
|
434
|
+
export interface Cell {
|
|
435
|
+
/** The character (grapheme cluster) in this cell */
|
|
436
|
+
char: string
|
|
437
|
+
/** Foreground color (ANSI code or RGB) */
|
|
438
|
+
fg: string | null
|
|
439
|
+
/** Background color (ANSI code or RGB) */
|
|
440
|
+
bg: string | null
|
|
441
|
+
/** Text attributes */
|
|
442
|
+
attrs: CellAttrs
|
|
443
|
+
/** True if this is a wide character (CJK) that takes 2 cells */
|
|
444
|
+
wide: boolean
|
|
445
|
+
/** True if this cell is the continuation of a wide character */
|
|
446
|
+
continuation: boolean
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Interface for the terminal buffer.
|
|
451
|
+
*/
|
|
452
|
+
export interface TerminalBuffer {
|
|
453
|
+
readonly width: number
|
|
454
|
+
readonly height: number
|
|
455
|
+
getCell(x: number, y: number): Cell
|
|
456
|
+
setCell(x: number, y: number, cell: Cell): void
|
|
457
|
+
clear(): void
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ============================================================================
|
|
461
|
+
// Event Types
|
|
462
|
+
// ============================================================================
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Keyboard event with key information and modifiers.
|
|
466
|
+
*/
|
|
467
|
+
export interface KeyEvent {
|
|
468
|
+
type: "key"
|
|
469
|
+
/** The key pressed (character or key name like 'ArrowUp') */
|
|
470
|
+
key: string
|
|
471
|
+
/** Ctrl modifier was held */
|
|
472
|
+
ctrl?: boolean
|
|
473
|
+
/** Meta/Alt modifier was held */
|
|
474
|
+
meta?: boolean
|
|
475
|
+
/** Shift modifier was held */
|
|
476
|
+
shift?: boolean
|
|
477
|
+
/** Alt/Option modifier was held */
|
|
478
|
+
alt?: boolean
|
|
479
|
+
/** Super/Cmd modifier was held. Requires Kitty protocol. */
|
|
480
|
+
super?: boolean
|
|
481
|
+
/** Hyper modifier was held. Requires Kitty protocol. */
|
|
482
|
+
hyper?: boolean
|
|
483
|
+
/** Kitty event type. Requires Kitty flag 2. */
|
|
484
|
+
eventType?: "press" | "repeat" | "release"
|
|
485
|
+
/** CapsLock is active. Kitty modifier bit 6. */
|
|
486
|
+
capsLock?: boolean
|
|
487
|
+
/** NumLock is active. Kitty modifier bit 7. */
|
|
488
|
+
numLock?: boolean
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Mouse event with position and button information.
|
|
493
|
+
*/
|
|
494
|
+
export interface MouseEvent {
|
|
495
|
+
type: "mouse"
|
|
496
|
+
/** X position in terminal columns (0-indexed) */
|
|
497
|
+
x: number
|
|
498
|
+
/** Y position in terminal rows (0-indexed) */
|
|
499
|
+
y: number
|
|
500
|
+
/** Mouse button (0=left, 1=middle, 2=right) */
|
|
501
|
+
button: number
|
|
502
|
+
/** Event action */
|
|
503
|
+
action: "down" | "up" | "move" | "wheel"
|
|
504
|
+
/** Wheel delta for scroll events */
|
|
505
|
+
delta?: number
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Terminal resize event.
|
|
510
|
+
*/
|
|
511
|
+
export interface ResizeEvent {
|
|
512
|
+
type: "resize"
|
|
513
|
+
/** New width in columns */
|
|
514
|
+
width: number
|
|
515
|
+
/** New height in rows */
|
|
516
|
+
height: number
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Terminal focus event.
|
|
521
|
+
*/
|
|
522
|
+
export interface FocusEvent {
|
|
523
|
+
type: "focus"
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Terminal blur event.
|
|
528
|
+
*/
|
|
529
|
+
export interface BlurEvent {
|
|
530
|
+
type: "blur"
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Signal event (SIGINT, SIGTERM, etc.).
|
|
535
|
+
*/
|
|
536
|
+
export interface SignalEvent {
|
|
537
|
+
type: "signal"
|
|
538
|
+
/** Signal name (e.g., 'SIGINT', 'SIGTERM') */
|
|
539
|
+
signal: string
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Custom event for extensibility.
|
|
544
|
+
*/
|
|
545
|
+
export interface CustomEvent {
|
|
546
|
+
type: "custom"
|
|
547
|
+
/** Event name */
|
|
548
|
+
name: string
|
|
549
|
+
/** Event data */
|
|
550
|
+
data: unknown
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Union of all event types.
|
|
555
|
+
*
|
|
556
|
+
* Events drive the render loop in interactive mode. When events are present,
|
|
557
|
+
* the render loop runs until exit() is called. When events are absent,
|
|
558
|
+
* the render completes when the UI is stable.
|
|
559
|
+
*/
|
|
560
|
+
export type Event = KeyEvent | MouseEvent | ResizeEvent | FocusEvent | BlurEvent | SignalEvent | CustomEvent
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Event source that can be subscribed to and unsubscribed from.
|
|
564
|
+
*/
|
|
565
|
+
export interface EventSource {
|
|
566
|
+
/** Subscribe to events, returns unsubscribe function */
|
|
567
|
+
subscribe(handler: (event: Event) => void): () => void
|
|
568
|
+
/** Convert to async iterable */
|
|
569
|
+
[Symbol.asyncIterator](): AsyncIterator<Event>
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ============================================================================
|
|
573
|
+
// TermDef - Minimal Render Configuration
|
|
574
|
+
// ============================================================================
|
|
575
|
+
|
|
576
|
+
// ColorLevel is re-exported from ansi in index.ts
|
|
577
|
+
// Import here for use in TermDef
|
|
578
|
+
import type { ColorLevel } from "@silvery/term/ansi"
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Minimal surface for configuring render().
|
|
582
|
+
*
|
|
583
|
+
* TermDef provides a simple way to configure rendering without requiring
|
|
584
|
+
* a full Term instance. It's useful for:
|
|
585
|
+
* - Static rendering (just width/height, no events)
|
|
586
|
+
* - Testing (mock dimensions and events)
|
|
587
|
+
* - Quick scripts (auto-detect everything from stdin/stdout)
|
|
588
|
+
*
|
|
589
|
+
* The presence of `events` (or `stdin` which auto-creates events)
|
|
590
|
+
* determines the render mode:
|
|
591
|
+
* - No events → static mode (render until stable)
|
|
592
|
+
* - Has events → interactive mode (render until exit() called)
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```tsx
|
|
596
|
+
* // Static render with custom width
|
|
597
|
+
* const output = await render(<App />, { width: 100 })
|
|
598
|
+
*
|
|
599
|
+
* // Interactive with stdin/stdout
|
|
600
|
+
* await render(<App />, { stdin: process.stdin, stdout: process.stdout })
|
|
601
|
+
*
|
|
602
|
+
* // Custom events
|
|
603
|
+
* await render(<App />, { events: myEventSource })
|
|
604
|
+
* ```
|
|
605
|
+
*/
|
|
606
|
+
export interface TermDef {
|
|
607
|
+
// -------------------------------------------------------------------------
|
|
608
|
+
// Output Configuration
|
|
609
|
+
// -------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
/** Output stream (used for dimensions if not specified) */
|
|
612
|
+
stdout?: NodeJS.WriteStream
|
|
613
|
+
|
|
614
|
+
/** Width in columns (default: stdout?.columns ?? 80) */
|
|
615
|
+
width?: number
|
|
616
|
+
|
|
617
|
+
/** Height in rows (default: stdout?.rows ?? 24) */
|
|
618
|
+
height?: number
|
|
619
|
+
|
|
620
|
+
/** Color support (true=detect, false=none, or specific level) */
|
|
621
|
+
colors?: boolean | ColorLevel | null
|
|
622
|
+
|
|
623
|
+
// -------------------------------------------------------------------------
|
|
624
|
+
// Input Configuration
|
|
625
|
+
// -------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Event source for interactive mode.
|
|
629
|
+
*
|
|
630
|
+
* When present, render runs until exit() is called.
|
|
631
|
+
* When absent, render completes when UI is stable.
|
|
632
|
+
*/
|
|
633
|
+
events?: AsyncIterable<Event> | EventSource
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Standard input stream.
|
|
637
|
+
*
|
|
638
|
+
* When provided (and events is not), automatically creates input events
|
|
639
|
+
* from stdin, enabling interactive mode.
|
|
640
|
+
*/
|
|
641
|
+
stdin?: NodeJS.ReadStream
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ============================================================================
|
|
645
|
+
// Render Context Types
|
|
646
|
+
// ============================================================================
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Options passed to the render function.
|
|
650
|
+
*/
|
|
651
|
+
export interface RenderOptions {
|
|
652
|
+
stdout?: NodeJS.WriteStream
|
|
653
|
+
stdin?: NodeJS.ReadStream
|
|
654
|
+
exitOnCtrlC?: boolean
|
|
655
|
+
debug?: boolean
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* The render instance returned by render().
|
|
660
|
+
*/
|
|
661
|
+
export interface RenderInstance {
|
|
662
|
+
/** Re-render with new element */
|
|
663
|
+
rerender: (element: React.ReactNode) => void
|
|
664
|
+
/** Unmount and clean up */
|
|
665
|
+
unmount: () => void
|
|
666
|
+
/** Wait for render to complete */
|
|
667
|
+
waitUntilExit: () => Promise<void>
|
|
668
|
+
/** Clear terminal output */
|
|
669
|
+
clear: () => void
|
|
670
|
+
}
|