@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 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
- this.emit(`\x1B[?1049${b ? "h" : "l"}`);
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;
@@ -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
- private readonly writer;
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
- /** Sets the foreground color and optionally writes formatted text. */
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
- /** Sets the background color and optionally writes formatted text. */
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
- /** Enables or disables the alternate screen buffer. */
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
- * Note: this is the rename of the old `scroll()` method, which was
107
- * misnamed DEC private mode 7 controls wrapping, not scrolling.
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
- /** Sets the scrolling region (DECSTBM), both rows 1-based and inclusive. */
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
- * Public constructor. Accepts any object with a `write(string)` method —
115
- * an xterm.js `Terminal`, a Node `Writable`, a Bun writer wrapper, or a mock.
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
- this.emit(`\x1B[?1049${b ? "h" : "l"}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrovm/terminal",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Fluent ANSI terminal library — 24-bit color, cursor control and screen management for Bun and Node.js",
5
5
  "author": "Juan Carlos González Amestoy",
6
6
  "license": "MIT",
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 _writeOut=(data:string)=>{
25
- const w=Bun.stdout.writer()
26
- w.write(data)
27
- w.flush()
28
- }
24
+ const _outWriter = Bun.stdout.writer()
25
+ const _errWriter = Bun.stderr.writer()
29
26
 
30
- const _writeErr=(data:string)=>{
31
- const w=Bun.stderr.writer()
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
- export const Terminal={
38
- out: createTerminal({ write: _writeOut }) ,
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
- private readonly writer: ITerminalWriter,
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
- /** Sets the foreground color and optionally writes formatted text. */
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
- /** Sets the background color and optionally writes formatted text. */
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
- up(n: number = 1): this { this.emit(`\x1b[${n}A`); return this }
171
- down(n: number = 1): this { this.emit(`\x1b[${n}B`); return this }
172
- right(n: number = 1): this { this.emit(`\x1b[${n}C`); return this }
173
- left(n: number = 1): this { this.emit(`\x1b[${n}D`); return this }
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
- /** Enables or disables the alternate screen buffer. */
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
- this.emit(`\x1b[?1049${b ? 'h' : 'l'}`)
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
- * Note: this is the rename of the old `scroll()` method, which was
222
- * misnamed DEC private mode 7 controls wrapping, not scrolling.
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
- /** Sets the scrolling region (DECSTBM), both rows 1-based and inclusive. */
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
- * Public constructor. Accepts any object with a `write(string)` method —
265
- * an xterm.js `Terminal`, a Node `Writable`, a Bun writer wrapper, or a mock.
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
-