@silvery/term 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.
Files changed (92) hide show
  1. package/package.json +54 -0
  2. package/src/adapters/canvas-adapter.ts +356 -0
  3. package/src/adapters/dom-adapter.ts +452 -0
  4. package/src/adapters/flexily-zero-adapter.ts +368 -0
  5. package/src/adapters/terminal-adapter.ts +305 -0
  6. package/src/adapters/yoga-adapter.ts +370 -0
  7. package/src/ansi/ansi.ts +251 -0
  8. package/src/ansi/constants.ts +76 -0
  9. package/src/ansi/detection.ts +441 -0
  10. package/src/ansi/hyperlink.ts +38 -0
  11. package/src/ansi/index.ts +201 -0
  12. package/src/ansi/patch-console.ts +159 -0
  13. package/src/ansi/sgr-codes.ts +34 -0
  14. package/src/ansi/storybook.ts +209 -0
  15. package/src/ansi/term.ts +724 -0
  16. package/src/ansi/types.ts +202 -0
  17. package/src/ansi/underline.ts +156 -0
  18. package/src/ansi/utils.ts +65 -0
  19. package/src/ansi-sanitize.ts +509 -0
  20. package/src/app.ts +571 -0
  21. package/src/bound-term.ts +94 -0
  22. package/src/bracketed-paste.ts +75 -0
  23. package/src/browser-renderer.ts +174 -0
  24. package/src/buffer.ts +1984 -0
  25. package/src/clipboard.ts +74 -0
  26. package/src/cursor-query.ts +85 -0
  27. package/src/device-attrs.ts +228 -0
  28. package/src/devtools.ts +123 -0
  29. package/src/dom/index.ts +194 -0
  30. package/src/errors.ts +39 -0
  31. package/src/focus-reporting.ts +48 -0
  32. package/src/hit-registry-core.ts +228 -0
  33. package/src/hit-registry.ts +176 -0
  34. package/src/index.ts +458 -0
  35. package/src/input.ts +119 -0
  36. package/src/inspector.ts +155 -0
  37. package/src/kitty-detect.ts +95 -0
  38. package/src/kitty-manager.ts +160 -0
  39. package/src/layout-engine.ts +296 -0
  40. package/src/layout.ts +26 -0
  41. package/src/measurer.ts +74 -0
  42. package/src/mode-query.ts +106 -0
  43. package/src/mouse-events.ts +419 -0
  44. package/src/mouse.ts +83 -0
  45. package/src/non-tty.ts +223 -0
  46. package/src/osc-markers.ts +32 -0
  47. package/src/osc-palette.ts +169 -0
  48. package/src/output.ts +406 -0
  49. package/src/pane-manager.ts +248 -0
  50. package/src/pipeline/CLAUDE.md +587 -0
  51. package/src/pipeline/content-phase-adapter.ts +976 -0
  52. package/src/pipeline/content-phase.ts +1765 -0
  53. package/src/pipeline/helpers.ts +42 -0
  54. package/src/pipeline/index.ts +416 -0
  55. package/src/pipeline/layout-phase.ts +686 -0
  56. package/src/pipeline/measure-phase.ts +198 -0
  57. package/src/pipeline/measure-stats.ts +21 -0
  58. package/src/pipeline/output-phase.ts +2593 -0
  59. package/src/pipeline/render-box.ts +343 -0
  60. package/src/pipeline/render-helpers.ts +243 -0
  61. package/src/pipeline/render-text.ts +1255 -0
  62. package/src/pipeline/types.ts +161 -0
  63. package/src/pipeline.ts +29 -0
  64. package/src/pixel-size.ts +119 -0
  65. package/src/render-adapter.ts +179 -0
  66. package/src/renderer.ts +1330 -0
  67. package/src/runtime/create-app.tsx +1845 -0
  68. package/src/runtime/create-buffer.ts +18 -0
  69. package/src/runtime/create-runtime.ts +325 -0
  70. package/src/runtime/diff.ts +56 -0
  71. package/src/runtime/event-handlers.ts +254 -0
  72. package/src/runtime/index.ts +119 -0
  73. package/src/runtime/keys.ts +8 -0
  74. package/src/runtime/layout.ts +164 -0
  75. package/src/runtime/run.tsx +318 -0
  76. package/src/runtime/term-provider.ts +399 -0
  77. package/src/runtime/terminal-lifecycle.ts +246 -0
  78. package/src/runtime/tick.ts +219 -0
  79. package/src/runtime/types.ts +210 -0
  80. package/src/scheduler.ts +723 -0
  81. package/src/screenshot.ts +57 -0
  82. package/src/scroll-region.ts +69 -0
  83. package/src/scroll-utils.ts +97 -0
  84. package/src/term-def.ts +267 -0
  85. package/src/terminal-caps.ts +5 -0
  86. package/src/terminal-colors.ts +216 -0
  87. package/src/termtest.ts +224 -0
  88. package/src/text-sizing.ts +109 -0
  89. package/src/toolbelt/index.ts +72 -0
  90. package/src/unicode.ts +1763 -0
  91. package/src/xterm/index.ts +491 -0
  92. package/src/xterm/xterm-provider.ts +204 -0
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Flexily Layout Engine Adapter
3
+ *
4
+ * Wraps Flexily to implement the LayoutEngine interface.
5
+ * Uses the default zero-allocation algorithm from flexily.
6
+ */
7
+
8
+ import {
9
+ ALIGN_AUTO,
10
+ ALIGN_BASELINE,
11
+ ALIGN_CENTER,
12
+ ALIGN_FLEX_END,
13
+ ALIGN_FLEX_START,
14
+ ALIGN_SPACE_AROUND,
15
+ ALIGN_SPACE_BETWEEN,
16
+ ALIGN_SPACE_EVENLY,
17
+ ALIGN_STRETCH,
18
+ DIRECTION_LTR,
19
+ DISPLAY_FLEX,
20
+ DISPLAY_NONE,
21
+ EDGE_ALL,
22
+ EDGE_BOTTOM,
23
+ EDGE_HORIZONTAL,
24
+ EDGE_LEFT,
25
+ EDGE_RIGHT,
26
+ EDGE_TOP,
27
+ EDGE_VERTICAL,
28
+ // Constants
29
+ FLEX_DIRECTION_COLUMN,
30
+ FLEX_DIRECTION_COLUMN_REVERSE,
31
+ FLEX_DIRECTION_ROW,
32
+ FLEX_DIRECTION_ROW_REVERSE,
33
+ Node as FlexilyNode,
34
+ GUTTER_ALL,
35
+ GUTTER_COLUMN,
36
+ GUTTER_ROW,
37
+ JUSTIFY_CENTER,
38
+ JUSTIFY_FLEX_END,
39
+ JUSTIFY_FLEX_START,
40
+ JUSTIFY_SPACE_AROUND,
41
+ JUSTIFY_SPACE_BETWEEN,
42
+ JUSTIFY_SPACE_EVENLY,
43
+ MEASURE_MODE_AT_MOST,
44
+ MEASURE_MODE_EXACTLY,
45
+ MEASURE_MODE_UNDEFINED,
46
+ OVERFLOW_HIDDEN,
47
+ OVERFLOW_SCROLL,
48
+ OVERFLOW_VISIBLE,
49
+ POSITION_TYPE_ABSOLUTE,
50
+ POSITION_TYPE_RELATIVE,
51
+ POSITION_TYPE_STATIC,
52
+ WRAP_NO_WRAP,
53
+ WRAP_WRAP,
54
+ WRAP_WRAP_REVERSE,
55
+ } from "flexily"
56
+
57
+ import type {
58
+ AlignValue,
59
+ DirectionValue,
60
+ DisplayValue,
61
+ EdgeValue,
62
+ FlexDirectionValue,
63
+ GutterValue,
64
+ JustifyValue,
65
+ LayoutConstants,
66
+ LayoutEngine,
67
+ LayoutNode,
68
+ MeasureFunc,
69
+ MeasureMode,
70
+ MeasureModeValue,
71
+ OverflowValue,
72
+ PositionTypeValue,
73
+ WrapValue,
74
+ } from "../layout-engine"
75
+
76
+ // ============================================================================
77
+ // Flexily Zero Node Adapter
78
+ // ============================================================================
79
+
80
+ /**
81
+ * Wraps a Flexily zero-alloc node to implement LayoutNode interface.
82
+ * Since Flexily already has a Yoga-compatible API, this is mostly delegation.
83
+ */
84
+ class FlexilyZeroNodeAdapter implements LayoutNode {
85
+ private node: FlexilyNode
86
+
87
+ constructor(node: FlexilyNode) {
88
+ this.node = node
89
+ }
90
+
91
+ /** Get the underlying Flexily node (for tree operations) */
92
+ getFlexilyNode(): FlexilyNode {
93
+ return this.node
94
+ }
95
+
96
+ // Tree operations
97
+ insertChild(child: LayoutNode, index: number): void {
98
+ const flexilyChild = (child as FlexilyZeroNodeAdapter).getFlexilyNode()
99
+ this.node.insertChild(flexilyChild, index)
100
+ }
101
+
102
+ removeChild(child: LayoutNode): void {
103
+ const flexilyChild = (child as FlexilyZeroNodeAdapter).getFlexilyNode()
104
+ this.node.removeChild(flexilyChild)
105
+ }
106
+
107
+ free(): void {
108
+ this.node.free()
109
+ }
110
+
111
+ // Measure function
112
+ setMeasureFunc(measureFunc: MeasureFunc): void {
113
+ this.node.setMeasureFunc((width, widthMode, height, heightMode) => {
114
+ const widthModeStr = this.measureModeToString(widthMode)
115
+ const heightModeStr = this.measureModeToString(heightMode)
116
+ return measureFunc(width, widthModeStr, height, heightModeStr)
117
+ })
118
+ }
119
+
120
+ // Dirty tracking - forces layout recalculation
121
+ markDirty(): void {
122
+ this.node.markDirty()
123
+ }
124
+
125
+ private measureModeToString(mode: number): MeasureMode {
126
+ if (mode === MEASURE_MODE_EXACTLY) return "exactly"
127
+ if (mode === MEASURE_MODE_AT_MOST) return "at-most"
128
+ return "undefined"
129
+ }
130
+
131
+ // Dimension setters
132
+ setWidth(value: number): void {
133
+ this.node.setWidth(value)
134
+ }
135
+ setWidthPercent(value: number): void {
136
+ this.node.setWidthPercent(value)
137
+ }
138
+ setWidthAuto(): void {
139
+ this.node.setWidthAuto()
140
+ }
141
+ setHeight(value: number): void {
142
+ this.node.setHeight(value)
143
+ }
144
+ setHeightPercent(value: number): void {
145
+ this.node.setHeightPercent(value)
146
+ }
147
+ setHeightAuto(): void {
148
+ this.node.setHeightAuto()
149
+ }
150
+ setMinWidth(value: number): void {
151
+ this.node.setMinWidth(value)
152
+ }
153
+ setMinWidthPercent(value: number): void {
154
+ this.node.setMinWidthPercent(value)
155
+ }
156
+ setMinHeight(value: number): void {
157
+ this.node.setMinHeight(value)
158
+ }
159
+ setMinHeightPercent(value: number): void {
160
+ this.node.setMinHeightPercent(value)
161
+ }
162
+ setMaxWidth(value: number): void {
163
+ this.node.setMaxWidth(value)
164
+ }
165
+ setMaxWidthPercent(value: number): void {
166
+ this.node.setMaxWidthPercent(value)
167
+ }
168
+ setMaxHeight(value: number): void {
169
+ this.node.setMaxHeight(value)
170
+ }
171
+ setMaxHeightPercent(value: number): void {
172
+ this.node.setMaxHeightPercent(value)
173
+ }
174
+
175
+ // Flex properties
176
+ setFlexGrow(value: number): void {
177
+ this.node.setFlexGrow(value)
178
+ }
179
+ setFlexShrink(value: number): void {
180
+ this.node.setFlexShrink(value)
181
+ }
182
+ setFlexBasis(value: number): void {
183
+ this.node.setFlexBasis(value)
184
+ }
185
+ setFlexBasisPercent(value: number): void {
186
+ this.node.setFlexBasisPercent(value)
187
+ }
188
+ setFlexBasisAuto(): void {
189
+ this.node.setFlexBasisAuto()
190
+ }
191
+ setFlexDirection(direction: number): void {
192
+ this.node.setFlexDirection(direction)
193
+ }
194
+ setFlexWrap(wrap: number): void {
195
+ this.node.setFlexWrap(wrap)
196
+ }
197
+
198
+ // Alignment
199
+ setAlignItems(align: number): void {
200
+ this.node.setAlignItems(align)
201
+ }
202
+ setAlignSelf(align: number): void {
203
+ this.node.setAlignSelf(align)
204
+ }
205
+ setAlignContent(align: number): void {
206
+ this.node.setAlignContent(align)
207
+ }
208
+ setJustifyContent(justify: number): void {
209
+ this.node.setJustifyContent(justify)
210
+ }
211
+
212
+ // Spacing
213
+ setPadding(edge: number, value: number): void {
214
+ this.node.setPadding(edge, value)
215
+ }
216
+ setMargin(edge: number, value: number): void {
217
+ this.node.setMargin(edge, value)
218
+ }
219
+ setBorder(edge: number, value: number): void {
220
+ this.node.setBorder(edge, value)
221
+ }
222
+ setGap(gutter: number, value: number): void {
223
+ this.node.setGap(gutter, value)
224
+ }
225
+
226
+ // Display & Position
227
+ setDisplay(display: number): void {
228
+ this.node.setDisplay(display)
229
+ }
230
+ setPositionType(positionType: number): void {
231
+ this.node.setPositionType(positionType)
232
+ }
233
+ setPosition(edge: number, value: number): void {
234
+ this.node.setPosition(edge, value)
235
+ }
236
+ setPositionPercent(edge: number, value: number): void {
237
+ this.node.setPositionPercent(edge, value)
238
+ }
239
+ setOverflow(overflow: number): void {
240
+ this.node.setOverflow(overflow)
241
+ }
242
+
243
+ // Aspect Ratio
244
+ setAspectRatio(value: number): void {
245
+ this.node.setAspectRatio(value)
246
+ }
247
+
248
+ // Layout calculation
249
+ calculateLayout(width: number, height: number, direction?: number): void {
250
+ this.node.calculateLayout(width, height, direction ?? DIRECTION_LTR)
251
+ }
252
+
253
+ // Layout results
254
+ getComputedLeft(): number {
255
+ return this.node.getComputedLeft()
256
+ }
257
+ getComputedTop(): number {
258
+ return this.node.getComputedTop()
259
+ }
260
+ getComputedWidth(): number {
261
+ return this.node.getComputedWidth()
262
+ }
263
+ getComputedHeight(): number {
264
+ return this.node.getComputedHeight()
265
+ }
266
+ }
267
+
268
+ // ============================================================================
269
+ // Flexily Zero Layout Engine
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Layout engine implementation using Flexily zero-allocation variant.
274
+ * Optimized for high-frequency layout with reduced GC pressure.
275
+ */
276
+ export class FlexilyZeroLayoutEngine implements LayoutEngine {
277
+ private _constants: LayoutConstants = {
278
+ // Flex Direction (cast from Flexily's plain numbers to branded types)
279
+ FLEX_DIRECTION_COLUMN: FLEX_DIRECTION_COLUMN as FlexDirectionValue,
280
+ FLEX_DIRECTION_COLUMN_REVERSE: FLEX_DIRECTION_COLUMN_REVERSE as FlexDirectionValue,
281
+ FLEX_DIRECTION_ROW: FLEX_DIRECTION_ROW as FlexDirectionValue,
282
+ FLEX_DIRECTION_ROW_REVERSE: FLEX_DIRECTION_ROW_REVERSE as FlexDirectionValue,
283
+
284
+ // Wrap
285
+ WRAP_NO_WRAP: WRAP_NO_WRAP as WrapValue,
286
+ WRAP_WRAP: WRAP_WRAP as WrapValue,
287
+ WRAP_WRAP_REVERSE: WRAP_WRAP_REVERSE as WrapValue,
288
+
289
+ // Align
290
+ ALIGN_AUTO: ALIGN_AUTO as AlignValue,
291
+ ALIGN_FLEX_START: ALIGN_FLEX_START as AlignValue,
292
+ ALIGN_CENTER: ALIGN_CENTER as AlignValue,
293
+ ALIGN_FLEX_END: ALIGN_FLEX_END as AlignValue,
294
+ ALIGN_STRETCH: ALIGN_STRETCH as AlignValue,
295
+ ALIGN_BASELINE: ALIGN_BASELINE as AlignValue,
296
+ ALIGN_SPACE_BETWEEN: ALIGN_SPACE_BETWEEN as AlignValue,
297
+ ALIGN_SPACE_AROUND: ALIGN_SPACE_AROUND as AlignValue,
298
+ ALIGN_SPACE_EVENLY: ALIGN_SPACE_EVENLY as AlignValue,
299
+
300
+ // Justify
301
+ JUSTIFY_FLEX_START: JUSTIFY_FLEX_START as JustifyValue,
302
+ JUSTIFY_CENTER: JUSTIFY_CENTER as JustifyValue,
303
+ JUSTIFY_FLEX_END: JUSTIFY_FLEX_END as JustifyValue,
304
+ JUSTIFY_SPACE_BETWEEN: JUSTIFY_SPACE_BETWEEN as JustifyValue,
305
+ JUSTIFY_SPACE_AROUND: JUSTIFY_SPACE_AROUND as JustifyValue,
306
+ JUSTIFY_SPACE_EVENLY: JUSTIFY_SPACE_EVENLY as JustifyValue,
307
+
308
+ // Edge
309
+ EDGE_LEFT: EDGE_LEFT as EdgeValue,
310
+ EDGE_TOP: EDGE_TOP as EdgeValue,
311
+ EDGE_RIGHT: EDGE_RIGHT as EdgeValue,
312
+ EDGE_BOTTOM: EDGE_BOTTOM as EdgeValue,
313
+ EDGE_HORIZONTAL: EDGE_HORIZONTAL as EdgeValue,
314
+ EDGE_VERTICAL: EDGE_VERTICAL as EdgeValue,
315
+ EDGE_ALL: EDGE_ALL as EdgeValue,
316
+
317
+ // Gutter
318
+ GUTTER_COLUMN: GUTTER_COLUMN as GutterValue,
319
+ GUTTER_ROW: GUTTER_ROW as GutterValue,
320
+ GUTTER_ALL: GUTTER_ALL as GutterValue,
321
+
322
+ // Display
323
+ DISPLAY_FLEX: DISPLAY_FLEX as DisplayValue,
324
+ DISPLAY_NONE: DISPLAY_NONE as DisplayValue,
325
+
326
+ // Position Type
327
+ POSITION_TYPE_STATIC: POSITION_TYPE_STATIC as PositionTypeValue,
328
+ POSITION_TYPE_RELATIVE: POSITION_TYPE_RELATIVE as PositionTypeValue,
329
+ POSITION_TYPE_ABSOLUTE: POSITION_TYPE_ABSOLUTE as PositionTypeValue,
330
+
331
+ // Overflow
332
+ OVERFLOW_VISIBLE: OVERFLOW_VISIBLE as OverflowValue,
333
+ OVERFLOW_HIDDEN: OVERFLOW_HIDDEN as OverflowValue,
334
+ OVERFLOW_SCROLL: OVERFLOW_SCROLL as OverflowValue,
335
+
336
+ // Direction
337
+ DIRECTION_LTR: DIRECTION_LTR as DirectionValue,
338
+
339
+ // Measure Mode
340
+ MEASURE_MODE_UNDEFINED: MEASURE_MODE_UNDEFINED as MeasureModeValue,
341
+ MEASURE_MODE_EXACTLY: MEASURE_MODE_EXACTLY as MeasureModeValue,
342
+ MEASURE_MODE_AT_MOST: MEASURE_MODE_AT_MOST as MeasureModeValue,
343
+ }
344
+
345
+ createNode(): LayoutNode {
346
+ return new FlexilyZeroNodeAdapter(FlexilyNode.create())
347
+ }
348
+
349
+ get constants(): LayoutConstants {
350
+ return this._constants
351
+ }
352
+
353
+ get name(): string {
354
+ return "flexily-zero"
355
+ }
356
+ }
357
+
358
+ // ============================================================================
359
+ // Initialization Helper
360
+ // ============================================================================
361
+
362
+ /**
363
+ * Create a Flexily zero-allocation layout engine.
364
+ * Unlike Yoga, Flexily doesn't require async initialization.
365
+ */
366
+ export function createFlexilyZeroEngine(): FlexilyZeroLayoutEngine {
367
+ return new FlexilyZeroLayoutEngine()
368
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Terminal Render Adapter
3
+ *
4
+ * Implements the RenderAdapter interface for terminal output.
5
+ * Uses character cells as units, ANSI codes for styling.
6
+ */
7
+
8
+ import { type Color, TerminalBuffer } from "../buffer"
9
+ import { outputPhase } from "../pipeline/output-phase"
10
+ import type {
11
+ BorderChars,
12
+ RenderAdapter,
13
+ RenderBuffer,
14
+ RenderStyle,
15
+ TextMeasureResult,
16
+ TextMeasureStyle,
17
+ TextMeasurer,
18
+ } from "../render-adapter"
19
+ import { type Measurer, displayWidth } from "../unicode"
20
+
21
+ // ============================================================================
22
+ // Border Characters
23
+ // ============================================================================
24
+
25
+ const BORDER_CHARS: Record<string, BorderChars> = {
26
+ single: {
27
+ topLeft: "┌",
28
+ topRight: "┐",
29
+ bottomLeft: "└",
30
+ bottomRight: "┘",
31
+ horizontal: "─",
32
+ vertical: "│",
33
+ },
34
+ double: {
35
+ topLeft: "╔",
36
+ topRight: "╗",
37
+ bottomLeft: "╚",
38
+ bottomRight: "╝",
39
+ horizontal: "═",
40
+ vertical: "║",
41
+ },
42
+ round: {
43
+ topLeft: "╭",
44
+ topRight: "╮",
45
+ bottomLeft: "╰",
46
+ bottomRight: "╯",
47
+ horizontal: "─",
48
+ vertical: "│",
49
+ },
50
+ bold: {
51
+ topLeft: "┏",
52
+ topRight: "┓",
53
+ bottomLeft: "┗",
54
+ bottomRight: "┛",
55
+ horizontal: "━",
56
+ vertical: "┃",
57
+ },
58
+ singleDouble: {
59
+ topLeft: "╓",
60
+ topRight: "╖",
61
+ bottomLeft: "╙",
62
+ bottomRight: "╜",
63
+ horizontal: "─",
64
+ vertical: "║",
65
+ },
66
+ doubleSingle: {
67
+ topLeft: "╒",
68
+ topRight: "╕",
69
+ bottomLeft: "╘",
70
+ bottomRight: "╛",
71
+ horizontal: "═",
72
+ vertical: "│",
73
+ },
74
+ classic: {
75
+ topLeft: "+",
76
+ topRight: "+",
77
+ bottomLeft: "+",
78
+ bottomRight: "+",
79
+ horizontal: "-",
80
+ vertical: "|",
81
+ },
82
+ }
83
+
84
+ // ============================================================================
85
+ // Terminal Measurer
86
+ // ============================================================================
87
+
88
+ /** Create a terminal text measurer, optionally using an explicit width measurer. */
89
+ export function createTerminalMeasurer(measurer?: Measurer): TextMeasurer {
90
+ const dw = measurer ? measurer.displayWidth.bind(measurer) : displayWidth
91
+ return {
92
+ measureText(text: string, _style?: TextMeasureStyle): TextMeasureResult {
93
+ return { width: dw(text), height: 1 }
94
+ },
95
+ getLineHeight(_style?: TextMeasureStyle): number {
96
+ return 1
97
+ },
98
+ }
99
+ }
100
+
101
+ /** Default terminal measurer (uses module-level displayWidth / scoped measurer). */
102
+ export const terminalMeasurer: TextMeasurer = createTerminalMeasurer()
103
+
104
+ // ============================================================================
105
+ // Terminal Render Buffer
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Wraps TerminalBuffer to implement the RenderBuffer interface.
110
+ */
111
+ export class TerminalRenderBuffer implements RenderBuffer {
112
+ private buffer: TerminalBuffer
113
+ private dw: (text: string) => number
114
+
115
+ constructor(width: number, height: number, measurer?: Measurer) {
116
+ this.buffer = new TerminalBuffer(width, height)
117
+ this.dw = measurer ? measurer.displayWidth.bind(measurer) : displayWidth
118
+ }
119
+
120
+ get width(): number {
121
+ return this.buffer.width
122
+ }
123
+
124
+ get height(): number {
125
+ return this.buffer.height
126
+ }
127
+
128
+ /**
129
+ * Get the underlying TerminalBuffer for output phase.
130
+ */
131
+ getTerminalBuffer(): TerminalBuffer {
132
+ return this.buffer
133
+ }
134
+
135
+ fillRect(x: number, y: number, width: number, height: number, style: RenderStyle): void {
136
+ const cellStyle = this.convertStyle(style)
137
+ for (let row = y; row < y + height; row++) {
138
+ for (let col = x; col < x + width; col++) {
139
+ if (this.buffer.inBounds(col, row)) {
140
+ this.buffer.setCell(col, row, {
141
+ char: " ",
142
+ ...cellStyle,
143
+ })
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ drawText(x: number, y: number, text: string, style: RenderStyle): void {
150
+ const cellStyle = this.convertStyle(style)
151
+ let col = x
152
+ for (const char of text) {
153
+ if (!this.buffer.inBounds(col, y)) break
154
+ const charWidth = this.dw(char)
155
+ this.buffer.setCell(col, y, {
156
+ char,
157
+ ...cellStyle,
158
+ wide: charWidth > 1,
159
+ })
160
+ // Mark continuation cells for wide characters
161
+ for (let i = 1; i < charWidth; i++) {
162
+ if (this.buffer.inBounds(col + i, y)) {
163
+ this.buffer.setCell(col + i, y, {
164
+ char: "",
165
+ ...cellStyle,
166
+ continuation: true,
167
+ })
168
+ }
169
+ }
170
+ col += charWidth
171
+ }
172
+ }
173
+
174
+ drawChar(x: number, y: number, char: string, style: RenderStyle): void {
175
+ if (this.buffer.inBounds(x, y)) {
176
+ this.buffer.setCell(x, y, {
177
+ char,
178
+ ...this.convertStyle(style),
179
+ })
180
+ }
181
+ }
182
+
183
+ inBounds(x: number, y: number): boolean {
184
+ return this.buffer.inBounds(x, y)
185
+ }
186
+
187
+ private convertStyle(style: RenderStyle): {
188
+ fg: Color
189
+ bg: Color
190
+ underlineColor: Color
191
+ attrs: {
192
+ bold?: boolean
193
+ dim?: boolean
194
+ italic?: boolean
195
+ underline?: boolean
196
+ underlineStyle?: "single" | "double" | "curly" | "dotted" | "dashed" | false
197
+ strikethrough?: boolean
198
+ inverse?: boolean
199
+ }
200
+ } {
201
+ return {
202
+ fg: this.parseColor(style.fg),
203
+ bg: this.parseColor(style.bg),
204
+ underlineColor: this.parseColor(style.attrs?.underlineColor),
205
+ attrs: {
206
+ bold: style.attrs?.bold,
207
+ dim: style.attrs?.dim,
208
+ italic: style.attrs?.italic,
209
+ underline: style.attrs?.underline,
210
+ underlineStyle: style.attrs?.underlineStyle,
211
+ strikethrough: style.attrs?.strikethrough,
212
+ inverse: style.attrs?.inverse,
213
+ },
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Parse a color string to the Color type used by TerminalBuffer.
219
+ */
220
+ private parseColor(color: string | undefined): Color {
221
+ if (!color) return null
222
+
223
+ // Hex color
224
+ if (color.startsWith("#")) {
225
+ const hex = color.slice(1)
226
+ if (hex.length === 6) {
227
+ return {
228
+ r: Number.parseInt(hex.slice(0, 2), 16),
229
+ g: Number.parseInt(hex.slice(2, 4), 16),
230
+ b: Number.parseInt(hex.slice(4, 6), 16),
231
+ }
232
+ }
233
+ if (hex.length === 3) {
234
+ return {
235
+ r: Number.parseInt(hex[0]! + hex[0]!, 16),
236
+ g: Number.parseInt(hex[1]! + hex[1]!, 16),
237
+ b: Number.parseInt(hex[2]! + hex[2]!, 16),
238
+ }
239
+ }
240
+ }
241
+
242
+ // RGB color
243
+ const rgbMatch = color.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)
244
+ if (rgbMatch) {
245
+ return {
246
+ r: Number.parseInt(rgbMatch[1]!, 10),
247
+ g: Number.parseInt(rgbMatch[2]!, 10),
248
+ b: Number.parseInt(rgbMatch[3]!, 10),
249
+ }
250
+ }
251
+
252
+ // Named ANSI colors - map to 256-color indices
253
+ const namedColors: Record<string, number> = {
254
+ black: 0,
255
+ red: 1,
256
+ green: 2,
257
+ yellow: 3,
258
+ blue: 4,
259
+ magenta: 5,
260
+ cyan: 6,
261
+ white: 7,
262
+ gray: 8,
263
+ grey: 8,
264
+ brightblack: 8,
265
+ brightred: 9,
266
+ brightgreen: 10,
267
+ brightyellow: 11,
268
+ brightblue: 12,
269
+ brightmagenta: 13,
270
+ brightcyan: 14,
271
+ brightwhite: 15,
272
+ }
273
+
274
+ const normalized = color.toLowerCase().replace(/[^a-z]/g, "")
275
+ const index = namedColors[normalized]
276
+ if (index !== undefined) {
277
+ return index
278
+ }
279
+
280
+ return null
281
+ }
282
+ }
283
+
284
+ // ============================================================================
285
+ // Terminal Adapter
286
+ // ============================================================================
287
+
288
+ export const terminalAdapter: RenderAdapter = {
289
+ name: "terminal",
290
+ measurer: terminalMeasurer,
291
+
292
+ createBuffer(width: number, height: number): RenderBuffer {
293
+ return new TerminalRenderBuffer(width, height)
294
+ },
295
+
296
+ flush(buffer: RenderBuffer, prevBuffer: RenderBuffer | null): string {
297
+ const termBuffer = (buffer as TerminalRenderBuffer).getTerminalBuffer()
298
+ const prevTermBuffer = prevBuffer ? (prevBuffer as TerminalRenderBuffer).getTerminalBuffer() : null
299
+ return outputPhase(prevTermBuffer, termBuffer)
300
+ },
301
+
302
+ getBorderChars(style: string): BorderChars {
303
+ return BORDER_CHARS[style] ?? BORDER_CHARS.single!
304
+ },
305
+ }