@retrovm/terminal 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Juan Carlos González Amestoy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,367 @@
1
+ # @retrovm/terminal
2
+
3
+ A fluent ANSI terminal library for Bun (and Node.js). Wraps any object with a `write(string)` method and exposes a chainable API for text output, 24-bit color, cursor control, and screen management.
4
+
5
+ Depends on [`@retrovm/color`](https://www.npmjs.com/package/@retrovm/color) (bundled — no extra install needed).
6
+
7
+ ## Features
8
+
9
+ - **Fluent API** — every method returns `this`, so calls chain naturally
10
+ - **24-bit RGB color** — foreground and background via hex strings, `rgb()`, or [`@retrovm/color`](https://www.npmjs.com/package/@retrovm/color) instances
11
+ - **Named color shortcuts** — `term.red(...)`, `term.bgBlue(...)` etc., generated automatically from the `Color` registry
12
+ - **Full cursor control** — move, save/restore, show/hide
13
+ - **Screen management** — clear, alternate buffer, scroll region, auto-wrap
14
+ - **Sink-agnostic** — works with Bun's stdout/stderr writers, Node.js `Writable`s, xterm.js terminals, or any test double
15
+ - **`NO_COLOR` support** — respects the [no-color.org](https://no-color.org/) convention out of the box
16
+ - **TypeScript-first** — strict types with full autocompletion for all color methods
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ ```sh
23
+ bun add @retrovm/terminal # or: npm install @retrovm/terminal
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Quick start
29
+
30
+ ### Bun
31
+
32
+ ```typescript
33
+ import { Terminal } from '@retrovm/terminal'
34
+
35
+ const term = Terminal.out // pre-wired to Bun.stdout
36
+ term.red('Hello ').bgBlue(' world ').reset().println()
37
+ ```
38
+
39
+ `Terminal.out` and `Terminal.err` are ready-made instances wired to `Bun.stdout` and `Bun.stderr`. Both are `ITerminal | undefined` — use `Terminal.out!` if you know stdout is available.
40
+
41
+ ### Any runtime (Node.js, xterm.js, custom)
42
+
43
+ ```typescript
44
+ import { createTerminal } from '@retrovm/terminal'
45
+
46
+ // Node.js writable
47
+ const term = createTerminal(process.stdout)
48
+
49
+ // xterm.js
50
+ import { Terminal as XTerm } from '@xterm/xterm'
51
+ const xterm = new XTerm()
52
+ xterm.open(document.getElementById('term')!)
53
+ const term = createTerminal(xterm)
54
+
55
+ // Custom / test
56
+ const lines: string[] = []
57
+ const term = createTerminal({ write: s => lines.push(s) })
58
+ ```
59
+
60
+ The only requirement is an object satisfying `ITerminalWriter` — `{ write(data: string): void }`.
61
+
62
+ ---
63
+
64
+ ## API reference
65
+
66
+ All methods return `this` (the `ITerminal` instance) unless noted otherwise.
67
+
68
+ ### Text output
69
+
70
+ #### `print(fmt?, ...args)`
71
+ Writes formatted text. Uses `util.format` semantics (`%s`, `%d`, `%o`, …).
72
+
73
+ ```typescript
74
+ term.print('Value: %d', 42) // "Value: 42"
75
+ term.print('Hello, %s!', 'world') // "Hello, world!"
76
+ ```
77
+
78
+ #### `println(fmt?, ...args)`
79
+ Same as `print` but appends `\r\n`.
80
+
81
+ ```typescript
82
+ term.println('Line 1').println('Line 2')
83
+ ```
84
+
85
+ ---
86
+
87
+ ### Color — foreground
88
+
89
+ #### `ink(color, fmt?, ...args)`
90
+ Sets the foreground color and optionally writes formatted text. Resets nothing — the color persists until changed or reset.
91
+
92
+ ```typescript
93
+ term.ink('#ff6600', 'Orange text')
94
+ term.ink('rgb(100, 200, 50)', 'Custom green')
95
+ term.ink(new Color('#3399ff'), 'Color instance')
96
+ term.ink('#aaaaaa') // set color only, write nothing
97
+ ```
98
+
99
+ #### `reset(fmt?, ...args)`
100
+ Resets **all** attributes (color, background, bold, etc.) and optionally writes text.
101
+
102
+ ```typescript
103
+ term.red('warning').reset(' — back to normal')
104
+ ```
105
+
106
+ #### `resetInk(fmt?, ...args)`
107
+ Resets only the foreground color (leaves background intact).
108
+
109
+ #### Named color shortcuts
110
+ Every named color in the `Color` registry gets an auto-generated method. Call them with or without text:
111
+
112
+ ```typescript
113
+ term.red('error')
114
+ term.lime('success')
115
+ term.cyan() // set color only
116
+ term.white('value: ').yellow('%d', n).reset()
117
+ ```
118
+
119
+ The full list depends on the `@retrovm/color` version. All are fully typed and appear in autocomplete.
120
+
121
+ ---
122
+
123
+ ### Color — background
124
+
125
+ #### `paper(color, fmt?, ...args)`
126
+ Sets the background color and optionally writes formatted text.
127
+
128
+ ```typescript
129
+ term.paper('#1a1a2e', ' dark bg ')
130
+ term.paper('rgb(30, 30, 30)') // set only
131
+ ```
132
+
133
+ #### `resetPaper(fmt?, ...args)`
134
+ Resets only the background color.
135
+
136
+ #### Named background shortcuts
137
+ Same as foreground but prefixed with `bg`:
138
+
139
+ ```typescript
140
+ term.bgRed(' error ')
141
+ term.bgBlue(' info ').white(' message ').reset()
142
+ ```
143
+
144
+ ---
145
+
146
+ ### Cursor movement
147
+
148
+ All movement methods accept an optional `n` (default `1`).
149
+
150
+ | Method | ANSI | Description |
151
+ |---|---|---|
152
+ | `up(n?)` | `ESC[nA` | Move cursor up `n` rows |
153
+ | `down(n?)` | `ESC[nB` | Move cursor down `n` rows |
154
+ | `right(n?)` | `ESC[nC` | Move cursor right `n` columns |
155
+ | `left(n?)` | `ESC[nD` | Move cursor left `n` columns |
156
+ | `column(n?)` | `ESC[nG` | Move to column `n` (1-based) |
157
+ | `row(n?)` | `ESC[nd` | Move to row `n` (1-based) |
158
+ | `moveTo(row, col)` | `ESC[row;colH` | Move to absolute position (both 1-based) |
159
+ | `saveCursor()` | `ESC[s` | Save current cursor position |
160
+ | `restoreCursor()` | `ESC[u` | Restore previously saved position |
161
+
162
+ ```typescript
163
+ term.moveTo(5, 1).print('Row 5, col 1')
164
+ term.saveCursor().moveTo(1, 1).print('top').restoreCursor()
165
+ term.column(1).print('back to start of line')
166
+ ```
167
+
168
+ #### `cursor(visible?)`
169
+ Shows or hides the cursor.
170
+
171
+ ```typescript
172
+ term.cursor(false) // hide
173
+ term.cursor(true) // show (default)
174
+ ```
175
+
176
+ ---
177
+
178
+ ### Screen
179
+
180
+ #### `cls()`
181
+ Clears the entire screen and moves the cursor to the home position (row 1, col 1).
182
+
183
+ ```typescript
184
+ term.cls()
185
+ ```
186
+
187
+ #### `clearLine()`
188
+ Clears from the cursor to the end of the current line.
189
+
190
+ ```typescript
191
+ term.column(20).clearLine() // clear everything after col 20
192
+ ```
193
+
194
+ #### `alt(enable?)`
195
+ Enables or disables the **alternate screen buffer**. The alternate buffer is a separate screen area — the original content is preserved and restored when you switch back. Essential for full-screen TUI applications.
196
+
197
+ ```typescript
198
+ term.alt(true).cursor(false).cls()
199
+ // ... full-screen UI ...
200
+ term.cursor(true).alt(false)
201
+ ```
202
+
203
+ #### `autoWrap(enable?)`
204
+ Enables or disables DECAWM (auto-wrap mode). When disabled, output past the last column stays at the last column instead of wrapping to the next line.
205
+
206
+ ```typescript
207
+ term.autoWrap(false)
208
+ ```
209
+
210
+ #### `scrollRegion(top, bottom)`
211
+ Sets the scrolling region to rows `top`–`bottom` (both 1-based, inclusive). Only that region scrolls; content above and below is pinned.
212
+
213
+ ```typescript
214
+ term.scrollRegion(3, 20) // only rows 3–20 scroll
215
+ term.scrollRegion(1, process.stdout.rows) // reset to full screen
216
+ ```
217
+
218
+ ---
219
+
220
+ ### Options
221
+
222
+ #### `plain`
223
+ Boolean property. When `true`, all ANSI escape sequences are suppressed and only plain text is emitted. Automatically set to `true` if `NO_COLOR` is present in the environment.
224
+
225
+ ```typescript
226
+ const term = createTerminal(writer, { plain: true })
227
+ term.plain = false // can be toggled at runtime
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Colors (`@retrovm/color`)
233
+
234
+ `@retrovm/color` is a bundled dependency — it ships with this package, nothing extra to install. The `Color` class integrates directly with `ink()` and `paper()`:
235
+
236
+ ```typescript
237
+ import { Color } from '@retrovm/color'
238
+
239
+ // Named colors (also available as term.red, term.bgBlue, etc.)
240
+ term.ink(Color.red, 'red text')
241
+
242
+ // HSV — cycle through the color wheel
243
+ term.ink(Color.fromHSV(0.6, 1, 1), 'blue') // hue 0=red, 0.33=green, 0.66=blue
244
+
245
+ // Gradient across a string
246
+ const str = 'Hello, world!'
247
+ for (let i = 0; i < str.length; i++) {
248
+ term.ink(Color.fromHSV(i / str.length, 1, 1), str[i])
249
+ }
250
+ term.reset()
251
+
252
+ // Interpolate between two hues
253
+ const H1 = 0.6, H2 = 0.38 // blue → cyan → green
254
+ for (let i = 0; i < 40; i++) {
255
+ term.ink(Color.fromHSV(H1 + (H2 - H1) * (i / 40), 1, 1), '█')
256
+ }
257
+ ```
258
+
259
+ Named shortcuts (`term.red`, `term.bgBlue`, …) are built from the static `Color` properties at construction time, so adding a color to the `Color` class automatically exposes it as a method — no changes needed here.
260
+
261
+ ---
262
+
263
+ ## Package exports
264
+
265
+ ```json
266
+ {
267
+ "exports": {
268
+ ".": {
269
+ "bun": "./src/bun.ts",
270
+ "types": "./src/bun.ts",
271
+ "import": "./src/terminal.ts"
272
+ }
273
+ }
274
+ }
275
+ ```
276
+
277
+ | Condition | File | Used by |
278
+ |---|---|---|
279
+ | `bun` | `src/bun.ts` | Bun runtime — exports pre-wired `Terminal.out` / `Terminal.err` plus re-exports everything from `terminal.ts` |
280
+ | `types` | `src/bun.ts` | TypeScript language server (VSCode, tsc) |
281
+ | `import` | `src/terminal.ts` | Node.js ESM, bundlers |
282
+
283
+ When running under Bun, `import { Terminal } from '@retrovm/terminal'` resolves to `src/bun.ts`. In Node.js it resolves to `src/terminal.ts`, which exports `createTerminal` and the `Terminal` class directly.
284
+
285
+ ---
286
+
287
+ ## Samples
288
+
289
+ Run any sample with `bun run sample/<name>.ts`.
290
+
291
+ ### `basic.ts`
292
+ Minimal hello-world in lime green.
293
+
294
+ ```typescript
295
+ import { Terminal } from '@retrovm/terminal'
296
+ Terminal.out!.lime('Hello, world!\n').reset()
297
+ ```
298
+
299
+ ### `rainbow.ts`
300
+ Cycles the full HSV hue wheel across a string, one color per character.
301
+
302
+ ```sh
303
+ bun run sample/rainbow.ts
304
+ ```
305
+
306
+ ### `progress.ts`
307
+ Animated multi-stage progress bar. Uses `column(1)` to rewrite the line in place — no alternate screen, no flicker. The filled blocks carry an HSV gradient from blue through cyan to green; the percentage label tracks the gradient front.
308
+
309
+ ```sh
310
+ bun run sample/progress.ts
311
+ ```
312
+
313
+ ### `matrix.ts`
314
+ Full-screen Matrix rain in the alternate buffer. Katakana + ASCII glyphs fall in columns with randomized speed and trail length. The head of each column is white; the trail fades from bright green to dark green. Exit with any key or after 15 seconds.
315
+
316
+ ```sh
317
+ bun run sample/matrix.ts
318
+ ```
319
+
320
+ ### `fire.ts`
321
+ Procedural fire simulation using the **half-block trick**: the `▄` character has its foreground and background set to different colors, doubling the effective vertical resolution. Heat propagates upward from a seeded bottom row, cooling as it rises. Color palette: black → dark red → orange → yellow → white.
322
+
323
+ ```sh
324
+ bun run sample/fire.ts
325
+ ```
326
+
327
+ ### `sysmon.ts`
328
+ Live system monitor that refreshes every second inside the alternate buffer. Two-panel layout drawn with box-drawing characters (`┌┬┐│├┤└┴┘─`). Left panel shows overall CPU average and per-core usage bars; right panel shows system RAM and process heap. All bars are color-coded: green below 70%, orange 70–90%, red above 90%.
329
+
330
+ ```sh
331
+ bun run sample/sysmon.ts
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Writing a custom sink
337
+
338
+ Any object satisfying `ITerminalWriter` (`{ write(data: string): void }`) works:
339
+
340
+ ```typescript
341
+ // Collect output for testing
342
+ const chunks: string[] = []
343
+ const term = createTerminal({ write: s => chunks.push(s) })
344
+ term.red('error').reset()
345
+ // chunks contains raw ANSI bytes
346
+
347
+ // Write to a file
348
+ import { createWriteStream } from 'node:fs'
349
+ const stream = createWriteStream('out.ans')
350
+ const term = createTerminal({ write: s => stream.write(s) })
351
+ ```
352
+
353
+ ---
354
+
355
+ ## `NO_COLOR`
356
+
357
+ If the environment variable `NO_COLOR` is set (to any value), `plain` defaults to `true` and all escape sequences are suppressed. The API stays identical — only the output changes.
358
+
359
+ ```sh
360
+ NO_COLOR=1 bun run sample/rainbow.ts # plain text, no color
361
+ ```
362
+
363
+ ---
364
+
365
+ ## License
366
+
367
+ MIT © Juan Carlos González Amestoy
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@retrovm/terminal",
3
+ "version": "0.1.0",
4
+ "description": "Fluent ANSI terminal library — 24-bit color, cursor control and screen management for Bun and Node.js",
5
+ "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
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/retrovm/terminal.git"
20
+ },
21
+ "type": "module",
22
+ "module": "src/terminal.ts",
23
+ "exports": {
24
+ ".": {
25
+ "bun": "./src/bun.ts",
26
+ "types": "./src/bun.ts",
27
+ "import": "./src/terminal.ts"
28
+ }
29
+ },
30
+ "files": [
31
+ "src"
32
+ ],
33
+ "devDependencies": {
34
+ "@types/bun": "latest"
35
+ },
36
+ "peerDependencies": {
37
+ "typescript": "^5"
38
+ },
39
+ "dependencies": {
40
+ "@retrovm/color": "^0.1.1"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
package/src/bun.ts ADDED
@@ -0,0 +1,40 @@
1
+ /*Copyright (c) 2026 Juan Carlos González Amestoy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.*/
20
+
21
+ import { createTerminal } from "./terminal"
22
+ export { createTerminal, type ITerminal, type ITerminalWriter } from "./terminal"
23
+
24
+ const _writeOut=(data:string)=>{
25
+ const w=Bun.stdout.writer()
26
+ w.write(data)
27
+ w.flush()
28
+ }
29
+
30
+ const _writeErr=(data:string)=>{
31
+ const w=Bun.stderr.writer()
32
+ w.write(data)
33
+ w.flush()
34
+ }
35
+
36
+
37
+ export const Terminal={
38
+ out: createTerminal({ write: _writeOut }) ,
39
+ err: createTerminal({ write: _writeErr }) ,
40
+ }
@@ -0,0 +1,263 @@
1
+ /*Copyright (c) 2026 Juan Carlos González Amestoy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.*/
20
+
21
+ import { Color } from '@retrovm/color'
22
+ import { format } from 'node:util'
23
+
24
+ /**
25
+ * Minimal sink interface. Anything with a `write(data: string)` method works:
26
+ * an xterm.js Terminal instance, a Node Writable, a Bun writer wrapper, or a
27
+ * test double that pushes to an array.
28
+ */
29
+ export interface ITerminalWriter {
30
+ write(data: string): void
31
+ }
32
+
33
+ /**
34
+ * Extracts from `Color` only the keys whose value is an instance of `Color`,
35
+ * so the generated method names exactly match the available named colors.
36
+ * Add a color to the `Color` class and both `ink<Name>` and `bg<Name>` methods
37
+ * appear automatically with full type safety and autocompletion.
38
+ */
39
+ type ColorName = {
40
+ [K in keyof typeof Color]: typeof Color[K] extends Color ? K : never
41
+ }[keyof typeof Color]
42
+
43
+ type ColorMethod = (fmt?: string, ...args: unknown[]) => ITerminal
44
+ type InkMethods = { [K in ColorName]: ColorMethod }
45
+ type BgMethods = { [K in ColorName as `bg${Capitalize<string & K>}`]: ColorMethod }
46
+
47
+ /**
48
+ * Public type of a Terminal instance: core methods plus all the auto-generated
49
+ * color shortcuts. Consumers see one cohesive type with autocompletion.
50
+ */
51
+ export type ITerminal = TerminalCore & InkMethods & BgMethods
52
+
53
+ /**
54
+ * Core terminal output. Wraps any object with a `write(string)` method and
55
+ * exposes a fluent API for ANSI styling and cursor control.
56
+ *
57
+ * Color shortcut methods (e.g. `red`, `bgBlue`) are attached at construction
58
+ * time from the `Color` registry. They are declared via the `ITerminal` type
59
+ * and dispatched through an index signature on the class.
60
+ */
61
+ class TerminalCore {
62
+ /** ANSI is suppressed when true; styling/cursor methods become no-ops. */
63
+ public plain: boolean
64
+
65
+ // Allows the auto-attached color methods to typecheck on `this`.
66
+ [key: string]: unknown
67
+
68
+ constructor(
69
+ private readonly writer: ITerminalWriter,
70
+ options: { plain?: boolean } = {},
71
+ ) {
72
+ // Honor NO_COLOR (https://no-color.org/) by default when running under
73
+ // Node/Bun; in browser/xterm contexts `process` may be undefined.
74
+ const envNoColor =
75
+ typeof process !== 'undefined' && !!process?.env?.NO_COLOR
76
+ this.plain = options.plain ?? envNoColor
77
+ }
78
+
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
+ /** Internal style helper: skips ANSI when `plain` is enabled. */
85
+ private style(seq: string, fmt: string, args: unknown[]): this {
86
+ const text = format(fmt, ...args)
87
+ this.emit(this.plain ? text : seq + text)
88
+ return this
89
+ }
90
+
91
+ // ─── Text output ──────────────────────────────────────────────────────────
92
+
93
+ /** Prints formatted text. Uses `util.format` semantics (`%s`, `%d`, …). */
94
+ print(fmt: string = '', ...args: unknown[]): this {
95
+ this.emit(format(fmt, ...args))
96
+ return this
97
+ }
98
+
99
+ /** Prints formatted text followed by a newline. */
100
+ println(fmt: string = '', ...args: unknown[]): this {
101
+ this.emit(format(fmt, ...args) + '\r\n')
102
+ return this
103
+ }
104
+
105
+ // ─── Color ────────────────────────────────────────────────────────────────
106
+
107
+ /** Sets the foreground color and optionally writes formatted text. */
108
+ ink(c: string | Color, fmt: string = '', ...args: unknown[]): this {
109
+ const color = typeof c === 'string' ? new Color(c) : c
110
+ return this.style(color.toAnsiRGB(), fmt, args)
111
+ }
112
+
113
+ /** Sets the background color and optionally writes formatted text. */
114
+ paper(c: string | Color, fmt: string = '', ...args: unknown[]): this {
115
+ const color = typeof c === 'string' ? new Color(c) : c
116
+ return this.style(color.toAnsiBackgroundRGB(), fmt, args)
117
+ }
118
+
119
+ /** Resets all attributes (color, background, intensity, …). */
120
+ reset(fmt: string = '', ...args: unknown[]): this {
121
+ return this.style('\x1b[0m', fmt, args)
122
+ }
123
+
124
+ /** Resets only the foreground color. */
125
+ resetInk(fmt: string = '', ...args: unknown[]): this {
126
+ return this.style('\x1b[39m', fmt, args)
127
+ }
128
+
129
+ /** Resets only the background color. */
130
+ resetPaper(fmt: string = '', ...args: unknown[]): this {
131
+ return this.style('\x1b[49m', fmt, args)
132
+ }
133
+
134
+ // ─── Screen ───────────────────────────────────────────────────────────────
135
+
136
+ /** Clears the screen and homes the cursor. */
137
+ cls(): this {
138
+ this.emit('\x1b[2J\x1b[H')
139
+ return this
140
+ }
141
+
142
+ /** Clears from cursor to end of line. */
143
+ clearLine(): this {
144
+ this.emit('\x1b[K')
145
+ return this
146
+ }
147
+
148
+ // ─── Cursor ───────────────────────────────────────────────────────────────
149
+
150
+ up(n: number = 1): this { this.emit(`\x1b[${n}A`); return this }
151
+ down(n: number = 1): this { this.emit(`\x1b[${n}B`); return this }
152
+ right(n: number = 1): this { this.emit(`\x1b[${n}C`); return this }
153
+ left(n: number = 1): this { this.emit(`\x1b[${n}D`); return this }
154
+
155
+ /** Moves the cursor to the given column (1-based). */
156
+ column(n: number = 1): this {
157
+ this.emit(`\x1b[${n}G`)
158
+ return this
159
+ }
160
+
161
+ /** Moves the cursor to the given row (1-based). */
162
+ row(n: number = 1): this {
163
+ this.emit(`\x1b[${n}d`)
164
+ return this
165
+ }
166
+
167
+ /** Moves the cursor to the given (row, col), both 1-based. */
168
+ moveTo(row: number, col: number): this {
169
+ this.emit(`\x1b[${row};${col}H`)
170
+ return this
171
+ }
172
+
173
+ /** Saves the current cursor position. */
174
+ saveCursor(): this {
175
+ this.emit('\x1b[s')
176
+ return this
177
+ }
178
+
179
+ /** Restores the previously saved cursor position. */
180
+ restoreCursor(): this {
181
+ this.emit('\x1b[u')
182
+ return this
183
+ }
184
+
185
+ /** Shows or hides the cursor. */
186
+ cursor(visible: boolean = true): this {
187
+ this.emit(visible ? '\x1b[?25h' : '\x1b[?25l')
188
+ return this
189
+ }
190
+
191
+ // ─── Modes ────────────────────────────────────────────────────────────────
192
+
193
+ /** Enables or disables the alternate screen buffer. */
194
+ alt(b: boolean = true): this {
195
+ this.emit(`\x1b[?1049${b ? 'h' : 'l'}`)
196
+ return this
197
+ }
198
+
199
+ /**
200
+ * Enables or disables auto-wrap (DECAWM, mode 7).
201
+ * Note: this is the rename of the old `scroll()` method, which was
202
+ * misnamed — DEC private mode 7 controls wrapping, not scrolling.
203
+ */
204
+ autoWrap(b: boolean = true): this {
205
+ this.emit(`\x1b[?7${b ? 'h' : 'l'}`)
206
+ return this
207
+ }
208
+
209
+ /** Sets the scrolling region (DECSTBM), both rows 1-based and inclusive. */
210
+ scrollRegion(top: number, bottom: number): this {
211
+ this.emit(`\x1b[${top};${bottom}r`)
212
+ return this
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Attaches one method per named color in the `Color` registry. Done once,
218
+ * on the prototype, so every Terminal instance shares the same functions and
219
+ * adding a new color to `Color` propagates automatically — no edits here.
220
+ */
221
+ function installColorMethods(): void {
222
+ const proto = TerminalCore.prototype as unknown as Record<string, unknown>
223
+ const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
224
+
225
+ for (const key of Object.keys(Color) as Array<keyof typeof Color>) {
226
+ const value = Color[key]
227
+ if (!(value instanceof Color)) continue
228
+
229
+ const name = key as string
230
+
231
+ proto[name] = function (this: TerminalCore, fmt: string = '', ...args: unknown[]) {
232
+ return (this as unknown as ITerminal).ink(value, fmt, ...args)
233
+ }
234
+
235
+ proto[`bg${cap(name)}`] = function (this: TerminalCore, fmt: string = '', ...args: unknown[]) {
236
+ return (this as unknown as ITerminal).paper(value, fmt, ...args)
237
+ }
238
+ }
239
+ }
240
+
241
+ installColorMethods()
242
+
243
+ /**
244
+ * Public constructor. Accepts any object with a `write(string)` method —
245
+ * an xterm.js `Terminal`, a Node `Writable`, a Bun writer wrapper, or a mock.
246
+ *
247
+ * @example
248
+ * import { Terminal as XTerm } from '@xterm/xterm'
249
+ * const xterm = new XTerm()
250
+ * xterm.open(document.getElementById('term')!)
251
+ * const term = createTerminal(xterm)
252
+ * term.red('Hello ').bgBlue(' world ').reset().println()
253
+ */
254
+ export function createTerminal(
255
+ writer: ITerminalWriter,
256
+ options: { plain?: boolean } = {},
257
+ ): ITerminal {
258
+ return new TerminalCore(writer, options) as unknown as ITerminal
259
+ }
260
+
261
+ export { TerminalCore }
262
+
263
+