@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/non-tty.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-TTY Mode Support for Silvery
|
|
3
|
+
*
|
|
4
|
+
* Provides detection and rendering modes for non-interactive environments:
|
|
5
|
+
* - Piped output (process.stdout.isTTY === false)
|
|
6
|
+
* - CI environments
|
|
7
|
+
* - TERM=dumb
|
|
8
|
+
*
|
|
9
|
+
* When in non-TTY mode, silvery avoids cursor positioning codes that garble
|
|
10
|
+
* output in non-interactive environments.
|
|
11
|
+
*
|
|
12
|
+
* Modes:
|
|
13
|
+
* - 'auto': Detect based on environment (default)
|
|
14
|
+
* - 'tty': Force TTY mode (normal cursor positioning)
|
|
15
|
+
* - 'line-by-line': Simple newline-separated output, no cursor movement
|
|
16
|
+
* - 'static': Single output at end (no updates)
|
|
17
|
+
* - 'plain': Strip all ANSI codes
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { stripAnsi } from "./unicode"
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Types
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Non-TTY rendering mode.
|
|
28
|
+
*
|
|
29
|
+
* - 'auto': Auto-detect based on environment
|
|
30
|
+
* - 'tty': Force TTY mode with cursor positioning
|
|
31
|
+
* - 'line-by-line': Output lines without cursor repositioning
|
|
32
|
+
* - 'static': Single final output only
|
|
33
|
+
* - 'plain': Strip all ANSI escape codes
|
|
34
|
+
*/
|
|
35
|
+
export type NonTTYMode = "auto" | "tty" | "line-by-line" | "static" | "plain"
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for non-TTY output.
|
|
39
|
+
*/
|
|
40
|
+
export interface NonTTYOptions {
|
|
41
|
+
/** The rendering mode. Default: 'auto' */
|
|
42
|
+
mode?: NonTTYMode
|
|
43
|
+
/** Output stream to check for TTY status. Default: process.stdout */
|
|
44
|
+
stdout?: NodeJS.WriteStream
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolved non-TTY mode after auto-detection.
|
|
49
|
+
*/
|
|
50
|
+
export type ResolvedNonTTYMode = Exclude<NonTTYMode, "auto">
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Detection
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if the environment is a TTY.
|
|
58
|
+
*
|
|
59
|
+
* Returns false if:
|
|
60
|
+
* - stdout.isTTY is false or undefined
|
|
61
|
+
* - TERM=dumb
|
|
62
|
+
* - CI environment variables are set
|
|
63
|
+
*/
|
|
64
|
+
export function isTTY(stdout: NodeJS.WriteStream = process.stdout): boolean {
|
|
65
|
+
// Check stdout.isTTY
|
|
66
|
+
if (!stdout.isTTY) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check TERM=dumb
|
|
71
|
+
if (process.env.TERM === "dumb") {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check common CI environment variables
|
|
76
|
+
if (
|
|
77
|
+
process.env.CI ||
|
|
78
|
+
process.env.GITHUB_ACTIONS ||
|
|
79
|
+
process.env.GITLAB_CI ||
|
|
80
|
+
process.env.JENKINS_URL ||
|
|
81
|
+
process.env.BUILDKITE ||
|
|
82
|
+
process.env.CIRCLECI ||
|
|
83
|
+
process.env.TRAVIS
|
|
84
|
+
) {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the non-TTY mode based on options and environment.
|
|
93
|
+
*
|
|
94
|
+
* When mode is 'auto':
|
|
95
|
+
* - If TTY detected: returns 'tty'
|
|
96
|
+
* - If non-TTY detected: returns 'line-by-line'
|
|
97
|
+
*/
|
|
98
|
+
export function resolveNonTTYMode(options: NonTTYOptions = {}): ResolvedNonTTYMode {
|
|
99
|
+
const { mode = "auto", stdout = process.stdout } = options
|
|
100
|
+
|
|
101
|
+
if (mode !== "auto") {
|
|
102
|
+
return mode
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Auto-detect based on environment
|
|
106
|
+
return isTTY(stdout) ? "tty" : "line-by-line"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Re-export stripAnsi from unicode.ts (canonical implementation)
|
|
110
|
+
export { stripAnsi } from "./unicode"
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Line-by-Line Output
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Convert buffer output to line-by-line format.
|
|
118
|
+
*
|
|
119
|
+
* Instead of using cursor positioning, outputs each line with a simple
|
|
120
|
+
* carriage return and clear-to-end-of-line sequence.
|
|
121
|
+
*
|
|
122
|
+
* @param content The rendered content (may contain ANSI codes but no cursor positioning)
|
|
123
|
+
* @param prevLineCount Number of lines in the previous frame (for clearing)
|
|
124
|
+
* @returns Output string suitable for non-TTY rendering
|
|
125
|
+
*/
|
|
126
|
+
export function toLineByLineOutput(content: string, prevLineCount: number): string {
|
|
127
|
+
const lines = content.split("\n")
|
|
128
|
+
let output = ""
|
|
129
|
+
|
|
130
|
+
// Move cursor up to overwrite previous content (if any)
|
|
131
|
+
if (prevLineCount > 0) {
|
|
132
|
+
// Move to start of first line
|
|
133
|
+
output += "\r"
|
|
134
|
+
// Move up
|
|
135
|
+
if (prevLineCount > 1) {
|
|
136
|
+
output += `\x1b[${prevLineCount - 1}A`
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Output each line
|
|
141
|
+
for (let i = 0; i < lines.length; i++) {
|
|
142
|
+
if (i > 0) {
|
|
143
|
+
output += "\n"
|
|
144
|
+
}
|
|
145
|
+
output += lines[i]
|
|
146
|
+
// Clear to end of line (removes leftover content from longer previous lines)
|
|
147
|
+
output += "\x1b[K"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Clear any remaining lines from previous frame
|
|
151
|
+
const extraLines = prevLineCount - lines.length
|
|
152
|
+
if (extraLines > 0) {
|
|
153
|
+
for (let i = 0; i < extraLines; i++) {
|
|
154
|
+
output += "\n\x1b[K"
|
|
155
|
+
}
|
|
156
|
+
// Move cursor back up to end of content
|
|
157
|
+
output += `\x1b[${extraLines}A`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return output
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Convert buffer output to plain text format.
|
|
165
|
+
*
|
|
166
|
+
* Strips all ANSI codes and outputs simple newline-separated text.
|
|
167
|
+
* No cursor movement or clearing.
|
|
168
|
+
*
|
|
169
|
+
* @param content The rendered content
|
|
170
|
+
* @param prevLineCount Number of lines in the previous frame (unused in plain mode)
|
|
171
|
+
* @returns Plain text output
|
|
172
|
+
*/
|
|
173
|
+
export function toPlainOutput(content: string, _prevLineCount: number): string {
|
|
174
|
+
// Strip ANSI codes
|
|
175
|
+
const plain = stripAnsi(content)
|
|
176
|
+
|
|
177
|
+
// Trim trailing whitespace from each line but preserve structure
|
|
178
|
+
const lines = plain.split("\n").map((line) => line.trimEnd())
|
|
179
|
+
|
|
180
|
+
// Remove trailing empty lines
|
|
181
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
182
|
+
lines.pop()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines.join("\n")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Output Helpers
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create an output transformer based on the non-TTY mode.
|
|
194
|
+
*
|
|
195
|
+
* @param mode The resolved non-TTY mode
|
|
196
|
+
* @returns A function that transforms output based on the mode
|
|
197
|
+
*/
|
|
198
|
+
export function createOutputTransformer(mode: ResolvedNonTTYMode): (content: string, prevLineCount: number) => string {
|
|
199
|
+
switch (mode) {
|
|
200
|
+
case "tty":
|
|
201
|
+
// Pass through unchanged
|
|
202
|
+
return (content) => content
|
|
203
|
+
|
|
204
|
+
case "line-by-line":
|
|
205
|
+
return toLineByLineOutput
|
|
206
|
+
|
|
207
|
+
case "static":
|
|
208
|
+
// For static mode, we return empty string for intermediate renders
|
|
209
|
+
// The final render is handled by the caller
|
|
210
|
+
return () => ""
|
|
211
|
+
|
|
212
|
+
case "plain":
|
|
213
|
+
return toPlainOutput
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Count the number of lines in a string.
|
|
219
|
+
*/
|
|
220
|
+
export function countLines(str: string): number {
|
|
221
|
+
if (!str) return 0
|
|
222
|
+
return str.split("\n").length
|
|
223
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSC 133 Semantic Prompt Markers
|
|
3
|
+
*
|
|
4
|
+
* Shell integration protocol that marks prompts and commands in terminal output.
|
|
5
|
+
* Terminals like iTerm2, Kitty, and WezTerm use these markers to provide
|
|
6
|
+
* "jump to previous/next prompt" navigation (Cmd+Up/Cmd+Down in iTerm2).
|
|
7
|
+
*
|
|
8
|
+
* Protocol: OSC 133
|
|
9
|
+
* - Prompt start: ESC ] 133 ; A BEL (before user input)
|
|
10
|
+
* - Prompt end: ESC ] 133 ; B BEL (after user input, before command output)
|
|
11
|
+
* - Command output start: ESC ] 133 ; C BEL (before command output)
|
|
12
|
+
* - Command output end: ESC ] 133 ; D ; N BEL (after command output, N = exit code)
|
|
13
|
+
*
|
|
14
|
+
* For a chat-style app, each "exchange" (user prompt + assistant response) maps to:
|
|
15
|
+
* - 133;A before the user's message
|
|
16
|
+
* - 133;B after the user's message
|
|
17
|
+
* - 133;C before the assistant's response
|
|
18
|
+
* - 133;D;0 after the assistant's response
|
|
19
|
+
*
|
|
20
|
+
* Supported by: iTerm2, Kitty, WezTerm, foot, Ghostty
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const OSC133 = {
|
|
24
|
+
/** Mark prompt start (before user input) */
|
|
25
|
+
promptStart: "\x1b]133;A\x07",
|
|
26
|
+
/** Mark prompt end (after user input, before command output) */
|
|
27
|
+
promptEnd: "\x1b]133;B\x07",
|
|
28
|
+
/** Mark command output start */
|
|
29
|
+
commandStart: "\x1b]133;C\x07",
|
|
30
|
+
/** Mark command output end (exit code defaults to 0 = success) */
|
|
31
|
+
commandEnd: (exitCode?: number) => `\x1b]133;D;${exitCode ?? 0}\x07`,
|
|
32
|
+
} as const
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSC 4 Terminal Color Palette Query/Set
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to query and set terminal color palette entries (indices 0-255)
|
|
5
|
+
* via the OSC 4 protocol. This enables runtime introspection of the terminal's
|
|
6
|
+
* actual color scheme.
|
|
7
|
+
*
|
|
8
|
+
* Protocol: OSC 4
|
|
9
|
+
* - Query: ESC ] 4 ; <index> ; ? BEL
|
|
10
|
+
* - Set: ESC ] 4 ; <index> ; <color> BEL
|
|
11
|
+
* - Response: ESC ] 4 ; <index> ; rgb:RRRR/GGGG/BBBB ST
|
|
12
|
+
*
|
|
13
|
+
* The response format uses 4-digit hex per channel (e.g., rgb:ffff/0000/ffff).
|
|
14
|
+
* Some terminals may use 2-digit hex (rgb:ff/00/ff). Both are handled.
|
|
15
|
+
*
|
|
16
|
+
* Terminators: BEL (\x07) or ST (ESC \) — both are accepted in responses.
|
|
17
|
+
*
|
|
18
|
+
* Supported by: xterm, Ghostty, Kitty, WezTerm, iTerm2, foot, Alacritty, rxvt-unicode
|
|
19
|
+
*
|
|
20
|
+
* ## Theme Detection Potential (km-tui integration)
|
|
21
|
+
*
|
|
22
|
+
* This module provides the primitives needed to auto-detect terminal color schemes:
|
|
23
|
+
*
|
|
24
|
+
* 1. Query colors 0-15 (ANSI 16 palette) on startup with queryMultiplePaletteColors()
|
|
25
|
+
* 2. Parse responses with parsePaletteResponse()
|
|
26
|
+
* 3. Derive dark/light mode from background luminance:
|
|
27
|
+
* - Query OSC 11 (background color) or use palette color 0 as proxy
|
|
28
|
+
* - Convert to relative luminance: L = 0.2126*R + 0.7152*G + 0.0722*B
|
|
29
|
+
* - L > 0.5 → light theme, L <= 0.5 → dark theme
|
|
30
|
+
* 4. Optionally adjust theme colors based on actual palette values
|
|
31
|
+
* (e.g., map $primary to the terminal's blue if it's close enough)
|
|
32
|
+
*
|
|
33
|
+
* This is NOT implemented here — just the raw OSC 4 primitives.
|
|
34
|
+
* The km-tui theme system would consume these to adapt its ThemeProvider.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const ESC = "\x1b"
|
|
38
|
+
const BEL = "\x07"
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Query
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Write an OSC 4 query sequence for a single palette color index.
|
|
46
|
+
*
|
|
47
|
+
* The terminal will respond with:
|
|
48
|
+
* ESC ] 4 ; <index> ; rgb:RRRR/GGGG/BBBB ST
|
|
49
|
+
*
|
|
50
|
+
* Use parsePaletteResponse() to decode the response.
|
|
51
|
+
*
|
|
52
|
+
* @param index Palette index (0-255)
|
|
53
|
+
* @param write Function to write data to the terminal (e.g., stdout.write.bind(stdout))
|
|
54
|
+
*/
|
|
55
|
+
export function queryPaletteColor(index: number, write: (data: string) => void): void {
|
|
56
|
+
if (index < 0 || index > 255) throw new RangeError(`Palette index must be 0-255, got ${index}`)
|
|
57
|
+
write(`${ESC}]4;${index};?${BEL}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write OSC 4 query sequences for multiple palette color indices.
|
|
62
|
+
*
|
|
63
|
+
* Sends one query per index. Terminals process these sequentially
|
|
64
|
+
* and respond with one OSC 4 response per query.
|
|
65
|
+
*
|
|
66
|
+
* @param indices Array of palette indices (each 0-255)
|
|
67
|
+
* @param write Function to write data to the terminal
|
|
68
|
+
*/
|
|
69
|
+
export function queryMultiplePaletteColors(indices: number[], write: (data: string) => void): void {
|
|
70
|
+
for (const index of indices) {
|
|
71
|
+
queryPaletteColor(index, write)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Set
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Write an OSC 4 sequence to set a palette color.
|
|
81
|
+
*
|
|
82
|
+
* The color can be in any X11 color format accepted by the terminal:
|
|
83
|
+
* - `rgb:RR/GG/BB` or `rgb:RRRR/GGGG/BBBB` (X11 rgb spec)
|
|
84
|
+
* - `#RRGGBB` (CSS hex — widely supported)
|
|
85
|
+
* - Named colors (e.g., `red`, `blue` — terminal-dependent)
|
|
86
|
+
*
|
|
87
|
+
* @param index Palette index (0-255)
|
|
88
|
+
* @param color Color specification string
|
|
89
|
+
* @param write Function to write data to the terminal
|
|
90
|
+
*/
|
|
91
|
+
export function setPaletteColor(index: number, color: string, write: (data: string) => void): void {
|
|
92
|
+
if (index < 0 || index > 255) throw new RangeError(`Palette index must be 0-255, got ${index}`)
|
|
93
|
+
write(`${ESC}]4;${index};${color}${BEL}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Response Parsing
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
/** OSC 4 response prefix */
|
|
101
|
+
const OSC4_PREFIX = `${ESC}]4;`
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Regex for the OSC 4 response body: `<index>;rgb:<R>/<G>/<B>`
|
|
105
|
+
* Captures: index, R, G, B (each 1-4 hex digits)
|
|
106
|
+
*/
|
|
107
|
+
const OSC4_BODY_RE = /^(\d+);rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})$/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse an OSC 4 palette color response.
|
|
111
|
+
*
|
|
112
|
+
* Handles both standard 4-digit hex (rgb:RRRR/GGGG/BBBB) and
|
|
113
|
+
* abbreviated 2-digit hex (rgb:RR/GG/BB) formats.
|
|
114
|
+
*
|
|
115
|
+
* Handles both BEL (\x07) and ST (ESC \) terminators.
|
|
116
|
+
*
|
|
117
|
+
* @param input Raw terminal input string
|
|
118
|
+
* @returns Parsed result with index and normalized color string, or null if not an OSC 4 response
|
|
119
|
+
*/
|
|
120
|
+
export function parsePaletteResponse(input: string): { index: number; color: string } | null {
|
|
121
|
+
const prefixIdx = input.indexOf(OSC4_PREFIX)
|
|
122
|
+
if (prefixIdx === -1) return null
|
|
123
|
+
|
|
124
|
+
const bodyStart = prefixIdx + OSC4_PREFIX.length
|
|
125
|
+
|
|
126
|
+
// Find terminator: BEL (\x07) or ST (ESC \)
|
|
127
|
+
let bodyEnd = input.indexOf(BEL, bodyStart)
|
|
128
|
+
if (bodyEnd === -1) {
|
|
129
|
+
bodyEnd = input.indexOf(`${ESC}\\`, bodyStart)
|
|
130
|
+
}
|
|
131
|
+
if (bodyEnd === -1) return null
|
|
132
|
+
|
|
133
|
+
const body = input.slice(bodyStart, bodyEnd)
|
|
134
|
+
const match = OSC4_BODY_RE.exec(body)
|
|
135
|
+
if (!match) return null
|
|
136
|
+
|
|
137
|
+
const index = Number.parseInt(match[1]!, 10)
|
|
138
|
+
if (index < 0 || index > 255) return null
|
|
139
|
+
|
|
140
|
+
// Normalize color channels to 2-digit hex (scale 4-digit to 2-digit)
|
|
141
|
+
const r = normalizeHexChannel(match[2]!)
|
|
142
|
+
const g = normalizeHexChannel(match[3]!)
|
|
143
|
+
const b = normalizeHexChannel(match[4]!)
|
|
144
|
+
|
|
145
|
+
return { index, color: `#${r}${g}${b}` }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Normalize a hex color channel to 2-digit hex.
|
|
150
|
+
*
|
|
151
|
+
* - 1-digit: repeat (e.g., "f" -> "ff")
|
|
152
|
+
* - 2-digit: as-is
|
|
153
|
+
* - 3-digit: take first 2 (e.g., "fff" -> "ff")
|
|
154
|
+
* - 4-digit: take first 2 (e.g., "ffff" -> "ff", "1a2b" -> "1a")
|
|
155
|
+
*/
|
|
156
|
+
function normalizeHexChannel(hex: string): string {
|
|
157
|
+
switch (hex.length) {
|
|
158
|
+
case 1:
|
|
159
|
+
return hex + hex
|
|
160
|
+
case 2:
|
|
161
|
+
return hex
|
|
162
|
+
case 3:
|
|
163
|
+
return hex.slice(0, 2)
|
|
164
|
+
case 4:
|
|
165
|
+
return hex.slice(0, 2)
|
|
166
|
+
default:
|
|
167
|
+
return hex.slice(0, 2)
|
|
168
|
+
}
|
|
169
|
+
}
|