@retrovm/terminal 0.1.3 → 0.2.1

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
@@ -27,6 +27,11 @@ class Color {
27
27
  this._g = g;
28
28
  this._b = b;
29
29
  this._a = a;
30
+ } else if (typeof rs === "number" && g === undefined && b === undefined) {
31
+ this._r = (rs & 255) / 255;
32
+ this._g = (rs >> 8 & 255) / 255;
33
+ this._b = (rs >> 16 & 255) / 255;
34
+ this._a = (rs >> 24 & 255) / 255;
30
35
  } else if (typeof rs === "string") {
31
36
  const match = rs.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i);
32
37
  if (match) {
@@ -126,6 +131,9 @@ class Color {
126
131
  this._ansiFg = undefined;
127
132
  this._ansiBg = undefined;
128
133
  }
134
+ toRGBA() {
135
+ return (Math.round(this._a * 255) << 24 | Math.round(this._b * 255) << 16 | Math.round(this._g * 255) << 8 | Math.round(this._r * 255)) >>> 0;
136
+ }
129
137
  toString() {
130
138
  return `#${Math.round(this._r * 255).toString(16).padStart(2, "0")}${Math.round(this._g * 255).toString(16).padStart(2, "0")}${Math.round(this._b * 255).toString(16).padStart(2, "0")}${Math.round(this._a * 255).toString(16).padStart(2, "0")}`;
131
139
  }
@@ -311,15 +319,14 @@ import { format } from "node:util";
311
319
  class TerminalCore {
312
320
  writer;
313
321
  plain;
322
+ _buffer = null;
323
+ _syncDepth = 0;
314
324
  _altScreen = false;
315
325
  constructor(writer, options = {}) {
316
326
  this.writer = writer;
317
327
  const envNoColor = typeof process !== "undefined" && !!process?.env?.NO_COLOR;
318
328
  this.plain = options.plain ?? envNoColor;
319
329
  }
320
- emit(data) {
321
- this.writer.write(data);
322
- }
323
330
  style(seq, fmt, args) {
324
331
  const text = format(fmt, ...args);
325
332
  this.emit(this.plain ? text : seq + text);
@@ -417,6 +424,51 @@ class TerminalCore {
417
424
  this.emit(`\x1B[${top};${bottom}r`);
418
425
  return this;
419
426
  }
427
+ buffer() {
428
+ if (this._buffer === null)
429
+ this._buffer = "";
430
+ return this;
431
+ }
432
+ flush() {
433
+ if (this._buffer !== null) {
434
+ const data = this._buffer;
435
+ this._buffer = null;
436
+ if (data.length > 0)
437
+ this.writer.write(data);
438
+ }
439
+ return this;
440
+ }
441
+ raw(data) {
442
+ this.emit(data);
443
+ return this;
444
+ }
445
+ sync(fn) {
446
+ const outer = this._syncDepth === 0;
447
+ const startedBuffer = outer && this._buffer === null;
448
+ if (outer) {
449
+ if (startedBuffer)
450
+ this.buffer();
451
+ this.emit("\x1B[?2026h");
452
+ }
453
+ this._syncDepth++;
454
+ try {
455
+ fn();
456
+ } finally {
457
+ this._syncDepth--;
458
+ if (outer) {
459
+ this.emit("\x1B[?2026l");
460
+ if (startedBuffer)
461
+ this.flush();
462
+ }
463
+ }
464
+ return this;
465
+ }
466
+ emit(data) {
467
+ if (this._buffer !== null)
468
+ this._buffer += data;
469
+ else
470
+ this.writer.write(data);
471
+ }
420
472
  }
421
473
  function installColorMethods() {
422
474
  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,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
- 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
- private _altScreen;
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
- /** 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
+ */
57
74
  ink(c: string | Color, fmt?: string, ...args: unknown[]): this;
58
- /** 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
+ */
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
- /** 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
+ */
104
140
  alt(b?: boolean): this;
105
141
  /**
106
- * Enables or disables auto-wrap (DECAWM, mode 7).
107
- * Note: this is the rename of the old `scroll()` method, which was
108
- * 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.
109
146
  */
110
147
  autoWrap(b?: boolean): this;
111
- /** 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
+ */
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
- * Public constructor. Accepts any object with a `write(string)` method —
116
- * 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`, …).
117
227
  *
118
228
  * @example
119
229
  * import { Terminal as XTerm } from '@xterm/xterm'
package/dist/terminal.js CHANGED
@@ -27,6 +27,11 @@ class Color {
27
27
  this._g = g;
28
28
  this._b = b;
29
29
  this._a = a;
30
+ } else if (typeof rs === "number" && g === undefined && b === undefined) {
31
+ this._r = (rs & 255) / 255;
32
+ this._g = (rs >> 8 & 255) / 255;
33
+ this._b = (rs >> 16 & 255) / 255;
34
+ this._a = (rs >> 24 & 255) / 255;
30
35
  } else if (typeof rs === "string") {
31
36
  const match = rs.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i);
32
37
  if (match) {
@@ -126,6 +131,9 @@ class Color {
126
131
  this._ansiFg = undefined;
127
132
  this._ansiBg = undefined;
128
133
  }
134
+ toRGBA() {
135
+ return (Math.round(this._a * 255) << 24 | Math.round(this._b * 255) << 16 | Math.round(this._g * 255) << 8 | Math.round(this._r * 255)) >>> 0;
136
+ }
129
137
  toString() {
130
138
  return `#${Math.round(this._r * 255).toString(16).padStart(2, "0")}${Math.round(this._g * 255).toString(16).padStart(2, "0")}${Math.round(this._b * 255).toString(16).padStart(2, "0")}${Math.round(this._a * 255).toString(16).padStart(2, "0")}`;
131
139
  }
@@ -311,15 +319,14 @@ import { format } from "node:util";
311
319
  class TerminalCore {
312
320
  writer;
313
321
  plain;
322
+ _buffer = null;
323
+ _syncDepth = 0;
314
324
  _altScreen = false;
315
325
  constructor(writer, options = {}) {
316
326
  this.writer = writer;
317
327
  const envNoColor = typeof process !== "undefined" && !!process?.env?.NO_COLOR;
318
328
  this.plain = options.plain ?? envNoColor;
319
329
  }
320
- emit(data) {
321
- this.writer.write(data);
322
- }
323
330
  style(seq, fmt, args) {
324
331
  const text = format(fmt, ...args);
325
332
  this.emit(this.plain ? text : seq + text);
@@ -417,6 +424,51 @@ class TerminalCore {
417
424
  this.emit(`\x1B[${top};${bottom}r`);
418
425
  return this;
419
426
  }
427
+ buffer() {
428
+ if (this._buffer === null)
429
+ this._buffer = "";
430
+ return this;
431
+ }
432
+ flush() {
433
+ if (this._buffer !== null) {
434
+ const data = this._buffer;
435
+ this._buffer = null;
436
+ if (data.length > 0)
437
+ this.writer.write(data);
438
+ }
439
+ return this;
440
+ }
441
+ raw(data) {
442
+ this.emit(data);
443
+ return this;
444
+ }
445
+ sync(fn) {
446
+ const outer = this._syncDepth === 0;
447
+ const startedBuffer = outer && this._buffer === null;
448
+ if (outer) {
449
+ if (startedBuffer)
450
+ this.buffer();
451
+ this.emit("\x1B[?2026h");
452
+ }
453
+ this._syncDepth++;
454
+ try {
455
+ fn();
456
+ } finally {
457
+ this._syncDepth--;
458
+ if (outer) {
459
+ this.emit("\x1B[?2026l");
460
+ if (startedBuffer)
461
+ this.flush();
462
+ }
463
+ }
464
+ return this;
465
+ }
466
+ emit(data) {
467
+ if (this._buffer !== null)
468
+ this._buffer += data;
469
+ else
470
+ this.writer.write(data);
471
+ }
420
472
  }
421
473
  function installColorMethods() {
422
474
  const proto = TerminalCore.prototype;
package/package.json CHANGED
@@ -1,28 +1,19 @@
1
1
  {
2
2
  "name": "@retrovm/terminal",
3
- "version": "0.1.3",
4
- "description": "Fluent ANSI terminal library — 24-bit color, cursor control and screen management for Bun and Node.js",
3
+ "version": "0.2.1",
5
4
  "author": "Juan Carlos González Amestoy",
6
- "license": "MIT",
7
- "keywords": [
8
- "terminal",
9
- "ansi",
10
- "color",
11
- "cursor",
12
- "tui",
13
- "cli",
14
- "bun",
15
- "fluent"
16
- ],
17
5
  "repository": {
18
6
  "type": "git",
19
7
  "url": "https://github.com/retrovm/terminal.git"
20
8
  },
21
- "type": "module",
22
9
  "module": "src/terminal.ts",
23
- "scripts": {
24
- "build": "bun build src/terminal.ts src/node.ts --outdir dist --target node && tsc -p tsconfig.build.json",
25
- "prepublishOnly": "bun run build"
10
+ "devDependencies": {
11
+ "@types/bun": "latest",
12
+ "@types/node": "^25.9.1",
13
+ "typescript": "^5.9.3"
14
+ },
15
+ "peerDependencies": {
16
+ "typescript": "^5"
26
17
  },
27
18
  "exports": {
28
19
  ".": {
@@ -35,22 +26,31 @@
35
26
  "import": "./dist/terminal.js"
36
27
  }
37
28
  },
29
+ "description": "Fluent ANSI terminal library — 24-bit color, cursor control and screen management for Bun and Node.js",
38
30
  "files": [
39
31
  "src",
40
32
  "dist"
41
33
  ],
42
- "devDependencies": {
43
- "@types/bun": "latest",
44
- "@types/node": "^25.8.0",
45
- "typescript": "^5"
34
+ "keywords": [
35
+ "terminal",
36
+ "ansi",
37
+ "color",
38
+ "cursor",
39
+ "tui",
40
+ "cli",
41
+ "bun",
42
+ "fluent"
43
+ ],
44
+ "license": "MIT",
45
+ "publishConfig": {
46
+ "access": "public"
46
47
  },
47
- "peerDependencies": {
48
- "typescript": "^5"
48
+ "scripts": {
49
+ "build": "bun build src/terminal.ts src/node.ts --outdir dist --target node && tsc -p tsconfig.build.json",
50
+ "prepublishOnly": "bun run build"
49
51
  },
52
+ "type": "module",
50
53
  "dependencies": {
51
- "@retrovm/color": "^0.1.1"
52
- },
53
- "publishConfig": {
54
- "access": "public"
54
+ "@retrovm/color": "^0.2.2"
55
55
  }
56
56
  }
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,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
- private _altScreen = false;
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
- private readonly writer: ITerminalWriter,
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
- /** 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
+ */
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
- /** 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
+ */
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
- up(n: number = 1): this { this.emit(`\x1b[${n}A`); return this }
173
- down(n: number = 1): this { this.emit(`\x1b[${n}B`); return this }
174
- right(n: number = 1): this { this.emit(`\x1b[${n}C`); return this }
175
- 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
+ }
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
- /** 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
+ */
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
- * Note: this is the rename of the old `scroll()` method, which was
227
- * 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.
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
- /** 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
+ */
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
- * Public constructor. Accepts any object with a `write(string)` method —
270
- * 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`, …).
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
-