vt100.js 0.2.1 → 0.2.3
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/README.md +94 -0
- package/package.json +1 -1
- package/src/index.ts +0 -1
- package/src/screen.ts +142 -339
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# vt100.js
|
|
2
|
+
|
|
3
|
+
Pure TypeScript VT100 terminal emulator. Zero dependencies, headless, fast.
|
|
4
|
+
|
|
5
|
+
Part of the [vterm](https://github.com/beorn/vterm) monorepo.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Full VT100/ANSI escape sequence parsing
|
|
10
|
+
- SGR attributes (bold, italic, underline styles, colors, strikethrough, etc.)
|
|
11
|
+
- 16-color, 256-color, and 24-bit truecolor support
|
|
12
|
+
- Cursor movement, save/restore (DECSC/DECRC)
|
|
13
|
+
- Screen modes (alternate screen, auto-wrap, origin mode, etc.)
|
|
14
|
+
- Scroll regions (DECSTBM) with content preservation
|
|
15
|
+
- Scrollback buffer with configurable limit
|
|
16
|
+
- Insert/delete characters and lines
|
|
17
|
+
- Wide character support (CJK, emoji)
|
|
18
|
+
- Zero dependencies — works in Bun, Node.js, and browsers
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install vt100.js
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { createVt100Screen } from "vt100.js"
|
|
30
|
+
|
|
31
|
+
const screen = createVt100Screen({ cols: 80, rows: 24 })
|
|
32
|
+
screen.process(new TextEncoder().encode("Hello, \x1b[1mBold\x1b[0m World!"))
|
|
33
|
+
|
|
34
|
+
console.log(screen.getText()) // "Hello, Bold World!"
|
|
35
|
+
console.log(screen.getCell(0, 7).bold) // true
|
|
36
|
+
console.log(screen.getCursorPosition()) // { x: 18, y: 0 }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `createVt100Screen(options)`
|
|
42
|
+
|
|
43
|
+
| Option | Type | Default | Description |
|
|
44
|
+
| ----------------- | -------- | -------- | -------------------- |
|
|
45
|
+
| `cols` | `number` | required | Terminal width |
|
|
46
|
+
| `rows` | `number` | required | Terminal height |
|
|
47
|
+
| `scrollbackLimit` | `number` | `1000` | Max scrollback lines |
|
|
48
|
+
|
|
49
|
+
### Screen methods
|
|
50
|
+
|
|
51
|
+
| Method | Description |
|
|
52
|
+
| ------------------------------ | ---------------------------------- |
|
|
53
|
+
| `process(data: Uint8Array)` | Feed raw terminal data |
|
|
54
|
+
| `getText()` | Get all text (scrollback + screen) |
|
|
55
|
+
| `getTextRange(sr, sc, er, ec)` | Get text in a range |
|
|
56
|
+
| `getLine(row)` | Get cells for a row |
|
|
57
|
+
| `getCell(row, col)` | Get a single cell |
|
|
58
|
+
| `getCursorPosition()` | Get cursor `{ x, y }` |
|
|
59
|
+
| `getCursorVisible()` | Check cursor visibility |
|
|
60
|
+
| `getMode(mode)` | Check terminal mode |
|
|
61
|
+
| `getTitle()` | Get window title |
|
|
62
|
+
| `getScrollbackLength()` | Number of scrollback lines |
|
|
63
|
+
| `getViewportOffset()` | Current viewport scroll offset |
|
|
64
|
+
| `scrollViewport(delta)` | Scroll viewport |
|
|
65
|
+
| `resize(cols, rows)` | Resize terminal |
|
|
66
|
+
| `reset()` | Reset to initial state |
|
|
67
|
+
|
|
68
|
+
### Cell properties
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
interface ScreenCell {
|
|
72
|
+
char: string
|
|
73
|
+
fg: CellColor | null // { r, g, b }
|
|
74
|
+
bg: CellColor | null
|
|
75
|
+
bold: boolean
|
|
76
|
+
faint: boolean
|
|
77
|
+
italic: boolean
|
|
78
|
+
underline: "none" | "single" | "double" | "curly" | "dotted" | "dashed"
|
|
79
|
+
strikethrough: boolean
|
|
80
|
+
inverse: boolean
|
|
81
|
+
hidden: boolean
|
|
82
|
+
wide: boolean
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## See also
|
|
87
|
+
|
|
88
|
+
- [vterm.js](../vterm/) — full-featured modern emulator (100% terminfo.dev coverage)
|
|
89
|
+
- [Termless](https://termless.dev) — headless terminal testing
|
|
90
|
+
- [Terminfo.dev](https://terminfo.dev) — terminal feature support tables
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/screen.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure TypeScript VT100 terminal emulator.
|
|
2
|
+
* Pure TypeScript VT100/VT220 terminal emulator.
|
|
3
3
|
*
|
|
4
4
|
* Inspired by the Rust vt100 crate — parses a terminal byte stream and
|
|
5
5
|
* maintains an in-memory screen representation with per-cell attributes,
|
|
6
6
|
* cursor tracking, and mode state.
|
|
7
7
|
*
|
|
8
|
-
* Zero dependencies. Handles: SGR (
|
|
9
|
-
* erase, scroll regions,
|
|
8
|
+
* Zero dependencies. Handles: SGR (8 standard colors, bold, underline, blink,
|
|
9
|
+
* reverse, hidden), cursor movement, erase, scroll regions, modes, OSC title,
|
|
10
|
+
* DA1/DSR responses, and more. No truecolor, no 256 colors, no wide chars —
|
|
11
|
+
* those belong in vterm.js.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
// ═══════════════════════════════════════════════════════
|
|
@@ -19,20 +21,15 @@ export interface CellColor {
|
|
|
19
21
|
b: number
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export type UnderlineStyle = "none" | "single" | "double" | "curly" | "dotted" | "dashed"
|
|
23
|
-
|
|
24
24
|
export interface ScreenCell {
|
|
25
25
|
char: string
|
|
26
26
|
fg: CellColor | null
|
|
27
27
|
bg: CellColor | null
|
|
28
28
|
bold: boolean
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
underline: UnderlineStyle
|
|
32
|
-
strikethrough: boolean
|
|
29
|
+
underline: boolean
|
|
30
|
+
blink: boolean
|
|
33
31
|
inverse: boolean
|
|
34
32
|
hidden: boolean
|
|
35
|
-
wide: boolean
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
/** Frozen sentinel for unwritten cells — never mutate, copy-on-write in writeChar(). */
|
|
@@ -41,13 +38,10 @@ const EMPTY_CELL: ScreenCell = Object.freeze({
|
|
|
41
38
|
fg: null,
|
|
42
39
|
bg: null,
|
|
43
40
|
bold: false,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
underline: "none" as UnderlineStyle,
|
|
47
|
-
strikethrough: false,
|
|
41
|
+
underline: false,
|
|
42
|
+
blink: false,
|
|
48
43
|
inverse: false,
|
|
49
44
|
hidden: false,
|
|
50
|
-
wide: false,
|
|
51
45
|
})
|
|
52
46
|
|
|
53
47
|
function emptyCell(): ScreenCell {
|
|
@@ -55,10 +49,10 @@ function emptyCell(): ScreenCell {
|
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
// ═══════════════════════════════════════════════════════
|
|
58
|
-
// ANSI
|
|
52
|
+
// ANSI 8-color palette (standard VT100/VT220 colors)
|
|
59
53
|
// ═══════════════════════════════════════════════════════
|
|
60
54
|
|
|
61
|
-
const
|
|
55
|
+
const ANSI_8: readonly CellColor[] = [
|
|
62
56
|
{ r: 0x00, g: 0x00, b: 0x00 }, // 0 Black
|
|
63
57
|
{ r: 0x80, g: 0x00, b: 0x00 }, // 1 Red
|
|
64
58
|
{ r: 0x00, g: 0x80, b: 0x00 }, // 2 Green
|
|
@@ -67,61 +61,8 @@ const ANSI_16: readonly CellColor[] = [
|
|
|
67
61
|
{ r: 0x80, g: 0x00, b: 0x80 }, // 5 Magenta
|
|
68
62
|
{ r: 0x00, g: 0x80, b: 0x80 }, // 6 Cyan
|
|
69
63
|
{ r: 0xc0, g: 0xc0, b: 0xc0 }, // 7 White
|
|
70
|
-
{ r: 0x80, g: 0x80, b: 0x80 }, // 8 Bright Black
|
|
71
|
-
{ r: 0xff, g: 0x00, b: 0x00 }, // 9 Bright Red
|
|
72
|
-
{ r: 0x00, g: 0xff, b: 0x00 }, // 10 Bright Green
|
|
73
|
-
{ r: 0xff, g: 0xff, b: 0x00 }, // 11 Bright Yellow
|
|
74
|
-
{ r: 0x00, g: 0x00, b: 0xff }, // 12 Bright Blue
|
|
75
|
-
{ r: 0xff, g: 0x00, b: 0xff }, // 13 Bright Magenta
|
|
76
|
-
{ r: 0x00, g: 0xff, b: 0xff }, // 14 Bright Cyan
|
|
77
|
-
{ r: 0xff, g: 0xff, b: 0xff }, // 15 Bright White
|
|
78
64
|
]
|
|
79
65
|
|
|
80
|
-
function buildPalette256(): CellColor[] {
|
|
81
|
-
const palette: CellColor[] = [...ANSI_16]
|
|
82
|
-
const levels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
|
|
83
|
-
for (let r = 0; r < 6; r++) {
|
|
84
|
-
for (let g = 0; g < 6; g++) {
|
|
85
|
-
for (let b = 0; b < 6; b++) {
|
|
86
|
-
palette.push({ r: levels[r]!, g: levels[g]!, b: levels[b]! })
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
for (let i = 0; i < 24; i++) {
|
|
91
|
-
const v = 8 + i * 10
|
|
92
|
-
palette.push({ r: v, g: v, b: v })
|
|
93
|
-
}
|
|
94
|
-
return palette
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const PALETTE_256 = buildPalette256()
|
|
98
|
-
|
|
99
|
-
// ═══════════════════════════════════════════════════════
|
|
100
|
-
// Unicode width (simplified — CJK detection)
|
|
101
|
-
// ═══════════════════════════════════════════════════════
|
|
102
|
-
|
|
103
|
-
function isWide(codePoint: number): boolean {
|
|
104
|
-
// CJK Unified Ideographs, CJK Compatibility Ideographs, etc.
|
|
105
|
-
return (
|
|
106
|
-
(codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
|
107
|
-
(codePoint >= 0x2e80 && codePoint <= 0x303e) || // CJK Radicals
|
|
108
|
-
(codePoint >= 0x3041 && codePoint <= 0x33bf) || // Hiragana, Katakana, Bopomofo, etc.
|
|
109
|
-
(codePoint >= 0x3400 && codePoint <= 0x4dbf) || // CJK Unified Extension A
|
|
110
|
-
(codePoint >= 0x4e00 && codePoint <= 0xa4cf) || // CJK Unified Ideographs
|
|
111
|
-
(codePoint >= 0xa960 && codePoint <= 0xa97c) || // Hangul Jamo Extended-A
|
|
112
|
-
(codePoint >= 0xac00 && codePoint <= 0xd7a3) || // Hangul Syllables
|
|
113
|
-
(codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK Compatibility Ideographs
|
|
114
|
-
(codePoint >= 0xfe10 && codePoint <= 0xfe19) || // Vertical Forms
|
|
115
|
-
(codePoint >= 0xfe30 && codePoint <= 0xfe6b) || // CJK Compatibility Forms
|
|
116
|
-
(codePoint >= 0xff01 && codePoint <= 0xff60) || // Fullwidth Forms
|
|
117
|
-
(codePoint >= 0xffe0 && codePoint <= 0xffe6) || // Fullwidth Signs
|
|
118
|
-
(codePoint >= 0x1f300 && codePoint <= 0x1f9ff) || // Misc Symbols/Emoticons
|
|
119
|
-
(codePoint >= 0x1fa00 && codePoint <= 0x1faff) || // Extended Symbols & Pictographs
|
|
120
|
-
(codePoint >= 0x20000 && codePoint <= 0x2fffd) || // CJK Extension B-F
|
|
121
|
-
(codePoint >= 0x30000 && codePoint <= 0x3fffd) // CJK Extension G+
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
66
|
// ═══════════════════════════════════════════════════════
|
|
126
67
|
// Screen
|
|
127
68
|
// ═══════════════════════════════════════════════════════
|
|
@@ -130,16 +71,16 @@ export interface ScreenOptions {
|
|
|
130
71
|
cols: number
|
|
131
72
|
rows: number
|
|
132
73
|
scrollbackLimit?: number
|
|
74
|
+
/** Callback for DA1/DSR responses — write these back to the PTY */
|
|
75
|
+
onResponse?: (data: string) => void
|
|
133
76
|
}
|
|
134
77
|
|
|
135
78
|
interface Attrs {
|
|
136
79
|
fg: CellColor | null
|
|
137
80
|
bg: CellColor | null
|
|
138
81
|
bold: boolean
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
underline: UnderlineStyle
|
|
142
|
-
strikethrough: boolean
|
|
82
|
+
underline: boolean
|
|
83
|
+
blink: boolean
|
|
143
84
|
inverse: boolean
|
|
144
85
|
hidden: boolean
|
|
145
86
|
}
|
|
@@ -167,11 +108,10 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
167
108
|
let cols = opts.cols
|
|
168
109
|
let rows = opts.rows
|
|
169
110
|
const scrollbackLimit = opts.scrollbackLimit ?? 1000
|
|
111
|
+
const onResponse = opts.onResponse
|
|
170
112
|
|
|
171
|
-
// Main
|
|
172
|
-
let
|
|
173
|
-
let altGrid: ScreenCell[][] = makeGrid(cols, rows)
|
|
174
|
-
let grid = mainGrid
|
|
113
|
+
// Main screen buffer (no alternate screen in VT100/VT220)
|
|
114
|
+
let grid: ScreenCell[][] = makeGrid(cols, rows)
|
|
175
115
|
let scrollback: ScreenCell[][] = []
|
|
176
116
|
|
|
177
117
|
// Cursor
|
|
@@ -202,13 +142,9 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
202
142
|
|
|
203
143
|
// Terminal state
|
|
204
144
|
let title = ""
|
|
205
|
-
let useAltScreen = false
|
|
206
|
-
let bracketedPaste = false
|
|
207
145
|
let applicationCursor = false
|
|
208
146
|
let applicationKeypad = false
|
|
209
147
|
let autoWrap = true
|
|
210
|
-
let mouseTracking = false
|
|
211
|
-
let focusTracking = false
|
|
212
148
|
let originMode = false
|
|
213
149
|
let insertMode = false
|
|
214
150
|
let reverseVideo = false
|
|
@@ -249,10 +185,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
249
185
|
fg: null,
|
|
250
186
|
bg: null,
|
|
251
187
|
bold: false,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
underline: "none",
|
|
255
|
-
strikethrough: false,
|
|
188
|
+
underline: false,
|
|
189
|
+
blink: false,
|
|
256
190
|
inverse: false,
|
|
257
191
|
hidden: false,
|
|
258
192
|
}
|
|
@@ -268,8 +202,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
268
202
|
// ── Scrolling ──
|
|
269
203
|
|
|
270
204
|
function scrollUp(top: number, bottom: number): void {
|
|
271
|
-
// Move top row to scrollback (only if
|
|
272
|
-
if (
|
|
205
|
+
// Move top row to scrollback (only if top of screen)
|
|
206
|
+
if (top === 0) {
|
|
273
207
|
scrollback.push(grid[0]!)
|
|
274
208
|
// Bulk trim when exceeding 2x limit to avoid O(n) shift() on every scroll
|
|
275
209
|
if (scrollback.length > scrollbackLimit * 2) {
|
|
@@ -293,12 +227,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
293
227
|
// ── Character writing ──
|
|
294
228
|
|
|
295
229
|
function writeChar(ch: string): void {
|
|
296
|
-
const codePoint = ch.codePointAt(0) ?? 0
|
|
297
|
-
const wide = isWide(codePoint)
|
|
298
|
-
const charWidth = wide ? 2 : 1
|
|
299
|
-
|
|
300
230
|
// Handle autowrap at end of line
|
|
301
|
-
if (curX
|
|
231
|
+
if (curX >= cols) {
|
|
302
232
|
if (autoWrap) {
|
|
303
233
|
curX = 0
|
|
304
234
|
curY++
|
|
@@ -307,17 +237,15 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
307
237
|
scrollUp(scrollTop, scrollBottom)
|
|
308
238
|
}
|
|
309
239
|
} else {
|
|
310
|
-
curX = cols -
|
|
240
|
+
curX = cols - 1
|
|
311
241
|
}
|
|
312
242
|
}
|
|
313
243
|
|
|
314
244
|
// Insert mode: shift existing characters right before writing
|
|
315
245
|
if (insertMode) {
|
|
316
246
|
const row = grid[curY]!
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
row.pop()
|
|
320
|
-
}
|
|
247
|
+
row.splice(curX, 0, EMPTY_CELL)
|
|
248
|
+
row.pop()
|
|
321
249
|
}
|
|
322
250
|
|
|
323
251
|
// Copy-on-write: if cell is the shared EMPTY_CELL sentinel, create a fresh object
|
|
@@ -331,35 +259,12 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
331
259
|
cell.fg = attrs.fg ? { ...attrs.fg } : null
|
|
332
260
|
cell.bg = attrs.bg ? { ...attrs.bg } : null
|
|
333
261
|
cell.bold = attrs.bold
|
|
334
|
-
cell.faint = attrs.faint
|
|
335
|
-
cell.italic = attrs.italic
|
|
336
262
|
cell.underline = attrs.underline
|
|
337
|
-
cell.
|
|
263
|
+
cell.blink = attrs.blink
|
|
338
264
|
cell.inverse = attrs.inverse
|
|
339
265
|
cell.hidden = attrs.hidden
|
|
340
|
-
cell.wide = wide
|
|
341
|
-
|
|
342
|
-
if (wide && curX + 1 < cols) {
|
|
343
|
-
// Spacer cell for wide character — always create fresh
|
|
344
|
-
let spacer = row[curX + 1]!
|
|
345
|
-
if (spacer === EMPTY_CELL) {
|
|
346
|
-
spacer = { ...EMPTY_CELL }
|
|
347
|
-
row[curX + 1] = spacer
|
|
348
|
-
}
|
|
349
|
-
spacer.char = ""
|
|
350
|
-
spacer.fg = null
|
|
351
|
-
spacer.bg = null
|
|
352
|
-
spacer.bold = false
|
|
353
|
-
spacer.faint = false
|
|
354
|
-
spacer.italic = false
|
|
355
|
-
spacer.underline = "none"
|
|
356
|
-
spacer.strikethrough = false
|
|
357
|
-
spacer.inverse = false
|
|
358
|
-
spacer.hidden = false
|
|
359
|
-
spacer.wide = false
|
|
360
|
-
}
|
|
361
266
|
|
|
362
|
-
curX
|
|
267
|
+
curX++
|
|
363
268
|
}
|
|
364
269
|
|
|
365
270
|
// ── CSI handler ──
|
|
@@ -454,9 +359,24 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
454
359
|
curX = 0
|
|
455
360
|
curY = originMode ? scrollTop : 0
|
|
456
361
|
break
|
|
457
|
-
case "n": // DSR - Device Status Report
|
|
362
|
+
case "n": // DSR - Device Status Report
|
|
363
|
+
if (onResponse) {
|
|
364
|
+
if (parts[0] === 5) {
|
|
365
|
+
// Status report - OK
|
|
366
|
+
onResponse("\x1b[0n")
|
|
367
|
+
} else if (parts[0] === 6) {
|
|
368
|
+
// CPR - Cursor position report (1-based)
|
|
369
|
+
onResponse(`\x1b[${curY + 1};${curX + 1}R`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
458
372
|
break
|
|
459
|
-
case "c": //
|
|
373
|
+
case "c": // DA1 - Primary Device Attributes
|
|
374
|
+
if (onResponse) {
|
|
375
|
+
if (params === "" || params === "0") {
|
|
376
|
+
// VT100 with Advanced Video Option (AVO)
|
|
377
|
+
onResponse("\x1b[?1;2c")
|
|
378
|
+
}
|
|
379
|
+
}
|
|
460
380
|
break
|
|
461
381
|
case "s": // SCP - Save Cursor Position
|
|
462
382
|
savedCurX = curX
|
|
@@ -473,6 +393,14 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
473
393
|
}
|
|
474
394
|
}
|
|
475
395
|
|
|
396
|
+
function handleCSIWithIntermediate(_params: string, intermediate: string, finalByte: string): void {
|
|
397
|
+
if (intermediate === "!" && finalByte === "p") {
|
|
398
|
+
// DECSTR - Soft Terminal Reset
|
|
399
|
+
softReset()
|
|
400
|
+
}
|
|
401
|
+
// Unknown intermediate sequences — ignore
|
|
402
|
+
}
|
|
403
|
+
|
|
476
404
|
function handleCSIPrivate(params: string, finalByte: string): void {
|
|
477
405
|
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
478
406
|
const set = finalByte === "h"
|
|
@@ -482,6 +410,12 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
482
410
|
case 1: // DECCKM - Application Cursor
|
|
483
411
|
applicationCursor = set
|
|
484
412
|
break
|
|
413
|
+
case 4: // IRM - Insert Mode (via DEC private)
|
|
414
|
+
insertMode = set
|
|
415
|
+
break
|
|
416
|
+
case 5: // DECSCNM - Reverse Video
|
|
417
|
+
reverseVideo = set
|
|
418
|
+
break
|
|
485
419
|
case 6: // DECOM - Origin Mode
|
|
486
420
|
originMode = set
|
|
487
421
|
break
|
|
@@ -491,51 +425,21 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
491
425
|
case 25: // DECTCEM - Cursor Visible
|
|
492
426
|
curVisible = set
|
|
493
427
|
break
|
|
494
|
-
case 47: // Alternate screen buffer (old)
|
|
495
|
-
case 1047: // Alternate screen buffer
|
|
496
|
-
if (set && !useAltScreen) {
|
|
497
|
-
useAltScreen = true
|
|
498
|
-
grid = altGrid
|
|
499
|
-
} else if (!set && useAltScreen) {
|
|
500
|
-
useAltScreen = false
|
|
501
|
-
grid = mainGrid
|
|
502
|
-
}
|
|
503
|
-
break
|
|
504
428
|
case 66: // DECNKM - Application Keypad
|
|
505
429
|
applicationKeypad = set
|
|
506
430
|
break
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
useAltScreen = true
|
|
520
|
-
altGrid = makeGrid(cols, rows)
|
|
521
|
-
grid = altGrid
|
|
522
|
-
curX = 0
|
|
523
|
-
curY = 0
|
|
524
|
-
} else if (!set && useAltScreen) {
|
|
525
|
-
useAltScreen = false
|
|
526
|
-
grid = mainGrid
|
|
527
|
-
curX = savedCurX
|
|
528
|
-
curY = savedCurY
|
|
529
|
-
clampCursor()
|
|
530
|
-
}
|
|
531
|
-
break
|
|
532
|
-
case 2004: // Bracketed paste
|
|
533
|
-
bracketedPaste = set
|
|
534
|
-
break
|
|
535
|
-
case 5: // DECSCNM - Reverse Video
|
|
536
|
-
reverseVideo = set
|
|
537
|
-
break
|
|
538
|
-
case 4: // IRM - Insert Mode (via DEC private)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Also handle standard (non-private) set/reset modes: CSI Ps h / CSI Ps l
|
|
436
|
+
function handleSetResetMode(params: string, finalByte: string): void {
|
|
437
|
+
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
438
|
+
const set = finalByte === "h"
|
|
439
|
+
|
|
440
|
+
for (const code of parts) {
|
|
441
|
+
switch (code) {
|
|
442
|
+
case 4: // IRM - Insert/Replace Mode
|
|
539
443
|
insertMode = set
|
|
540
444
|
break
|
|
541
445
|
}
|
|
@@ -635,20 +539,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
635
539
|
// ── SGR (Select Graphic Rendition) ──
|
|
636
540
|
|
|
637
541
|
function handleSGR(rawParams: string): void {
|
|
638
|
-
|
|
639
|
-
const segments = rawParams.split(";")
|
|
640
|
-
const params: number[] = []
|
|
641
|
-
// Map from param index to colon sub-parameters (e.g., index of "4" -> [4, 3])
|
|
642
|
-
const subParams = new Map<number, number[]>()
|
|
643
|
-
for (const seg of segments) {
|
|
644
|
-
if (seg.includes(":")) {
|
|
645
|
-
const subs = seg.split(":").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
646
|
-
subParams.set(params.length, subs)
|
|
647
|
-
params.push(subs[0]!)
|
|
648
|
-
} else {
|
|
649
|
-
params.push(seg === "" ? 0 : parseInt(seg, 10))
|
|
650
|
-
}
|
|
651
|
-
}
|
|
542
|
+
const params = rawParams.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
652
543
|
|
|
653
544
|
if (params.length === 0 || (params.length === 1 && params[0] === 0)) {
|
|
654
545
|
attrs = resetAttrs()
|
|
@@ -665,66 +556,26 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
665
556
|
case 1:
|
|
666
557
|
attrs.bold = true
|
|
667
558
|
break
|
|
668
|
-
case
|
|
669
|
-
attrs.
|
|
559
|
+
case 4:
|
|
560
|
+
attrs.underline = true
|
|
670
561
|
break
|
|
671
|
-
case
|
|
672
|
-
attrs.
|
|
562
|
+
case 5: // Blink
|
|
563
|
+
attrs.blink = true
|
|
673
564
|
break
|
|
674
|
-
case 4: {
|
|
675
|
-
// SGR 4 with optional sub-parameter: 4:0=none, 4:1=single, 4:3=curly, etc.
|
|
676
|
-
const subs = subParams.get(i)
|
|
677
|
-
if (subs && subs.length > 1) {
|
|
678
|
-
const sub = subs[1]!
|
|
679
|
-
switch (sub) {
|
|
680
|
-
case 0:
|
|
681
|
-
attrs.underline = "none"
|
|
682
|
-
break
|
|
683
|
-
case 1:
|
|
684
|
-
attrs.underline = "single"
|
|
685
|
-
break
|
|
686
|
-
case 2:
|
|
687
|
-
attrs.underline = "double"
|
|
688
|
-
break
|
|
689
|
-
case 3:
|
|
690
|
-
attrs.underline = "curly"
|
|
691
|
-
break
|
|
692
|
-
case 4:
|
|
693
|
-
attrs.underline = "dotted"
|
|
694
|
-
break
|
|
695
|
-
case 5:
|
|
696
|
-
attrs.underline = "dashed"
|
|
697
|
-
break
|
|
698
|
-
default:
|
|
699
|
-
attrs.underline = "single"
|
|
700
|
-
break
|
|
701
|
-
}
|
|
702
|
-
} else {
|
|
703
|
-
attrs.underline = "single"
|
|
704
|
-
}
|
|
705
|
-
break
|
|
706
|
-
}
|
|
707
565
|
case 7:
|
|
708
566
|
attrs.inverse = true
|
|
709
567
|
break
|
|
710
568
|
case 8: // Hidden/conceal
|
|
711
569
|
attrs.hidden = true
|
|
712
570
|
break
|
|
713
|
-
case
|
|
714
|
-
attrs.strikethrough = true
|
|
715
|
-
break
|
|
716
|
-
case 21: // Double underline
|
|
717
|
-
attrs.underline = "double"
|
|
718
|
-
break
|
|
719
|
-
case 22: // Normal intensity (neither bold nor faint)
|
|
571
|
+
case 22: // Normal intensity (turn off bold)
|
|
720
572
|
attrs.bold = false
|
|
721
|
-
attrs.faint = false
|
|
722
|
-
break
|
|
723
|
-
case 23:
|
|
724
|
-
attrs.italic = false
|
|
725
573
|
break
|
|
726
574
|
case 24:
|
|
727
|
-
attrs.underline =
|
|
575
|
+
attrs.underline = false
|
|
576
|
+
break
|
|
577
|
+
case 25: // Blink off
|
|
578
|
+
attrs.blink = false
|
|
728
579
|
break
|
|
729
580
|
case 27:
|
|
730
581
|
attrs.inverse = false
|
|
@@ -732,9 +583,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
732
583
|
case 28: // Reveal (turn off hidden/conceal)
|
|
733
584
|
attrs.hidden = false
|
|
734
585
|
break
|
|
735
|
-
case 29:
|
|
736
|
-
attrs.strikethrough = false
|
|
737
|
-
break
|
|
738
586
|
// Foreground colors 30-37
|
|
739
587
|
case 30:
|
|
740
588
|
case 31:
|
|
@@ -744,17 +592,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
744
592
|
case 35:
|
|
745
593
|
case 36:
|
|
746
594
|
case 37:
|
|
747
|
-
attrs.fg = { ...
|
|
748
|
-
break
|
|
749
|
-
case 38: {
|
|
750
|
-
// Extended foreground: 38;5;N (256) or 38;2;R;G;B (truecolor)
|
|
751
|
-
const result = parseExtendedColor(params, i)
|
|
752
|
-
if (result) {
|
|
753
|
-
attrs.fg = result.color
|
|
754
|
-
i = result.nextIndex - 1 // -1 because loop increments
|
|
755
|
-
}
|
|
595
|
+
attrs.fg = { ...ANSI_8[code - 30]! }
|
|
756
596
|
break
|
|
757
|
-
}
|
|
758
597
|
case 39: // Default foreground
|
|
759
598
|
attrs.fg = null
|
|
760
599
|
break
|
|
@@ -767,70 +606,25 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
767
606
|
case 45:
|
|
768
607
|
case 46:
|
|
769
608
|
case 47:
|
|
770
|
-
attrs.bg = { ...
|
|
771
|
-
break
|
|
772
|
-
case 48: {
|
|
773
|
-
// Extended background: 48;5;N (256) or 48;2;R;G;B (truecolor)
|
|
774
|
-
const result = parseExtendedColor(params, i)
|
|
775
|
-
if (result) {
|
|
776
|
-
attrs.bg = result.color
|
|
777
|
-
i = result.nextIndex - 1
|
|
778
|
-
}
|
|
609
|
+
attrs.bg = { ...ANSI_8[code - 40]! }
|
|
779
610
|
break
|
|
780
|
-
}
|
|
781
611
|
case 49: // Default background
|
|
782
612
|
attrs.bg = null
|
|
783
613
|
break
|
|
784
|
-
//
|
|
785
|
-
case
|
|
786
|
-
case
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
case 97:
|
|
793
|
-
attrs.fg = { ...PALETTE_256[code - 90 + 8]! }
|
|
794
|
-
break
|
|
795
|
-
// Bright background 100-107
|
|
796
|
-
case 100:
|
|
797
|
-
case 101:
|
|
798
|
-
case 102:
|
|
799
|
-
case 103:
|
|
800
|
-
case 104:
|
|
801
|
-
case 105:
|
|
802
|
-
case 106:
|
|
803
|
-
case 107:
|
|
804
|
-
attrs.bg = { ...PALETTE_256[code - 100 + 8]! }
|
|
614
|
+
// Skip extended color sequences (not supported, but must consume params)
|
|
615
|
+
case 38:
|
|
616
|
+
case 48:
|
|
617
|
+
if (i + 1 < params.length && params[i + 1] === 2) {
|
|
618
|
+
i += 4 // skip 38;2;R;G;B or 48;2;R;G;B
|
|
619
|
+
} else if (i + 1 < params.length && params[i + 1] === 5) {
|
|
620
|
+
i += 2 // skip 38;5;N or 48;5;N
|
|
621
|
+
}
|
|
805
622
|
break
|
|
806
623
|
}
|
|
807
624
|
i++
|
|
808
625
|
}
|
|
809
626
|
}
|
|
810
627
|
|
|
811
|
-
function parseExtendedColor(params: number[], startIndex: number): { color: CellColor; nextIndex: number } | null {
|
|
812
|
-
if (startIndex + 1 >= params.length) return null
|
|
813
|
-
|
|
814
|
-
const type = params[startIndex + 1]
|
|
815
|
-
if (type === 5 && startIndex + 2 < params.length) {
|
|
816
|
-
// 256-color: 38;5;N
|
|
817
|
-
const idx = params[startIndex + 2]!
|
|
818
|
-
const color = PALETTE_256[idx] ?? { r: 0, g: 0, b: 0 }
|
|
819
|
-
return { color: { ...color }, nextIndex: startIndex + 3 }
|
|
820
|
-
} else if (type === 2 && startIndex + 4 < params.length) {
|
|
821
|
-
// Truecolor: 38;2;R;G;B
|
|
822
|
-
return {
|
|
823
|
-
color: {
|
|
824
|
-
r: params[startIndex + 2]!,
|
|
825
|
-
g: params[startIndex + 3]!,
|
|
826
|
-
b: params[startIndex + 4]!,
|
|
827
|
-
},
|
|
828
|
-
nextIndex: startIndex + 5,
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
return null
|
|
832
|
-
}
|
|
833
|
-
|
|
834
628
|
// ── OSC handler ──
|
|
835
629
|
|
|
836
630
|
function handleOSC(oscString: string): void {
|
|
@@ -883,16 +677,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
883
677
|
// CR - Carriage Return
|
|
884
678
|
curX = 0
|
|
885
679
|
} else if (code >= 0x20) {
|
|
886
|
-
|
|
887
|
-
let char = ch
|
|
888
|
-
if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
|
|
889
|
-
const nextCode = text.charCodeAt(i + 1)
|
|
890
|
-
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
|
891
|
-
char = ch + text[i + 1]!
|
|
892
|
-
i++
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
writeChar(char)
|
|
680
|
+
writeChar(ch)
|
|
896
681
|
}
|
|
897
682
|
break
|
|
898
683
|
|
|
@@ -957,6 +742,14 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
957
742
|
scrollUp(scrollTop, scrollBottom)
|
|
958
743
|
}
|
|
959
744
|
parserState = "ground"
|
|
745
|
+
} else if (ch === "=") {
|
|
746
|
+
// DECKPAM - Application Keypad Mode
|
|
747
|
+
applicationKeypad = true
|
|
748
|
+
parserState = "ground"
|
|
749
|
+
} else if (ch === ">") {
|
|
750
|
+
// DECKPNM - Normal Keypad Mode
|
|
751
|
+
applicationKeypad = false
|
|
752
|
+
parserState = "ground"
|
|
960
753
|
} else {
|
|
961
754
|
// Unknown escape — return to ground
|
|
962
755
|
parserState = "ground"
|
|
@@ -968,8 +761,27 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
968
761
|
// Final byte — dispatch CSI
|
|
969
762
|
if (escBuf.startsWith("?")) {
|
|
970
763
|
handleCSIPrivate(escBuf.substring(1), ch)
|
|
764
|
+
} else if (ch === "h" || ch === "l") {
|
|
765
|
+
// Standard set/reset mode (non-private)
|
|
766
|
+
handleSetResetMode(escBuf, ch)
|
|
971
767
|
} else {
|
|
972
|
-
|
|
768
|
+
// Check for intermediate bytes (0x20-0x2F range, e.g., "!" in CSI ! p)
|
|
769
|
+
// Find first intermediate byte in escBuf
|
|
770
|
+
let intermediateIdx = -1
|
|
771
|
+
for (let j = 0; j < escBuf.length; j++) {
|
|
772
|
+
const c = escBuf.charCodeAt(j)
|
|
773
|
+
if (c >= 0x20 && c <= 0x2f) {
|
|
774
|
+
intermediateIdx = j
|
|
775
|
+
break
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (intermediateIdx >= 0) {
|
|
779
|
+
const paramPart = escBuf.substring(0, intermediateIdx)
|
|
780
|
+
const intermediatePart = escBuf.substring(intermediateIdx)
|
|
781
|
+
handleCSIWithIntermediate(paramPart, intermediatePart, ch)
|
|
782
|
+
} else {
|
|
783
|
+
handleCSI(escBuf, ch)
|
|
784
|
+
}
|
|
973
785
|
}
|
|
974
786
|
parserState = "ground"
|
|
975
787
|
} else if (escBuf.length >= 256) {
|
|
@@ -1017,9 +829,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1017
829
|
}
|
|
1018
830
|
|
|
1019
831
|
function fullReset(): void {
|
|
1020
|
-
|
|
1021
|
-
altGrid = makeGrid(cols, rows)
|
|
1022
|
-
grid = mainGrid
|
|
832
|
+
grid = makeGrid(cols, rows)
|
|
1023
833
|
scrollback = []
|
|
1024
834
|
curX = 0
|
|
1025
835
|
curY = 0
|
|
@@ -1029,13 +839,9 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1029
839
|
savedState = { curX: 0, curY: 0, attrs: resetAttrs(), originMode: false, autoWrap: true }
|
|
1030
840
|
attrs = resetAttrs()
|
|
1031
841
|
title = ""
|
|
1032
|
-
useAltScreen = false
|
|
1033
|
-
bracketedPaste = false
|
|
1034
842
|
applicationCursor = false
|
|
1035
843
|
applicationKeypad = false
|
|
1036
844
|
autoWrap = true
|
|
1037
|
-
mouseTracking = false
|
|
1038
|
-
focusTracking = false
|
|
1039
845
|
originMode = false
|
|
1040
846
|
insertMode = false
|
|
1041
847
|
reverseVideo = false
|
|
@@ -1047,17 +853,29 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1047
853
|
oscBuf = ""
|
|
1048
854
|
}
|
|
1049
855
|
|
|
856
|
+
function softReset(): void {
|
|
857
|
+
attrs = resetAttrs()
|
|
858
|
+
applicationCursor = false
|
|
859
|
+
applicationKeypad = false
|
|
860
|
+
autoWrap = true
|
|
861
|
+
originMode = false
|
|
862
|
+
insertMode = false
|
|
863
|
+
reverseVideo = false
|
|
864
|
+
curVisible = true
|
|
865
|
+
scrollTop = 0
|
|
866
|
+
scrollBottom = rows - 1
|
|
867
|
+
savedState = { curX: 0, curY: 0, attrs: resetAttrs(), originMode: false, autoWrap: true }
|
|
868
|
+
savedCurX = 0
|
|
869
|
+
savedCurY = 0
|
|
870
|
+
}
|
|
871
|
+
|
|
1050
872
|
function resize(newCols: number, newRows: number): void {
|
|
1051
|
-
const
|
|
1052
|
-
const newAlt = makeGrid(newCols, newRows)
|
|
873
|
+
const newGrid = makeGrid(newCols, newRows)
|
|
1053
874
|
|
|
1054
|
-
// Copy content from old
|
|
1055
|
-
copyGrid(
|
|
1056
|
-
copyGrid(altGrid, newAlt, Math.min(cols, newCols), Math.min(rows, newRows))
|
|
875
|
+
// Copy content from old grid
|
|
876
|
+
copyGrid(grid, newGrid, Math.min(cols, newCols), Math.min(rows, newRows))
|
|
1057
877
|
|
|
1058
|
-
|
|
1059
|
-
altGrid = newAlt
|
|
1060
|
-
grid = useAltScreen ? altGrid : mainGrid
|
|
878
|
+
grid = newGrid
|
|
1061
879
|
cols = newCols
|
|
1062
880
|
rows = newRows
|
|
1063
881
|
scrollTop = 0
|
|
@@ -1108,13 +926,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1108
926
|
let line = ""
|
|
1109
927
|
for (let i = 0; i < row.length; i++) {
|
|
1110
928
|
const cell = row[i]!
|
|
1111
|
-
if (cell.
|
|
1112
|
-
line += cell.char
|
|
1113
|
-
} else if (cell.char === "") {
|
|
1114
|
-
// Skip spacer cells after wide chars, otherwise treat as space
|
|
1115
|
-
if (i > 0 && row[i - 1]?.wide) {
|
|
1116
|
-
continue
|
|
1117
|
-
}
|
|
929
|
+
if (cell.char === "") {
|
|
1118
930
|
line += " "
|
|
1119
931
|
} else {
|
|
1120
932
|
line += cell.char
|
|
@@ -1137,7 +949,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1137
949
|
for (let col = colStart; col < colEnd; col++) {
|
|
1138
950
|
const cell = r[col]
|
|
1139
951
|
if (!cell) continue
|
|
1140
|
-
if (cell.char === "" && col > 0 && r[col - 1]?.wide) continue // Skip spacer
|
|
1141
952
|
line += cell.char || " "
|
|
1142
953
|
}
|
|
1143
954
|
parts.push(line.replace(/\s+$/, ""))
|
|
@@ -1148,22 +959,14 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1148
959
|
|
|
1149
960
|
function getMode(mode: string): boolean {
|
|
1150
961
|
switch (mode) {
|
|
1151
|
-
case "altScreen":
|
|
1152
|
-
return useAltScreen
|
|
1153
962
|
case "cursorVisible":
|
|
1154
963
|
return curVisible
|
|
1155
|
-
case "bracketedPaste":
|
|
1156
|
-
return bracketedPaste
|
|
1157
964
|
case "applicationCursor":
|
|
1158
965
|
return applicationCursor
|
|
1159
966
|
case "applicationKeypad":
|
|
1160
967
|
return applicationKeypad
|
|
1161
968
|
case "autoWrap":
|
|
1162
969
|
return autoWrap
|
|
1163
|
-
case "mouseTracking":
|
|
1164
|
-
return mouseTracking
|
|
1165
|
-
case "focusTracking":
|
|
1166
|
-
return focusTracking
|
|
1167
970
|
case "originMode":
|
|
1168
971
|
return originMode
|
|
1169
972
|
case "insertMode":
|