@retrovm/terminal 0.1.3 → 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 +47 -3
- package/dist/terminal.d.ts +124 -14
- package/dist/terminal.js +47 -3
- package/package.json +1 -1
- package/src/bun.ts +7 -14
- package/src/terminal.ts +188 -29
package/dist/node.js
CHANGED
|
@@ -311,15 +311,14 @@ import { format } from "node:util";
|
|
|
311
311
|
class TerminalCore {
|
|
312
312
|
writer;
|
|
313
313
|
plain;
|
|
314
|
+
_buffer = null;
|
|
315
|
+
_syncDepth = 0;
|
|
314
316
|
_altScreen = false;
|
|
315
317
|
constructor(writer, options = {}) {
|
|
316
318
|
this.writer = writer;
|
|
317
319
|
const envNoColor = typeof process !== "undefined" && !!process?.env?.NO_COLOR;
|
|
318
320
|
this.plain = options.plain ?? envNoColor;
|
|
319
321
|
}
|
|
320
|
-
emit(data) {
|
|
321
|
-
this.writer.write(data);
|
|
322
|
-
}
|
|
323
322
|
style(seq, fmt, args) {
|
|
324
323
|
const text = format(fmt, ...args);
|
|
325
324
|
this.emit(this.plain ? text : seq + text);
|
|
@@ -417,6 +416,51 @@ class TerminalCore {
|
|
|
417
416
|
this.emit(`\x1B[${top};${bottom}r`);
|
|
418
417
|
return this;
|
|
419
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
|
+
}
|
|
420
464
|
}
|
|
421
465
|
function installColorMethods() {
|
|
422
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,25 +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
|
-
|
|
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;
|
|
44
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
|
+
*/
|
|
45
56
|
constructor(writer: ITerminalWriter, options?: {
|
|
46
57
|
plain?: boolean;
|
|
47
58
|
});
|
|
48
|
-
/** Internal write helper. Single point of contact with the sink. */
|
|
49
|
-
private emit;
|
|
50
59
|
/** Internal style helper: skips ANSI when `plain` is enabled. */
|
|
51
60
|
private style;
|
|
52
61
|
/** Prints formatted text. Uses `util.format` semantics (`%s`, `%d`, …). */
|
|
53
62
|
print(fmt?: string, ...args: unknown[]): this;
|
|
54
63
|
/** Prints formatted text followed by a newline. */
|
|
55
64
|
println(fmt?: string, ...args: unknown[]): this;
|
|
56
|
-
/**
|
|
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
|
+
*/
|
|
57
74
|
ink(c: string | Color, fmt?: string, ...args: unknown[]): this;
|
|
58
|
-
/**
|
|
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
|
+
*/
|
|
59
84
|
paper(c: string | Color, fmt?: string, ...args: unknown[]): this;
|
|
60
85
|
/**
|
|
61
86
|
* Resets all text attributes (color, background, bold, dim, italic,
|
|
@@ -84,9 +109,13 @@ declare class TerminalCore {
|
|
|
84
109
|
cls(): this;
|
|
85
110
|
/** Clears from cursor to end of line. */
|
|
86
111
|
clearLine(): this;
|
|
112
|
+
/** Moves the cursor up by `n` rows (CUU). */
|
|
87
113
|
up(n?: number): this;
|
|
114
|
+
/** Moves the cursor down by `n` rows (CUD). */
|
|
88
115
|
down(n?: number): this;
|
|
116
|
+
/** Moves the cursor right by `n` columns (CUF). */
|
|
89
117
|
right(n?: number): this;
|
|
118
|
+
/** Moves the cursor left by `n` columns (CUB). */
|
|
90
119
|
left(n?: number): this;
|
|
91
120
|
/** Moves the cursor to the given column (1-based). */
|
|
92
121
|
column(n?: number): this;
|
|
@@ -100,20 +129,101 @@ declare class TerminalCore {
|
|
|
100
129
|
restoreCursor(): this;
|
|
101
130
|
/** Shows or hides the cursor. */
|
|
102
131
|
cursor(visible?: boolean): this;
|
|
103
|
-
/**
|
|
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
|
+
*/
|
|
104
140
|
alt(b?: boolean): this;
|
|
105
141
|
/**
|
|
106
|
-
* Enables or disables auto-wrap (DECAWM, mode 7).
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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.
|
|
109
146
|
*/
|
|
110
147
|
autoWrap(b?: boolean): this;
|
|
111
|
-
/**
|
|
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
|
+
*/
|
|
112
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;
|
|
113
215
|
}
|
|
114
216
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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`, …).
|
|
117
227
|
*
|
|
118
228
|
* @example
|
|
119
229
|
* import { Terminal as XTerm } from '@xterm/xterm'
|
package/dist/terminal.js
CHANGED
|
@@ -311,15 +311,14 @@ import { format } from "node:util";
|
|
|
311
311
|
class TerminalCore {
|
|
312
312
|
writer;
|
|
313
313
|
plain;
|
|
314
|
+
_buffer = null;
|
|
315
|
+
_syncDepth = 0;
|
|
314
316
|
_altScreen = false;
|
|
315
317
|
constructor(writer, options = {}) {
|
|
316
318
|
this.writer = writer;
|
|
317
319
|
const envNoColor = typeof process !== "undefined" && !!process?.env?.NO_COLOR;
|
|
318
320
|
this.plain = options.plain ?? envNoColor;
|
|
319
321
|
}
|
|
320
|
-
emit(data) {
|
|
321
|
-
this.writer.write(data);
|
|
322
|
-
}
|
|
323
322
|
style(seq, fmt, args) {
|
|
324
323
|
const text = format(fmt, ...args);
|
|
325
324
|
this.emit(this.plain ? text : seq + text);
|
|
@@ -417,6 +416,51 @@ class TerminalCore {
|
|
|
417
416
|
this.emit(`\x1B[${top};${bottom}r`);
|
|
418
417
|
return this;
|
|
419
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
|
+
}
|
|
420
464
|
}
|
|
421
465
|
function installColorMethods() {
|
|
422
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,27 +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
|
-
|
|
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;
|
|
66
71
|
|
|
67
72
|
// Allows the auto-attached color methods to typecheck on `this`.
|
|
68
73
|
[key: string]: unknown
|
|
69
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
|
+
*/
|
|
70
81
|
constructor(
|
|
71
|
-
|
|
82
|
+
protected readonly writer: ITerminalWriter,
|
|
72
83
|
options: { plain?: boolean } = {},
|
|
73
84
|
) {
|
|
74
85
|
// Honor NO_COLOR (https://no-color.org/) by default when running under
|
|
75
86
|
// Node/Bun; in browser/xterm contexts `process` may be undefined.
|
|
76
|
-
const envNoColor =
|
|
77
|
-
typeof process !== 'undefined' && !!process?.env?.NO_COLOR
|
|
87
|
+
const envNoColor = typeof process !== 'undefined' && !!process?.env?.NO_COLOR
|
|
78
88
|
this.plain = options.plain ?? envNoColor
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
/** Internal write helper. Single point of contact with the sink. */
|
|
82
|
-
private emit(data: string): void {
|
|
83
|
-
this.writer.write(data)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
91
|
/** Internal style helper: skips ANSI when `plain` is enabled. */
|
|
87
92
|
private style(seq: string, fmt: string, args: unknown[]): this {
|
|
88
93
|
const text = format(fmt, ...args)
|
|
@@ -106,13 +111,29 @@ class TerminalCore {
|
|
|
106
111
|
|
|
107
112
|
// ─── Color ────────────────────────────────────────────────────────────────
|
|
108
113
|
|
|
109
|
-
/**
|
|
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
|
+
*/
|
|
110
123
|
ink(c: string | Color, fmt: string = '', ...args: unknown[]): this {
|
|
111
124
|
const color = typeof c === 'string' ? new Color(c) : c
|
|
112
125
|
return this.style(color.toAnsiRGB(), fmt, args)
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
/**
|
|
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
|
+
*/
|
|
116
137
|
paper(c: string | Color, fmt: string = '', ...args: unknown[]): this {
|
|
117
138
|
const color = typeof c === 'string' ? new Color(c) : c
|
|
118
139
|
return this.style(color.toAnsiBackgroundRGB(), fmt, args)
|
|
@@ -169,10 +190,29 @@ class TerminalCore {
|
|
|
169
190
|
|
|
170
191
|
// ─── Cursor ───────────────────────────────────────────────────────────────
|
|
171
192
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
176
216
|
|
|
177
217
|
/** Moves the cursor to the given column (1-based). */
|
|
178
218
|
column(n: number = 1): this {
|
|
@@ -212,7 +252,14 @@ class TerminalCore {
|
|
|
212
252
|
|
|
213
253
|
// ─── Modes ────────────────────────────────────────────────────────────────
|
|
214
254
|
|
|
215
|
-
/**
|
|
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
|
+
*/
|
|
216
263
|
alt(b: boolean = true): this {
|
|
217
264
|
if (b !== this._altScreen) {
|
|
218
265
|
this.emit(`\x1b[?1049${b ? 'h' : 'l'}`)
|
|
@@ -222,20 +269,129 @@ class TerminalCore {
|
|
|
222
269
|
}
|
|
223
270
|
|
|
224
271
|
/**
|
|
225
|
-
* Enables or disables auto-wrap (DECAWM, mode 7).
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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.
|
|
228
276
|
*/
|
|
229
277
|
autoWrap(b: boolean = true): this {
|
|
230
278
|
this.emit(`\x1b[?7${b ? 'h' : 'l'}`)
|
|
231
279
|
return this
|
|
232
280
|
}
|
|
233
281
|
|
|
234
|
-
/**
|
|
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
|
+
*/
|
|
235
291
|
scrollRegion(top: number, bottom: number): this {
|
|
236
292
|
this.emit(`\x1b[${top};${bottom}r`)
|
|
237
293
|
return this
|
|
238
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
|
+
}
|
|
239
395
|
}
|
|
240
396
|
|
|
241
397
|
/**
|
|
@@ -266,8 +422,16 @@ function installColorMethods(): void {
|
|
|
266
422
|
installColorMethods()
|
|
267
423
|
|
|
268
424
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
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`, …).
|
|
271
435
|
*
|
|
272
436
|
* @example
|
|
273
437
|
* import { Terminal as XTerm } from '@xterm/xterm'
|
|
@@ -276,13 +440,8 @@ installColorMethods()
|
|
|
276
440
|
* const term = createTerminal(xterm)
|
|
277
441
|
* term.red('Hello ').bgBlue(' world ').reset().println()
|
|
278
442
|
*/
|
|
279
|
-
export function createTerminal(
|
|
280
|
-
writer: ITerminalWriter,
|
|
281
|
-
options: { plain?: boolean } = {},
|
|
282
|
-
): ITerminal {
|
|
443
|
+
export function createTerminal(writer: ITerminalWriter, options: { plain?: boolean } = {}): ITerminal {
|
|
283
444
|
return new TerminalCore(writer, options) as unknown as ITerminal
|
|
284
445
|
}
|
|
285
446
|
|
|
286
447
|
export { TerminalCore }
|
|
287
|
-
|
|
288
|
-
|