@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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal capability detection.
|
|
3
|
+
*
|
|
4
|
+
* Detects:
|
|
5
|
+
* - Cursor control (can reposition cursor)
|
|
6
|
+
* - Input capability (can read raw keystrokes)
|
|
7
|
+
* - Color level (basic, 256, truecolor)
|
|
8
|
+
* - Unicode support (can render unicode symbols)
|
|
9
|
+
* - Extended underline support (curly, dotted, etc)
|
|
10
|
+
* - Terminal capabilities profile (TerminalCaps)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawnSync } from "child_process"
|
|
14
|
+
import type { ColorLevel } from "./types"
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Cursor Detection
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect if terminal supports cursor control (repositioning).
|
|
22
|
+
* Returns false for dumb terminals and piped output.
|
|
23
|
+
*/
|
|
24
|
+
export function detectCursor(stdout: NodeJS.WriteStream): boolean {
|
|
25
|
+
// Not a TTY - no cursor control
|
|
26
|
+
if (!stdout.isTTY) return false
|
|
27
|
+
|
|
28
|
+
// Dumb terminal - no cursor control
|
|
29
|
+
if (process.env.TERM === "dumb") return false
|
|
30
|
+
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Input Detection
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detect if terminal can read raw keystrokes.
|
|
40
|
+
* Requires stdin to be a TTY with raw mode support.
|
|
41
|
+
*/
|
|
42
|
+
export function detectInput(stdin: NodeJS.ReadStream): boolean {
|
|
43
|
+
// Not a TTY - no raw input
|
|
44
|
+
if (!stdin.isTTY) return false
|
|
45
|
+
|
|
46
|
+
// Check if setRawMode is available
|
|
47
|
+
return typeof stdin.setRawMode === "function"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Color Detection
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Known CI environments that may not support colors well.
|
|
56
|
+
*/
|
|
57
|
+
const CI_ENVS = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL", "BUILDKITE", "CIRCLECI", "TRAVIS"]
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Detect color level supported by terminal.
|
|
61
|
+
* Returns null if no color support.
|
|
62
|
+
*
|
|
63
|
+
* Checks (in order):
|
|
64
|
+
* 1. NO_COLOR env var - forces no color
|
|
65
|
+
* 2. FORCE_COLOR env var - forces color level
|
|
66
|
+
* 3. COLORTERM=truecolor - truecolor support
|
|
67
|
+
* 4. TERM patterns - detect from terminal type
|
|
68
|
+
* 5. CI detection - basic colors in CI
|
|
69
|
+
*/
|
|
70
|
+
export function detectColor(stdout: NodeJS.WriteStream): ColorLevel | null {
|
|
71
|
+
// NO_COLOR takes precedence (see https://no-color.org/)
|
|
72
|
+
if (process.env.NO_COLOR !== undefined) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// FORCE_COLOR overrides detection
|
|
77
|
+
const forceColor = process.env.FORCE_COLOR
|
|
78
|
+
if (forceColor !== undefined) {
|
|
79
|
+
if (forceColor === "0" || forceColor === "false") return null
|
|
80
|
+
if (forceColor === "1") return "basic"
|
|
81
|
+
if (forceColor === "2") return "256"
|
|
82
|
+
if (forceColor === "3") return "truecolor"
|
|
83
|
+
// Any other truthy value defaults to basic
|
|
84
|
+
return "basic"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Non-TTY without FORCE_COLOR - no colors
|
|
88
|
+
if (!stdout.isTTY) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Dumb terminal
|
|
93
|
+
if (process.env.TERM === "dumb") {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// COLORTERM=truecolor indicates 24-bit support
|
|
98
|
+
const colorTerm = process.env.COLORTERM
|
|
99
|
+
if (colorTerm === "truecolor" || colorTerm === "24bit") {
|
|
100
|
+
return "truecolor"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check TERM for color hints
|
|
104
|
+
const term = process.env.TERM ?? ""
|
|
105
|
+
|
|
106
|
+
// Known truecolor terminals
|
|
107
|
+
if (
|
|
108
|
+
term.includes("truecolor") ||
|
|
109
|
+
term.includes("24bit") ||
|
|
110
|
+
term.includes("xterm-ghostty") ||
|
|
111
|
+
term.includes("xterm-kitty") ||
|
|
112
|
+
term.includes("wezterm")
|
|
113
|
+
) {
|
|
114
|
+
return "truecolor"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 256-color terminals
|
|
118
|
+
if (term.includes("256color") || term.includes("256")) {
|
|
119
|
+
return "256"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Modern macOS terminals typically support truecolor
|
|
123
|
+
const termProgram = process.env.TERM_PROGRAM
|
|
124
|
+
if (termProgram === "iTerm.app" || termProgram === "Apple_Terminal") {
|
|
125
|
+
return termProgram === "iTerm.app" ? "truecolor" : "256"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Ghostty, WezTerm, Kitty via TERM_PROGRAM
|
|
129
|
+
if (termProgram === "Ghostty" || termProgram === "WezTerm") {
|
|
130
|
+
return "truecolor"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Kitty via env var
|
|
134
|
+
if (process.env.KITTY_WINDOW_ID) {
|
|
135
|
+
return "truecolor"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// xterm-color variants get basic colors
|
|
139
|
+
if (term.includes("xterm") || term.includes("color") || term.includes("ansi")) {
|
|
140
|
+
return "basic"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// CI environments usually support basic colors
|
|
144
|
+
if (CI_ENVS.some((env) => process.env[env] !== undefined)) {
|
|
145
|
+
return "basic"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Windows Terminal (modern)
|
|
149
|
+
if (process.env.WT_SESSION) {
|
|
150
|
+
return "truecolor"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Default: basic colors if TTY
|
|
154
|
+
return "basic"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Unicode Detection
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Detect if terminal can render unicode symbols.
|
|
163
|
+
* Based on TERM, locale, and known terminal apps.
|
|
164
|
+
*/
|
|
165
|
+
export function detectUnicode(): boolean {
|
|
166
|
+
// CI environments - often UTF-8 capable but be conservative
|
|
167
|
+
if (process.env.CI) {
|
|
168
|
+
// GitHub Actions is UTF-8
|
|
169
|
+
if (process.env.GITHUB_ACTIONS) return true
|
|
170
|
+
// Other CI - check LANG
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check locale for UTF-8
|
|
174
|
+
const lang = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_CTYPE ?? ""
|
|
175
|
+
if (lang.toLowerCase().includes("utf-8") || lang.toLowerCase().includes("utf8")) {
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Windows Terminal
|
|
180
|
+
if (process.env.WT_SESSION) {
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Modern terminal programs
|
|
185
|
+
const termProgram = process.env.TERM_PROGRAM ?? ""
|
|
186
|
+
if (["iTerm.app", "Ghostty", "WezTerm", "Apple_Terminal"].includes(termProgram)) {
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Kitty
|
|
191
|
+
if (process.env.KITTY_WINDOW_ID) {
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check TERM for modern terminals
|
|
196
|
+
const term = process.env.TERM ?? ""
|
|
197
|
+
if (term.includes("xterm") || term.includes("rxvt") || term.includes("screen") || term.includes("tmux")) {
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Default: assume no unicode for safety
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// =============================================================================
|
|
206
|
+
// Extended Underline Detection
|
|
207
|
+
// =============================================================================
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Known terminals with extended underline support.
|
|
211
|
+
*/
|
|
212
|
+
const EXTENDED_UNDERLINE_TERMS = ["xterm-ghostty", "xterm-kitty", "wezterm", "xterm-256color"]
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Known terminal programs with extended underline support.
|
|
216
|
+
*/
|
|
217
|
+
const EXTENDED_UNDERLINE_PROGRAMS = ["Ghostty", "iTerm.app", "WezTerm"]
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Detect if terminal supports extended underline styles.
|
|
221
|
+
* (curly, dotted, dashed, double)
|
|
222
|
+
*
|
|
223
|
+
* Extended underlines use SGR 4:x (style) and SGR 58;2;r;g;b (color).
|
|
224
|
+
* These are NOT supported by Terminal.app, which misinterprets them
|
|
225
|
+
* as background colors causing visual artifacts.
|
|
226
|
+
*/
|
|
227
|
+
export function detectExtendedUnderline(): boolean {
|
|
228
|
+
const term = process.env.TERM ?? ""
|
|
229
|
+
const termProgram = process.env.TERM_PROGRAM ?? ""
|
|
230
|
+
|
|
231
|
+
// Apple Terminal doesn't support extended underlines - check FIRST
|
|
232
|
+
// because it often sets TERM=xterm-256color which would otherwise match
|
|
233
|
+
if (termProgram === "Apple_Terminal") {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check TERM variable for known modern terminals
|
|
238
|
+
if (EXTENDED_UNDERLINE_TERMS.some((t) => term.includes(t))) {
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check TERM_PROGRAM for known terminal applications
|
|
243
|
+
if (EXTENDED_UNDERLINE_PROGRAMS.some((p) => termProgram.includes(p))) {
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Kitty sets KITTY_WINDOW_ID
|
|
248
|
+
if (process.env.KITTY_WINDOW_ID) {
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Default to false for unknown terminals
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// =============================================================================
|
|
257
|
+
// Terminal Capabilities Profile
|
|
258
|
+
// =============================================================================
|
|
259
|
+
|
|
260
|
+
export interface TerminalCaps {
|
|
261
|
+
/** Terminal program name (from TERM_PROGRAM) */
|
|
262
|
+
program: string
|
|
263
|
+
/** TERM value */
|
|
264
|
+
term: string
|
|
265
|
+
/** Color support level */
|
|
266
|
+
colorLevel: "none" | "basic" | "256" | "truecolor"
|
|
267
|
+
/** Kitty keyboard protocol supported */
|
|
268
|
+
kittyKeyboard: boolean
|
|
269
|
+
/** Kitty graphics protocol (inline images) */
|
|
270
|
+
kittyGraphics: boolean
|
|
271
|
+
/** Sixel graphics supported */
|
|
272
|
+
sixel: boolean
|
|
273
|
+
/** OSC 52 clipboard */
|
|
274
|
+
osc52: boolean
|
|
275
|
+
/** OSC 8 hyperlinks */
|
|
276
|
+
hyperlinks: boolean
|
|
277
|
+
/** OSC 9/99 notifications */
|
|
278
|
+
notifications: boolean
|
|
279
|
+
/** Bracketed paste mode */
|
|
280
|
+
bracketedPaste: boolean
|
|
281
|
+
/** SGR mouse tracking */
|
|
282
|
+
mouse: boolean
|
|
283
|
+
/** Synchronized output (DEC 2026) */
|
|
284
|
+
syncOutput: boolean
|
|
285
|
+
/** Unicode/emoji support */
|
|
286
|
+
unicode: boolean
|
|
287
|
+
/** SGR 4:x underline style subparameters (curly, dotted, dashed) */
|
|
288
|
+
underlineStyles: boolean
|
|
289
|
+
/** SGR 58 underline color */
|
|
290
|
+
underlineColor: boolean
|
|
291
|
+
/** Text-presentation emoji (⚠, ☑, ⭐) rendered as 2-wide.
|
|
292
|
+
* Modern terminals (Ghostty, iTerm, Kitty) render these at emoji width (2 cells).
|
|
293
|
+
* Terminal.app renders them at text width (1 cell). */
|
|
294
|
+
textEmojiWide: boolean
|
|
295
|
+
/** OSC 66 text sizing protocol likely supported (Kitty 0.40+, Ghostty) */
|
|
296
|
+
textSizingSupported: boolean
|
|
297
|
+
/** Heuristic: likely dark background (for theme selection) */
|
|
298
|
+
darkBackground: boolean
|
|
299
|
+
/** Heuristic: likely has Nerd Font installed (for icon selection) */
|
|
300
|
+
nerdfont: boolean
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Default capabilities (assumes modern terminal with full support).
|
|
305
|
+
*/
|
|
306
|
+
export function defaultCaps(): TerminalCaps {
|
|
307
|
+
return {
|
|
308
|
+
program: "",
|
|
309
|
+
term: "",
|
|
310
|
+
colorLevel: "truecolor",
|
|
311
|
+
kittyKeyboard: false,
|
|
312
|
+
kittyGraphics: false,
|
|
313
|
+
sixel: false,
|
|
314
|
+
osc52: false,
|
|
315
|
+
hyperlinks: false,
|
|
316
|
+
notifications: false,
|
|
317
|
+
bracketedPaste: true,
|
|
318
|
+
mouse: true,
|
|
319
|
+
syncOutput: false,
|
|
320
|
+
unicode: true,
|
|
321
|
+
underlineStyles: true,
|
|
322
|
+
underlineColor: true,
|
|
323
|
+
textEmojiWide: true,
|
|
324
|
+
textSizingSupported: false,
|
|
325
|
+
darkBackground: true,
|
|
326
|
+
nerdfont: false,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Cached result of macOS dark mode detection.
|
|
332
|
+
* Computed lazily on first access to avoid spawnSync at module load time.
|
|
333
|
+
*/
|
|
334
|
+
let cachedMacOSDarkMode: boolean | undefined
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Check if macOS is in dark mode by reading the system appearance preference.
|
|
338
|
+
* Uses `defaults read -g AppleInterfaceStyle` — returns "Dark" when dark mode
|
|
339
|
+
* is active, exits non-zero when light mode. ~2ms via spawnSync.
|
|
340
|
+
*
|
|
341
|
+
* Result is cached after first call to avoid repeated process spawns.
|
|
342
|
+
*/
|
|
343
|
+
function detectMacOSDarkMode(): boolean {
|
|
344
|
+
if (cachedMacOSDarkMode !== undefined) return cachedMacOSDarkMode
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const result = spawnSync("defaults", ["read", "-g", "AppleInterfaceStyle"], {
|
|
348
|
+
encoding: "utf-8",
|
|
349
|
+
timeout: 500,
|
|
350
|
+
})
|
|
351
|
+
cachedMacOSDarkMode = result.stdout?.trim() === "Dark"
|
|
352
|
+
} catch {
|
|
353
|
+
cachedMacOSDarkMode = false
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return cachedMacOSDarkMode
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Detect terminal capabilities from environment variables.
|
|
360
|
+
* Synchronous. Minimal I/O: may run `defaults` on macOS for Apple_Terminal.
|
|
361
|
+
*/
|
|
362
|
+
export function detectTerminalCaps(): TerminalCaps {
|
|
363
|
+
const program = process.env.TERM_PROGRAM ?? ""
|
|
364
|
+
const term = process.env.TERM ?? ""
|
|
365
|
+
const colorTerm = process.env.COLORTERM ?? ""
|
|
366
|
+
const noColor = process.env.NO_COLOR !== undefined
|
|
367
|
+
|
|
368
|
+
const isAppleTerminal = program === "Apple_Terminal"
|
|
369
|
+
|
|
370
|
+
let colorLevel: TerminalCaps["colorLevel"] = "none"
|
|
371
|
+
if (!noColor) {
|
|
372
|
+
if (isAppleTerminal) {
|
|
373
|
+
colorLevel = "256"
|
|
374
|
+
} else if (colorTerm === "truecolor" || colorTerm === "24bit") {
|
|
375
|
+
colorLevel = "truecolor"
|
|
376
|
+
} else if (term.includes("256color")) {
|
|
377
|
+
colorLevel = "256"
|
|
378
|
+
} else if (process.stdout?.isTTY) {
|
|
379
|
+
colorLevel = "basic"
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const isKitty = term === "xterm-kitty"
|
|
384
|
+
const isITerm = program === "iTerm.app"
|
|
385
|
+
const isGhostty = program === "ghostty"
|
|
386
|
+
const isWezTerm = program === "WezTerm"
|
|
387
|
+
const isAlacritty = program === "Alacritty"
|
|
388
|
+
const isFoot = term === "foot" || term === "foot-extra"
|
|
389
|
+
const isModern = isKitty || isITerm || isGhostty || isWezTerm || isFoot
|
|
390
|
+
|
|
391
|
+
// Kitty v0.40+ supports OSC 66 text sizing
|
|
392
|
+
let isKittyWithTextSizing = false
|
|
393
|
+
if (isKitty) {
|
|
394
|
+
const version = process.env.TERM_PROGRAM_VERSION ?? ""
|
|
395
|
+
const parts = version.split(".")
|
|
396
|
+
const major = Number(parts[0]) || 0
|
|
397
|
+
const minor = Number(parts[1]) || 0
|
|
398
|
+
isKittyWithTextSizing = major > 0 || (major === 0 && minor >= 40)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let darkBackground = !isAppleTerminal
|
|
402
|
+
const colorFgBg = process.env.COLORFGBG
|
|
403
|
+
if (colorFgBg) {
|
|
404
|
+
const parts = colorFgBg.split(";")
|
|
405
|
+
const bg = parseInt(parts[parts.length - 1] ?? "", 10)
|
|
406
|
+
if (!isNaN(bg)) {
|
|
407
|
+
darkBackground = bg < 7
|
|
408
|
+
}
|
|
409
|
+
} else if (isAppleTerminal) {
|
|
410
|
+
darkBackground = detectMacOSDarkMode()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let nerdfont = isModern || isAlacritty
|
|
414
|
+
const nfEnv = process.env.NERDFONT
|
|
415
|
+
if (nfEnv === "0" || nfEnv === "false") nerdfont = false
|
|
416
|
+
else if (nfEnv === "1" || nfEnv === "true") nerdfont = true
|
|
417
|
+
|
|
418
|
+
const underlineExtensions = isModern || isAlacritty
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
program,
|
|
422
|
+
term,
|
|
423
|
+
colorLevel,
|
|
424
|
+
kittyKeyboard: isKitty || isGhostty || isWezTerm || isFoot,
|
|
425
|
+
kittyGraphics: isKitty || isGhostty,
|
|
426
|
+
sixel: isFoot || isWezTerm,
|
|
427
|
+
osc52: isModern || isAlacritty,
|
|
428
|
+
hyperlinks: isModern || isAlacritty,
|
|
429
|
+
notifications: isITerm || isKitty,
|
|
430
|
+
bracketedPaste: true,
|
|
431
|
+
mouse: true,
|
|
432
|
+
syncOutput: isModern || isAlacritty,
|
|
433
|
+
unicode: true,
|
|
434
|
+
underlineStyles: underlineExtensions,
|
|
435
|
+
underlineColor: underlineExtensions,
|
|
436
|
+
textEmojiWide: !isAppleTerminal,
|
|
437
|
+
textSizingSupported: isKittyWithTextSizing, // Ghostty parses OSC 66 but doesn't render it (v1.3.0)
|
|
438
|
+
darkBackground,
|
|
439
|
+
nerdfont,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal hyperlink functions using OSC 8.
|
|
3
|
+
*
|
|
4
|
+
* OSC 8 hyperlinks create clickable links in supporting terminals.
|
|
5
|
+
* Unsupported terminals will display just the text.
|
|
6
|
+
*
|
|
7
|
+
* @see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { buildHyperlink } from "./constants"
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Hyperlink Functions
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a clickable hyperlink in supporting terminals.
|
|
18
|
+
* Falls back to showing just the text (no URL) on unsupported terminals.
|
|
19
|
+
*
|
|
20
|
+
* @param text - Display text for the link
|
|
21
|
+
* @param url - Target URL (http://, https://, file://, etc.)
|
|
22
|
+
* @returns Text wrapped in OSC 8 hyperlink escape codes
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* hyperlink('Click here', 'https://example.com')
|
|
27
|
+
* hyperlink('Open file', 'file:///path/to/file.txt')
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @note Most modern terminals support OSC 8 hyperlinks:
|
|
31
|
+
* - Ghostty, Kitty, WezTerm, iTerm2, Terminal.app, GNOME Terminal
|
|
32
|
+
* - VS Code integrated terminal
|
|
33
|
+
*/
|
|
34
|
+
export function hyperlink(text: string, url: string): string {
|
|
35
|
+
// Most modern terminals support OSC 8, so we emit it unconditionally.
|
|
36
|
+
// Unsupported terminals will just show the text.
|
|
37
|
+
return buildHyperlink(text, url)
|
|
38
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI primitives — Term factory, styling, detection, underlines, hyperlinks.
|
|
3
|
+
*
|
|
4
|
+
* Merged from the former `@silvery/ansi` package into `@silvery/term`.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { createTerm, patchConsole } from '@silvery/term'
|
|
8
|
+
*
|
|
9
|
+
* using term = createTerm()
|
|
10
|
+
* term.red('error')
|
|
11
|
+
* term.bold.green('success')
|
|
12
|
+
*
|
|
13
|
+
* using patched = patchConsole(console)
|
|
14
|
+
* patched.subscribe(() => console.log('new entry'))
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Term API (NewWay)
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export { createTerm } from "./term"
|
|
25
|
+
export type { Term, StyleChain, TermState, TermEvents } from "./term"
|
|
26
|
+
|
|
27
|
+
import { createTerm as _createTerm } from "./term"
|
|
28
|
+
import type { Term } from "./term"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default term instance for convenience, lazily initialized on first access.
|
|
32
|
+
* This avoids running terminal detection (including spawnSync) at import time.
|
|
33
|
+
* Use this for simple scripts. For apps, prefer createTerm() with `using`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { term } from '@silvery/term'
|
|
38
|
+
*
|
|
39
|
+
* console.log(term.green('success'))
|
|
40
|
+
* if (term.hasColor()) { ... }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
let _lazyTerm: Term | undefined
|
|
44
|
+
export const term: Term = new Proxy({} as Term, {
|
|
45
|
+
get(_target, prop, receiver) {
|
|
46
|
+
if (!_lazyTerm) _lazyTerm = _createTerm()
|
|
47
|
+
return Reflect.get(_lazyTerm, prop, receiver)
|
|
48
|
+
},
|
|
49
|
+
apply(_target, thisArg, args) {
|
|
50
|
+
if (!_lazyTerm) _lazyTerm = _createTerm()
|
|
51
|
+
return Reflect.apply(_lazyTerm as unknown as (...args: unknown[]) => unknown, thisArg, args)
|
|
52
|
+
},
|
|
53
|
+
has(_target, prop) {
|
|
54
|
+
if (!_lazyTerm) _lazyTerm = _createTerm()
|
|
55
|
+
return Reflect.has(_lazyTerm, prop)
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export { patchConsole } from "./patch-console"
|
|
60
|
+
export type { PatchedConsole, PatchConsoleOptions, ConsoleStats } from "./patch-console"
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Types
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
export type {
|
|
67
|
+
UnderlineStyle,
|
|
68
|
+
RGB,
|
|
69
|
+
ColorLevel,
|
|
70
|
+
Color,
|
|
71
|
+
AnsiColorName,
|
|
72
|
+
StyleOptions,
|
|
73
|
+
ConsoleMethod,
|
|
74
|
+
ConsoleEntry,
|
|
75
|
+
CreateTermOptions,
|
|
76
|
+
TermEmulator,
|
|
77
|
+
TermEmulatorBackend,
|
|
78
|
+
TermScreen,
|
|
79
|
+
} from "./types"
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// Detection Functions
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
detectCursor,
|
|
87
|
+
detectInput,
|
|
88
|
+
detectColor,
|
|
89
|
+
detectUnicode,
|
|
90
|
+
detectExtendedUnderline,
|
|
91
|
+
detectTerminalCaps,
|
|
92
|
+
defaultCaps,
|
|
93
|
+
} from "./detection"
|
|
94
|
+
export type { TerminalCaps } from "./detection"
|
|
95
|
+
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// Utilities
|
|
98
|
+
// =============================================================================
|
|
99
|
+
|
|
100
|
+
export { ANSI_REGEX, stripAnsi, displayLength } from "./utils"
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Underline Functions
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
underline,
|
|
108
|
+
curlyUnderline,
|
|
109
|
+
dottedUnderline,
|
|
110
|
+
dashedUnderline,
|
|
111
|
+
doubleUnderline,
|
|
112
|
+
underlineColor,
|
|
113
|
+
styledUnderline,
|
|
114
|
+
} from "./underline"
|
|
115
|
+
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Hyperlink Functions
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
export { hyperlink } from "./hyperlink"
|
|
121
|
+
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// ANSI Terminal Control Helpers
|
|
124
|
+
// =============================================================================
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
enterAltScreen,
|
|
128
|
+
leaveAltScreen,
|
|
129
|
+
clearScreen,
|
|
130
|
+
clearLine,
|
|
131
|
+
cursorTo,
|
|
132
|
+
cursorHome,
|
|
133
|
+
cursorHide,
|
|
134
|
+
cursorShow,
|
|
135
|
+
cursorStyle,
|
|
136
|
+
setTitle,
|
|
137
|
+
enableMouse,
|
|
138
|
+
disableMouse,
|
|
139
|
+
enableBracketedPaste,
|
|
140
|
+
disableBracketedPaste,
|
|
141
|
+
enableSyncUpdate,
|
|
142
|
+
disableSyncUpdate,
|
|
143
|
+
setScrollRegion,
|
|
144
|
+
resetScrollRegion,
|
|
145
|
+
scrollUp,
|
|
146
|
+
scrollDown,
|
|
147
|
+
enableKittyKeyboard,
|
|
148
|
+
disableKittyKeyboard,
|
|
149
|
+
} from "./ansi"
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Background Override — Compose styled text inside Box with backgroundColor
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* SGR code recognized by silvery to signal intentional bg override.
|
|
157
|
+
* When text is wrapped with this, silvery won't warn/throw about chalk bg + silvery bg conflicts.
|
|
158
|
+
* Exported for silvery to detect this marker in text content.
|
|
159
|
+
*/
|
|
160
|
+
export const BG_OVERRIDE_CODE = 9999
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Compose styled text with an explicit background inside a Box that has its own
|
|
164
|
+
* `backgroundColor`. This is the correct way to layer chalk/term background
|
|
165
|
+
* colors on top of an silvery Box background.
|
|
166
|
+
*
|
|
167
|
+
* Without `bgOverride`, silvery throws (by default) when it detects both an ANSI
|
|
168
|
+
* background in the text content AND a `backgroundColor` prop on an ancestor
|
|
169
|
+
* Box, because the two conflict and produce visual artifacts (the ANSI bg
|
|
170
|
+
* only covers the text, leaving gaps at line edges).
|
|
171
|
+
*
|
|
172
|
+
* `bgOverride` wraps the text with a private SGR marker that tells silvery
|
|
173
|
+
* "this background is intentional — don't throw." Use it when you need
|
|
174
|
+
* pixel-precise background control within a styled container.
|
|
175
|
+
*
|
|
176
|
+
* @param text - Text containing ANSI background codes (e.g. from `chalk.bgBlack()`)
|
|
177
|
+
* @returns The same text prefixed with a marker SGR code
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```tsx
|
|
181
|
+
* import { bgOverride } from '@silvery/chalk'
|
|
182
|
+
*
|
|
183
|
+
* // Without bgOverride — silvery throws:
|
|
184
|
+
* <Box backgroundColor="cyan">
|
|
185
|
+
* <Text>{chalk.bgBlack('text')}</Text> // Error!
|
|
186
|
+
* </Box>
|
|
187
|
+
*
|
|
188
|
+
* // With bgOverride — explicitly allowed:
|
|
189
|
+
* <Box backgroundColor="cyan">
|
|
190
|
+
* <Text>{bgOverride(chalk.bgBlack('text'))}</Text> // OK
|
|
191
|
+
* </Box>
|
|
192
|
+
*
|
|
193
|
+
* // Also works with term styling:
|
|
194
|
+
* <Box backgroundColor="$surface-bg">
|
|
195
|
+
* <Text>{bgOverride(term.bgRgb(30, 30, 30)('highlighted'))}</Text>
|
|
196
|
+
* </Box>
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export function bgOverride(text: string): string {
|
|
200
|
+
return `\x1b[${BG_OVERRIDE_CODE}m${text}`
|
|
201
|
+
}
|