@retrovm/terminal 0.2.0 → 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/README.md +117 -1
- package/dist/node.js +52 -2
- package/dist/terminal.d.ts +43 -0
- package/dist/terminal.js +50 -0
- package/package.json +27 -27
- package/src/bun.ts +32 -32
- package/src/node.ts +2 -2
- package/src/terminal.ts +535 -447
package/src/terminal.ts
CHANGED
|
@@ -1,447 +1,535 @@
|
|
|
1
|
-
/*Copyright (c) 2026 Juan Carlos González Amestoy
|
|
2
|
-
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
-
in the Software without restriction, including without limitation the rights
|
|
6
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
-
furnished to do so, subject to the following conditions:
|
|
9
|
-
|
|
10
|
-
The above copyright notice and this permission notice shall be included in all
|
|
11
|
-
copies or substantial portions of the Software.
|
|
12
|
-
|
|
13
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
-
SOFTWARE.*/
|
|
20
|
-
|
|
21
|
-
import { Color } from '@retrovm/color'
|
|
22
|
-
import { format } from 'node:util'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Minimal sink interface. Anything with a `write(data: string)` method works:
|
|
26
|
-
* an xterm.js Terminal instance, a Node Writable, a Bun writer wrapper, or a
|
|
27
|
-
* test double that pushes to an array.
|
|
28
|
-
*/
|
|
29
|
-
export interface ITerminalWriter {
|
|
30
|
-
write(data: string): void
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
type
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
type
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
protected
|
|
69
|
-
/**
|
|
70
|
-
protected
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
*
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1
|
+
/*Copyright (c) 2026 Juan Carlos González Amestoy
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.*/
|
|
20
|
+
|
|
21
|
+
import { Color } from '@retrovm/color'
|
|
22
|
+
import { format } from 'node:util'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal sink interface. Anything with a `write(data: string)` method works:
|
|
26
|
+
* an xterm.js Terminal instance, a Node Writable, a Bun writer wrapper, or a
|
|
27
|
+
* test double that pushes to an array.
|
|
28
|
+
*/
|
|
29
|
+
export interface ITerminalWriter {
|
|
30
|
+
write(data: string): void
|
|
31
|
+
width?:()=>number
|
|
32
|
+
height?:()=>number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extracts from `Color` only the keys whose value is an instance of `Color`,
|
|
37
|
+
* so the generated method names exactly match the available named colors.
|
|
38
|
+
* Add a color to the `Color` class and both `ink<Name>` and `bg<Name>` methods
|
|
39
|
+
* appear automatically with full type safety and autocompletion.
|
|
40
|
+
*/
|
|
41
|
+
type ColorName = {
|
|
42
|
+
[K in keyof typeof Color]: (typeof Color)[K] extends Color ? K : never
|
|
43
|
+
}[keyof typeof Color]
|
|
44
|
+
|
|
45
|
+
type ColorMethod = (fmt?: string, ...args: unknown[]) => ITerminal
|
|
46
|
+
type InkMethods = { [K in ColorName]: ColorMethod }
|
|
47
|
+
type BgMethods = { [K in ColorName as `bg${Capitalize<string & K>}`]: ColorMethod }
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Public type of a Terminal instance: core methods plus all the auto-generated
|
|
51
|
+
* color shortcuts. Consumers see one cohesive type with autocompletion.
|
|
52
|
+
*/
|
|
53
|
+
export type ITerminal = TerminalCore & InkMethods & BgMethods
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Core terminal output. Wraps any object with a `write(string)` method and
|
|
57
|
+
* exposes a fluent API for ANSI styling and cursor control.
|
|
58
|
+
*
|
|
59
|
+
* Color shortcut methods (e.g. `red`, `bgBlue`) are attached at construction
|
|
60
|
+
* time from the `Color` registry. They are declared via the `ITerminal` type
|
|
61
|
+
* and dispatched through an index signature on the class.
|
|
62
|
+
*/
|
|
63
|
+
class TerminalCore {
|
|
64
|
+
/** ANSI is suppressed when true; styling/cursor methods become no-ops. */
|
|
65
|
+
public plain: boolean
|
|
66
|
+
|
|
67
|
+
/** Accumulated output when in buffered mode; `null` when unbuffered. */
|
|
68
|
+
protected _buffer: string | null = null
|
|
69
|
+
/** Nesting depth of active `sync()` calls; only the outermost emits mode 2026. */
|
|
70
|
+
protected _syncDepth = 0
|
|
71
|
+
/** Tracks whether the alternate screen buffer is currently active. */
|
|
72
|
+
protected _altScreen = false;
|
|
73
|
+
|
|
74
|
+
// Allows the auto-attached color methods to typecheck on `this`.
|
|
75
|
+
[key: string]: unknown
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param writer - Any object that implements `write(data: string)`.
|
|
79
|
+
* @param options.plain - When `true`, all ANSI escape sequences are stripped
|
|
80
|
+
* and only plain text is emitted. Defaults to `true` when the `NO_COLOR`
|
|
81
|
+
* environment variable is set; `false` otherwise.
|
|
82
|
+
*/
|
|
83
|
+
constructor(
|
|
84
|
+
protected readonly writer: ITerminalWriter,
|
|
85
|
+
options: { plain?: boolean } = {},
|
|
86
|
+
) {
|
|
87
|
+
// Honor NO_COLOR (https://no-color.org/) by default when running under
|
|
88
|
+
// Node/Bun; in browser/xterm contexts `process` may be undefined.
|
|
89
|
+
const envNoColor = typeof process !== 'undefined' && !!process?.env?.NO_COLOR
|
|
90
|
+
this.plain = options.plain ?? envNoColor
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Internal style helper: skips ANSI when `plain` is enabled. */
|
|
94
|
+
private style(seq: string, fmt: string, args: unknown[]): this {
|
|
95
|
+
const text = format(fmt, ...args)
|
|
96
|
+
this.emit(this.plain ? text : seq + text)
|
|
97
|
+
return this
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Text output ──────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/** Prints formatted text. Uses `util.format` semantics (`%s`, `%d`, …). */
|
|
103
|
+
print(fmt: string = '', ...args: unknown[]): this {
|
|
104
|
+
this.emit(format(fmt, ...args))
|
|
105
|
+
return this
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Prints formatted text followed by a newline. */
|
|
109
|
+
println(fmt: string = '', ...args: unknown[]): this {
|
|
110
|
+
this.emit(format(fmt, ...args) + '\r\n')
|
|
111
|
+
return this
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Color ────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Sets the foreground color and optionally writes formatted text.
|
|
118
|
+
*
|
|
119
|
+
* @param c - A `Color` instance or a CSS-compatible color string (`'#f80'`,
|
|
120
|
+
* `'#ff8800'`, `'red'`). Strings are parsed by `Color` on every call;
|
|
121
|
+
* prefer passing a pre-built `Color` instance in hot loops.
|
|
122
|
+
* @param fmt - `util.format`-style template string.
|
|
123
|
+
* @param args - Substitution values for `fmt`.
|
|
124
|
+
*/
|
|
125
|
+
ink(c: string | Color, fmt: string = '', ...args: unknown[]): this {
|
|
126
|
+
const color = typeof c === 'string' ? new Color(c) : c
|
|
127
|
+
return this.style(color.toAnsiRGB(), fmt, args)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Sets the background color and optionally writes formatted text.
|
|
132
|
+
*
|
|
133
|
+
* @param c - A `Color` instance or a CSS-compatible color string (`'#f80'`,
|
|
134
|
+
* `'#ff8800'`, `'red'`). Strings are parsed by `Color` on every call;
|
|
135
|
+
* prefer passing a pre-built `Color` instance in hot loops.
|
|
136
|
+
* @param fmt - `util.format`-style template string.
|
|
137
|
+
* @param args - Substitution values for `fmt`.
|
|
138
|
+
*/
|
|
139
|
+
paper(c: string | Color, fmt: string = '', ...args: unknown[]): this {
|
|
140
|
+
const color = typeof c === 'string' ? new Color(c) : c
|
|
141
|
+
return this.style(color.toAnsiBackgroundRGB(), fmt, args)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resets all text attributes (color, background, bold, dim, italic,
|
|
146
|
+
* underline, blink, reverse, …) by emitting `\x1b[0m`.
|
|
147
|
+
*
|
|
148
|
+
* Unlike {@link reset}, this does **not** touch the cursor visibility
|
|
149
|
+
* or the alternate screen buffer.
|
|
150
|
+
*/
|
|
151
|
+
resetText(fmt: string = '', ...args: unknown[]): this {
|
|
152
|
+
return this.style('\x1b[0m', fmt, args)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fully resets the terminal to a clean state.
|
|
157
|
+
*
|
|
158
|
+
* In order:
|
|
159
|
+
* 1. Shows the cursor (`cursor(true)`)
|
|
160
|
+
* 2. Exits the alternate screen buffer (`alt(false)`)
|
|
161
|
+
* 3. Resets all text attributes — color, background, intensity, etc. (`\x1b[0m`)
|
|
162
|
+
*
|
|
163
|
+
* Safe to call as a teardown step after any TUI or interactive session.
|
|
164
|
+
*/
|
|
165
|
+
reset(fmt: string = '', ...args: unknown[]): this {
|
|
166
|
+
return this.cursor(true).alt(false).style('\x1b[0m', fmt, args)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Resets only the foreground color. */
|
|
170
|
+
resetInk(fmt: string = '', ...args: unknown[]): this {
|
|
171
|
+
return this.style('\x1b[39m', fmt, args)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Resets only the background color. */
|
|
175
|
+
resetPaper(fmt: string = '', ...args: unknown[]): this {
|
|
176
|
+
return this.style('\x1b[49m', fmt, args)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Screen ───────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/** Clears the screen and homes the cursor. */
|
|
182
|
+
cls(): this {
|
|
183
|
+
this.emit('\x1b[2J\x1b[H')
|
|
184
|
+
return this
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Clears from cursor to end of line. */
|
|
188
|
+
clearLine(): this {
|
|
189
|
+
this.emit('\x1b[K')
|
|
190
|
+
return this
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Cursor ───────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/** Moves the cursor up by `n` rows (CUU). */
|
|
196
|
+
up(n: number = 1): this {
|
|
197
|
+
this.emit(`\x1b[${n}A`)
|
|
198
|
+
return this
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Moves the cursor down by `n` rows (CUD). */
|
|
202
|
+
down(n: number = 1): this {
|
|
203
|
+
this.emit(`\x1b[${n}B`)
|
|
204
|
+
return this
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Moves the cursor right by `n` columns (CUF). */
|
|
208
|
+
right(n: number = 1): this {
|
|
209
|
+
this.emit(`\x1b[${n}C`)
|
|
210
|
+
return this
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Moves the cursor left by `n` columns (CUB). */
|
|
214
|
+
left(n: number = 1): this {
|
|
215
|
+
this.emit(`\x1b[${n}D`)
|
|
216
|
+
return this
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Moves the cursor to the given column (1-based). */
|
|
220
|
+
column(n: number = 1): this {
|
|
221
|
+
this.emit(`\x1b[${n}G`)
|
|
222
|
+
return this
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Moves the cursor to the given row (1-based). */
|
|
226
|
+
row(n: number = 1): this {
|
|
227
|
+
this.emit(`\x1b[${n}d`)
|
|
228
|
+
return this
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Moves the cursor to the given (row, col), both 1-based. */
|
|
232
|
+
moveTo(row: number, col: number): this {
|
|
233
|
+
this.emit(`\x1b[${row};${col}H`)
|
|
234
|
+
return this
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Saves the current cursor position. */
|
|
238
|
+
saveCursor(): this {
|
|
239
|
+
this.emit('\x1b[s')
|
|
240
|
+
return this
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Restores the previously saved cursor position. */
|
|
244
|
+
restoreCursor(): this {
|
|
245
|
+
this.emit('\x1b[u')
|
|
246
|
+
return this
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Shows or hides the cursor. */
|
|
250
|
+
cursor(visible: boolean = true): this {
|
|
251
|
+
this.emit(visible ? '\x1b[?25h' : '\x1b[?25l')
|
|
252
|
+
return this
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Modes ────────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Switches to (`true`) or exits (`false`) the alternate screen buffer
|
|
259
|
+
* (DECSET/DECRST 1049). The alternate screen saves the current scrollback
|
|
260
|
+
* and cursor state on entry, presents a blank canvas for TUI rendering, and
|
|
261
|
+
* restores the original view on exit — exactly what `vim`, `less`, and
|
|
262
|
+
* similar programs do. Calls are idempotent: switching to a buffer already
|
|
263
|
+
* active emits nothing.
|
|
264
|
+
*/
|
|
265
|
+
alt(b: boolean = true): this {
|
|
266
|
+
if (b !== this._altScreen) {
|
|
267
|
+
this.emit(`\x1b[?1049${b ? 'h' : 'l'}`)
|
|
268
|
+
this._altScreen = b
|
|
269
|
+
}
|
|
270
|
+
return this
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Enables or disables auto-wrap (DECAWM, DEC private mode 7). When enabled
|
|
275
|
+
* (the default in most terminals), the cursor wraps to the next line when
|
|
276
|
+
* it reaches the right margin. Disable it to overwrite characters in place,
|
|
277
|
+
* which is useful for progress bars and fixed-width TUI cells.
|
|
278
|
+
*/
|
|
279
|
+
autoWrap(b: boolean = true): this {
|
|
280
|
+
this.emit(`\x1b[?7${b ? 'h' : 'l'}`)
|
|
281
|
+
return this
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Sets the scrolling region (DECSTBM). Scroll and line-feed operations are
|
|
286
|
+
* confined to rows `top`–`bottom`, leaving content outside the region
|
|
287
|
+
* undisturbed. Both values are 1-based and inclusive. Useful for keeping a
|
|
288
|
+
* status bar or header fixed while the main content area scrolls normally.
|
|
289
|
+
*
|
|
290
|
+
* @param top - First row of the scrolling region (1-based).
|
|
291
|
+
* @param bottom - Last row of the scrolling region (1-based).
|
|
292
|
+
*/
|
|
293
|
+
scrollRegion(top: number, bottom: number): this {
|
|
294
|
+
this.emit(`\x1b[${top};${bottom}r`)
|
|
295
|
+
return this
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Buffered output ──────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Enters buffered mode. Subsequent emissions accumulate in memory until
|
|
302
|
+
* `flush()` commits them in a single `write()` to the underlying sink.
|
|
303
|
+
*
|
|
304
|
+
* Essential for animation loops and full-screen redraws on terminals
|
|
305
|
+
* that perform poorly with many small writes (macOS Terminal, GNOME
|
|
306
|
+
* Terminal, Konsole). A 80×24 frame can easily produce thousands of
|
|
307
|
+
* style transitions; coalescing them into one write turns thousands
|
|
308
|
+
* of syscalls into one.
|
|
309
|
+
*
|
|
310
|
+
* Calling `buffer()` while already buffered is a no-op — buffered
|
|
311
|
+
* mode is a single state, not a stack.
|
|
312
|
+
*/
|
|
313
|
+
buffer(): this {
|
|
314
|
+
if (this._buffer === null) this._buffer = ''
|
|
315
|
+
return this
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Commits the accumulated buffer in a single `write()` and exits
|
|
320
|
+
* buffered mode. Calling `flush()` when not buffered is a no-op.
|
|
321
|
+
*/
|
|
322
|
+
flush(): this {
|
|
323
|
+
if (this._buffer !== null) {
|
|
324
|
+
const data = this._buffer
|
|
325
|
+
this._buffer = null
|
|
326
|
+
if (data.length > 0) this.writer.write(data)
|
|
327
|
+
}
|
|
328
|
+
return this
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Writes a pre-built string directly, bypassing `util.format`. Useful
|
|
333
|
+
* in hot loops where the caller has already constructed the exact bytes
|
|
334
|
+
* to emit (e.g. precomputed SGR sequences from a palette LUT) and wants
|
|
335
|
+
* to avoid the per-call formatting overhead.
|
|
336
|
+
*
|
|
337
|
+
* Honors buffered mode like every other emission.
|
|
338
|
+
*/
|
|
339
|
+
raw(data: string): this {
|
|
340
|
+
this.emit(data)
|
|
341
|
+
return this
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Synchronized output ──────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Runs `fn` inside a synchronized-output block (DEC private mode 2026).
|
|
348
|
+
* The terminal accumulates all changes from the block and presents them
|
|
349
|
+
* atomically when the block ends, eliminating tearing in TUIs and
|
|
350
|
+
* animations. Terminals that don't support mode 2026 (e.g. macOS
|
|
351
|
+
* Terminal.app, plain xterm) ignore the escape silently.
|
|
352
|
+
*
|
|
353
|
+
* Automatically enables buffered mode for the duration of the block —
|
|
354
|
+
* synchronized output without buffering would defeat its purpose, since
|
|
355
|
+
* each tiny write would still race the terminal's refresh.
|
|
356
|
+
*
|
|
357
|
+
* Re-entrant: nested `sync()` calls share the outer block (mode 2026
|
|
358
|
+
* is not stackable in the protocol).
|
|
359
|
+
*
|
|
360
|
+
* The block is guarded by try/finally so the closing escape and flush
|
|
361
|
+
* always run, even if `fn` throws — important because mode 2026 has a
|
|
362
|
+
* ~150ms server-side timeout and leaving it open looks like a freeze.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* out.sync(() => {
|
|
366
|
+
* for (let row = 0; row < H; row++) {
|
|
367
|
+
* for (let col = 0; col < W; col++) {
|
|
368
|
+
* out.moveTo(row + 1, col + 1).paper(bg).ink(fg, '▄')
|
|
369
|
+
* }
|
|
370
|
+
* }
|
|
371
|
+
* })
|
|
372
|
+
*/
|
|
373
|
+
sync(fn: () => void): this {
|
|
374
|
+
const outer = this._syncDepth === 0
|
|
375
|
+
const startedBuffer = outer && this._buffer === null
|
|
376
|
+
if (outer) {
|
|
377
|
+
if (startedBuffer) this.buffer()
|
|
378
|
+
this.emit('\x1b[?2026h')
|
|
379
|
+
}
|
|
380
|
+
this._syncDepth++
|
|
381
|
+
try {
|
|
382
|
+
fn()
|
|
383
|
+
} finally {
|
|
384
|
+
this._syncDepth--
|
|
385
|
+
if (outer) {
|
|
386
|
+
this.emit('\x1b[?2026l')
|
|
387
|
+
if (startedBuffer) this.flush()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return this
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private emit(data: string): void {
|
|
394
|
+
if (this._buffer !== null) this._buffer += data
|
|
395
|
+
else this.writer.write(data)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Renders a raw pixel buffer to the terminal using the half-block trick.
|
|
400
|
+
*
|
|
401
|
+
* Each pair of vertically adjacent pixels is encoded as a single `▄`
|
|
402
|
+
* character: the upper pixel becomes the background color and the lower
|
|
403
|
+
* pixel becomes the foreground color, effectively doubling the vertical
|
|
404
|
+
* resolution — one character row covers two pixel rows.
|
|
405
|
+
*
|
|
406
|
+
* Pixel format: little-endian RGBA packed into a `Uint32Array`. Each
|
|
407
|
+
* element is `0xAABBGGRR` — R in the low byte, G in the next, B in the
|
|
408
|
+
* next, alpha ignored. This matches the layout produced by
|
|
409
|
+
* `Canvas.getImageData` and most image-decoding libraries when read as a
|
|
410
|
+
* `Uint32Array` on a little-endian system.
|
|
411
|
+
*
|
|
412
|
+
* The entire frame is assembled into one string before writing, which is
|
|
413
|
+
* far cheaper than calling `ink`/`paper`/`raw` per pixel. The write is
|
|
414
|
+
* wrapped in `sync()` to reduce tearing on capable terminals.
|
|
415
|
+
*
|
|
416
|
+
* @param fb - Pixel buffer. Length must be at least `w * h`.
|
|
417
|
+
* @param w - Width in pixels (equals the number of terminal columns used).
|
|
418
|
+
* @param h - Height in pixels. Must be even; an odd last row is ignored.
|
|
419
|
+
*/
|
|
420
|
+
public framebuffer(fb:Uint32Array,w:number,h:number): void {
|
|
421
|
+
const ch:string[]=[]
|
|
422
|
+
|
|
423
|
+
for(let y=0;y<h;y+=2) {
|
|
424
|
+
for(let x=0;x<w;x++) {
|
|
425
|
+
const i=y*w+x
|
|
426
|
+
const c1=fb[i]!
|
|
427
|
+
const c2=fb[i+w]!
|
|
428
|
+
const b1=(c1>>16)&0xFF
|
|
429
|
+
const g1=(c1>>8)&0xFF
|
|
430
|
+
const r1=c1&0xFF
|
|
431
|
+
const b2=(c2>>16)&0xFF
|
|
432
|
+
const g2=(c2>>8)&0xFF
|
|
433
|
+
const r2=c2&0xFF
|
|
434
|
+
|
|
435
|
+
ch.push('\x1b[48;2;')
|
|
436
|
+
ch.push(r1.toString())
|
|
437
|
+
ch.push(';')
|
|
438
|
+
ch.push(g1.toString())
|
|
439
|
+
ch.push(';')
|
|
440
|
+
ch.push(b1.toString())
|
|
441
|
+
ch.push('m\x1b[38;2;')
|
|
442
|
+
ch.push(r2.toString())
|
|
443
|
+
ch.push(';')
|
|
444
|
+
ch.push(g2.toString())
|
|
445
|
+
ch.push(';')
|
|
446
|
+
ch.push(b2.toString())
|
|
447
|
+
ch.push('m▄')
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this.sync(() => {
|
|
452
|
+
this.raw(ch.join(''))
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ─── Dimensions ───────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Current width of the terminal in columns, as reported by the writer.
|
|
460
|
+
* Returns `-1` if the writer does not implement `width()`.
|
|
461
|
+
*
|
|
462
|
+
* Called on every access so it always reflects the current size, even after
|
|
463
|
+
* a resize event. The built-in Bun and Node writers read
|
|
464
|
+
* `process.stdout.columns` each time.
|
|
465
|
+
*/
|
|
466
|
+
public get width(): number {
|
|
467
|
+
if (this.writer.width) return this.writer.width()
|
|
468
|
+
return -1
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Current height of the terminal in rows, as reported by the writer.
|
|
473
|
+
* Returns `-1` if the writer does not implement `height()`.
|
|
474
|
+
*
|
|
475
|
+
* Called on every access so it always reflects the current size, even after
|
|
476
|
+
* a resize event. The built-in Bun and Node writers read
|
|
477
|
+
* `process.stdout.rows` each time.
|
|
478
|
+
*/
|
|
479
|
+
public get height(): number {
|
|
480
|
+
if (this.writer.height) return this.writer.height()
|
|
481
|
+
return -1
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Attaches one method per named color in the `Color` registry. Done once,
|
|
487
|
+
* on the prototype, so every Terminal instance shares the same functions and
|
|
488
|
+
* adding a new color to `Color` propagates automatically — no edits here.
|
|
489
|
+
*/
|
|
490
|
+
function installColorMethods(): void {
|
|
491
|
+
const proto = TerminalCore.prototype as unknown as Record<string, unknown>
|
|
492
|
+
const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
|
|
493
|
+
|
|
494
|
+
for (const key of Object.keys(Color) as Array<keyof typeof Color>) {
|
|
495
|
+
const value = Color[key]
|
|
496
|
+
if (!(value instanceof Color)) continue
|
|
497
|
+
|
|
498
|
+
const name = key as string
|
|
499
|
+
|
|
500
|
+
proto[name] = function (this: TerminalCore, fmt: string = '', ...args: unknown[]) {
|
|
501
|
+
return (this as unknown as ITerminal).ink(value, fmt, ...args)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
proto[`bg${cap(name)}`] = function (this: TerminalCore, fmt: string = '', ...args: unknown[]) {
|
|
505
|
+
return (this as unknown as ITerminal).paper(value, fmt, ...args)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
installColorMethods()
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Creates a new `Terminal` instance backed by `writer`.
|
|
514
|
+
*
|
|
515
|
+
* Accepts any object with a `write(string)` method — an xterm.js `Terminal`,
|
|
516
|
+
* a Node `Writable`, a Bun writer wrapper, or a test double.
|
|
517
|
+
*
|
|
518
|
+
* @param writer - The output sink. Must implement `write(data: string): void`.
|
|
519
|
+
* @param options.plain - Suppress all ANSI sequences and emit plain text only.
|
|
520
|
+
* Defaults to `true` when `NO_COLOR` is set in the environment.
|
|
521
|
+
* @returns A fully typed `ITerminal` with core methods and auto-generated
|
|
522
|
+
* named-color shortcuts (`red`, `bgBlue`, …).
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* import { Terminal as XTerm } from '@xterm/xterm'
|
|
526
|
+
* const xterm = new XTerm()
|
|
527
|
+
* xterm.open(document.getElementById('term')!)
|
|
528
|
+
* const term = createTerminal(xterm)
|
|
529
|
+
* term.red('Hello ').bgBlue(' world ').reset().println()
|
|
530
|
+
*/
|
|
531
|
+
export function createTerminal(writer: ITerminalWriter, options: { plain?: boolean } = {}): ITerminal {
|
|
532
|
+
return new TerminalCore(writer, options) as unknown as ITerminal
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export { TerminalCore }
|