@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,724 @@
1
+ /**
2
+ * Term interface and createTerm() factory.
3
+ *
4
+ * Term is the central abstraction for terminal interaction:
5
+ * - Detection: hasCursor(), hasInput(), hasColor(), hasUnicode()
6
+ * - Dimensions: cols, rows
7
+ * - I/O: stdout, stdin, write(), writeLine()
8
+ * - Provider: getState(), subscribe(), events() — typed key/mouse/resize
9
+ * - Styling: Chainable styles via Proxy (term.bold.red('text'))
10
+ * - Lifecycle: Disposable pattern via Symbol.dispose
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Styling
15
+ * const term = createTerm()
16
+ * console.log(term.bold.red('error'))
17
+ *
18
+ * // Full terminal app
19
+ * using term = createTerm()
20
+ * await run(<App />, term)
21
+ * ```
22
+ */
23
+
24
+ import { Chalk, type ChalkInstance } from "chalk"
25
+ import type {
26
+ ColorLevel,
27
+ CreateTermOptions,
28
+ TermEmulator,
29
+ TermEmulatorBackend,
30
+ TermScreen,
31
+ TerminalCaps,
32
+ } from "./types"
33
+ import { defaultCaps, detectColor, detectCursor, detectInput, detectTerminalCaps, detectUnicode } from "./detection"
34
+ import type { ProviderEvent } from "../runtime/types"
35
+ import { createTermProvider, type TermState, type TermEvents } from "../runtime/term-provider"
36
+
37
+ // Re-export Provider-related types for convenience
38
+ export type { TermState, TermEvents } from "../runtime/term-provider"
39
+
40
+ // =============================================================================
41
+ // ANSI Utilities
42
+ // =============================================================================
43
+
44
+ /**
45
+ * ANSI escape code pattern for stripping.
46
+ */
47
+ const ANSI_REGEX =
48
+ /\x1b\[[0-9;:]*m|\x9b[0-9;:]*m|\x1b\]8;;[^\x07\x1b]*(?:\x07|\x1b\\)|\x9d8;;[^\x07\x1b\x9c]*(?:\x07|\x1b\\|\x9c)/g
49
+
50
+ /**
51
+ * Strip all ANSI escape codes from a string.
52
+ */
53
+ function stripAnsi(text: string): string {
54
+ return text.replace(ANSI_REGEX, "")
55
+ }
56
+
57
+ // =============================================================================
58
+ // Style Chain Types
59
+ // =============================================================================
60
+
61
+ /**
62
+ * All chalk style method names that can be chained.
63
+ */
64
+ type ChalkStyleName =
65
+ // Modifiers
66
+ | "reset"
67
+ | "bold"
68
+ | "dim"
69
+ | "italic"
70
+ | "underline"
71
+ | "overline"
72
+ | "inverse"
73
+ | "hidden"
74
+ | "strikethrough"
75
+ | "visible"
76
+ // Foreground colors
77
+ | "black"
78
+ | "red"
79
+ | "green"
80
+ | "yellow"
81
+ | "blue"
82
+ | "magenta"
83
+ | "cyan"
84
+ | "white"
85
+ | "gray"
86
+ | "grey"
87
+ | "blackBright"
88
+ | "redBright"
89
+ | "greenBright"
90
+ | "yellowBright"
91
+ | "blueBright"
92
+ | "magentaBright"
93
+ | "cyanBright"
94
+ | "whiteBright"
95
+ // Background colors
96
+ | "bgBlack"
97
+ | "bgRed"
98
+ | "bgGreen"
99
+ | "bgYellow"
100
+ | "bgBlue"
101
+ | "bgMagenta"
102
+ | "bgCyan"
103
+ | "bgWhite"
104
+ | "bgGray"
105
+ | "bgGrey"
106
+ | "bgBlackBright"
107
+ | "bgRedBright"
108
+ | "bgGreenBright"
109
+ | "bgYellowBright"
110
+ | "bgBlueBright"
111
+ | "bgMagentaBright"
112
+ | "bgCyanBright"
113
+ | "bgWhiteBright"
114
+
115
+ /**
116
+ * StyleChain provides chainable styling methods.
117
+ * Each property returns a new chain, and the chain is callable.
118
+ */
119
+ export type StyleChain = {
120
+ /**
121
+ * Apply styles to text.
122
+ */
123
+ (text: string): string
124
+ (template: TemplateStringsArray, ...values: unknown[]): string
125
+
126
+ /**
127
+ * RGB foreground color.
128
+ */
129
+ rgb(r: number, g: number, b: number): StyleChain
130
+
131
+ /**
132
+ * Hex foreground color.
133
+ */
134
+ hex(color: string): StyleChain
135
+
136
+ /**
137
+ * 256-color foreground.
138
+ */
139
+ ansi256(code: number): StyleChain
140
+
141
+ /**
142
+ * RGB background color.
143
+ */
144
+ bgRgb(r: number, g: number, b: number): StyleChain
145
+
146
+ /**
147
+ * Hex background color.
148
+ */
149
+ bgHex(color: string): StyleChain
150
+
151
+ /**
152
+ * 256-color background.
153
+ */
154
+ bgAnsi256(code: number): StyleChain
155
+ } & {
156
+ /**
157
+ * Chainable style properties.
158
+ */
159
+ readonly [K in ChalkStyleName]: StyleChain
160
+ }
161
+
162
+ // =============================================================================
163
+ // Term Interface
164
+ // =============================================================================
165
+
166
+ /**
167
+ * Term — the central abstraction for terminal interaction.
168
+ *
169
+ * Term is both a styling helper (chainable ANSI via Proxy) and a
170
+ * Provider (state + typed events). Pass it to `run()` or `createApp()`.
171
+ *
172
+ * Provides:
173
+ * - Capability detection (cached on creation)
174
+ * - Dimensions (live from stream)
175
+ * - I/O (stdout, stdin, write, writeLine)
176
+ * - Provider (getState, subscribe, events — key/mouse/resize)
177
+ * - Styling (chainable via Proxy)
178
+ * - Disposable lifecycle
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * using term = createTerm()
183
+ * await run(<App />, term)
184
+ * ```
185
+ */
186
+ export interface Term extends Disposable, StyleChain {
187
+ // -------------------------------------------------------------------------
188
+ // Detection Methods
189
+ // -------------------------------------------------------------------------
190
+
191
+ /**
192
+ * Check if terminal supports cursor control (repositioning).
193
+ * Returns false for dumb terminals and piped output.
194
+ */
195
+ hasCursor(): boolean
196
+
197
+ /**
198
+ * Check if terminal can read raw keystrokes.
199
+ * Requires stdin to be a TTY with raw mode support.
200
+ */
201
+ hasInput(): boolean
202
+
203
+ /**
204
+ * Check color level supported by terminal.
205
+ * Returns null if no color support.
206
+ */
207
+ hasColor(): ColorLevel | null
208
+
209
+ /**
210
+ * Check if terminal can render unicode symbols.
211
+ */
212
+ hasUnicode(): boolean
213
+
214
+ /**
215
+ * Terminal capabilities profile.
216
+ * Detected when stdin is a TTY, undefined otherwise.
217
+ * Override via createTerm({ caps: { ... } }).
218
+ */
219
+ readonly caps: TerminalCaps | undefined
220
+
221
+ // -------------------------------------------------------------------------
222
+ // Dimensions
223
+ // -------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Terminal width in columns.
227
+ * Undefined if not a TTY or dimensions unavailable.
228
+ */
229
+ readonly cols: number | undefined
230
+
231
+ /**
232
+ * Terminal height in rows.
233
+ * Undefined if not a TTY or dimensions unavailable.
234
+ */
235
+ readonly rows: number | undefined
236
+
237
+ // -------------------------------------------------------------------------
238
+ // Streams
239
+ // -------------------------------------------------------------------------
240
+
241
+ /**
242
+ * Output stream (defaults to process.stdout).
243
+ */
244
+ readonly stdout: NodeJS.WriteStream
245
+
246
+ /**
247
+ * Input stream (defaults to process.stdin).
248
+ */
249
+ readonly stdin: NodeJS.ReadStream
250
+
251
+ // -------------------------------------------------------------------------
252
+ // I/O Methods
253
+ // -------------------------------------------------------------------------
254
+
255
+ /**
256
+ * Write string to stdout.
257
+ */
258
+ write(str: string): void
259
+
260
+ /**
261
+ * Write string followed by newline to stdout.
262
+ */
263
+ writeLine(str: string): void
264
+
265
+ // -------------------------------------------------------------------------
266
+ // Provider (state + events)
267
+ // -------------------------------------------------------------------------
268
+
269
+ /**
270
+ * Get current terminal state (dimensions).
271
+ * Always returns defined values (falls back to 80x24).
272
+ */
273
+ getState(): TermState
274
+
275
+ /**
276
+ * Subscribe to terminal state changes (resize).
277
+ * Returns unsubscribe function.
278
+ */
279
+ subscribe(listener: (state: TermState) => void): () => void
280
+
281
+ /**
282
+ * Event stream — yields typed key, mouse, and resize events.
283
+ * Enables raw mode on stdin when iterated. Cleans up on return.
284
+ */
285
+ events(): AsyncIterable<ProviderEvent<TermEvents>>
286
+
287
+ // -------------------------------------------------------------------------
288
+ // Utilities
289
+ // -------------------------------------------------------------------------
290
+
291
+ /**
292
+ * Strip ANSI escape codes from string.
293
+ */
294
+ stripAnsi(str: string): string
295
+
296
+ // -------------------------------------------------------------------------
297
+ // Terminal Emulator (present when created with a termless backend)
298
+ // -------------------------------------------------------------------------
299
+
300
+ /**
301
+ * Visible screen region. Only available when created with a terminal backend.
302
+ * Provides getText(), getLines(), containsText() for assertions.
303
+ */
304
+ readonly screen?: TermScreen
305
+
306
+ /**
307
+ * Scrollback region. Only available when created with a terminal backend.
308
+ * Provides getText(), getLines(), containsText() for assertions.
309
+ */
310
+ readonly scrollback?: TermScreen
311
+
312
+ /**
313
+ * Resize the terminal emulator. Only available when created with a terminal backend.
314
+ * Resizes the underlying emulator and triggers a re-render in the app.
315
+ */
316
+ resize?(cols: number, rows: number): void
317
+ }
318
+
319
+ // =============================================================================
320
+ // createTerm Factory
321
+ // =============================================================================
322
+
323
+ /**
324
+ * Create a Term instance.
325
+ *
326
+ * Factory overloads:
327
+ * - `createTerm()` — Node.js terminal (auto-detect from process.stdin/stdout)
328
+ * - `createTerm({ stdout, stdin, ... })` — Node.js with custom streams/overrides
329
+ * - `createTerm({ cols, rows })` — Headless for testing (no I/O, fixed dims)
330
+ * - `createTerm(backend, { cols, rows })` — Terminal emulator backend (termless) for testing
331
+ * - `createTerm(emulator)` — Pre-created termless Terminal
332
+ *
333
+ * Detection results are cached at creation time for consistency.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * // Full terminal app
338
+ * using term = createTerm()
339
+ * await run(<App />, term)
340
+ *
341
+ * // Headless for testing
342
+ * const term = createTerm({ cols: 80, rows: 24 })
343
+ *
344
+ * // Terminal emulator (termless) for full ANSI testing
345
+ * using term = createTerm(createXtermBackend(), { cols: 80, rows: 24 })
346
+ * await run(<App />, term)
347
+ * expect(term.screen).toContainText("Hello")
348
+ *
349
+ * // Custom streams
350
+ * const term = createTerm({ stdout: customStream })
351
+ * ```
352
+ */
353
+ export function createTerm(options?: CreateTermOptions): Term
354
+ export function createTerm(dims: { cols: number; rows: number }): Term
355
+ export function createTerm(backend: TermEmulatorBackend, dims: { cols: number; rows: number }): Term
356
+ export function createTerm(emulator: TermEmulator): Term
357
+ export function createTerm(
358
+ first?: CreateTermOptions | { cols: number; rows: number } | TermEmulator | TermEmulatorBackend,
359
+ second?: { cols: number; rows: number },
360
+ ): Term {
361
+ // Two-arg: createTerm(backend, { cols, rows }) — raw backend + dims
362
+ if (second && first && isTermBackend(first)) {
363
+ // Lazy require — @termless/core is an optional dependency, only needed
364
+ // for emulator backends. Using a variable prevents static analysis from
365
+ // trying to resolve it at bundle/parse time.
366
+ const mod = "@termless/core"
367
+ const { createTerminal } = require(mod) as {
368
+ createTerminal: (opts: { backend: TermEmulatorBackend; cols: number; rows: number }) => TermEmulator
369
+ }
370
+ const emulator = createTerminal({ backend: first as TermEmulatorBackend, ...second })
371
+ return createBackendTerm(emulator)
372
+ }
373
+ // Detect terminal emulator (termless Terminal): has feed + screen
374
+ if (first && isTermEmulator(first)) {
375
+ return createBackendTerm(first as TermEmulator)
376
+ }
377
+ // Detect headless dims: has cols + rows but no stdout/stdin/color/caps
378
+ if (first && isHeadlessDims(first)) {
379
+ return createHeadlessTerm(first as { cols: number; rows: number })
380
+ }
381
+ return createNodeTerm((first as CreateTermOptions) ?? {})
382
+ }
383
+
384
+ /** Detect terminal emulator (termless Terminal): has feed() + screen */
385
+ function isTermEmulator(obj: unknown): obj is TermEmulator {
386
+ if (typeof obj !== "object" || obj === null) return false
387
+ const o = obj as Record<string, unknown>
388
+ return typeof o.feed === "function" && typeof o.screen === "object" && o.screen !== null
389
+ }
390
+
391
+ /** Detect terminal emulator backend (termless TerminalBackend): has init() + name */
392
+ function isTermBackend(obj: unknown): obj is TermEmulatorBackend {
393
+ if (typeof obj !== "object" || obj === null) return false
394
+ const o = obj as Record<string, unknown>
395
+ return typeof o.init === "function" && typeof o.name === "string" && typeof o.destroy === "function"
396
+ }
397
+
398
+ /** Detect headless dims: has cols and rows numbers, no stdout */
399
+ function isHeadlessDims(obj: unknown): boolean {
400
+ if (typeof obj !== "object" || obj === null) return false
401
+ const o = obj as Record<string, unknown>
402
+ return typeof o.cols === "number" && typeof o.rows === "number" && !("stdout" in o) && !("stdin" in o)
403
+ }
404
+
405
+ /**
406
+ * Create a Node.js terminal with full Provider capabilities.
407
+ */
408
+ function createNodeTerm(options: CreateTermOptions): Term {
409
+ const stdout = options.stdout ?? process.stdout
410
+ const stdin = options.stdin ?? process.stdin
411
+
412
+ // Cache detection results
413
+ const cachedCursor = options.cursor ?? detectCursor(stdout)
414
+ const cachedInput = detectInput(stdin)
415
+ const cachedColor = options.color !== undefined ? options.color : detectColor(stdout)
416
+ const cachedUnicode = options.unicode ?? detectUnicode()
417
+
418
+ // Detect terminal capabilities (only when interactive)
419
+ const detectedCaps = options.caps
420
+ ? { ...defaultCaps(), ...options.caps }
421
+ : stdin.isTTY
422
+ ? detectTerminalCaps()
423
+ : undefined
424
+
425
+ // Create chalk instance with appropriate color level
426
+ const chalkLevel = cachedColor === null ? 0 : cachedColor === "basic" ? 1 : cachedColor === "256" ? 2 : 3
427
+ const chalkInstance = new Chalk({ level: chalkLevel })
428
+
429
+ // Lazy Provider — only created when getState/subscribe/events is called.
430
+ // This avoids adding a resize listener for styling-only usage.
431
+ let provider: ReturnType<typeof createTermProvider> | null = null
432
+ const getProvider = () => {
433
+ if (!provider) {
434
+ provider = createTermProvider(stdin, stdout, {
435
+ cols: stdout.columns || 80,
436
+ rows: stdout.rows || 24,
437
+ })
438
+ }
439
+ return provider
440
+ }
441
+
442
+ // Base term object with methods
443
+ const termBase = {
444
+ // Detection methods
445
+ hasCursor: () => cachedCursor,
446
+ hasInput: () => cachedInput,
447
+ hasColor: () => cachedColor,
448
+ hasUnicode: () => cachedUnicode,
449
+
450
+ // Terminal capabilities
451
+ caps: detectedCaps,
452
+
453
+ // Streams
454
+ stdout,
455
+ stdin,
456
+
457
+ // I/O methods
458
+ write: (str: string) => {
459
+ stdout.write(str)
460
+ },
461
+ writeLine: (str: string) => {
462
+ stdout.write(str + "\n")
463
+ },
464
+
465
+ // Provider methods (lazy — Provider created on first access)
466
+ getState: (): TermState => getProvider().getState(),
467
+ subscribe: (listener: (state: TermState) => void): (() => void) => getProvider().subscribe(listener),
468
+ events: (): AsyncIterable<ProviderEvent<TermEvents>> => getProvider().events(),
469
+
470
+ // Utilities
471
+ stripAnsi,
472
+
473
+ // Disposable — also disposes the Provider if created
474
+ [Symbol.dispose]: () => {
475
+ if (provider) provider[Symbol.dispose]()
476
+ },
477
+ }
478
+
479
+ // Create proxy that wraps chalk for styling
480
+ const term = createStyleProxy(chalkInstance, termBase)
481
+
482
+ // Add dynamic dimension getters
483
+ Object.defineProperty(term, "cols", {
484
+ get: () => (stdout.isTTY ? stdout.columns : undefined),
485
+ enumerable: true,
486
+ })
487
+
488
+ Object.defineProperty(term, "rows", {
489
+ get: () => (stdout.isTTY ? stdout.rows : undefined),
490
+ enumerable: true,
491
+ })
492
+
493
+ return term as Term
494
+ }
495
+
496
+ /**
497
+ * Create a headless terminal for testing — no I/O, fixed dimensions.
498
+ */
499
+ function createHeadlessTerm(dims: { cols: number; rows: number }): Term {
500
+ const state: TermState = { cols: dims.cols, rows: dims.rows }
501
+ let disposed = false
502
+ const controller = new AbortController()
503
+
504
+ const chalkInstance = new Chalk({ level: 0 })
505
+
506
+ const termBase = {
507
+ hasCursor: () => false,
508
+ hasInput: () => false,
509
+ hasColor: () => null as ColorLevel | null,
510
+ hasUnicode: () => false,
511
+ caps: undefined as TerminalCaps | undefined,
512
+ stdout: process.stdout,
513
+ stdin: process.stdin,
514
+ write: () => {},
515
+ writeLine: () => {},
516
+ getState: (): TermState => state,
517
+ subscribe: (): (() => void) => () => {},
518
+ async *events(): AsyncIterable<ProviderEvent<TermEvents>> {
519
+ if (disposed) return
520
+ await new Promise<void>((resolve) => {
521
+ controller.signal.addEventListener("abort", () => resolve(), { once: true })
522
+ })
523
+ },
524
+ stripAnsi,
525
+ [Symbol.dispose]: () => {
526
+ if (disposed) return
527
+ disposed = true
528
+ controller.abort()
529
+ },
530
+ }
531
+
532
+ const term = createStyleProxy(chalkInstance, termBase)
533
+
534
+ Object.defineProperty(term, "cols", { get: () => dims.cols, enumerable: true })
535
+ Object.defineProperty(term, "rows", { get: () => dims.rows, enumerable: true })
536
+
537
+ return term as Term
538
+ }
539
+
540
+ /**
541
+ * Create a terminal backed by a termless emulator — real ANSI processing, screen/scrollback.
542
+ */
543
+ function createBackendTerm(emulator: TermEmulator): Term {
544
+ let disposed = false
545
+ const controller = new AbortController()
546
+
547
+ const chalkInstance = new Chalk({ level: 3 }) // Emulators support truecolor
548
+
549
+ // Subscriber support for resize notifications
550
+ const listeners = new Set<(state: TermState) => void>()
551
+
552
+ // Event queue for resize events (consumed by events() async generator)
553
+ const eventQueue: ProviderEvent<TermEvents>[] = []
554
+ let eventResolve: (() => void) | null = null
555
+
556
+ const termBase = {
557
+ hasCursor: () => true,
558
+ hasInput: () => false,
559
+ hasColor: () => "truecolor" as ColorLevel | null,
560
+ hasUnicode: () => true,
561
+ caps: undefined as TerminalCaps | undefined,
562
+ stdout: process.stdout,
563
+ stdin: process.stdin,
564
+ write: (str: string) => emulator.feed(str),
565
+ writeLine: (str: string) => emulator.feed(str + "\n"),
566
+ getState: (): TermState => ({ cols: emulator.cols, rows: emulator.rows }),
567
+ subscribe: (listener: (state: TermState) => void): (() => void) => {
568
+ listeners.add(listener)
569
+ return () => listeners.delete(listener)
570
+ },
571
+ async *events(): AsyncIterable<ProviderEvent<TermEvents>> {
572
+ if (disposed) return
573
+ while (!disposed && !controller.signal.aborted) {
574
+ if (eventQueue.length === 0) {
575
+ await new Promise<void>((resolve) => {
576
+ eventResolve = resolve
577
+ controller.signal.addEventListener("abort", () => resolve(), { once: true })
578
+ })
579
+ }
580
+ if (disposed || controller.signal.aborted) break
581
+ while (eventQueue.length > 0) {
582
+ yield eventQueue.shift()!
583
+ }
584
+ }
585
+ },
586
+ /** Resize the emulator and notify listeners/events */
587
+ resize: (cols: number, rows: number) => {
588
+ emulator.resize(cols, rows)
589
+ const state: TermState = { cols, rows }
590
+ listeners.forEach((l) => l(state))
591
+ eventQueue.push({ type: "resize", data: { cols, rows } })
592
+ if (eventResolve) {
593
+ const resolve = eventResolve
594
+ eventResolve = null
595
+ resolve()
596
+ }
597
+ },
598
+ stripAnsi,
599
+ // Store emulator for run() to detect and auto-wire writable
600
+ _emulator: emulator,
601
+ [Symbol.dispose]: () => {
602
+ if (disposed) return
603
+ disposed = true
604
+ controller.abort()
605
+ listeners.clear()
606
+ emulator.close().catch(() => {})
607
+ },
608
+ }
609
+
610
+ // Add getters on termBase — Proxy intercepts all property access through termBase first,
611
+ // so Object.defineProperty on the Proxy result won't work for these.
612
+ Object.defineProperty(termBase, "cols", { get: () => emulator.cols, enumerable: true })
613
+ Object.defineProperty(termBase, "rows", { get: () => emulator.rows, enumerable: true })
614
+ Object.defineProperty(termBase, "screen", { get: () => emulator.screen, enumerable: true })
615
+ Object.defineProperty(termBase, "scrollback", {
616
+ get: () => emulator.scrollback,
617
+ enumerable: true,
618
+ })
619
+
620
+ const term = createStyleProxy(chalkInstance, termBase)
621
+
622
+ return term as Term
623
+ }
624
+
625
+ // =============================================================================
626
+ // Style Proxy Implementation
627
+ // =============================================================================
628
+
629
+ /**
630
+ * Create a proxy that combines term methods with chalk styling.
631
+ *
632
+ * The proxy makes the term object:
633
+ * - Callable: term('text') applies current styles
634
+ * - Chainable: term.bold.red('text') chains styles
635
+ */
636
+ function createStyleProxy(chalkInstance: ChalkInstance, termBase: object): Term {
637
+ return createChainProxy(chalkInstance, termBase)
638
+ }
639
+
640
+ /**
641
+ * Create a chainable proxy that wraps a chalk instance.
642
+ */
643
+ function createChainProxy(currentChalk: ChalkInstance, termBase: object): Term {
644
+ const handler: ProxyHandler<ChalkInstance> = {
645
+ // Make the proxy callable
646
+ apply(_target, _thisArg, args) {
647
+ // Handle both regular calls and template literals
648
+ if (args.length === 1 && typeof args[0] === "string") {
649
+ return currentChalk(args[0])
650
+ }
651
+ // Template literal call
652
+ if (args.length > 0 && Array.isArray(args[0]) && "raw" in args[0]) {
653
+ return currentChalk(args[0] as TemplateStringsArray, ...args.slice(1))
654
+ }
655
+ return currentChalk(String(args[0] ?? ""))
656
+ },
657
+
658
+ // Handle property access for chaining
659
+ get(target, prop, receiver) {
660
+ // Check termBase first for term-specific methods/properties
661
+ if (prop in termBase) {
662
+ const value = (termBase as Record<string | symbol, unknown>)[prop]
663
+ // Return methods bound to termBase, or values directly
664
+ if (typeof value === "function") {
665
+ return value
666
+ }
667
+ return value
668
+ }
669
+
670
+ // Handle symbol properties
671
+ if (typeof prop === "symbol") {
672
+ if (prop === Symbol.dispose) {
673
+ return (termBase as Record<symbol, unknown>)[Symbol.dispose]
674
+ }
675
+ return Reflect.get(target, prop, receiver)
676
+ }
677
+
678
+ // Handle chalk methods that take arguments and return a new chain
679
+ if (prop === "rgb" || prop === "bgRgb") {
680
+ return (r: number, g: number, b: number) => {
681
+ const newChalk = currentChalk[prop](r, g, b) as ChalkInstance
682
+ return createChainProxy(newChalk, termBase)
683
+ }
684
+ }
685
+
686
+ if (prop === "hex" || prop === "bgHex") {
687
+ return (color: string) => {
688
+ const newChalk = currentChalk[prop](color) as ChalkInstance
689
+ return createChainProxy(newChalk, termBase)
690
+ }
691
+ }
692
+
693
+ if (prop === "ansi256" || prop === "bgAnsi256") {
694
+ return (code: number) => {
695
+ const newChalk = currentChalk[prop](code) as ChalkInstance
696
+ return createChainProxy(newChalk, termBase)
697
+ }
698
+ }
699
+
700
+ // Handle style properties (bold, red, etc.) - return new chain
701
+ const chalkProp = currentChalk[prop as keyof ChalkInstance]
702
+ if (chalkProp !== undefined) {
703
+ // If it's a chalk chain property, wrap it in a new proxy
704
+ if (typeof chalkProp === "function" || typeof chalkProp === "object") {
705
+ return createChainProxy(chalkProp as ChalkInstance, termBase)
706
+ }
707
+ return chalkProp
708
+ }
709
+
710
+ return undefined
711
+ },
712
+
713
+ // Report that we have term properties
714
+ has(_target, prop) {
715
+ if (prop in termBase) return true
716
+ if (typeof prop === "string" && prop in currentChalk) return true
717
+ return false
718
+ },
719
+ }
720
+
721
+ // Use a function as the proxy target so it's callable
722
+ const proxyTarget = Object.assign(function () {}, currentChalk)
723
+ return new Proxy(proxyTarget, handler) as unknown as Term
724
+ }