@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,155 @@
1
+ /**
2
+ * silvery Inspector — Debug introspection for rendering pipeline.
3
+ *
4
+ * Activate with SILVERY_DEV=1 env var or by calling enableInspector().
5
+ * Outputs debug info to stderr or a log file (never to the TUI stdout).
6
+ *
7
+ * Features:
8
+ * - Component tree dump (with layout rects)
9
+ * - Focus path display
10
+ * - Render stats (frame time, dirty rows, cell changes)
11
+ * - Dirty region visualization
12
+ *
13
+ * This is DISTINCT from React DevTools (devtools.ts). This inspector provides
14
+ * silvery-specific introspection: render pipeline stats, focus tree, dirty regions,
15
+ * layout info.
16
+ */
17
+
18
+ import type { createWriteStream as createWriteStreamType } from "node:fs"
19
+ import type { RenderStats } from "./scheduler"
20
+ import type { TeaNode } from "@silvery/tea/types"
21
+
22
+ // =============================================================================
23
+ // Types
24
+ // =============================================================================
25
+
26
+ export interface InspectorOptions {
27
+ /** Output stream (default: process.stderr) */
28
+ output?: NodeJS.WritableStream
29
+ /** Log file path (overrides output stream) */
30
+ logFile?: string
31
+ /** Include layout rects in tree dump */
32
+ showLayout?: boolean
33
+ /** Include style info in tree dump */
34
+ showStyles?: boolean
35
+ }
36
+
37
+ // =============================================================================
38
+ // State
39
+ // =============================================================================
40
+
41
+ let inspectorEnabled = false
42
+ let inspectorOutput: NodeJS.WritableStream = process.stderr
43
+
44
+ // =============================================================================
45
+ // Public API
46
+ // =============================================================================
47
+
48
+ /** Enable the silvery inspector. */
49
+ export function enableInspector(options?: InspectorOptions): void {
50
+ inspectorEnabled = true
51
+ if (options?.logFile) {
52
+ // Dynamic require to avoid pulling in fs for non-inspector users
53
+ const fs: { createWriteStream: typeof createWriteStreamType } = require("node:fs")
54
+ inspectorOutput = fs.createWriteStream(options.logFile, { flags: "a" })
55
+ } else if (options?.output) {
56
+ inspectorOutput = options.output
57
+ } else {
58
+ inspectorOutput = process.stderr
59
+ }
60
+ }
61
+
62
+ /** Disable the inspector. */
63
+ export function disableInspector(): void {
64
+ inspectorEnabled = false
65
+ }
66
+
67
+ /** Check if inspector is active. */
68
+ export function isInspectorEnabled(): boolean {
69
+ return inspectorEnabled
70
+ }
71
+
72
+ /**
73
+ * Log render stats after each frame.
74
+ *
75
+ * Called by the scheduler after executeRender completes. When the inspector
76
+ * is disabled this is a no-op (zero overhead).
77
+ */
78
+ export function inspectFrame(stats: RenderStats): void {
79
+ if (!inspectorEnabled) return
80
+ const line =
81
+ `[silvery] frame #${stats.renderCount} ` +
82
+ `${stats.lastRenderTime.toFixed(1)}ms ` +
83
+ `avg=${stats.avgRenderTime.toFixed(1)}ms ` +
84
+ `skipped=${stats.skippedCount}\n`
85
+ inspectorOutput.write(line)
86
+ }
87
+
88
+ /**
89
+ * Dump the component tree structure as indented text.
90
+ *
91
+ * Walks the SilveryNode tree and formats each node with its type, testID,
92
+ * layout rect, and dirty flags.
93
+ */
94
+ export function inspectTree(rootNode: TeaNode, options?: { depth?: number; showLayout?: boolean }): string {
95
+ const maxDepth = options?.depth ?? 10
96
+ const showLayout = options?.showLayout ?? true
97
+ const lines: string[] = []
98
+
99
+ function walk(node: TeaNode, indent: number): void {
100
+ if (indent > maxDepth) return
101
+
102
+ const prefix = " ".repeat(indent)
103
+ const type = node.type
104
+ const testID = (node.props as Record<string, unknown>)?.testID
105
+ const idStr = testID ? ` #${testID}` : ""
106
+
107
+ // Layout rect from computed layout node or contentRect
108
+ let rectStr = ""
109
+ if (showLayout) {
110
+ if (node.contentRect) {
111
+ const r = node.contentRect
112
+ rectStr = ` [${r.x},${r.y} ${r.width}x${r.height}]`
113
+ } else if (node.layoutNode) {
114
+ const ln = node.layoutNode
115
+ rectStr = ` [${ln.getComputedLeft()},${ln.getComputedTop()} ${ln.getComputedWidth()}x${ln.getComputedHeight()}]`
116
+ }
117
+ }
118
+
119
+ // Dirty flags
120
+ const dirtyFlags: string[] = []
121
+ if (node.layoutDirty) dirtyFlags.push("layout")
122
+ if (node.contentDirty) dirtyFlags.push("content")
123
+ if (node.paintDirty) dirtyFlags.push("paint")
124
+ if (node.bgDirty) dirtyFlags.push("bg")
125
+ if (node.subtreeDirty) dirtyFlags.push("subtree")
126
+ if (node.childrenDirty) dirtyFlags.push("children")
127
+ const dirtyStr = dirtyFlags.length > 0 ? ` dirty=[${dirtyFlags.join(",")}]` : ""
128
+
129
+ // Text content (for text nodes)
130
+ const textStr = node.textContent
131
+ ? ` "${node.textContent.slice(0, 30)}${node.textContent.length > 30 ? "..." : ""}"`
132
+ : ""
133
+
134
+ lines.push(`${prefix}${type}${idStr}${rectStr}${dirtyStr}${textStr}`)
135
+
136
+ for (const child of node.children) {
137
+ walk(child, indent + 1)
138
+ }
139
+ }
140
+
141
+ walk(rootNode, 0)
142
+ return lines.join("\n")
143
+ }
144
+
145
+ /**
146
+ * Auto-enable if SILVERY_DEV=1 is set.
147
+ *
148
+ * Call this at startup to respect the environment variable convention.
149
+ */
150
+ export function autoEnableInspector(): void {
151
+ if (process.env.SILVERY_DEV === "1" || process.env.SILVERY_DEV === "true") {
152
+ const logFile = process.env.SILVERY_DEV_LOG
153
+ enableInspector(logFile ? { logFile } : undefined)
154
+ }
155
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Kitty keyboard protocol detection.
3
+ *
4
+ * Sends CSI ? u and parses the response to determine whether the terminal
5
+ * supports the Kitty keyboard protocol and which flags it reports.
6
+ */
7
+
8
+ import { queryKittyKeyboard } from "./output"
9
+
10
+ export interface KittyDetectResult {
11
+ /** Whether the terminal responded to the Kitty protocol query */
12
+ supported: boolean
13
+ /** Bitfield of KittyFlags the terminal reported supporting (0 if unsupported) */
14
+ flags: number
15
+ /** Any non-response data that was read during detection (regular input that arrived) */
16
+ buffered?: string
17
+ }
18
+
19
+ /** Regex to match a Kitty keyboard query response: CSI ? <flags> u */
20
+ const KITTY_RESPONSE_RE = /\x1b\[\?(\d+)u/
21
+
22
+ /**
23
+ * Detect Kitty keyboard protocol support.
24
+ *
25
+ * Sends CSI ? u to the terminal and waits for a response.
26
+ * Supported terminals respond with CSI ? flags u.
27
+ * Unsupported terminals either ignore the query or echo it.
28
+ *
29
+ * @param write Function to write to stdout
30
+ * @param read Function to read a chunk from stdin (should resolve with data or null on timeout)
31
+ * @param timeoutMs How long to wait for response (default: 200ms)
32
+ */
33
+ export async function detectKittySupport(
34
+ write: (data: string) => void,
35
+ read: (timeoutMs: number) => Promise<string | null>,
36
+ timeoutMs = 200,
37
+ ): Promise<KittyDetectResult> {
38
+ write(queryKittyKeyboard())
39
+
40
+ const data = await read(timeoutMs)
41
+ if (data == null) {
42
+ return { supported: false, flags: 0 }
43
+ }
44
+
45
+ const match = KITTY_RESPONSE_RE.exec(data)
46
+ if (!match) {
47
+ return { supported: false, flags: 0, buffered: data }
48
+ }
49
+
50
+ const flags = parseInt(match[1]!, 10)
51
+ // Anything outside the matched response is buffered input
52
+ const before = data.slice(0, match.index)
53
+ const after = data.slice(match.index + match[0].length)
54
+ const buffered = before + after
55
+ return { supported: true, flags, buffered: buffered || undefined }
56
+ }
57
+
58
+ /**
59
+ * Detect Kitty support using real stdin/stdout.
60
+ * Convenience wrapper around detectKittySupport.
61
+ */
62
+ export async function detectKittyFromStdio(
63
+ stdout: { write: (s: string) => boolean | void },
64
+ stdin: NodeJS.ReadStream,
65
+ timeoutMs = 200,
66
+ ): Promise<KittyDetectResult> {
67
+ const wasRaw = stdin.isRaw
68
+ if (!wasRaw) stdin.setRawMode(true)
69
+
70
+ try {
71
+ const write = (s: string) => {
72
+ stdout.write(s)
73
+ }
74
+
75
+ const read = (ms: number): Promise<string | null> =>
76
+ new Promise((resolve) => {
77
+ const timer = setTimeout(() => {
78
+ stdin.removeListener("data", onData)
79
+ resolve(null)
80
+ }, ms)
81
+
82
+ function onData(chunk: Buffer) {
83
+ clearTimeout(timer)
84
+ stdin.removeListener("data", onData)
85
+ resolve(chunk.toString())
86
+ }
87
+
88
+ stdin.on("data", onData)
89
+ })
90
+
91
+ return await detectKittySupport(write, read, timeoutMs)
92
+ } finally {
93
+ if (!wasRaw) stdin.setRawMode(false)
94
+ }
95
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Kitty keyboard protocol manager.
3
+ *
4
+ * Handles lifecycle (enable/disable/auto-detect) for the Kitty keyboard
5
+ * protocol. Used by both test and interactive rendering paths.
6
+ *
7
+ * @see https://sw.kovidgoyal.net/kitty/keyboard-protocol/
8
+ */
9
+
10
+ import { enableKittyKeyboard, disableKittyKeyboard, queryKittyKeyboard } from "./output"
11
+
12
+ /** Regex to match a Kitty keyboard query response: CSI ? <digits> u */
13
+ const KITTY_RESPONSE_RE = /\x1b\[\?(\d+)u/
14
+
15
+ /** Regex to match a partial Kitty keyboard query response: ESC [ ? <digits> (at least one digit, no trailing 'u') */
16
+ const KITTY_PARTIAL_RE = /\x1b\[\?\d+$/
17
+
18
+ /** Kitty protocol manager handle. */
19
+ export interface KittyManager {
20
+ /** Whether the kitty keyboard protocol is currently enabled. */
21
+ enabled: boolean
22
+ /** Disable the protocol and clean up any pending detection. */
23
+ cleanup(): void
24
+ }
25
+
26
+ /** Options for configuring the kitty keyboard protocol manager. */
27
+ export interface KittyManagerOptions {
28
+ /** Detection mode: "enabled" activates immediately, "auto" probes the terminal, "disabled" does nothing. */
29
+ mode?: "auto" | "enabled" | "disabled"
30
+ /** Bitmask of KittyFlags to enable. Defaults to KittyFlags.DISAMBIGUATE (1). */
31
+ flags?: number
32
+ }
33
+
34
+ /**
35
+ * Create a kitty protocol manager that handles setup and teardown.
36
+ *
37
+ * Supports three modes:
38
+ * - "enabled": enable immediately if stdin/stdout are TTYs
39
+ * - "auto": probe the terminal for support, enable if detected
40
+ * - "disabled" / undefined: do nothing
41
+ */
42
+ export function createKittyManager(
43
+ stdin: NodeJS.ReadStream,
44
+ stdout: NodeJS.WriteStream,
45
+ opts: KittyManagerOptions | undefined,
46
+ ): KittyManager {
47
+ let enabled = false
48
+ let cancelDetection: (() => void) | undefined
49
+
50
+ function enable(flagBitmask: number): void {
51
+ stdout.write(enableKittyKeyboard(flagBitmask))
52
+ enabled = true
53
+ }
54
+
55
+ if (opts) {
56
+ const mode = opts.mode ?? "auto"
57
+ const flagBitmask = opts.flags ?? 1 // Default: DISAMBIGUATE
58
+ const isTTY = (stdin as any)?.isTTY && (stdout as any)?.isTTY
59
+
60
+ if (isTTY) {
61
+ if (mode === "enabled") {
62
+ enable(flagBitmask)
63
+ } else if (mode === "auto") {
64
+ cancelDetection = initKittyAutoDetection(stdin, stdout, flagBitmask, enable)
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ get enabled() {
71
+ return enabled
72
+ },
73
+ cleanup() {
74
+ if (cancelDetection) {
75
+ cancelDetection()
76
+ cancelDetection = undefined
77
+ }
78
+ if (enabled) {
79
+ stdout.write(disableKittyKeyboard())
80
+ enabled = false
81
+ }
82
+ },
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Initialize kitty keyboard auto-detection.
88
+ *
89
+ * Queries the terminal for support, listens for the response, and enables
90
+ * the protocol if supported. Returns a cleanup function to cancel detection.
91
+ *
92
+ * Uses a synchronous event-based approach (not async) because render() must
93
+ * return synchronously. Delegates to @silvery/term for escape sequences.
94
+ */
95
+ function initKittyAutoDetection(
96
+ stdin: NodeJS.ReadStream,
97
+ stdout: NodeJS.WriteStream,
98
+ flagBitmask: number,
99
+ onEnable: (flags: number) => void,
100
+ ): () => void {
101
+ // Buffer incoming data as raw bytes to preserve binary integrity (e.g., split UTF-8 sequences).
102
+ // We always work with the concatenated raw bytes and only decode to string for regex matching.
103
+ const rawChunks: Buffer[] = []
104
+ let cleaned = false
105
+ let unmounted = false
106
+
107
+ /** Decode the full concatenated buffer to string for regex matching. */
108
+ function getBufferAsString(): string {
109
+ return Buffer.concat(rawChunks).toString()
110
+ }
111
+
112
+ const cleanup = (): void => {
113
+ if (cleaned) return
114
+ cleaned = true
115
+ clearTimeout(timer)
116
+ stdin.removeListener("data", onData)
117
+
118
+ // Re-emit any buffered data that wasn't the protocol response.
119
+ // Strip both complete protocol responses and partial protocol prefixes
120
+ // (e.g., "\x1b[?1" without the trailing "u") — these are protocol artifacts, not user data.
121
+ const allBytes = Buffer.concat(rawChunks)
122
+ rawChunks.length = 0
123
+ const fullString = allBytes.toString()
124
+ let remaining = fullString.replace(KITTY_RESPONSE_RE, "")
125
+ remaining = remaining.replace(KITTY_PARTIAL_RE, "")
126
+
127
+ if (remaining.length > 0) {
128
+ // Find where the remaining content starts in the original byte stream
129
+ // by computing the byte offset of the protocol prefix that was stripped.
130
+ const protocolPrefix = fullString.slice(0, fullString.indexOf(remaining))
131
+ const prefixByteLen = Buffer.byteLength(protocolPrefix)
132
+ stdin.unshift(allBytes.subarray(prefixByteLen))
133
+ }
134
+ }
135
+
136
+ const onData = (data: Uint8Array | string): void => {
137
+ // Buffer raw bytes. For strings, convert to Buffer to preserve byte-level integrity.
138
+ rawChunks.push(typeof data === "string" ? Buffer.from(data) : Buffer.from(data))
139
+
140
+ // Decode the full accumulated buffer to check for the protocol response.
141
+ // This ensures correct handling of multi-byte sequences split across chunks.
142
+ if (KITTY_RESPONSE_RE.test(getBufferAsString())) {
143
+ cleanup()
144
+ if (!unmounted) {
145
+ onEnable(flagBitmask)
146
+ }
147
+ }
148
+ }
149
+
150
+ // Attach listener before writing the query so synchronous responses are not missed
151
+ stdin.on("data", onData)
152
+ const timer = setTimeout(cleanup, 200)
153
+
154
+ stdout.write(queryKittyKeyboard())
155
+
156
+ return () => {
157
+ unmounted = true
158
+ cleanup()
159
+ }
160
+ }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Layout Engine Abstraction
3
+ *
4
+ * Provides a pluggable interface for layout engines (Yoga, Flexily, etc.)
5
+ * This allows silvery to use different layout backends without code changes.
6
+ */
7
+
8
+ // ============================================================================
9
+ // Measure Function Types
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Measure mode determines how the width/height constraint should be interpreted.
14
+ */
15
+ export type MeasureMode = "undefined" | "exactly" | "at-most"
16
+
17
+ /**
18
+ * Measure function callback for intrinsic sizing.
19
+ * Called when a node needs to determine its size based on content.
20
+ */
21
+ export type MeasureFunc = (
22
+ width: number,
23
+ widthMode: MeasureMode,
24
+ height: number,
25
+ heightMode: MeasureMode,
26
+ ) => { width: number; height: number }
27
+
28
+ // ============================================================================
29
+ // Layout Node Interface
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Abstract layout node interface.
34
+ * Represents a single node in the layout tree.
35
+ */
36
+ export interface LayoutNode {
37
+ // Tree operations
38
+ insertChild(child: LayoutNode, index: number): void
39
+ removeChild(child: LayoutNode): void
40
+ free(): void
41
+
42
+ // Measure function
43
+ setMeasureFunc(measureFunc: MeasureFunc): void
44
+
45
+ // Dirty tracking
46
+ markDirty(): void
47
+
48
+ // Dimension setters
49
+ setWidth(value: number): void
50
+ setWidthPercent(value: number): void
51
+ setWidthAuto(): void
52
+ setHeight(value: number): void
53
+ setHeightPercent(value: number): void
54
+ setHeightAuto(): void
55
+ setMinWidth(value: number): void
56
+ setMinWidthPercent(value: number): void
57
+ setMinHeight(value: number): void
58
+ setMinHeightPercent(value: number): void
59
+ setMaxWidth(value: number): void
60
+ setMaxWidthPercent(value: number): void
61
+ setMaxHeight(value: number): void
62
+ setMaxHeightPercent(value: number): void
63
+
64
+ // Flex properties
65
+ setFlexGrow(value: number): void
66
+ setFlexShrink(value: number): void
67
+ setFlexBasis(value: number): void
68
+ setFlexBasisPercent(value: number): void
69
+ setFlexBasisAuto(): void
70
+ setFlexDirection(direction: number): void
71
+ setFlexWrap(wrap: number): void
72
+
73
+ // Alignment
74
+ setAlignItems(align: number): void
75
+ setAlignSelf(align: number): void
76
+ setAlignContent(align: number): void
77
+ setJustifyContent(justify: number): void
78
+
79
+ // Spacing
80
+ setPadding(edge: number, value: number): void
81
+ setMargin(edge: number, value: number): void
82
+ setBorder(edge: number, value: number): void
83
+ setGap(gutter: number, value: number): void
84
+
85
+ // Display & Position
86
+ setDisplay(display: number): void
87
+ setPositionType(positionType: number): void
88
+ setPosition(edge: number, value: number): void
89
+ setPositionPercent(edge: number, value: number): void
90
+ setOverflow(overflow: number): void
91
+
92
+ // Aspect Ratio
93
+ setAspectRatio(value: number): void
94
+
95
+ // Layout calculation
96
+ calculateLayout(width: number, height: number, direction?: number): void
97
+
98
+ // Layout results
99
+ getComputedLeft(): number
100
+ getComputedTop(): number
101
+ getComputedWidth(): number
102
+ getComputedHeight(): number
103
+ }
104
+
105
+ // ============================================================================
106
+ // Branded Types for Type Safety
107
+ // ============================================================================
108
+
109
+ /**
110
+ * Branded types prevent accidentally mixing up layout constant categories.
111
+ * E.g., you can't pass an AlignValue where a FlexDirectionValue is expected.
112
+ */
113
+ export type FlexDirectionValue = number & { readonly __brand: "FlexDirection" }
114
+ export type WrapValue = number & { readonly __brand: "Wrap" }
115
+ export type AlignValue = number & { readonly __brand: "Align" }
116
+ export type JustifyValue = number & { readonly __brand: "Justify" }
117
+ export type EdgeValue = number & { readonly __brand: "Edge" }
118
+ export type GutterValue = number & { readonly __brand: "Gutter" }
119
+ export type DisplayValue = number & { readonly __brand: "Display" }
120
+ export type PositionTypeValue = number & { readonly __brand: "PositionType" }
121
+ export type OverflowValue = number & { readonly __brand: "Overflow" }
122
+ export type DirectionValue = number & { readonly __brand: "Direction" }
123
+ export type MeasureModeValue = number & { readonly __brand: "MeasureMode" }
124
+
125
+ // ============================================================================
126
+ // Layout Constants Interface
127
+ // ============================================================================
128
+
129
+ /**
130
+ * Constants for layout configuration.
131
+ * These are the same across Yoga and Flexily.
132
+ * Uses branded types for compile-time safety.
133
+ */
134
+ export interface LayoutConstants {
135
+ // Flex Direction
136
+ FLEX_DIRECTION_COLUMN: FlexDirectionValue
137
+ FLEX_DIRECTION_COLUMN_REVERSE: FlexDirectionValue
138
+ FLEX_DIRECTION_ROW: FlexDirectionValue
139
+ FLEX_DIRECTION_ROW_REVERSE: FlexDirectionValue
140
+
141
+ // Wrap
142
+ WRAP_NO_WRAP: WrapValue
143
+ WRAP_WRAP: WrapValue
144
+ WRAP_WRAP_REVERSE: WrapValue
145
+
146
+ // Align
147
+ ALIGN_AUTO: AlignValue
148
+ ALIGN_FLEX_START: AlignValue
149
+ ALIGN_CENTER: AlignValue
150
+ ALIGN_FLEX_END: AlignValue
151
+ ALIGN_STRETCH: AlignValue
152
+ ALIGN_BASELINE: AlignValue
153
+ ALIGN_SPACE_BETWEEN: AlignValue
154
+ ALIGN_SPACE_AROUND: AlignValue
155
+ ALIGN_SPACE_EVENLY: AlignValue
156
+
157
+ // Justify
158
+ JUSTIFY_FLEX_START: JustifyValue
159
+ JUSTIFY_CENTER: JustifyValue
160
+ JUSTIFY_FLEX_END: JustifyValue
161
+ JUSTIFY_SPACE_BETWEEN: JustifyValue
162
+ JUSTIFY_SPACE_AROUND: JustifyValue
163
+ JUSTIFY_SPACE_EVENLY: JustifyValue
164
+
165
+ // Edge
166
+ EDGE_LEFT: EdgeValue
167
+ EDGE_TOP: EdgeValue
168
+ EDGE_RIGHT: EdgeValue
169
+ EDGE_BOTTOM: EdgeValue
170
+ EDGE_HORIZONTAL: EdgeValue
171
+ EDGE_VERTICAL: EdgeValue
172
+ EDGE_ALL: EdgeValue
173
+
174
+ // Gutter
175
+ GUTTER_COLUMN: GutterValue
176
+ GUTTER_ROW: GutterValue
177
+ GUTTER_ALL: GutterValue
178
+
179
+ // Display
180
+ DISPLAY_FLEX: DisplayValue
181
+ DISPLAY_NONE: DisplayValue
182
+
183
+ // Position Type
184
+ POSITION_TYPE_STATIC: PositionTypeValue
185
+ POSITION_TYPE_RELATIVE: PositionTypeValue
186
+ POSITION_TYPE_ABSOLUTE: PositionTypeValue
187
+
188
+ // Overflow
189
+ OVERFLOW_VISIBLE: OverflowValue
190
+ OVERFLOW_HIDDEN: OverflowValue
191
+ OVERFLOW_SCROLL: OverflowValue
192
+
193
+ // Direction
194
+ DIRECTION_LTR: DirectionValue
195
+
196
+ // Measure Mode
197
+ MEASURE_MODE_UNDEFINED: MeasureModeValue
198
+ MEASURE_MODE_EXACTLY: MeasureModeValue
199
+ MEASURE_MODE_AT_MOST: MeasureModeValue
200
+ }
201
+
202
+ // ============================================================================
203
+ // Layout Engine Interface
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Abstract layout engine interface.
208
+ * Implementations can wrap Yoga, Flexily, or other layout engines.
209
+ */
210
+ export interface LayoutEngine {
211
+ /** Create a new layout node */
212
+ createNode(): LayoutNode
213
+
214
+ /** Layout constants for this engine */
215
+ readonly constants: LayoutConstants
216
+
217
+ /** Engine name for debugging */
218
+ readonly name: string
219
+ }
220
+
221
+ // ============================================================================
222
+ // Global Layout Engine Management
223
+ // ============================================================================
224
+
225
+ let layoutEngine: LayoutEngine | null = null
226
+
227
+ /**
228
+ * Set the global layout engine instance.
229
+ * Must be called before rendering.
230
+ */
231
+ export function setLayoutEngine(engine: LayoutEngine): void {
232
+ layoutEngine = engine
233
+ }
234
+
235
+ /**
236
+ * Get the global layout engine instance.
237
+ * Throws if not initialized.
238
+ */
239
+ export function getLayoutEngine(): LayoutEngine {
240
+ if (!layoutEngine) {
241
+ throw new Error("Layout engine not initialized. Call setLayoutEngine() or initYoga()/initFlexily() first.")
242
+ }
243
+ return layoutEngine
244
+ }
245
+
246
+ /**
247
+ * Check if a layout engine is initialized.
248
+ */
249
+ export function isLayoutEngineInitialized(): boolean {
250
+ return layoutEngine !== null
251
+ }
252
+
253
+ /**
254
+ * Get the layout constants from the current engine.
255
+ * Convenience function for accessing constants.
256
+ */
257
+ export function getConstants(): LayoutConstants {
258
+ return getLayoutEngine().constants
259
+ }
260
+
261
+ // ============================================================================
262
+ // Default Engine Initialization
263
+ // ============================================================================
264
+
265
+ /**
266
+ * Layout engine type for configuration.
267
+ *
268
+ * - 'flexily': Zero-allocation Flexily (default, optimized for high-frequency layout)
269
+ * - 'flexily-classic': Classic Flexily algorithm (for debugging/compatibility)
270
+ * - 'yoga': Facebook's WASM-based flexbox (most mature)
271
+ */
272
+ export type LayoutEngineType = "flexily" | "yoga"
273
+
274
+ /**
275
+ * Initialize the layout engine if not already set.
276
+ *
277
+ * @param engineType - 'flexily', 'flexily-classic', or 'yoga'. If not provided, checks
278
+ * SILVERY_ENGINE env var, then defaults to 'flexily'.
279
+ */
280
+ export async function ensureDefaultLayoutEngine(engineType?: LayoutEngineType): Promise<void> {
281
+ if (isLayoutEngineInitialized()) {
282
+ return
283
+ }
284
+
285
+ // Resolve engine type: option → env → 'flexily'
286
+ const resolved = engineType ?? (process.env.SILVERY_ENGINE?.toLowerCase() as LayoutEngineType) ?? "flexily"
287
+
288
+ if (resolved === "yoga") {
289
+ const { initYogaEngine } = await import("./adapters/yoga-adapter.js")
290
+ setLayoutEngine(await initYogaEngine())
291
+ } else {
292
+ // 'flexily' (default) uses zero-allocation engine
293
+ const { createFlexilyZeroEngine } = await import("./adapters/flexily-zero-adapter.js")
294
+ setLayoutEngine(createFlexilyZeroEngine())
295
+ }
296
+ }