@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,42 @@
1
+ /**
2
+ * Shared helper functions for silvery pipeline phases.
3
+ */
4
+
5
+ import type { BoxProps } from "@silvery/tea/types"
6
+
7
+ /**
8
+ * Get padding values from props.
9
+ */
10
+ export function getPadding(props: BoxProps): {
11
+ top: number
12
+ bottom: number
13
+ left: number
14
+ right: number
15
+ } {
16
+ return {
17
+ top: props.paddingTop ?? props.paddingY ?? props.padding ?? 0,
18
+ bottom: props.paddingBottom ?? props.paddingY ?? props.padding ?? 0,
19
+ left: props.paddingLeft ?? props.paddingX ?? props.padding ?? 0,
20
+ right: props.paddingRight ?? props.paddingX ?? props.padding ?? 0,
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Get border size (1 or 0 for each side).
26
+ */
27
+ export function getBorderSize(props: BoxProps): {
28
+ top: number
29
+ bottom: number
30
+ left: number
31
+ right: number
32
+ } {
33
+ if (!props.borderStyle) {
34
+ return { top: 0, bottom: 0, left: 0, right: 0 }
35
+ }
36
+ return {
37
+ top: props.borderTop !== false ? 1 : 0,
38
+ bottom: props.borderBottom !== false ? 1 : 0,
39
+ left: props.borderLeft !== false ? 1 : 0,
40
+ right: props.borderRight !== false ? 1 : 0,
41
+ }
42
+ }
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Silvery Render Pipeline
3
+ *
4
+ * The 5-phase rendering architecture:
5
+ *
6
+ * Phase 0: RECONCILIATION (React)
7
+ * React reconciliation builds the SilveryNode tree.
8
+ * Components register layout constraints via props.
9
+ *
10
+ * Phase 1: MEASURE (for fit-content nodes)
11
+ * Traverse nodes with width/height="fit-content"
12
+ * Measure intrinsic content size
13
+ * Set Yoga constraints based on measurement
14
+ *
15
+ * Phase 2: LAYOUT
16
+ * Run yoga.calculateLayout()
17
+ * Propagate computed dimensions to all nodes
18
+ * Notify useContentRect() subscribers
19
+ *
20
+ * Phase 3: CONTENT RENDER
21
+ * Render each node to the TerminalBuffer
22
+ * Handle text truncation, styling, borders
23
+ *
24
+ * Phase 4: DIFF & OUTPUT
25
+ * Compare current buffer with previous
26
+ * Emit minimal ANSI sequences for changes
27
+ */
28
+
29
+ import { createLogger } from "loggily"
30
+ import type { TerminalBuffer } from "../buffer"
31
+ import type { CursorState } from "@silvery/react/hooks/useCursor"
32
+ import type { TeaNode } from "@silvery/tea/types"
33
+ import { runWithMeasurer, type Measurer } from "../unicode"
34
+ import type { OutputPhaseFn } from "./output-phase"
35
+ import type { PipelineContext } from "./types"
36
+
37
+ const log = createLogger("silvery:pipeline")
38
+ const baseLog = createLogger("@silvery/react")
39
+
40
+ // Re-export types
41
+ export type {
42
+ CellChange,
43
+ BorderChars,
44
+ PipelineContext,
45
+ NodeRenderState,
46
+ ClipBounds,
47
+ ContentPhaseStats,
48
+ NodeTraceEntry,
49
+ BgConflictMode,
50
+ } from "./types"
51
+
52
+ // Re-export phase functions
53
+ export { measurePhase } from "./measure-phase"
54
+ export {
55
+ layoutPhase,
56
+ rectEqual,
57
+ scrollPhase,
58
+ stickyPhase,
59
+ screenRectPhase,
60
+ notifyLayoutSubscribers,
61
+ } from "./layout-phase"
62
+ export { contentPhase, clearBgConflictWarnings, setBgConflictMode } from "./content-phase"
63
+ export { contentPhaseAdapter } from "./content-phase-adapter"
64
+ export { outputPhase } from "./output-phase"
65
+
66
+ import { contentPhaseAdapter } from "./content-phase-adapter"
67
+ import { clearBgConflictWarnings, contentPhase } from "./content-phase"
68
+ import { layoutPhase, notifyLayoutSubscribers, screenRectPhase, scrollPhase, stickyPhase } from "./layout-phase"
69
+ // Import for orchestration
70
+ import { measurePhase } from "./measure-phase"
71
+ import { outputPhase } from "./output-phase"
72
+
73
+ // ============================================================================
74
+ // Execute Render (Orchestration)
75
+ // ============================================================================
76
+
77
+ /**
78
+ * Options for executeRender.
79
+ */
80
+ export interface ExecuteRenderOptions {
81
+ /**
82
+ * Render mode: fullscreen or inline.
83
+ * Default: 'fullscreen'
84
+ */
85
+ mode?: "fullscreen" | "inline"
86
+
87
+ /**
88
+ * Skip notifying layout subscribers.
89
+ * Use for static/one-shot renders where layout feedback isn't needed.
90
+ * Default: false
91
+ */
92
+ skipLayoutNotifications?: boolean
93
+
94
+ /**
95
+ * Skip scroll state updates.
96
+ * Use for fresh render comparisons (SILVERY_STRICT) to avoid mutating state.
97
+ * Default: false
98
+ */
99
+ skipScrollStateUpdates?: boolean
100
+
101
+ /**
102
+ * Number of lines written to stdout between renders (inline mode only).
103
+ * Used to adjust cursor positioning when external code (e.g., useScrollback)
104
+ * writes directly to stdout between renders.
105
+ * Default: 0
106
+ */
107
+ scrollbackOffset?: number
108
+
109
+ /**
110
+ * Terminal height in rows (inline mode only).
111
+ * Used to clamp cursor-up offset when content exceeds terminal height.
112
+ * Without this, content taller than the terminal causes rendering corruption
113
+ * because cursor-up can't reach lines that scrolled off screen.
114
+ */
115
+ termRows?: number
116
+
117
+ /**
118
+ * Cursor position from useCursor() (inline mode only).
119
+ * When provided, the output phase positions the real terminal cursor
120
+ * at this location instead of leaving it at the end of content.
121
+ */
122
+ cursorPos?: CursorState | null
123
+ }
124
+
125
+ /**
126
+ * Pipeline configuration from withRender().
127
+ * Carries term-scoped width measurer and output phase.
128
+ */
129
+ export interface PipelineConfig {
130
+ /** Width measurer scoped to terminal capabilities */
131
+ readonly measurer: Measurer
132
+ /** Output phase function scoped to terminal capabilities */
133
+ readonly outputPhaseFn: OutputPhaseFn
134
+ }
135
+
136
+ /**
137
+ * Execute the full render pipeline.
138
+ *
139
+ * Pass null for prevBuffer on the first render; pass the returned buffer on
140
+ * subsequent renders to enable incremental content rendering (<1ms vs 20-30ms).
141
+ * SILVERY_DEV=1 warns at runtime if prevBuffer is null after the first frame.
142
+ */
143
+ export function executeRender(
144
+ root: TeaNode,
145
+ width: number,
146
+ height: number,
147
+ prevBuffer: TerminalBuffer | null,
148
+ options: ExecuteRenderOptions | "fullscreen" | "inline" = "fullscreen",
149
+ config?: PipelineConfig,
150
+ ): { output: string; buffer: TerminalBuffer } {
151
+ // Create PipelineContext from config measurer (if provided).
152
+ // The context is threaded explicitly through the pipeline, eliminating
153
+ // the need for pipeline functions to read the _scopedMeasurer global.
154
+ const ctx: PipelineContext | undefined = config?.measurer ? { measurer: config.measurer } : undefined
155
+
156
+ if (config?.measurer) {
157
+ // Keep runWithMeasurer for backward compat: output-phase and other
158
+ // non-pipeline consumers still read the scoped measurer global.
159
+ return runWithMeasurer(config.measurer, () => {
160
+ return executeRenderCore(root, width, height, prevBuffer, options, config, ctx)
161
+ })
162
+ }
163
+ return executeRenderCore(root, width, height, prevBuffer, options, config, ctx)
164
+ }
165
+
166
+ /** Internal: runs the full pipeline. */
167
+ function executeRenderCore(
168
+ root: TeaNode,
169
+ width: number,
170
+ height: number,
171
+ prevBuffer: TerminalBuffer | null,
172
+ options: ExecuteRenderOptions | "fullscreen" | "inline" = "fullscreen",
173
+ config?: PipelineConfig,
174
+ ctx?: PipelineContext,
175
+ ): { output: string; buffer: TerminalBuffer } {
176
+ // Normalize options (string shorthand for mode)
177
+ const opts: ExecuteRenderOptions = typeof options === "string" ? { mode: options } : options
178
+ const {
179
+ mode = "fullscreen",
180
+ skipLayoutNotifications = false,
181
+ skipScrollStateUpdates = false,
182
+ scrollbackOffset = 0,
183
+ termRows,
184
+ cursorPos,
185
+ } = opts
186
+ // Dev warning: prevBuffer null after first render means incremental is disabled.
187
+ // Intentional null (SILVERY_STRICT, static/one-shot) passes skipLayoutNotifications.
188
+ // console.warn (not loggily) — must fire regardless of logger config.
189
+ if (process?.env?.SILVERY_DEV && prevBuffer === null && root.prevLayout !== null && !skipLayoutNotifications) {
190
+ console.warn(
191
+ "[silvery] executeRender called with prevBuffer=null on frame 2+ — " +
192
+ "incremental content rendering is disabled (full render every frame). " +
193
+ "Track the returned buffer and pass it as prevBuffer on subsequent renders.",
194
+ )
195
+ }
196
+
197
+ const start = performance.now()
198
+
199
+ using render = baseLog.span("pipeline", { width, height, mode })
200
+
201
+ // Clear per-render caches
202
+ clearBgConflictWarnings()
203
+
204
+ // Phase 1: Measure (for fit-content nodes)
205
+ let tMeasure: number
206
+ {
207
+ using _measure = render.span("measure")
208
+ const t1 = performance.now()
209
+ measurePhase(root, ctx)
210
+ tMeasure = performance.now() - t1
211
+ log.debug?.(`measure: ${tMeasure.toFixed(2)}ms`)
212
+ }
213
+
214
+ // Phase 2: Layout
215
+ let tLayout: number
216
+ {
217
+ using _layout = render.span("layout")
218
+ const t2 = performance.now()
219
+ layoutPhase(root, width, height)
220
+ tLayout = performance.now() - t2
221
+ log.debug?.(`layout: ${tLayout.toFixed(2)}ms`)
222
+ }
223
+
224
+ // Phase 2.5: Scroll calculation (for overflow='scroll' containers)
225
+ let tScroll: number
226
+ {
227
+ using _scroll = render.span("scroll")
228
+ const t2s = performance.now()
229
+ scrollPhase(root, { skipStateUpdates: skipScrollStateUpdates })
230
+ tScroll = performance.now() - t2s
231
+ }
232
+
233
+ // Phase 2.55: Sticky phase (non-scroll container sticky children)
234
+ stickyPhase(root)
235
+
236
+ // Phase 2.6: Screen rect calculation (screen-relative positions)
237
+ let tScreenRect: number
238
+ {
239
+ using _screenRect = render.span("screenRect")
240
+ const t2r = performance.now()
241
+ screenRectPhase(root)
242
+ tScreenRect = performance.now() - t2r
243
+ }
244
+
245
+ // Phase 2.7: Notify layout subscribers
246
+ // This runs AFTER screenRectPhase so useScreenRectCallback reads correct positions
247
+ // Skip for static renders where no one will respond to the feedback
248
+ let tNotify = 0
249
+ if (!skipLayoutNotifications) {
250
+ using _notify = render.span("notify")
251
+ const t2n = performance.now()
252
+ notifyLayoutSubscribers(root)
253
+ tNotify = performance.now() - t2n
254
+ }
255
+
256
+ // Phase 3: Content render (incremental if we have prevBuffer)
257
+ let buffer: TerminalBuffer
258
+ let tContent: number
259
+ {
260
+ using _content = render.span("content")
261
+ const t3 = performance.now()
262
+ buffer = contentPhase(root, prevBuffer, ctx)
263
+ tContent = performance.now() - t3
264
+ log.debug?.(`content: ${tContent.toFixed(2)}ms`)
265
+ }
266
+
267
+ // Phase 4: Diff and output
268
+ let output: string
269
+ let tOutput: number
270
+ {
271
+ using outputSpan = render.span("output")
272
+ const t4 = performance.now()
273
+ const outputFn = config?.outputPhaseFn ?? outputPhase
274
+ try {
275
+ output = outputFn(prevBuffer, buffer, mode, scrollbackOffset, termRows, cursorPos)
276
+ } catch (e) {
277
+ // Output phase (SILVERY_STRICT_OUTPUT) may throw a diagnostic error.
278
+ // Attach the content-phase buffer so callers can still save it for
279
+ // incremental rendering continuity — the buffer is correct even when
280
+ // the ANSI output verification fails.
281
+ if (e instanceof Error) {
282
+ ;(e as any).__silvery_buffer = buffer
283
+ }
284
+ throw e
285
+ }
286
+ tOutput = performance.now() - t4
287
+ outputSpan.spanData.bytes = output.length
288
+ log.debug?.(`output: ${tOutput.toFixed(2)}ms (${output.length} bytes)`)
289
+ }
290
+
291
+ const total = performance.now() - start
292
+ log.debug?.(`total pipeline: ${total.toFixed(2)}ms`)
293
+
294
+ // Expose phase timing and render count for benchmarking and diagnostics
295
+ ;(globalThis as any).__silvery_last_pipeline = {
296
+ measure: tMeasure,
297
+ layout: tLayout,
298
+ scroll: tScroll,
299
+ screenRect: tScreenRect,
300
+ notify: tNotify,
301
+ content: tContent,
302
+ output: tOutput,
303
+ total,
304
+ incremental: prevBuffer !== null,
305
+ }
306
+ ;(globalThis as any).__silvery_render_count = ((globalThis as any).__silvery_render_count ?? 0) + 1
307
+
308
+ return { output, buffer }
309
+ }
310
+
311
+ // ============================================================================
312
+ // Execute Render (Adapter-aware)
313
+ // ============================================================================
314
+
315
+ import { type RenderBuffer, getRenderAdapter, hasRenderAdapter } from "../render-adapter"
316
+
317
+ /**
318
+ * Execute the full render pipeline using the current RenderAdapter.
319
+ *
320
+ * This version works with any adapter (terminal, canvas, etc.) and returns
321
+ * a RenderBuffer instead of a TerminalBuffer.
322
+ *
323
+ * @param root The root SilveryNode
324
+ * @param width Width in adapter units (cells for terminal, pixels for canvas)
325
+ * @param height Height in adapter units
326
+ * @param prevBuffer Previous buffer for diffing (null on first render)
327
+ * @param options Render options
328
+ * @returns Object with output (if any) and current buffer
329
+ */
330
+ export function executeRenderAdapter(
331
+ root: TeaNode,
332
+ width: number,
333
+ height: number,
334
+ prevBuffer: RenderBuffer | null,
335
+ options: ExecuteRenderOptions | "fullscreen" | "inline" = "fullscreen",
336
+ ): { output: string | void; buffer: RenderBuffer } {
337
+ if (!hasRenderAdapter()) {
338
+ throw new Error("executeRenderAdapter called without a render adapter set")
339
+ }
340
+
341
+ const opts: ExecuteRenderOptions = typeof options === "string" ? { mode: options } : options
342
+ const { skipLayoutNotifications = false } = opts
343
+ const start = Date.now()
344
+ const adapter = getRenderAdapter()
345
+
346
+ using render = baseLog.span("pipeline-adapter", {
347
+ width,
348
+ height,
349
+ adapter: adapter.name,
350
+ })
351
+
352
+ // Clear per-render caches
353
+ clearBgConflictWarnings()
354
+
355
+ // Phase 1: Measure
356
+ {
357
+ using _measure = render.span("measure")
358
+ const t1 = Date.now()
359
+ measurePhase(root)
360
+ log.debug?.(`measure: ${Date.now() - t1}ms`)
361
+ }
362
+
363
+ // Phase 2: Layout
364
+ {
365
+ using _layout = render.span("layout")
366
+ const t2 = Date.now()
367
+ layoutPhase(root, width, height)
368
+ log.debug?.(`layout: ${Date.now() - t2}ms`)
369
+ }
370
+
371
+ // Phase 2.5: Scroll calculation
372
+ {
373
+ using _scroll = render.span("scroll")
374
+ scrollPhase(root)
375
+ }
376
+
377
+ // Phase 2.55: Sticky phase (non-scroll container sticky children)
378
+ stickyPhase(root)
379
+
380
+ // Phase 2.6: Screen rect calculation
381
+ {
382
+ using _screenRect = render.span("screenRect")
383
+ screenRectPhase(root)
384
+ }
385
+
386
+ // Phase 2.7: Notify layout subscribers
387
+ if (!skipLayoutNotifications) {
388
+ using _notify = render.span("notify")
389
+ notifyLayoutSubscribers(root)
390
+ }
391
+
392
+ // Phase 3: Content render (adapter-aware)
393
+ let buffer: RenderBuffer
394
+ {
395
+ using _content = render.span("content")
396
+ const t3 = Date.now()
397
+ buffer = contentPhaseAdapter(root)
398
+ log.debug?.(`content: ${Date.now() - t3}ms`)
399
+ }
400
+
401
+ // Phase 4: Flush via adapter
402
+ let output: string | void
403
+ {
404
+ using outputSpan = render.span("output")
405
+ const t4 = Date.now()
406
+ output = adapter.flush(buffer, prevBuffer)
407
+ if (typeof output === "string") {
408
+ outputSpan.spanData.bytes = output.length
409
+ }
410
+ log.debug?.(`output: ${Date.now() - t4}ms`)
411
+ }
412
+
413
+ log.debug?.(`total pipeline: ${Date.now() - start}ms`)
414
+
415
+ return { output, buffer }
416
+ }