@retrovm/terminal 0.1.2 → 0.2.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/dist/node.js +52 -4
- package/dist/terminal.d.ts +124 -13
- package/dist/terminal.js +52 -4
- package/package.json +1 -1
- package/src/bun.ts +7 -14
- package/src/terminal.ts +193 -29
package/dist/node.js
CHANGED
|
@@ -311,14 +311,14 @@ import { format } from "node:util";
|
|
|
311
311
|
class TerminalCore {
|
|
312
312
|
writer;
|
|
313
313
|
plain;
|
|
314
|
+
_buffer = null;
|
|
315
|
+
_syncDepth = 0;
|
|
316
|
+
_altScreen = false;
|
|
314
317
|
constructor(writer, options = {}) {
|
|
315
318
|
this.writer = writer;
|
|
316
319
|
const envNoColor = typeof process !== "undefined" && !!process?.env?.NO_COLOR;
|
|
317
320
|
this.plain = options.plain ?? envNoColor;
|
|
318
321
|
}
|
|
319
|
-
emit(data) {
|
|
320
|
-
this.writer.write(data);
|
|
321
|
-
}
|
|
322
322
|
style(seq, fmt, args) {
|
|
323
323
|
const text = format(fmt, ...args);
|
|
324
324
|
this.emit(this.plain ? text : seq + text);
|
|
@@ -402,7 +402,10 @@ class TerminalCore {
|
|
|
402
402
|
return this;
|
|
403
403
|
}
|
|
404
404
|
alt(b = true) {
|
|
405
|
-
|
|
405
|
+
if (b !== this._altScreen) {
|
|
406
|
+
this.emit(`\x1B[?1049${b ? "h" : "l"}`);
|
|
407
|
+
this._altScreen = b;
|
|
408
|
+
}
|
|
406
409
|
return this;
|
|
407
410
|
}
|
|
408
411
|
autoWrap(b = true) {
|
|
@@ -413,6 +416,51 @@ class TerminalCore {
|
|
|
413
416
|
this.emit(`\x1B[${top};${bottom}r`);
|
|
414
417
|
return this;
|
|
415
418
|
}
|
|
419
|
+
buffer() {
|
|
420
|
+
if (this._buffer === null)
|
|
421
|
+
this._buffer = "";
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
flush() {
|
|
425
|
+
if (this._buffer !== null) {
|
|
426
|
+
const data = this._buffer;
|
|
427
|
+
this._buffer = null;
|
|
428
|
+
if (data.length > 0)
|
|
429
|
+
this.writer.write(data);
|
|
430
|
+
}
|
|
431
|
+
return this;
|
|
432
|
+
}
|
|
433
|
+
raw(data) {
|
|
434
|
+
this.emit(data);
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
437
|
+
sync(fn) {
|
|
438
|
+
const outer = this._syncDepth === 0;
|
|
439
|
+
const startedBuffer = outer && this._buffer === null;
|
|
440
|
+
if (outer) {
|
|
441
|
+
if (startedBuffer)
|
|
442
|
+
this.buffer();
|
|
443
|
+
this.emit("\x1B[?2026h");
|
|
444
|
+
}
|
|
445
|
+
this._syncDepth++;
|
|
446
|
+
try {
|
|
447
|
+
fn();
|
|
448
|
+
} finally {
|
|
449
|
+
this._syncDepth--;
|
|
450
|
+
if (outer) {
|
|
451
|
+
this.emit("\x1B[?2026l");
|
|
452
|
+
if (startedBuffer)
|
|
453
|
+
this.flush();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return this;
|
|
457
|
+
}
|
|
458
|
+
emit(data) {
|
|
459
|
+
if (this._buffer !== null)
|
|
460
|
+
this._buffer += data;
|
|
461
|
+
else
|
|
462
|
+
this.writer.write(data);
|
|
463
|
+
}
|
|
416
464
|
}
|
|
417
465
|
function installColorMethods() {
|
|
418
466
|
const proto = TerminalCore.prototype;
|
package/dist/terminal.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface ITerminalWriter {
|
|
|
14
14
|
* appear automatically with full type safety and autocompletion.
|
|
15
15
|
*/
|
|
16
16
|
type ColorName = {
|
|
17
|
-
[K in keyof typeof Color]: typeof Color[K] extends Color ? K : never;
|
|
17
|
+
[K in keyof typeof Color]: (typeof Color)[K] extends Color ? K : never;
|
|
18
18
|
}[keyof typeof Color];
|
|
19
19
|
type ColorMethod = (fmt?: string, ...args: unknown[]) => ITerminal;
|
|
20
20
|
type InkMethods = {
|
|
@@ -37,24 +37,50 @@ export type ITerminal = TerminalCore & InkMethods & BgMethods;
|
|
|
37
37
|
* and dispatched through an index signature on the class.
|
|
38
38
|
*/
|
|
39
39
|
declare class TerminalCore {
|
|
40
|
-
|
|
40
|
+
protected readonly writer: ITerminalWriter;
|
|
41
41
|
/** ANSI is suppressed when true; styling/cursor methods become no-ops. */
|
|
42
42
|
plain: boolean;
|
|
43
|
+
/** Accumulated output when in buffered mode; `null` when unbuffered. */
|
|
44
|
+
protected _buffer: string | null;
|
|
45
|
+
/** Nesting depth of active `sync()` calls; only the outermost emits mode 2026. */
|
|
46
|
+
protected _syncDepth: number;
|
|
47
|
+
/** Tracks whether the alternate screen buffer is currently active. */
|
|
48
|
+
protected _altScreen: boolean;
|
|
43
49
|
[key: string]: unknown;
|
|
50
|
+
/**
|
|
51
|
+
* @param writer - Any object that implements `write(data: string)`.
|
|
52
|
+
* @param options.plain - When `true`, all ANSI escape sequences are stripped
|
|
53
|
+
* and only plain text is emitted. Defaults to `true` when the `NO_COLOR`
|
|
54
|
+
* environment variable is set; `false` otherwise.
|
|
55
|
+
*/
|
|
44
56
|
constructor(writer: ITerminalWriter, options?: {
|
|
45
57
|
plain?: boolean;
|
|
46
58
|
});
|
|
47
|
-
/** Internal write helper. Single point of contact with the sink. */
|
|
48
|
-
private emit;
|
|
49
59
|
/** Internal style helper: skips ANSI when `plain` is enabled. */
|
|
50
60
|
private style;
|
|
51
61
|
/** Prints formatted text. Uses `util.format` semantics (`%s`, `%d`, …). */
|
|
52
62
|
print(fmt?: string, ...args: unknown[]): this;
|
|
53
63
|
/** Prints formatted text followed by a newline. */
|
|
54
64
|
println(fmt?: string, ...args: unknown[]): this;
|
|
55
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Sets the foreground color and optionally writes formatted text.
|
|
67
|
+
*
|
|
68
|
+
* @param c - A `Color` instance or a CSS-compatible color string (`'#f80'`,
|
|
69
|
+
* `'#ff8800'`, `'red'`). Strings are parsed by `Color` on every call;
|
|
70
|
+
* prefer passing a pre-built `Color` instance in hot loops.
|
|
71
|
+
* @param fmt - `util.format`-style template string.
|
|
72
|
+
* @param args - Substitution values for `fmt`.
|
|
73
|
+
*/
|
|
56
74
|
ink(c: string | Color, fmt?: string, ...args: unknown[]): this;
|
|
57
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Sets the background color and optionally writes formatted text.
|
|
77
|
+
*
|
|
78
|
+
* @param c - A `Color` instance or a CSS-compatible color string (`'#f80'`,
|
|
79
|
+
* `'#ff8800'`, `'red'`). Strings are parsed by `Color` on every call;
|
|
80
|
+
* prefer passing a pre-built `Color` instance in hot loops.
|
|
81
|
+
* @param fmt - `util.format`-style template string.
|
|
82
|
+
* @param args - Substitution values for `fmt`.
|
|
83
|
+
*/
|
|
58
84
|
paper(c: string | Color, fmt?: string, ...args: unknown[]): this;
|
|
59
85
|
/**
|
|
60
86
|
* Resets all text attributes (color, background, bold, dim, italic,
|
|
@@ -83,9 +109,13 @@ declare class TerminalCore {
|
|
|
83
109
|
cls(): this;
|
|
84
110
|
/** Clears from cursor to end of line. */
|
|
85
111
|
clearLine(): this;
|
|
112
|
+
/** Moves the cursor up by `n` rows (CUU). */
|
|
86
113
|
up(n?: number): this;
|
|
114
|
+
/** Moves the cursor down by `n` rows (CUD). */
|
|
87
115
|
down(n?: number): this;
|
|
116
|
+
/** Moves the cursor right by `n` columns (CUF). */
|
|
88
117
|
right(n?: number): this;
|
|
118
|
+
/** Moves the cursor left by `n` columns (CUB). */
|
|
89
119
|
left(n?: number): this;
|
|
90
120
|
/** Moves the cursor to the given column (1-based). */
|
|
91
121
|
column(n?: number): this;
|
|
@@ -99,20 +129,101 @@ declare class TerminalCore {
|
|
|
99
129
|
restoreCursor(): this;
|
|
100
130
|
/** Shows or hides the cursor. */
|
|
101
131
|
cursor(visible?: boolean): this;
|
|
102
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Switches to (`true`) or exits (`false`) the alternate screen buffer
|
|
134
|
+
* (DECSET/DECRST 1049). The alternate screen saves the current scrollback
|
|
135
|
+
* and cursor state on entry, presents a blank canvas for TUI rendering, and
|
|
136
|
+
* restores the original view on exit — exactly what `vim`, `less`, and
|
|
137
|
+
* similar programs do. Calls are idempotent: switching to a buffer already
|
|
138
|
+
* active emits nothing.
|
|
139
|
+
*/
|
|
103
140
|
alt(b?: boolean): this;
|
|
104
141
|
/**
|
|
105
|
-
* Enables or disables auto-wrap (DECAWM, mode 7).
|
|
106
|
-
*
|
|
107
|
-
*
|
|
142
|
+
* Enables or disables auto-wrap (DECAWM, DEC private mode 7). When enabled
|
|
143
|
+
* (the default in most terminals), the cursor wraps to the next line when
|
|
144
|
+
* it reaches the right margin. Disable it to overwrite characters in place,
|
|
145
|
+
* which is useful for progress bars and fixed-width TUI cells.
|
|
108
146
|
*/
|
|
109
147
|
autoWrap(b?: boolean): this;
|
|
110
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Sets the scrolling region (DECSTBM). Scroll and line-feed operations are
|
|
150
|
+
* confined to rows `top`–`bottom`, leaving content outside the region
|
|
151
|
+
* undisturbed. Both values are 1-based and inclusive. Useful for keeping a
|
|
152
|
+
* status bar or header fixed while the main content area scrolls normally.
|
|
153
|
+
*
|
|
154
|
+
* @param top - First row of the scrolling region (1-based).
|
|
155
|
+
* @param bottom - Last row of the scrolling region (1-based).
|
|
156
|
+
*/
|
|
111
157
|
scrollRegion(top: number, bottom: number): this;
|
|
158
|
+
/**
|
|
159
|
+
* Enters buffered mode. Subsequent emissions accumulate in memory until
|
|
160
|
+
* `flush()` commits them in a single `write()` to the underlying sink.
|
|
161
|
+
*
|
|
162
|
+
* Essential for animation loops and full-screen redraws on terminals
|
|
163
|
+
* that perform poorly with many small writes (macOS Terminal, GNOME
|
|
164
|
+
* Terminal, Konsole). A 80×24 frame can easily produce thousands of
|
|
165
|
+
* style transitions; coalescing them into one write turns thousands
|
|
166
|
+
* of syscalls into one.
|
|
167
|
+
*
|
|
168
|
+
* Calling `buffer()` while already buffered is a no-op — buffered
|
|
169
|
+
* mode is a single state, not a stack.
|
|
170
|
+
*/
|
|
171
|
+
buffer(): this;
|
|
172
|
+
/**
|
|
173
|
+
* Commits the accumulated buffer in a single `write()` and exits
|
|
174
|
+
* buffered mode. Calling `flush()` when not buffered is a no-op.
|
|
175
|
+
*/
|
|
176
|
+
flush(): this;
|
|
177
|
+
/**
|
|
178
|
+
* Writes a pre-built string directly, bypassing `util.format`. Useful
|
|
179
|
+
* in hot loops where the caller has already constructed the exact bytes
|
|
180
|
+
* to emit (e.g. precomputed SGR sequences from a palette LUT) and wants
|
|
181
|
+
* to avoid the per-call formatting overhead.
|
|
182
|
+
*
|
|
183
|
+
* Honors buffered mode like every other emission.
|
|
184
|
+
*/
|
|
185
|
+
raw(data: string): this;
|
|
186
|
+
/**
|
|
187
|
+
* Runs `fn` inside a synchronized-output block (DEC private mode 2026).
|
|
188
|
+
* The terminal accumulates all changes from the block and presents them
|
|
189
|
+
* atomically when the block ends, eliminating tearing in TUIs and
|
|
190
|
+
* animations. Terminals that don't support mode 2026 (e.g. macOS
|
|
191
|
+
* Terminal.app, plain xterm) ignore the escape silently.
|
|
192
|
+
*
|
|
193
|
+
* Automatically enables buffered mode for the duration of the block —
|
|
194
|
+
* synchronized output without buffering would defeat its purpose, since
|
|
195
|
+
* each tiny write would still race the terminal's refresh.
|
|
196
|
+
*
|
|
197
|
+
* Re-entrant: nested `sync()` calls share the outer block (mode 2026
|
|
198
|
+
* is not stackable in the protocol).
|
|
199
|
+
*
|
|
200
|
+
* The block is guarded by try/finally so the closing escape and flush
|
|
201
|
+
* always run, even if `fn` throws — important because mode 2026 has a
|
|
202
|
+
* ~150ms server-side timeout and leaving it open looks like a freeze.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* out.sync(() => {
|
|
206
|
+
* for (let row = 0; row < H; row++) {
|
|
207
|
+
* for (let col = 0; col < W; col++) {
|
|
208
|
+
* out.moveTo(row + 1, col + 1).paper(bg).ink(fg, '▄')
|
|
209
|
+
* }
|
|
210
|
+
* }
|
|
211
|
+
* })
|
|
212
|
+
*/
|
|
213
|
+
sync(fn: () => void): this;
|
|
214
|
+
private emit;
|
|
112
215
|
}
|
|
113
216
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
217
|
+
* Creates a new `Terminal` instance backed by `writer`.
|
|
218
|
+
*
|
|
219
|
+
* Accepts any object with a `write(string)` method — an xterm.js `Terminal`,
|
|
220
|
+
* a Node `Writable`, a Bun writer wrapper, or a test double.
|
|
221
|
+
*
|
|
222
|
+
* @param writer - The output sink. Must implement `write(data: string): void`.
|
|
223
|
+
* @param options.plain - Suppress all ANSI sequences and emit plain text only.
|
|
224
|
+
* Defaults to `true` when `NO_COLOR` is set in the environment.
|
|
225
|
+
* @returns A fully typed `ITerminal` with core methods and auto-generated
|
|
226
|
+
* named-color shortcuts (`red`, `bgBlue`, …).
|
|
116
227
|
*
|
|
117
228
|
* @example
|
|
118
229
|
* import { Terminal as XTerm } from '@xterm/xterm'
|
package/dist/terminal.js
CHANGED
|
@@ -311,14 +311,14 @@ import { format } from "node:util";
|
|
|
311
311
|
class TerminalCore {
|
|
312
312
|
writer;
|
|
313
313
|
plain;
|
|
314
|
+
_buffer = null;
|
|
315
|
+
_syncDepth = 0;
|
|
316
|
+
_altScreen = false;
|
|
314
317
|
constructor(writer, options = {}) {
|
|
315
318
|
this.writer = writer;
|
|
316
319
|
const envNoColor = typeof process !== "undefined" && !!process?.env?.NO_COLOR;
|
|
317
320
|
this.plain = options.plain ?? envNoColor;
|
|
318
321
|
}
|
|
319
|
-
emit(data) {
|
|
320
|
-
this.writer.write(data);
|
|
321
|
-
}
|
|
322
322
|
style(seq, fmt, args) {
|
|
323
323
|
const text = format(fmt, ...args);
|
|
324
324
|
this.emit(this.plain ? text : seq + text);
|
|
@@ -402,7 +402,10 @@ class TerminalCore {
|
|
|
402
402
|
return this;
|
|
403
403
|
}
|
|
404
404
|
alt(b = true) {
|
|
405
|
-
|
|
405
|
+
if (b !== this._altScreen) {
|
|
406
|
+
this.emit(`\x1B[?1049${b ? "h" : "l"}`);
|
|
407
|
+
this._altScreen = b;
|
|
408
|
+
}
|
|
406
409
|
return this;
|
|
407
410
|
}
|
|
408
411
|
autoWrap(b = true) {
|
|
@@ -413,6 +416,51 @@ class TerminalCore {
|
|
|
413
416
|
this.emit(`\x1B[${top};${bottom}r`);
|
|
414
417
|
return this;
|
|
415
418
|
}
|
|
419
|
+
buffer() {
|
|
420
|
+
if (this._buffer === null)
|
|
421
|
+
this._buffer = "";
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
flush() {
|
|
425
|
+
if (this._buffer !== null) {
|
|
426
|
+
const data = this._buffer;
|
|
427
|
+
this._buffer = null;
|
|
428
|
+
if (data.length > 0)
|
|
429
|
+
this.writer.write(data);
|
|
430
|
+
}
|
|
431
|
+
return this;
|
|
432
|
+
}
|
|
433
|
+
raw(data) {
|
|
434
|
+
this.emit(data);
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
437
|
+
sync(fn) {
|
|
438
|
+
const outer = this._syncDepth === 0;
|
|
439
|
+
const startedBuffer = outer && this._buffer === null;
|
|
440
|
+
if (outer) {
|
|
441
|
+
if (startedBuffer)
|
|
442
|
+
this.buffer();
|
|
443
|
+
this.emit("\x1B[?2026h");
|
|
444
|
+
}
|
|
445
|
+
this._syncDepth++;
|
|
446
|
+
try {
|
|
447
|
+
fn();
|
|
448
|
+
} finally {
|
|
449
|
+
this._syncDepth--;
|
|
450
|
+
if (outer) {
|
|
451
|
+
this.emit("\x1B[?2026l");
|
|
452
|
+
if (startedBuffer)
|
|
453
|
+
this.flush();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return this;
|
|
457
|
+
}
|
|
458
|
+
emit(data) {
|
|
459
|
+
if (this._buffer !== null)
|
|
460
|
+
this._buffer += data;
|
|
461
|
+
else
|
|
462
|
+
this.writer.write(data);
|
|
463
|
+
}
|
|
416
464
|
}
|
|
417
465
|
function installColorMethods() {
|
|
418
466
|
const proto = TerminalCore.prototype;
|
package/package.json
CHANGED
package/src/bun.ts
CHANGED
|
@@ -21,20 +21,13 @@ SOFTWARE.*/
|
|
|
21
21
|
import { createTerminal } from "./terminal"
|
|
22
22
|
export { createTerminal, type ITerminal, type ITerminalWriter } from "./terminal"
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
w.write(data)
|
|
27
|
-
w.flush()
|
|
28
|
-
}
|
|
24
|
+
const _outWriter = Bun.stdout.writer()
|
|
25
|
+
const _errWriter = Bun.stderr.writer()
|
|
29
26
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
w.write(data)
|
|
33
|
-
w.flush()
|
|
34
|
-
}
|
|
27
|
+
const _writeOut = (data: string) => { _outWriter.write(data); _outWriter.flush() }
|
|
28
|
+
const _writeErr = (data: string) => { _errWriter.write(data); _errWriter.flush() }
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
err: createTerminal({ write: _writeErr }) ,
|
|
30
|
+
export const Terminal = {
|
|
31
|
+
out: createTerminal({ write: _writeOut }),
|
|
32
|
+
err: createTerminal({ write: _writeErr }),
|
|
40
33
|
}
|
package/src/terminal.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface ITerminalWriter {
|
|
|
37
37
|
* appear automatically with full type safety and autocompletion.
|
|
38
38
|
*/
|
|
39
39
|
type ColorName = {
|
|
40
|
-
[K in keyof typeof Color]: typeof Color[K] extends Color ? K : never
|
|
40
|
+
[K in keyof typeof Color]: (typeof Color)[K] extends Color ? K : never
|
|
41
41
|
}[keyof typeof Color]
|
|
42
42
|
|
|
43
43
|
type ColorMethod = (fmt?: string, ...args: unknown[]) => ITerminal
|
|
@@ -62,25 +62,32 @@ class TerminalCore {
|
|
|
62
62
|
/** ANSI is suppressed when true; styling/cursor methods become no-ops. */
|
|
63
63
|
public plain: boolean
|
|
64
64
|
|
|
65
|
+
/** Accumulated output when in buffered mode; `null` when unbuffered. */
|
|
66
|
+
protected _buffer: string | null = null
|
|
67
|
+
/** Nesting depth of active `sync()` calls; only the outermost emits mode 2026. */
|
|
68
|
+
protected _syncDepth = 0
|
|
69
|
+
/** Tracks whether the alternate screen buffer is currently active. */
|
|
70
|
+
protected _altScreen = false;
|
|
71
|
+
|
|
65
72
|
// Allows the auto-attached color methods to typecheck on `this`.
|
|
66
73
|
[key: string]: unknown
|
|
67
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @param writer - Any object that implements `write(data: string)`.
|
|
77
|
+
* @param options.plain - When `true`, all ANSI escape sequences are stripped
|
|
78
|
+
* and only plain text is emitted. Defaults to `true` when the `NO_COLOR`
|
|
79
|
+
* environment variable is set; `false` otherwise.
|
|
80
|
+
*/
|
|
68
81
|
constructor(
|
|
69
|
-
|
|
82
|
+
protected readonly writer: ITerminalWriter,
|
|
70
83
|
options: { plain?: boolean } = {},
|
|
71
84
|
) {
|
|
72
85
|
// Honor NO_COLOR (https://no-color.org/) by default when running under
|
|
73
86
|
// Node/Bun; in browser/xterm contexts `process` may be undefined.
|
|
74
|
-
const envNoColor =
|
|
75
|
-
typeof process !== 'undefined' && !!process?.env?.NO_COLOR
|
|
87
|
+
const envNoColor = typeof process !== 'undefined' && !!process?.env?.NO_COLOR
|
|
76
88
|
this.plain = options.plain ?? envNoColor
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
/** Internal write helper. Single point of contact with the sink. */
|
|
80
|
-
private emit(data: string): void {
|
|
81
|
-
this.writer.write(data)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
91
|
/** Internal style helper: skips ANSI when `plain` is enabled. */
|
|
85
92
|
private style(seq: string, fmt: string, args: unknown[]): this {
|
|
86
93
|
const text = format(fmt, ...args)
|
|
@@ -104,13 +111,29 @@ class TerminalCore {
|
|
|
104
111
|
|
|
105
112
|
// ─── Color ────────────────────────────────────────────────────────────────
|
|
106
113
|
|
|
107
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Sets the foreground color and optionally writes formatted text.
|
|
116
|
+
*
|
|
117
|
+
* @param c - A `Color` instance or a CSS-compatible color string (`'#f80'`,
|
|
118
|
+
* `'#ff8800'`, `'red'`). Strings are parsed by `Color` on every call;
|
|
119
|
+
* prefer passing a pre-built `Color` instance in hot loops.
|
|
120
|
+
* @param fmt - `util.format`-style template string.
|
|
121
|
+
* @param args - Substitution values for `fmt`.
|
|
122
|
+
*/
|
|
108
123
|
ink(c: string | Color, fmt: string = '', ...args: unknown[]): this {
|
|
109
124
|
const color = typeof c === 'string' ? new Color(c) : c
|
|
110
125
|
return this.style(color.toAnsiRGB(), fmt, args)
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Sets the background color and optionally writes formatted text.
|
|
130
|
+
*
|
|
131
|
+
* @param c - A `Color` instance or a CSS-compatible color string (`'#f80'`,
|
|
132
|
+
* `'#ff8800'`, `'red'`). Strings are parsed by `Color` on every call;
|
|
133
|
+
* prefer passing a pre-built `Color` instance in hot loops.
|
|
134
|
+
* @param fmt - `util.format`-style template string.
|
|
135
|
+
* @param args - Substitution values for `fmt`.
|
|
136
|
+
*/
|
|
114
137
|
paper(c: string | Color, fmt: string = '', ...args: unknown[]): this {
|
|
115
138
|
const color = typeof c === 'string' ? new Color(c) : c
|
|
116
139
|
return this.style(color.toAnsiBackgroundRGB(), fmt, args)
|
|
@@ -167,10 +190,29 @@ class TerminalCore {
|
|
|
167
190
|
|
|
168
191
|
// ─── Cursor ───────────────────────────────────────────────────────────────
|
|
169
192
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
/** Moves the cursor up by `n` rows (CUU). */
|
|
194
|
+
up(n: number = 1): this {
|
|
195
|
+
this.emit(`\x1b[${n}A`)
|
|
196
|
+
return this
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Moves the cursor down by `n` rows (CUD). */
|
|
200
|
+
down(n: number = 1): this {
|
|
201
|
+
this.emit(`\x1b[${n}B`)
|
|
202
|
+
return this
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Moves the cursor right by `n` columns (CUF). */
|
|
206
|
+
right(n: number = 1): this {
|
|
207
|
+
this.emit(`\x1b[${n}C`)
|
|
208
|
+
return this
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Moves the cursor left by `n` columns (CUB). */
|
|
212
|
+
left(n: number = 1): this {
|
|
213
|
+
this.emit(`\x1b[${n}D`)
|
|
214
|
+
return this
|
|
215
|
+
}
|
|
174
216
|
|
|
175
217
|
/** Moves the cursor to the given column (1-based). */
|
|
176
218
|
column(n: number = 1): this {
|
|
@@ -210,27 +252,146 @@ class TerminalCore {
|
|
|
210
252
|
|
|
211
253
|
// ─── Modes ────────────────────────────────────────────────────────────────
|
|
212
254
|
|
|
213
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* Switches to (`true`) or exits (`false`) the alternate screen buffer
|
|
257
|
+
* (DECSET/DECRST 1049). The alternate screen saves the current scrollback
|
|
258
|
+
* and cursor state on entry, presents a blank canvas for TUI rendering, and
|
|
259
|
+
* restores the original view on exit — exactly what `vim`, `less`, and
|
|
260
|
+
* similar programs do. Calls are idempotent: switching to a buffer already
|
|
261
|
+
* active emits nothing.
|
|
262
|
+
*/
|
|
214
263
|
alt(b: boolean = true): this {
|
|
215
|
-
|
|
264
|
+
if (b !== this._altScreen) {
|
|
265
|
+
this.emit(`\x1b[?1049${b ? 'h' : 'l'}`)
|
|
266
|
+
this._altScreen = b
|
|
267
|
+
}
|
|
216
268
|
return this
|
|
217
269
|
}
|
|
218
270
|
|
|
219
271
|
/**
|
|
220
|
-
* Enables or disables auto-wrap (DECAWM, mode 7).
|
|
221
|
-
*
|
|
222
|
-
*
|
|
272
|
+
* Enables or disables auto-wrap (DECAWM, DEC private mode 7). When enabled
|
|
273
|
+
* (the default in most terminals), the cursor wraps to the next line when
|
|
274
|
+
* it reaches the right margin. Disable it to overwrite characters in place,
|
|
275
|
+
* which is useful for progress bars and fixed-width TUI cells.
|
|
223
276
|
*/
|
|
224
277
|
autoWrap(b: boolean = true): this {
|
|
225
278
|
this.emit(`\x1b[?7${b ? 'h' : 'l'}`)
|
|
226
279
|
return this
|
|
227
280
|
}
|
|
228
281
|
|
|
229
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* Sets the scrolling region (DECSTBM). Scroll and line-feed operations are
|
|
284
|
+
* confined to rows `top`–`bottom`, leaving content outside the region
|
|
285
|
+
* undisturbed. Both values are 1-based and inclusive. Useful for keeping a
|
|
286
|
+
* status bar or header fixed while the main content area scrolls normally.
|
|
287
|
+
*
|
|
288
|
+
* @param top - First row of the scrolling region (1-based).
|
|
289
|
+
* @param bottom - Last row of the scrolling region (1-based).
|
|
290
|
+
*/
|
|
230
291
|
scrollRegion(top: number, bottom: number): this {
|
|
231
292
|
this.emit(`\x1b[${top};${bottom}r`)
|
|
232
293
|
return this
|
|
233
294
|
}
|
|
295
|
+
|
|
296
|
+
// ─── Buffered output ──────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Enters buffered mode. Subsequent emissions accumulate in memory until
|
|
300
|
+
* `flush()` commits them in a single `write()` to the underlying sink.
|
|
301
|
+
*
|
|
302
|
+
* Essential for animation loops and full-screen redraws on terminals
|
|
303
|
+
* that perform poorly with many small writes (macOS Terminal, GNOME
|
|
304
|
+
* Terminal, Konsole). A 80×24 frame can easily produce thousands of
|
|
305
|
+
* style transitions; coalescing them into one write turns thousands
|
|
306
|
+
* of syscalls into one.
|
|
307
|
+
*
|
|
308
|
+
* Calling `buffer()` while already buffered is a no-op — buffered
|
|
309
|
+
* mode is a single state, not a stack.
|
|
310
|
+
*/
|
|
311
|
+
buffer(): this {
|
|
312
|
+
if (this._buffer === null) this._buffer = ''
|
|
313
|
+
return this
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Commits the accumulated buffer in a single `write()` and exits
|
|
318
|
+
* buffered mode. Calling `flush()` when not buffered is a no-op.
|
|
319
|
+
*/
|
|
320
|
+
flush(): this {
|
|
321
|
+
if (this._buffer !== null) {
|
|
322
|
+
const data = this._buffer
|
|
323
|
+
this._buffer = null
|
|
324
|
+
if (data.length > 0) this.writer.write(data)
|
|
325
|
+
}
|
|
326
|
+
return this
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Writes a pre-built string directly, bypassing `util.format`. Useful
|
|
331
|
+
* in hot loops where the caller has already constructed the exact bytes
|
|
332
|
+
* to emit (e.g. precomputed SGR sequences from a palette LUT) and wants
|
|
333
|
+
* to avoid the per-call formatting overhead.
|
|
334
|
+
*
|
|
335
|
+
* Honors buffered mode like every other emission.
|
|
336
|
+
*/
|
|
337
|
+
raw(data: string): this {
|
|
338
|
+
this.emit(data)
|
|
339
|
+
return this
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ─── Synchronized output ──────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Runs `fn` inside a synchronized-output block (DEC private mode 2026).
|
|
346
|
+
* The terminal accumulates all changes from the block and presents them
|
|
347
|
+
* atomically when the block ends, eliminating tearing in TUIs and
|
|
348
|
+
* animations. Terminals that don't support mode 2026 (e.g. macOS
|
|
349
|
+
* Terminal.app, plain xterm) ignore the escape silently.
|
|
350
|
+
*
|
|
351
|
+
* Automatically enables buffered mode for the duration of the block —
|
|
352
|
+
* synchronized output without buffering would defeat its purpose, since
|
|
353
|
+
* each tiny write would still race the terminal's refresh.
|
|
354
|
+
*
|
|
355
|
+
* Re-entrant: nested `sync()` calls share the outer block (mode 2026
|
|
356
|
+
* is not stackable in the protocol).
|
|
357
|
+
*
|
|
358
|
+
* The block is guarded by try/finally so the closing escape and flush
|
|
359
|
+
* always run, even if `fn` throws — important because mode 2026 has a
|
|
360
|
+
* ~150ms server-side timeout and leaving it open looks like a freeze.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* out.sync(() => {
|
|
364
|
+
* for (let row = 0; row < H; row++) {
|
|
365
|
+
* for (let col = 0; col < W; col++) {
|
|
366
|
+
* out.moveTo(row + 1, col + 1).paper(bg).ink(fg, '▄')
|
|
367
|
+
* }
|
|
368
|
+
* }
|
|
369
|
+
* })
|
|
370
|
+
*/
|
|
371
|
+
sync(fn: () => void): this {
|
|
372
|
+
const outer = this._syncDepth === 0
|
|
373
|
+
const startedBuffer = outer && this._buffer === null
|
|
374
|
+
if (outer) {
|
|
375
|
+
if (startedBuffer) this.buffer()
|
|
376
|
+
this.emit('\x1b[?2026h')
|
|
377
|
+
}
|
|
378
|
+
this._syncDepth++
|
|
379
|
+
try {
|
|
380
|
+
fn()
|
|
381
|
+
} finally {
|
|
382
|
+
this._syncDepth--
|
|
383
|
+
if (outer) {
|
|
384
|
+
this.emit('\x1b[?2026l')
|
|
385
|
+
if (startedBuffer) this.flush()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return this
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private emit(data: string): void {
|
|
392
|
+
if (this._buffer !== null) this._buffer += data
|
|
393
|
+
else this.writer.write(data)
|
|
394
|
+
}
|
|
234
395
|
}
|
|
235
396
|
|
|
236
397
|
/**
|
|
@@ -261,8 +422,16 @@ function installColorMethods(): void {
|
|
|
261
422
|
installColorMethods()
|
|
262
423
|
|
|
263
424
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
425
|
+
* Creates a new `Terminal` instance backed by `writer`.
|
|
426
|
+
*
|
|
427
|
+
* Accepts any object with a `write(string)` method — an xterm.js `Terminal`,
|
|
428
|
+
* a Node `Writable`, a Bun writer wrapper, or a test double.
|
|
429
|
+
*
|
|
430
|
+
* @param writer - The output sink. Must implement `write(data: string): void`.
|
|
431
|
+
* @param options.plain - Suppress all ANSI sequences and emit plain text only.
|
|
432
|
+
* Defaults to `true` when `NO_COLOR` is set in the environment.
|
|
433
|
+
* @returns A fully typed `ITerminal` with core methods and auto-generated
|
|
434
|
+
* named-color shortcuts (`red`, `bgBlue`, …).
|
|
266
435
|
*
|
|
267
436
|
* @example
|
|
268
437
|
* import { Terminal as XTerm } from '@xterm/xterm'
|
|
@@ -271,13 +440,8 @@ installColorMethods()
|
|
|
271
440
|
* const term = createTerminal(xterm)
|
|
272
441
|
* term.red('Hello ').bgBlue(' world ').reset().println()
|
|
273
442
|
*/
|
|
274
|
-
export function createTerminal(
|
|
275
|
-
writer: ITerminalWriter,
|
|
276
|
-
options: { plain?: boolean } = {},
|
|
277
|
-
): ITerminal {
|
|
443
|
+
export function createTerminal(writer: ITerminalWriter, options: { plain?: boolean } = {}): ITerminal {
|
|
278
444
|
return new TerminalCore(writer, options) as unknown as ITerminal
|
|
279
445
|
}
|
|
280
446
|
|
|
281
447
|
export { TerminalCore }
|
|
282
|
-
|
|
283
|
-
|