@silvery/term 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +54 -0
- package/src/adapters/canvas-adapter.ts +356 -0
- package/src/adapters/dom-adapter.ts +452 -0
- package/src/adapters/flexily-zero-adapter.ts +368 -0
- package/src/adapters/terminal-adapter.ts +305 -0
- package/src/adapters/yoga-adapter.ts +370 -0
- package/src/ansi/ansi.ts +251 -0
- package/src/ansi/constants.ts +76 -0
- package/src/ansi/detection.ts +441 -0
- package/src/ansi/hyperlink.ts +38 -0
- package/src/ansi/index.ts +201 -0
- package/src/ansi/patch-console.ts +159 -0
- package/src/ansi/sgr-codes.ts +34 -0
- package/src/ansi/storybook.ts +209 -0
- package/src/ansi/term.ts +724 -0
- package/src/ansi/types.ts +202 -0
- package/src/ansi/underline.ts +156 -0
- package/src/ansi/utils.ts +65 -0
- package/src/ansi-sanitize.ts +509 -0
- package/src/app.ts +571 -0
- package/src/bound-term.ts +94 -0
- package/src/bracketed-paste.ts +75 -0
- package/src/browser-renderer.ts +174 -0
- package/src/buffer.ts +1984 -0
- package/src/clipboard.ts +74 -0
- package/src/cursor-query.ts +85 -0
- package/src/device-attrs.ts +228 -0
- package/src/devtools.ts +123 -0
- package/src/dom/index.ts +194 -0
- package/src/errors.ts +39 -0
- package/src/focus-reporting.ts +48 -0
- package/src/hit-registry-core.ts +228 -0
- package/src/hit-registry.ts +176 -0
- package/src/index.ts +458 -0
- package/src/input.ts +119 -0
- package/src/inspector.ts +155 -0
- package/src/kitty-detect.ts +95 -0
- package/src/kitty-manager.ts +160 -0
- package/src/layout-engine.ts +296 -0
- package/src/layout.ts +26 -0
- package/src/measurer.ts +74 -0
- package/src/mode-query.ts +106 -0
- package/src/mouse-events.ts +419 -0
- package/src/mouse.ts +83 -0
- package/src/non-tty.ts +223 -0
- package/src/osc-markers.ts +32 -0
- package/src/osc-palette.ts +169 -0
- package/src/output.ts +406 -0
- package/src/pane-manager.ts +248 -0
- package/src/pipeline/CLAUDE.md +587 -0
- package/src/pipeline/content-phase-adapter.ts +976 -0
- package/src/pipeline/content-phase.ts +1765 -0
- package/src/pipeline/helpers.ts +42 -0
- package/src/pipeline/index.ts +416 -0
- package/src/pipeline/layout-phase.ts +686 -0
- package/src/pipeline/measure-phase.ts +198 -0
- package/src/pipeline/measure-stats.ts +21 -0
- package/src/pipeline/output-phase.ts +2593 -0
- package/src/pipeline/render-box.ts +343 -0
- package/src/pipeline/render-helpers.ts +243 -0
- package/src/pipeline/render-text.ts +1255 -0
- package/src/pipeline/types.ts +161 -0
- package/src/pipeline.ts +29 -0
- package/src/pixel-size.ts +119 -0
- package/src/render-adapter.ts +179 -0
- package/src/renderer.ts +1330 -0
- package/src/runtime/create-app.tsx +1845 -0
- package/src/runtime/create-buffer.ts +18 -0
- package/src/runtime/create-runtime.ts +325 -0
- package/src/runtime/diff.ts +56 -0
- package/src/runtime/event-handlers.ts +254 -0
- package/src/runtime/index.ts +119 -0
- package/src/runtime/keys.ts +8 -0
- package/src/runtime/layout.ts +164 -0
- package/src/runtime/run.tsx +318 -0
- package/src/runtime/term-provider.ts +399 -0
- package/src/runtime/terminal-lifecycle.ts +246 -0
- package/src/runtime/tick.ts +219 -0
- package/src/runtime/types.ts +210 -0
- package/src/scheduler.ts +723 -0
- package/src/screenshot.ts +57 -0
- package/src/scroll-region.ts +69 -0
- package/src/scroll-utils.ts +97 -0
- package/src/term-def.ts +267 -0
- package/src/terminal-caps.ts +5 -0
- package/src/terminal-colors.ts +216 -0
- package/src/termtest.ts +224 -0
- package/src/text-sizing.ts +109 -0
- package/src/toolbelt/index.ts +72 -0
- package/src/unicode.ts +1763 -0
- package/src/xterm/index.ts +491 -0
- package/src/xterm/xterm-provider.ts +204 -0
package/src/ansi/term.ts
ADDED
|
@@ -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
|
+
}
|