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