@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
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
+ }