@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,161 @@
1
+ /**
2
+ * Shared types for the Silvery render pipeline.
3
+ */
4
+
5
+ import type { Cell } from "../buffer"
6
+ import type { Measurer } from "../unicode"
7
+
8
+ /**
9
+ * Context threaded through the render pipeline.
10
+ *
11
+ * Carries per-render resources that were previously accessed via module-level
12
+ * globals (e.g., `_scopedMeasurer` + `runWithMeasurer()`). Threading context
13
+ * explicitly eliminates save/restore patterns and makes the pipeline pure.
14
+ *
15
+ * Phase 1: measurer only.
16
+ * Phase 2: NodeRenderState for per-node params.
17
+ * Phase 3: instrumentation/diagnostics fields (optional — fall back to
18
+ * module-level globals when absent for backward compat).
19
+ */
20
+ export interface PipelineContext {
21
+ readonly measurer: Measurer
22
+ // Phase 3: instrumentation (all optional for backward compat)
23
+ readonly instrumentEnabled?: boolean
24
+ readonly stats?: ContentPhaseStats
25
+ readonly nodeTrace?: NodeTraceEntry[]
26
+ readonly nodeTraceEnabled?: boolean
27
+ readonly bgConflictMode?: BgConflictMode
28
+ readonly warnedBgConflicts?: Set<string>
29
+ }
30
+
31
+ /**
32
+ * Background conflict detection mode.
33
+ * Set via SILVERY_BG_CONFLICT env var: 'ignore' | 'warn' | 'throw'
34
+ */
35
+ export type BgConflictMode = "ignore" | "warn" | "throw"
36
+
37
+ /**
38
+ * Per-node trace entry for SILVERY_STRICT diagnosis.
39
+ */
40
+ export interface NodeTraceEntry {
41
+ id: string
42
+ type: string
43
+ depth: number
44
+ rect: string
45
+ prevLayout: string
46
+ hasPrev: boolean
47
+ ancestorCleared: boolean
48
+ flags: string
49
+ decision: string
50
+ layoutChanged: boolean
51
+ contentAreaAffected?: boolean
52
+ parentRegionCleared?: boolean
53
+ parentRegionChanged?: boolean
54
+ childHasPrev?: boolean
55
+ childAncestorCleared?: boolean
56
+ skipBgFill?: boolean
57
+ bgColor?: string
58
+ }
59
+
60
+ /**
61
+ * Mutable stats counters for content phase instrumentation.
62
+ * Reset after each contentPhase call.
63
+ */
64
+ export interface ContentPhaseStats {
65
+ nodesVisited: number
66
+ nodesRendered: number
67
+ nodesSkipped: number
68
+ textNodes: number
69
+ boxNodes: number
70
+ clearOps: number
71
+ // Per-flag breakdown: why nodes weren't skipped
72
+ noPrevBuffer: number
73
+ flagContentDirty: number
74
+ flagPaintDirty: number
75
+ flagLayoutChanged: number
76
+ flagSubtreeDirty: number
77
+ flagChildrenDirty: number
78
+ flagChildPositionChanged: number
79
+ flagAncestorLayoutChanged: number
80
+ // Scroll container diagnostics
81
+ scrollContainerCount: number
82
+ scrollViewportCleared: number
83
+ scrollClearReason: string
84
+ // Normal container diagnostics
85
+ normalChildrenRepaint: number
86
+ normalRepaintReason: string
87
+ // Cascade diagnostics
88
+ cascadeMinDepth: number
89
+ cascadeNodes: string
90
+ // Top-level prevBuffer diagnostics
91
+ _prevBufferNull: number
92
+ _prevBufferDimMismatch: number
93
+ _hasPrevBuffer: number
94
+ _layoutW: number
95
+ _layoutH: number
96
+ _prevW: number
97
+ _prevH: number
98
+ _callCount: number
99
+ }
100
+
101
+ /**
102
+ * Clip bounds for viewport clipping.
103
+ */
104
+ export type ClipBounds = { top: number; bottom: number; left?: number; right?: number }
105
+
106
+ /**
107
+ * Per-node render state that changes at each tree level.
108
+ *
109
+ * Groups the parameters that vary per-node during tree traversal:
110
+ * - scrollOffset: accumulated scroll offset from scroll containers
111
+ * - clipBounds: viewport clipping rectangle (from overflow containers)
112
+ * - hasPrevBuffer: whether the buffer was cloned from a previous frame
113
+ * - ancestorCleared: whether an ancestor already cleared this node's region
114
+ *
115
+ * Contrast with frame-scoped params (buffer, ctx) which stay the same
116
+ * for the entire render pass.
117
+ */
118
+ export interface NodeRenderState {
119
+ scrollOffset: number
120
+ clipBounds?: ClipBounds
121
+ hasPrevBuffer: boolean
122
+ ancestorCleared: boolean
123
+ /** True when the buffer was cloned from prevBuffer (stale pixels exist).
124
+ * False when the buffer is a fresh TerminalBuffer (no stale pixels).
125
+ * Unlike hasPrevBuffer (which can be false per-node on a cloned buffer),
126
+ * this is constant for the entire render pass. Used to prevent clearExcessArea
127
+ * from writing inherited bg into a fresh buffer — no stale pixels to clear. */
128
+ bufferIsCloned: boolean
129
+ /** True when any ancestor had layoutChangedThisFrame = true.
130
+ * Propagated top-down to prevent descendants from being skipped when their
131
+ * own dirty flags are clean but their pixels are at wrong positions in the
132
+ * cloned buffer (because an ancestor moved/resized). Without this, the
133
+ * hasPrevBuffer cascade handles most cases, but this adds a direct safety
134
+ * net in the skip condition itself. */
135
+ ancestorLayoutChanged?: boolean
136
+ }
137
+
138
+ /**
139
+ * Cell change for diffing.
140
+ */
141
+ export interface CellChange {
142
+ x: number
143
+ y: number
144
+ cell: Cell
145
+ }
146
+
147
+ /**
148
+ * Border character sets.
149
+ */
150
+ export interface BorderChars {
151
+ topLeft: string
152
+ topRight: string
153
+ bottomLeft: string
154
+ bottomRight: string
155
+ horizontal: string
156
+ vertical: string
157
+ /** Bottom horizontal character. When absent, falls back to `horizontal`. */
158
+ bottomHorizontal?: string
159
+ /** Right vertical character. When absent, falls back to `vertical`. */
160
+ rightVertical?: string
161
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Silvery Render Pipeline
3
+ *
4
+ * Re-exports from the pipeline/ directory for backwards compatibility.
5
+ * See pipeline/index.ts for the main implementation.
6
+ */
7
+
8
+ export {
9
+ // Types
10
+ type CellChange,
11
+ type BorderChars,
12
+ type ExecuteRenderOptions,
13
+ type PipelineConfig,
14
+ // Phase functions
15
+ measurePhase,
16
+ layoutPhase,
17
+ rectEqual,
18
+ scrollPhase,
19
+ screenRectPhase,
20
+ contentPhase,
21
+ outputPhase,
22
+ // Utilities
23
+ clearBgConflictWarnings,
24
+ setBgConflictMode,
25
+ // Orchestration
26
+ executeRender,
27
+ executeRenderAdapter,
28
+ type PipelineContext,
29
+ } from "./pipeline/index"
@@ -0,0 +1,119 @@
1
+ /**
2
+ * CSI 14t/18t — Terminal Pixel and Text Area Size Queries
3
+ *
4
+ * Queries the terminal for window dimensions in pixels and characters.
5
+ *
6
+ * Protocols:
7
+ *
8
+ * Text Area Pixels (CSI 14t):
9
+ * Query: CSI 14 t
10
+ * Response: CSI 4 ; height ; width t
11
+ *
12
+ * Text Area Size in Characters (CSI 18t):
13
+ * Query: CSI 18 t
14
+ * Response: CSI 8 ; rows ; cols t
15
+ *
16
+ * Cell size can be derived by dividing pixel dimensions by character dimensions.
17
+ *
18
+ * Supported by: xterm, Ghostty, Kitty, WezTerm, foot, iTerm2
19
+ */
20
+
21
+ /** Regex for CSI 4 ; height ; width t (pixel size response) */
22
+ const PIXEL_RESPONSE_RE = /\x1b\[4;(\d+);(\d+)t/
23
+
24
+ /** Regex for CSI 8 ; rows ; cols t (text area size response) */
25
+ const TEXT_AREA_RESPONSE_RE = /\x1b\[8;(\d+);(\d+)t/
26
+
27
+ // ============================================================================
28
+ // Pixel Size Query
29
+ // ============================================================================
30
+
31
+ /**
32
+ * Query the terminal text area size in pixels.
33
+ *
34
+ * @param write Function to write to stdout
35
+ * @param read Function to read a chunk from stdin
36
+ * @param timeoutMs How long to wait for response (default: 200ms)
37
+ * @returns Width and height in pixels, or null on timeout/unsupported
38
+ */
39
+ export async function queryTextAreaPixels(
40
+ write: (data: string) => void,
41
+ read: (timeoutMs: number) => Promise<string | null>,
42
+ timeoutMs = 200,
43
+ ): Promise<{ width: number; height: number } | null> {
44
+ write("\x1b[14t")
45
+
46
+ const data = await read(timeoutMs)
47
+ if (data == null) return null
48
+
49
+ const match = PIXEL_RESPONSE_RE.exec(data)
50
+ if (!match) return null
51
+
52
+ return {
53
+ height: parseInt(match[1]!, 10),
54
+ width: parseInt(match[2]!, 10),
55
+ }
56
+ }
57
+
58
+ // ============================================================================
59
+ // Text Area Size Query (characters)
60
+ // ============================================================================
61
+
62
+ /**
63
+ * Query the terminal text area size in characters (rows x columns).
64
+ *
65
+ * @param write Function to write to stdout
66
+ * @param read Function to read a chunk from stdin
67
+ * @param timeoutMs How long to wait for response (default: 200ms)
68
+ * @returns Rows and columns, or null on timeout/unsupported
69
+ */
70
+ export async function queryTextAreaSize(
71
+ write: (data: string) => void,
72
+ read: (timeoutMs: number) => Promise<string | null>,
73
+ timeoutMs = 200,
74
+ ): Promise<{ cols: number; rows: number } | null> {
75
+ write("\x1b[18t")
76
+
77
+ const data = await read(timeoutMs)
78
+ if (data == null) return null
79
+
80
+ const match = TEXT_AREA_RESPONSE_RE.exec(data)
81
+ if (!match) return null
82
+
83
+ return {
84
+ rows: parseInt(match[1]!, 10),
85
+ cols: parseInt(match[2]!, 10),
86
+ }
87
+ }
88
+
89
+ // ============================================================================
90
+ // Cell Size (derived)
91
+ // ============================================================================
92
+
93
+ /**
94
+ * Query the terminal cell size in pixels by querying both pixel
95
+ * dimensions and character dimensions, then dividing.
96
+ *
97
+ * @param write Function to write to stdout
98
+ * @param read Function to read a chunk from stdin
99
+ * @param timeoutMs Per-query timeout (default: 200ms)
100
+ * @returns Cell width and height in pixels, or null if either query fails
101
+ */
102
+ export async function queryCellSize(
103
+ write: (data: string) => void,
104
+ read: (timeoutMs: number) => Promise<string | null>,
105
+ timeoutMs = 200,
106
+ ): Promise<{ width: number; height: number } | null> {
107
+ const pixels = await queryTextAreaPixels(write, read, timeoutMs)
108
+ if (pixels == null) return null
109
+
110
+ const size = await queryTextAreaSize(write, read, timeoutMs)
111
+ if (size == null) return null
112
+
113
+ if (size.cols === 0 || size.rows === 0) return null
114
+
115
+ return {
116
+ width: pixels.width / size.cols,
117
+ height: pixels.height / size.rows,
118
+ }
119
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Render Adapter Abstraction
3
+ *
4
+ * This module defines the interfaces that allow silvery to render to different
5
+ * targets (terminal, canvas, etc.) while keeping the core layout and
6
+ * reconciliation logic portable.
7
+ */
8
+
9
+ // ============================================================================
10
+ // Text Measurement
11
+ // ============================================================================
12
+
13
+ export interface TextMeasureStyle {
14
+ bold?: boolean
15
+ italic?: boolean
16
+ fontSize?: number
17
+ fontFamily?: string
18
+ }
19
+
20
+ export interface TextMeasureResult {
21
+ width: number
22
+ height: number
23
+ }
24
+
25
+ export interface TextMeasurer {
26
+ /**
27
+ * Measure text dimensions.
28
+ * Returns width in adapter units (cells for terminal, pixels for canvas).
29
+ */
30
+ measureText(text: string, style?: TextMeasureStyle): TextMeasureResult
31
+
32
+ /**
33
+ * Get the line height for the given style.
34
+ */
35
+ getLineHeight(style?: TextMeasureStyle): number
36
+ }
37
+
38
+ // ============================================================================
39
+ // Render Buffer
40
+ // ============================================================================
41
+
42
+ export interface RenderStyle {
43
+ fg?: string
44
+ bg?: string
45
+ attrs?: {
46
+ bold?: boolean
47
+ dim?: boolean
48
+ italic?: boolean
49
+ underline?: boolean
50
+ underlineStyle?: "single" | "double" | "curly" | "dotted" | "dashed"
51
+ underlineColor?: string
52
+ strikethrough?: boolean
53
+ inverse?: boolean
54
+ }
55
+ }
56
+
57
+ export interface RenderBuffer {
58
+ readonly width: number
59
+ readonly height: number
60
+
61
+ /**
62
+ * Fill a rectangle with a style.
63
+ */
64
+ fillRect(x: number, y: number, width: number, height: number, style: RenderStyle): void
65
+
66
+ /**
67
+ * Draw text at a position.
68
+ */
69
+ drawText(x: number, y: number, text: string, style: RenderStyle): void
70
+
71
+ /**
72
+ * Draw a single character at a position.
73
+ */
74
+ drawChar(x: number, y: number, char: string, style: RenderStyle): void
75
+
76
+ /**
77
+ * Check if coordinates are within bounds.
78
+ */
79
+ inBounds(x: number, y: number): boolean
80
+ }
81
+
82
+ // ============================================================================
83
+ // Border Characters
84
+ // ============================================================================
85
+
86
+ export interface BorderChars {
87
+ topLeft: string
88
+ topRight: string
89
+ bottomLeft: string
90
+ bottomRight: string
91
+ horizontal: string
92
+ vertical: string
93
+ }
94
+
95
+ // ============================================================================
96
+ // Render Adapter
97
+ // ============================================================================
98
+
99
+ export interface RenderAdapter {
100
+ /** Adapter name for debugging */
101
+ name: string
102
+
103
+ /** Text measurement for this adapter */
104
+ measurer: TextMeasurer
105
+
106
+ /**
107
+ * Create a buffer for rendering.
108
+ */
109
+ createBuffer(width: number, height: number): RenderBuffer
110
+
111
+ /**
112
+ * Flush the buffer to the output (terminal, canvas, etc.).
113
+ * For terminal: returns ANSI diff string.
114
+ * For canvas: draws directly, returns void.
115
+ */
116
+ flush(buffer: RenderBuffer, prevBuffer: RenderBuffer | null): string | void
117
+
118
+ /**
119
+ * Get border characters for the given style.
120
+ */
121
+ getBorderChars(style: string): BorderChars
122
+ }
123
+
124
+ // ============================================================================
125
+ // Global Adapter Management
126
+ // ============================================================================
127
+
128
+ // NOTE: currentAdapter is intentionally a module-level singleton, not threaded
129
+ // through PipelineContext. Unlike the width measurer (which varies per terminal
130
+ // and needs per-render scoping), the render adapter defines the render *target*
131
+ // (terminal vs canvas) and is set exactly once at startup. It never changes for
132
+ // the lifetime of the process. Threading it through every pipeline function
133
+ // would add significant plumbing for zero benefit -- there's no concurrency
134
+ // or per-instance variation to protect against.
135
+ let currentAdapter: RenderAdapter | null = null
136
+
137
+ /**
138
+ * Set the current render adapter.
139
+ */
140
+ export function setRenderAdapter(adapter: RenderAdapter): void {
141
+ currentAdapter = adapter
142
+ }
143
+
144
+ /**
145
+ * Get the current render adapter.
146
+ * Throws if no adapter is set.
147
+ */
148
+ export function getRenderAdapter(): RenderAdapter {
149
+ if (!currentAdapter) {
150
+ throw new Error("No render adapter set. Call setRenderAdapter() first.")
151
+ }
152
+ return currentAdapter
153
+ }
154
+
155
+ /**
156
+ * Check if a render adapter has been set.
157
+ */
158
+ export function hasRenderAdapter(): boolean {
159
+ return currentAdapter !== null
160
+ }
161
+
162
+ /**
163
+ * Get the text measurer from the current adapter.
164
+ */
165
+ export function getTextMeasurer(): TextMeasurer {
166
+ return getRenderAdapter().measurer
167
+ }
168
+
169
+ /**
170
+ * Ensure a render adapter is initialized.
171
+ * If no adapter is set, lazily imports and sets the terminal adapter.
172
+ */
173
+ export async function ensureRenderAdapterInitialized(): Promise<void> {
174
+ if (hasRenderAdapter()) return
175
+
176
+ // Lazy import to avoid circular dependencies
177
+ const { terminalAdapter } = await import("./adapters/terminal-adapter.js")
178
+ setRenderAdapter(terminalAdapter)
179
+ }