@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.
- package/package.json +54 -0
- package/src/adapters/canvas-adapter.ts +356 -0
- package/src/adapters/dom-adapter.ts +452 -0
- package/src/adapters/flexily-zero-adapter.ts +368 -0
- package/src/adapters/terminal-adapter.ts +305 -0
- package/src/adapters/yoga-adapter.ts +370 -0
- package/src/ansi/ansi.ts +251 -0
- package/src/ansi/constants.ts +76 -0
- package/src/ansi/detection.ts +441 -0
- package/src/ansi/hyperlink.ts +38 -0
- package/src/ansi/index.ts +201 -0
- package/src/ansi/patch-console.ts +159 -0
- package/src/ansi/sgr-codes.ts +34 -0
- package/src/ansi/storybook.ts +209 -0
- package/src/ansi/term.ts +724 -0
- package/src/ansi/types.ts +202 -0
- package/src/ansi/underline.ts +156 -0
- package/src/ansi/utils.ts +65 -0
- package/src/ansi-sanitize.ts +509 -0
- package/src/app.ts +571 -0
- package/src/bound-term.ts +94 -0
- package/src/bracketed-paste.ts +75 -0
- package/src/browser-renderer.ts +174 -0
- package/src/buffer.ts +1984 -0
- package/src/clipboard.ts +74 -0
- package/src/cursor-query.ts +85 -0
- package/src/device-attrs.ts +228 -0
- package/src/devtools.ts +123 -0
- package/src/dom/index.ts +194 -0
- package/src/errors.ts +39 -0
- package/src/focus-reporting.ts +48 -0
- package/src/hit-registry-core.ts +228 -0
- package/src/hit-registry.ts +176 -0
- package/src/index.ts +458 -0
- package/src/input.ts +119 -0
- package/src/inspector.ts +155 -0
- package/src/kitty-detect.ts +95 -0
- package/src/kitty-manager.ts +160 -0
- package/src/layout-engine.ts +296 -0
- package/src/layout.ts +26 -0
- package/src/measurer.ts +74 -0
- package/src/mode-query.ts +106 -0
- package/src/mouse-events.ts +419 -0
- package/src/mouse.ts +83 -0
- package/src/non-tty.ts +223 -0
- package/src/osc-markers.ts +32 -0
- package/src/osc-palette.ts +169 -0
- package/src/output.ts +406 -0
- package/src/pane-manager.ts +248 -0
- package/src/pipeline/CLAUDE.md +587 -0
- package/src/pipeline/content-phase-adapter.ts +976 -0
- package/src/pipeline/content-phase.ts +1765 -0
- package/src/pipeline/helpers.ts +42 -0
- package/src/pipeline/index.ts +416 -0
- package/src/pipeline/layout-phase.ts +686 -0
- package/src/pipeline/measure-phase.ts +198 -0
- package/src/pipeline/measure-stats.ts +21 -0
- package/src/pipeline/output-phase.ts +2593 -0
- package/src/pipeline/render-box.ts +343 -0
- package/src/pipeline/render-helpers.ts +243 -0
- package/src/pipeline/render-text.ts +1255 -0
- package/src/pipeline/types.ts +161 -0
- package/src/pipeline.ts +29 -0
- package/src/pixel-size.ts +119 -0
- package/src/render-adapter.ts +179 -0
- package/src/renderer.ts +1330 -0
- package/src/runtime/create-app.tsx +1845 -0
- package/src/runtime/create-buffer.ts +18 -0
- package/src/runtime/create-runtime.ts +325 -0
- package/src/runtime/diff.ts +56 -0
- package/src/runtime/event-handlers.ts +254 -0
- package/src/runtime/index.ts +119 -0
- package/src/runtime/keys.ts +8 -0
- package/src/runtime/layout.ts +164 -0
- package/src/runtime/run.tsx +318 -0
- package/src/runtime/term-provider.ts +399 -0
- package/src/runtime/terminal-lifecycle.ts +246 -0
- package/src/runtime/tick.ts +219 -0
- package/src/runtime/types.ts +210 -0
- package/src/scheduler.ts +723 -0
- package/src/screenshot.ts +57 -0
- package/src/scroll-region.ts +69 -0
- package/src/scroll-utils.ts +97 -0
- package/src/term-def.ts +267 -0
- package/src/terminal-caps.ts +5 -0
- package/src/terminal-colors.ts +216 -0
- package/src/termtest.ts +224 -0
- package/src/text-sizing.ts +109 -0
- package/src/toolbelt/index.ts +72 -0
- package/src/unicode.ts +1763 -0
- package/src/xterm/index.ts +491 -0
- package/src/xterm/xterm-provider.ts +204 -0
package/src/inspector.ts
ADDED
|
@@ -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
|
+
}
|