@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 +21 -0
- package/README.md +367 -0
- package/package.json +45 -0
- package/src/bun.ts +40 -0
- package/src/terminal.ts +263 -0
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
|
+
}
|
package/src/terminal.ts
ADDED
|
@@ -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
|
+
|