@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/clipboard.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSC 52 Clipboard Support
|
|
3
|
+
*
|
|
4
|
+
* Provides clipboard access via the OSC 52 terminal protocol.
|
|
5
|
+
* This works across SSH sessions and in terminals that support it.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: OSC 52
|
|
8
|
+
* - Copy: ESC ] 52 ; c ; <base64> BEL
|
|
9
|
+
* - Query: ESC ] 52 ; c ; ? BEL
|
|
10
|
+
* - Response: ESC ] 52 ; c ; <base64> BEL (or ST terminator)
|
|
11
|
+
*
|
|
12
|
+
* Supported by: Ghostty, Kitty, WezTerm, iTerm2, xterm, foot, tmux
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ESC = "\x1b"
|
|
16
|
+
const BEL = "\x07"
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Clipboard Operations
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Copy text to the system clipboard via OSC 52.
|
|
24
|
+
* Encodes the text as base64 and writes the OSC 52 sequence to stdout.
|
|
25
|
+
*/
|
|
26
|
+
export function copyToClipboard(stdout: NodeJS.WriteStream, text: string): void {
|
|
27
|
+
const base64 = Buffer.from(text).toString("base64")
|
|
28
|
+
stdout.write(`${ESC}]52;c;${base64}${BEL}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Request clipboard contents via OSC 52.
|
|
33
|
+
* Writes the OSC 52 query sequence. The terminal will respond with
|
|
34
|
+
* an OSC 52 response containing the clipboard contents as base64.
|
|
35
|
+
* Use parseClipboardResponse() to decode the response.
|
|
36
|
+
*/
|
|
37
|
+
export function requestClipboard(stdout: NodeJS.WriteStream): void {
|
|
38
|
+
stdout.write(`${ESC}]52;c;?${BEL}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Response Parsing
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/** OSC 52 response prefix */
|
|
46
|
+
const OSC52_PREFIX = `${ESC}]52;c;`
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse an OSC 52 clipboard response and decode the base64 content.
|
|
50
|
+
*
|
|
51
|
+
* Returns the decoded clipboard text, or null if the input is not
|
|
52
|
+
* an OSC 52 clipboard response.
|
|
53
|
+
*
|
|
54
|
+
* Handles both BEL (\x07) and ST (ESC \) terminators.
|
|
55
|
+
*/
|
|
56
|
+
export function parseClipboardResponse(input: string): string | null {
|
|
57
|
+
const prefixIdx = input.indexOf(OSC52_PREFIX)
|
|
58
|
+
if (prefixIdx === -1) return null
|
|
59
|
+
|
|
60
|
+
const contentStart = prefixIdx + OSC52_PREFIX.length
|
|
61
|
+
|
|
62
|
+
// Reject the query marker — it's not a response
|
|
63
|
+
if (input[contentStart] === "?") return null
|
|
64
|
+
|
|
65
|
+
// Find terminator: BEL (\x07) or ST (ESC \)
|
|
66
|
+
let contentEnd = input.indexOf(BEL, contentStart)
|
|
67
|
+
if (contentEnd === -1) {
|
|
68
|
+
contentEnd = input.indexOf(`${ESC}\\`, contentStart)
|
|
69
|
+
}
|
|
70
|
+
if (contentEnd === -1) return null
|
|
71
|
+
|
|
72
|
+
const base64 = input.slice(contentStart, contentEnd)
|
|
73
|
+
return Buffer.from(base64, "base64").toString("utf-8")
|
|
74
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSI 6n Cursor Position Query
|
|
3
|
+
*
|
|
4
|
+
* Queries the terminal for the current cursor position using the standard
|
|
5
|
+
* Device Status Report (DSR) mechanism.
|
|
6
|
+
*
|
|
7
|
+
* Protocol:
|
|
8
|
+
* - Query: CSI 6 n (\x1b[6n)
|
|
9
|
+
* - Response: CSI {row} ; {col} R (\x1b[{row};{col}R)
|
|
10
|
+
*
|
|
11
|
+
* Row and column are 1-indexed in the protocol response.
|
|
12
|
+
*
|
|
13
|
+
* Supported by: virtually all terminals (VT100+)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Regex to match a CPR response: CSI row ; col R */
|
|
17
|
+
const CPR_RESPONSE_RE = /\x1b\[(\d+);(\d+)R/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Query the terminal cursor position.
|
|
21
|
+
*
|
|
22
|
+
* Sends CSI 6n and parses the CPR response.
|
|
23
|
+
* Returns 1-indexed row and column.
|
|
24
|
+
*
|
|
25
|
+
* @param write Function to write to stdout
|
|
26
|
+
* @param read Function to read a chunk from stdin (resolves with data or null on timeout)
|
|
27
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
28
|
+
*/
|
|
29
|
+
export async function queryCursorPosition(
|
|
30
|
+
write: (data: string) => void,
|
|
31
|
+
read: (timeoutMs: number) => Promise<string | null>,
|
|
32
|
+
timeoutMs = 200,
|
|
33
|
+
): Promise<{ row: number; col: number } | null> {
|
|
34
|
+
write("\x1b[6n")
|
|
35
|
+
|
|
36
|
+
const data = await read(timeoutMs)
|
|
37
|
+
if (data == null) return null
|
|
38
|
+
|
|
39
|
+
const match = CPR_RESPONSE_RE.exec(data)
|
|
40
|
+
if (!match) return null
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
row: parseInt(match[1]!, 10),
|
|
44
|
+
col: parseInt(match[2]!, 10),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Query cursor position using real stdin/stdout.
|
|
50
|
+
* Convenience wrapper around queryCursorPosition.
|
|
51
|
+
*/
|
|
52
|
+
export async function queryCursorFromStdio(
|
|
53
|
+
stdout: { write: (s: string) => boolean | void },
|
|
54
|
+
stdin: NodeJS.ReadStream,
|
|
55
|
+
timeoutMs = 200,
|
|
56
|
+
): Promise<{ row: number; col: number } | null> {
|
|
57
|
+
const wasRaw = stdin.isRaw
|
|
58
|
+
if (!wasRaw) stdin.setRawMode(true)
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const write = (s: string) => {
|
|
62
|
+
stdout.write(s)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const read = (ms: number): Promise<string | null> =>
|
|
66
|
+
new Promise((resolve) => {
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
stdin.removeListener("data", onData)
|
|
69
|
+
resolve(null)
|
|
70
|
+
}, ms)
|
|
71
|
+
|
|
72
|
+
function onData(chunk: Buffer) {
|
|
73
|
+
clearTimeout(timer)
|
|
74
|
+
stdin.removeListener("data", onData)
|
|
75
|
+
resolve(chunk.toString())
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stdin.on("data", onData)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return await queryCursorPosition(write, read, timeoutMs)
|
|
82
|
+
} finally {
|
|
83
|
+
if (!wasRaw) stdin.setRawMode(false)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Attributes (DA1/DA2/DA3) + XTVERSION Queries
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to query terminal identity and capabilities using
|
|
5
|
+
* the standard VT device attribute escape sequences.
|
|
6
|
+
*
|
|
7
|
+
* Protocols:
|
|
8
|
+
*
|
|
9
|
+
* DA1 (Primary Device Attributes):
|
|
10
|
+
* Query: CSI c (\x1b[c)
|
|
11
|
+
* Response: CSI ? Ps ; Ps ; ... c
|
|
12
|
+
*
|
|
13
|
+
* DA2 (Secondary Device Attributes):
|
|
14
|
+
* Query: CSI > c (\x1b[>c)
|
|
15
|
+
* Response: CSI > Pt ; Pv ; Pc c
|
|
16
|
+
* Where Pt=terminal type, Pv=firmware version, Pc=ROM cartridge id
|
|
17
|
+
*
|
|
18
|
+
* DA3 (Tertiary Device Attributes):
|
|
19
|
+
* Query: CSI = c (\x1b[=c)
|
|
20
|
+
* Response: DCS ! | hex-encoded-id ST (\x1bP!|{hex}\x1b\\)
|
|
21
|
+
*
|
|
22
|
+
* XTVERSION (Terminal Name + Version):
|
|
23
|
+
* Query: CSI > 0 q (\x1b[>0q)
|
|
24
|
+
* Response: DCS > | name(version) ST (\x1bP>|{text}\x1b\\)
|
|
25
|
+
*
|
|
26
|
+
* Supported by: xterm, Ghostty, Kitty, WezTerm, foot, VTE-based terminals
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Regex for DA1 response: CSI ? params c */
|
|
30
|
+
const DA1_RESPONSE_RE = /\x1b\[\?([\d;]+)c/
|
|
31
|
+
|
|
32
|
+
/** Regex for DA2 response: CSI > params c */
|
|
33
|
+
const DA2_RESPONSE_RE = /\x1b\[>([\d;]+)c/
|
|
34
|
+
|
|
35
|
+
/** Regex for DA3 response: DCS ! | hex ST */
|
|
36
|
+
const DA3_RESPONSE_RE = /\x1bP!\|([0-9a-fA-F]*)\x1b\\/
|
|
37
|
+
|
|
38
|
+
/** Regex for XTVERSION response: DCS > | text ST */
|
|
39
|
+
const XTVERSION_RESPONSE_RE = /\x1bP>\|([^\x1b]*)\x1b\\/
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// DA1 — Primary Device Attributes
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Query primary device attributes (DA1).
|
|
47
|
+
*
|
|
48
|
+
* Returns the list of attribute parameters the terminal reports.
|
|
49
|
+
* Common params: 1=132-cols, 4=sixel, 6=selective-erase, 22=ANSI-color
|
|
50
|
+
*
|
|
51
|
+
* @param write Function to write to stdout
|
|
52
|
+
* @param read Function to read a chunk from stdin
|
|
53
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
54
|
+
*/
|
|
55
|
+
export async function queryPrimaryDA(
|
|
56
|
+
write: (data: string) => void,
|
|
57
|
+
read: (timeoutMs: number) => Promise<string | null>,
|
|
58
|
+
timeoutMs = 200,
|
|
59
|
+
): Promise<{ params: number[] } | null> {
|
|
60
|
+
write("\x1b[c")
|
|
61
|
+
|
|
62
|
+
const data = await read(timeoutMs)
|
|
63
|
+
if (data == null) return null
|
|
64
|
+
|
|
65
|
+
const match = DA1_RESPONSE_RE.exec(data)
|
|
66
|
+
if (!match) return null
|
|
67
|
+
|
|
68
|
+
const params = match[1]!.split(";").map((s) => parseInt(s, 10))
|
|
69
|
+
return { params }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// DA2 — Secondary Device Attributes
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Query secondary device attributes (DA2).
|
|
78
|
+
*
|
|
79
|
+
* Returns terminal type, firmware version, and ROM cartridge id.
|
|
80
|
+
* Common type values: 0=VT100, 1=VT220, 41=xterm, 65=VT500
|
|
81
|
+
*
|
|
82
|
+
* @param write Function to write to stdout
|
|
83
|
+
* @param read Function to read a chunk from stdin
|
|
84
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
85
|
+
*/
|
|
86
|
+
export async function querySecondaryDA(
|
|
87
|
+
write: (data: string) => void,
|
|
88
|
+
read: (timeoutMs: number) => Promise<string | null>,
|
|
89
|
+
timeoutMs = 200,
|
|
90
|
+
): Promise<{ type: number; version: number; id: number } | null> {
|
|
91
|
+
write("\x1b[>c")
|
|
92
|
+
|
|
93
|
+
const data = await read(timeoutMs)
|
|
94
|
+
if (data == null) return null
|
|
95
|
+
|
|
96
|
+
const match = DA2_RESPONSE_RE.exec(data)
|
|
97
|
+
if (!match) return null
|
|
98
|
+
|
|
99
|
+
const parts = match[1]!.split(";")
|
|
100
|
+
if (parts.length < 3) return null
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
type: parseInt(parts[0]!, 10),
|
|
104
|
+
version: parseInt(parts[1]!, 10),
|
|
105
|
+
id: parseInt(parts[2]!, 10),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// DA3 — Tertiary Device Attributes
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Query tertiary device attributes (DA3).
|
|
115
|
+
*
|
|
116
|
+
* Returns a hex-encoded unit ID string. Decode with Buffer.from(hex, 'hex').
|
|
117
|
+
*
|
|
118
|
+
* @param write Function to write to stdout
|
|
119
|
+
* @param read Function to read a chunk from stdin
|
|
120
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
121
|
+
*/
|
|
122
|
+
export async function queryTertiaryDA(
|
|
123
|
+
write: (data: string) => void,
|
|
124
|
+
read: (timeoutMs: number) => Promise<string | null>,
|
|
125
|
+
timeoutMs = 200,
|
|
126
|
+
): Promise<string | null> {
|
|
127
|
+
write("\x1b[=c")
|
|
128
|
+
|
|
129
|
+
const data = await read(timeoutMs)
|
|
130
|
+
if (data == null) return null
|
|
131
|
+
|
|
132
|
+
const match = DA3_RESPONSE_RE.exec(data)
|
|
133
|
+
if (!match) return null
|
|
134
|
+
|
|
135
|
+
return match[1]!
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// XTVERSION — Terminal Name + Version
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Query the terminal name and version via XTVERSION.
|
|
144
|
+
*
|
|
145
|
+
* Returns the version string as reported by the terminal, e.g.:
|
|
146
|
+
* - "xterm(388)"
|
|
147
|
+
* - "tmux 3.4"
|
|
148
|
+
* - "WezTerm 20230712-072601-f4abf8fd"
|
|
149
|
+
*
|
|
150
|
+
* @param write Function to write to stdout
|
|
151
|
+
* @param read Function to read a chunk from stdin
|
|
152
|
+
* @param timeoutMs How long to wait for response (default: 200ms)
|
|
153
|
+
*/
|
|
154
|
+
export async function queryTerminalVersion(
|
|
155
|
+
write: (data: string) => void,
|
|
156
|
+
read: (timeoutMs: number) => Promise<string | null>,
|
|
157
|
+
timeoutMs = 200,
|
|
158
|
+
): Promise<string | null> {
|
|
159
|
+
write("\x1b[>0q")
|
|
160
|
+
|
|
161
|
+
const data = await read(timeoutMs)
|
|
162
|
+
if (data == null) return null
|
|
163
|
+
|
|
164
|
+
const match = XTVERSION_RESPONSE_RE.exec(data)
|
|
165
|
+
if (!match) return null
|
|
166
|
+
|
|
167
|
+
return match[1]!
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Combined Query
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
/** Combined device attributes result. */
|
|
175
|
+
export interface DeviceAttributes {
|
|
176
|
+
da1: { params: number[] } | null
|
|
177
|
+
da2: { type: number; version: number; id: number } | null
|
|
178
|
+
version: string | null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Query all device attributes: DA1, DA2, and XTVERSION.
|
|
183
|
+
*
|
|
184
|
+
* Convenience wrapper that queries all three sequentially.
|
|
185
|
+
* DA3 is omitted from the combined query as it's rarely needed.
|
|
186
|
+
*
|
|
187
|
+
* @param stdout Writable stream (e.g., process.stdout)
|
|
188
|
+
* @param stdin Readable stream (e.g., process.stdin)
|
|
189
|
+
* @param timeoutMs Per-query timeout (default: 200ms)
|
|
190
|
+
*/
|
|
191
|
+
export async function queryDeviceAttributes(
|
|
192
|
+
stdout: { write: (s: string) => boolean | void },
|
|
193
|
+
stdin: NodeJS.ReadStream,
|
|
194
|
+
timeoutMs = 200,
|
|
195
|
+
): Promise<DeviceAttributes> {
|
|
196
|
+
const wasRaw = stdin.isRaw
|
|
197
|
+
if (!wasRaw) stdin.setRawMode(true)
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const write = (s: string) => {
|
|
201
|
+
stdout.write(s)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const read = (ms: number): Promise<string | null> =>
|
|
205
|
+
new Promise((resolve) => {
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
stdin.removeListener("data", onData)
|
|
208
|
+
resolve(null)
|
|
209
|
+
}, ms)
|
|
210
|
+
|
|
211
|
+
function onData(chunk: Buffer) {
|
|
212
|
+
clearTimeout(timer)
|
|
213
|
+
stdin.removeListener("data", onData)
|
|
214
|
+
resolve(chunk.toString())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
stdin.on("data", onData)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const da1 = await queryPrimaryDA(write, read, timeoutMs)
|
|
221
|
+
const da2 = await querySecondaryDA(write, read, timeoutMs)
|
|
222
|
+
const version = await queryTerminalVersion(write, read, timeoutMs)
|
|
223
|
+
|
|
224
|
+
return { da1, da2, version }
|
|
225
|
+
} finally {
|
|
226
|
+
if (!wasRaw) stdin.setRawMode(false)
|
|
227
|
+
}
|
|
228
|
+
}
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React DevTools integration for silvery.
|
|
3
|
+
*
|
|
4
|
+
* Provides optional connection to React DevTools standalone app for
|
|
5
|
+
* debugging TUI component trees. Requires `react-devtools-core` to be
|
|
6
|
+
* installed (optional peer dependency).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* 1. Install: `bun add -d react-devtools-core`
|
|
10
|
+
* 2. Run devtools: `npx react-devtools`
|
|
11
|
+
* 3. Launch app with: `DEBUG_DEVTOOLS=1 bun run app.ts`
|
|
12
|
+
*
|
|
13
|
+
* Or call `connectDevTools()` manually from your app code.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { reconciler } from "@silvery/react/reconciler"
|
|
19
|
+
|
|
20
|
+
let connected = false
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Connect to React DevTools standalone app.
|
|
24
|
+
*
|
|
25
|
+
* This lazy-loads `react-devtools-core` so it has zero impact on
|
|
26
|
+
* production bundles. The connection is established via WebSocket
|
|
27
|
+
* to the devtools electron app (default: ws://localhost:8097).
|
|
28
|
+
*
|
|
29
|
+
* Safe to call multiple times -- subsequent calls are no-ops.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { connectDevTools } from '@silvery/react';
|
|
34
|
+
* await connectDevTools();
|
|
35
|
+
* // Now open React DevTools standalone to inspect the component tree
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export async function connectDevTools(): Promise<boolean> {
|
|
39
|
+
if (connected) return true
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Polyfill WebSocket for Node.js environments (required by react-devtools-core)
|
|
43
|
+
if (typeof globalThis.WebSocket === "undefined") {
|
|
44
|
+
try {
|
|
45
|
+
// @ts-expect-error -- ws is an optional peer dependency
|
|
46
|
+
const ws = await import("ws")
|
|
47
|
+
globalThis.WebSocket = ws.default ?? ws
|
|
48
|
+
} catch {
|
|
49
|
+
// ws not available -- devtools won't be able to connect
|
|
50
|
+
console.warn(
|
|
51
|
+
"silvery devtools: WebSocket polyfill (ws) not available. " +
|
|
52
|
+
"Install ws for DevTools support: bun add -d ws",
|
|
53
|
+
)
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Ensure window/self exist for react-devtools-core internals
|
|
59
|
+
if (typeof globalThis.window === "undefined") {
|
|
60
|
+
// @ts-expect-error -- polyfill for devtools
|
|
61
|
+
globalThis.window = globalThis
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Configure component filters to hide silvery internals from the DevTools tree.
|
|
65
|
+
// Filter types from react-devtools-shared/src/types.js:
|
|
66
|
+
// 1 = ComponentFilterElementType, value 7 = HostComponent
|
|
67
|
+
// 2 = ComponentFilterDisplayName (regex on displayName)
|
|
68
|
+
if (!globalThis.__REACT_DEVTOOLS_COMPONENT_FILTERS__) {
|
|
69
|
+
globalThis.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = [
|
|
70
|
+
{ type: 1, value: 7, isEnabled: true },
|
|
71
|
+
{ type: 2, value: "SilveryApp", isEnabled: true, isValid: true },
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// @ts-expect-error -- react-devtools-core has no type declarations
|
|
76
|
+
const devtools = await import("react-devtools-core")
|
|
77
|
+
devtools.initialize()
|
|
78
|
+
devtools.connectToDevTools()
|
|
79
|
+
|
|
80
|
+
// Inject renderer info so DevTools can identify silvery.
|
|
81
|
+
// rendererPackageName and rendererVersion are read from the host config
|
|
82
|
+
// passed to Reconciler() -- see reconciler/host-config.ts.
|
|
83
|
+
reconciler.injectIntoDevTools()
|
|
84
|
+
|
|
85
|
+
connected = true
|
|
86
|
+
return true
|
|
87
|
+
} catch (error: unknown) {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
89
|
+
console.warn(
|
|
90
|
+
`silvery devtools: Failed to connect to React DevTools. ` +
|
|
91
|
+
`Install react-devtools-core: bun add -d react-devtools-core\n` +
|
|
92
|
+
` Error: ${message}`,
|
|
93
|
+
)
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if DevTools are currently connected.
|
|
100
|
+
*/
|
|
101
|
+
export function isDevToolsConnected(): boolean {
|
|
102
|
+
return connected
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Auto-connect to DevTools if DEBUG_DEVTOOLS=1 environment variable is set.
|
|
107
|
+
* Called internally during render initialization.
|
|
108
|
+
*/
|
|
109
|
+
export async function autoConnectDevTools(): Promise<void> {
|
|
110
|
+
if (process.env.DEBUG_DEVTOOLS === "1" || process.env.DEBUG_DEVTOOLS === "true") {
|
|
111
|
+
await connectDevTools()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Global type augmentation for devtools polyfills
|
|
116
|
+
declare global {
|
|
117
|
+
var __REACT_DEVTOOLS_COMPONENT_FILTERS__: Array<{
|
|
118
|
+
type: number
|
|
119
|
+
value: number | string
|
|
120
|
+
isEnabled: boolean
|
|
121
|
+
isValid?: boolean
|
|
122
|
+
}>
|
|
123
|
+
}
|
package/src/dom/index.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Provides a browser-friendly API for rendering silvery components to DOM elements.
|
|
5
|
+
* This module sets up the DOM adapter and provides render functions.
|
|
6
|
+
*
|
|
7
|
+
* Advantages over Canvas:
|
|
8
|
+
* - Native text selection and copying
|
|
9
|
+
* - Screen reader accessibility
|
|
10
|
+
* - Browser font rendering
|
|
11
|
+
* - CSS integration
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { renderToDOM, Box, Text, useContentRect } from '@silvery/term/dom';
|
|
16
|
+
*
|
|
17
|
+
* function App() {
|
|
18
|
+
* const { width, height } = useContentRect();
|
|
19
|
+
* return (
|
|
20
|
+
* <Box flexDirection="column">
|
|
21
|
+
* <Text>Container size: {width}px × {height}px</Text>
|
|
22
|
+
* </Box>
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* const container = document.getElementById('app');
|
|
27
|
+
* renderToDOM(<App />, container);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { ReactElement } from "react"
|
|
32
|
+
import { type DOMAdapterConfig, DOMRenderBuffer, createDOMAdapter, injectDOMStyles } from "../adapters/dom-adapter"
|
|
33
|
+
import { createBrowserRenderer, initBrowserRenderer, renderOnce } from "../browser-renderer"
|
|
34
|
+
import type { RenderBuffer } from "../render-adapter"
|
|
35
|
+
|
|
36
|
+
// Re-export components and hooks for convenience
|
|
37
|
+
export { Box, type BoxProps } from "@silvery/react/components/Box"
|
|
38
|
+
export { Text, type TextProps } from "@silvery/react/components/Text"
|
|
39
|
+
export { useContentRect, useScreenRect } from "@silvery/react/hooks/useLayout"
|
|
40
|
+
export { useApp } from "@silvery/react/hooks/useApp"
|
|
41
|
+
|
|
42
|
+
// Re-export adapter utilities
|
|
43
|
+
export { createDOMAdapter, DOMRenderBuffer, injectDOMStyles, type DOMAdapterConfig } from "../adapters/dom-adapter"
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Types
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export interface DOMRenderOptions extends DOMAdapterConfig {
|
|
50
|
+
/** Width of the container (default: container.clientWidth or 800) */
|
|
51
|
+
width?: number
|
|
52
|
+
/** Height of the container (default: container.clientHeight or 600) */
|
|
53
|
+
height?: number
|
|
54
|
+
/** Inject global CSS styles (default: true) */
|
|
55
|
+
injectStyles?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DOMInstance {
|
|
59
|
+
/** Re-render with a new element */
|
|
60
|
+
rerender: (element: ReactElement) => void
|
|
61
|
+
/** Unmount and clean up */
|
|
62
|
+
unmount: () => void
|
|
63
|
+
/** Dispose (alias for unmount) — enables `using` */
|
|
64
|
+
[Symbol.dispose](): void
|
|
65
|
+
/** Get the current buffer */
|
|
66
|
+
getBuffer: () => RenderBuffer | null
|
|
67
|
+
/** Force a re-render */
|
|
68
|
+
refresh: () => void
|
|
69
|
+
/** Get the container element */
|
|
70
|
+
getContainer: () => HTMLElement
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Initialization
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
const domAdapterFactory = { createAdapter: (config: DOMAdapterConfig) => createDOMAdapter(config) }
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Initialize the DOM rendering system.
|
|
81
|
+
* Called automatically by renderToDOM, but can be called manually.
|
|
82
|
+
*/
|
|
83
|
+
export function initDOMRenderer(config: DOMAdapterConfig = {}): void {
|
|
84
|
+
initBrowserRenderer(domAdapterFactory, config)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Render Functions
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Render a React element to a DOM container.
|
|
93
|
+
*
|
|
94
|
+
* @param element - React element to render
|
|
95
|
+
* @param container - Target DOM element
|
|
96
|
+
* @param options - Render options (font size, colors, etc.)
|
|
97
|
+
* @returns DOMInstance for controlling the render
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* const container = document.getElementById('app');
|
|
102
|
+
* const instance = renderToDOM(<App />, container, { fontSize: 16 });
|
|
103
|
+
*
|
|
104
|
+
* // Later: update the component
|
|
105
|
+
* instance.rerender(<App newProps />);
|
|
106
|
+
*
|
|
107
|
+
* // Clean up
|
|
108
|
+
* instance.unmount();
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function renderToDOM(
|
|
112
|
+
element: ReactElement,
|
|
113
|
+
container: HTMLElement,
|
|
114
|
+
options: DOMRenderOptions = {},
|
|
115
|
+
): DOMInstance {
|
|
116
|
+
const { injectStyles = true, ...adapterConfig } = options
|
|
117
|
+
|
|
118
|
+
if (injectStyles) {
|
|
119
|
+
injectDOMStyles(adapterConfig.classPrefix)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
initDOMRenderer(adapterConfig)
|
|
123
|
+
|
|
124
|
+
const pixelWidth = options.width ?? (container.clientWidth || 800)
|
|
125
|
+
const pixelHeight = options.height ?? (container.clientHeight || 600)
|
|
126
|
+
|
|
127
|
+
// Convert pixel dimensions to cell dimensions for the layout engine.
|
|
128
|
+
// The layout engine operates in cell units (columns x rows), not pixels.
|
|
129
|
+
// We estimate cell size from font metrics: charWidth ~ fontSize * 0.6, lineHeight ~ fontSize * lineHeight.
|
|
130
|
+
const fontSize = adapterConfig.fontSize ?? 14
|
|
131
|
+
const lineHeightMultiplier = adapterConfig.lineHeight ?? 1.2
|
|
132
|
+
const charWidth = fontSize * 0.6
|
|
133
|
+
const lineHeight = fontSize * lineHeightMultiplier
|
|
134
|
+
const cols = Math.floor(pixelWidth / charWidth)
|
|
135
|
+
const rows = Math.floor(pixelHeight / lineHeight)
|
|
136
|
+
|
|
137
|
+
const base = createBrowserRenderer<DOMRenderBuffer>(
|
|
138
|
+
element,
|
|
139
|
+
cols,
|
|
140
|
+
rows,
|
|
141
|
+
(buffer) => {
|
|
142
|
+
buffer.setContainer(container)
|
|
143
|
+
buffer.render()
|
|
144
|
+
},
|
|
145
|
+
() => {
|
|
146
|
+
container.innerHTML = ""
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...base,
|
|
152
|
+
getContainer(): HTMLElement {
|
|
153
|
+
return container
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Render a React element to DOM once and return the HTML string.
|
|
160
|
+
* Useful for server-side rendering or static generation.
|
|
161
|
+
*
|
|
162
|
+
* @param element - React element to render
|
|
163
|
+
* @param width - Container width in pixels
|
|
164
|
+
* @param height - Container height in pixels
|
|
165
|
+
* @param options - Render options
|
|
166
|
+
* @returns HTML string representation
|
|
167
|
+
*/
|
|
168
|
+
export function renderDOMOnce(
|
|
169
|
+
element: ReactElement,
|
|
170
|
+
width: number,
|
|
171
|
+
height: number,
|
|
172
|
+
options: DOMAdapterConfig = {},
|
|
173
|
+
): string {
|
|
174
|
+
initDOMRenderer(options)
|
|
175
|
+
|
|
176
|
+
// Convert pixel dimensions to cell dimensions for the layout engine
|
|
177
|
+
const fontSize = options.fontSize ?? 14
|
|
178
|
+
const lineHeightMultiplier = options.lineHeight ?? 1.2
|
|
179
|
+
const charWidth = fontSize * 0.6
|
|
180
|
+
const lineHeight = fontSize * lineHeightMultiplier
|
|
181
|
+
const cols = Math.floor(width / charWidth)
|
|
182
|
+
const rows = Math.floor(height / lineHeight)
|
|
183
|
+
|
|
184
|
+
const buffer = renderOnce<DOMRenderBuffer>(element, cols, rows)
|
|
185
|
+
|
|
186
|
+
if (typeof document !== "undefined") {
|
|
187
|
+
const tempContainer = document.createElement("div")
|
|
188
|
+
buffer.setContainer(tempContainer)
|
|
189
|
+
buffer.render()
|
|
190
|
+
return tempContainer.innerHTML
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return "<!-- DOM rendering requires browser environment -->"
|
|
194
|
+
}
|