vt100.js 0.2.2 → 0.3.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/package.json +2 -2
- package/src/index.ts +0 -1
- package/src/screen.ts +83 -452
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vt100.js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "VT100 terminal emulator — monochrome, cursor, scroll regions. Pure TypeScript, zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
7
7
|
"headless",
|
package/src/index.ts
CHANGED
package/src/screen.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure TypeScript VT100 terminal emulator.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Strict DEC VT100 (1978) implementation: monochrome display, cursor movement,
|
|
5
|
+
* scroll regions, auto-wrap, origin mode, DEC special graphics charset.
|
|
6
|
+
* SGR supports bold, underline, blink, and reverse — NO colors (VT100 is
|
|
7
|
+
* monochrome). No insert mode, no insert/delete chars/lines — those are
|
|
8
|
+
* VT102/VT220 features.
|
|
7
9
|
*
|
|
8
|
-
* Zero dependencies.
|
|
9
|
-
*
|
|
10
|
+
* Zero dependencies. For colors and VT220 features, use vt220.js.
|
|
11
|
+
* For truecolor, 256 colors, and wide chars, use 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,87 +38,16 @@ 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 {
|
|
54
48
|
return { ...EMPTY_CELL }
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
// ═══════════════════════════════════════════════════════
|
|
58
|
-
// ANSI 256-color palette
|
|
59
|
-
// ═══════════════════════════════════════════════════════
|
|
60
|
-
|
|
61
|
-
const ANSI_16: readonly CellColor[] = [
|
|
62
|
-
{ r: 0x00, g: 0x00, b: 0x00 }, // 0 Black
|
|
63
|
-
{ r: 0x80, g: 0x00, b: 0x00 }, // 1 Red
|
|
64
|
-
{ r: 0x00, g: 0x80, b: 0x00 }, // 2 Green
|
|
65
|
-
{ r: 0x80, g: 0x80, b: 0x00 }, // 3 Yellow
|
|
66
|
-
{ r: 0x00, g: 0x00, b: 0x80 }, // 4 Blue
|
|
67
|
-
{ r: 0x80, g: 0x00, b: 0x80 }, // 5 Magenta
|
|
68
|
-
{ r: 0x00, g: 0x80, b: 0x80 }, // 6 Cyan
|
|
69
|
-
{ 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
|
-
]
|
|
79
|
-
|
|
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
51
|
// ═══════════════════════════════════════════════════════
|
|
126
52
|
// Screen
|
|
127
53
|
// ═══════════════════════════════════════════════════════
|
|
@@ -130,16 +56,16 @@ export interface ScreenOptions {
|
|
|
130
56
|
cols: number
|
|
131
57
|
rows: number
|
|
132
58
|
scrollbackLimit?: number
|
|
59
|
+
/** Callback for DA1/DSR responses — write these back to the PTY */
|
|
60
|
+
onResponse?: (data: string) => void
|
|
133
61
|
}
|
|
134
62
|
|
|
135
63
|
interface Attrs {
|
|
136
64
|
fg: CellColor | null
|
|
137
65
|
bg: CellColor | null
|
|
138
66
|
bold: boolean
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
underline: UnderlineStyle
|
|
142
|
-
strikethrough: boolean
|
|
67
|
+
underline: boolean
|
|
68
|
+
blink: boolean
|
|
143
69
|
inverse: boolean
|
|
144
70
|
hidden: boolean
|
|
145
71
|
}
|
|
@@ -167,11 +93,10 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
167
93
|
let cols = opts.cols
|
|
168
94
|
let rows = opts.rows
|
|
169
95
|
const scrollbackLimit = opts.scrollbackLimit ?? 1000
|
|
96
|
+
const onResponse = opts.onResponse
|
|
170
97
|
|
|
171
|
-
// Main
|
|
172
|
-
let
|
|
173
|
-
let altGrid: ScreenCell[][] = makeGrid(cols, rows)
|
|
174
|
-
let grid = mainGrid
|
|
98
|
+
// Main screen buffer (no alternate screen in VT100)
|
|
99
|
+
let grid: ScreenCell[][] = makeGrid(cols, rows)
|
|
175
100
|
let scrollback: ScreenCell[][] = []
|
|
176
101
|
|
|
177
102
|
// Cursor
|
|
@@ -202,15 +127,10 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
202
127
|
|
|
203
128
|
// Terminal state
|
|
204
129
|
let title = ""
|
|
205
|
-
let useAltScreen = false
|
|
206
|
-
let bracketedPaste = false
|
|
207
130
|
let applicationCursor = false
|
|
208
131
|
let applicationKeypad = false
|
|
209
132
|
let autoWrap = true
|
|
210
|
-
let mouseTracking = false
|
|
211
|
-
let focusTracking = false
|
|
212
133
|
let originMode = false
|
|
213
|
-
let insertMode = false
|
|
214
134
|
let reverseVideo = false
|
|
215
135
|
|
|
216
136
|
// Scroll region (inclusive, 0-based)
|
|
@@ -249,10 +169,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
249
169
|
fg: null,
|
|
250
170
|
bg: null,
|
|
251
171
|
bold: false,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
underline: "none",
|
|
255
|
-
strikethrough: false,
|
|
172
|
+
underline: false,
|
|
173
|
+
blink: false,
|
|
256
174
|
inverse: false,
|
|
257
175
|
hidden: false,
|
|
258
176
|
}
|
|
@@ -268,8 +186,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
268
186
|
// ── Scrolling ──
|
|
269
187
|
|
|
270
188
|
function scrollUp(top: number, bottom: number): void {
|
|
271
|
-
// Move top row to scrollback (only if
|
|
272
|
-
if (
|
|
189
|
+
// Move top row to scrollback (only if top of screen)
|
|
190
|
+
if (top === 0) {
|
|
273
191
|
scrollback.push(grid[0]!)
|
|
274
192
|
// Bulk trim when exceeding 2x limit to avoid O(n) shift() on every scroll
|
|
275
193
|
if (scrollback.length > scrollbackLimit * 2) {
|
|
@@ -293,12 +211,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
293
211
|
// ── Character writing ──
|
|
294
212
|
|
|
295
213
|
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
214
|
// Handle autowrap at end of line
|
|
301
|
-
if (curX
|
|
215
|
+
if (curX >= cols) {
|
|
302
216
|
if (autoWrap) {
|
|
303
217
|
curX = 0
|
|
304
218
|
curY++
|
|
@@ -307,16 +221,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
307
221
|
scrollUp(scrollTop, scrollBottom)
|
|
308
222
|
}
|
|
309
223
|
} else {
|
|
310
|
-
curX = cols -
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Insert mode: shift existing characters right before writing
|
|
315
|
-
if (insertMode) {
|
|
316
|
-
const row = grid[curY]!
|
|
317
|
-
for (let i = 0; i < charWidth; i++) {
|
|
318
|
-
row.splice(curX, 0, EMPTY_CELL)
|
|
319
|
-
row.pop()
|
|
224
|
+
curX = cols - 1
|
|
320
225
|
}
|
|
321
226
|
}
|
|
322
227
|
|
|
@@ -331,35 +236,12 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
331
236
|
cell.fg = attrs.fg ? { ...attrs.fg } : null
|
|
332
237
|
cell.bg = attrs.bg ? { ...attrs.bg } : null
|
|
333
238
|
cell.bold = attrs.bold
|
|
334
|
-
cell.faint = attrs.faint
|
|
335
|
-
cell.italic = attrs.italic
|
|
336
239
|
cell.underline = attrs.underline
|
|
337
|
-
cell.
|
|
240
|
+
cell.blink = attrs.blink
|
|
338
241
|
cell.inverse = attrs.inverse
|
|
339
242
|
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
243
|
|
|
362
|
-
curX
|
|
244
|
+
curX++
|
|
363
245
|
}
|
|
364
246
|
|
|
365
247
|
// ── CSI handler ──
|
|
@@ -410,21 +292,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
410
292
|
case "K": // EL - Erase in Line
|
|
411
293
|
handleEraseLine(parts[0] ?? 0)
|
|
412
294
|
break
|
|
413
|
-
case "L": // IL - Insert Lines
|
|
414
|
-
handleInsertLines(Math.max(parts[0] ?? 1, 1))
|
|
415
|
-
break
|
|
416
|
-
case "M": // DL - Delete Lines
|
|
417
|
-
handleDeleteLines(Math.max(parts[0] ?? 1, 1))
|
|
418
|
-
break
|
|
419
|
-
case "P": // DCH - Delete Characters
|
|
420
|
-
handleDeleteChars(Math.max(parts[0] ?? 1, 1))
|
|
421
|
-
break
|
|
422
|
-
case "@": // ICH - Insert Characters
|
|
423
|
-
handleInsertChars(Math.max(parts[0] ?? 1, 1))
|
|
424
|
-
break
|
|
425
|
-
case "X": // ECH - Erase Characters
|
|
426
|
-
handleEraseChars(Math.max(parts[0] ?? 1, 1))
|
|
427
|
-
break
|
|
428
295
|
case "S": // SU - Scroll Up
|
|
429
296
|
for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) {
|
|
430
297
|
scrollUp(scrollTop, scrollBottom)
|
|
@@ -454,9 +321,24 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
454
321
|
curX = 0
|
|
455
322
|
curY = originMode ? scrollTop : 0
|
|
456
323
|
break
|
|
457
|
-
case "n": // DSR - Device Status Report
|
|
324
|
+
case "n": // DSR - Device Status Report
|
|
325
|
+
if (onResponse) {
|
|
326
|
+
if (parts[0] === 5) {
|
|
327
|
+
// Status report - OK
|
|
328
|
+
onResponse("\x1b[0n")
|
|
329
|
+
} else if (parts[0] === 6) {
|
|
330
|
+
// CPR - Cursor position report (1-based)
|
|
331
|
+
onResponse(`\x1b[${curY + 1};${curX + 1}R`)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
458
334
|
break
|
|
459
|
-
case "c": //
|
|
335
|
+
case "c": // DA1 - Primary Device Attributes
|
|
336
|
+
if (onResponse) {
|
|
337
|
+
if (params === "" || params === "0") {
|
|
338
|
+
// VT100 with Advanced Video Option (AVO)
|
|
339
|
+
onResponse("\x1b[?1;2c")
|
|
340
|
+
}
|
|
341
|
+
}
|
|
460
342
|
break
|
|
461
343
|
case "s": // SCP - Save Cursor Position
|
|
462
344
|
savedCurX = curX
|
|
@@ -482,6 +364,9 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
482
364
|
case 1: // DECCKM - Application Cursor
|
|
483
365
|
applicationCursor = set
|
|
484
366
|
break
|
|
367
|
+
case 5: // DECSCNM - Reverse Video
|
|
368
|
+
reverseVideo = set
|
|
369
|
+
break
|
|
485
370
|
case 6: // DECOM - Origin Mode
|
|
486
371
|
originMode = set
|
|
487
372
|
break
|
|
@@ -491,53 +376,9 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
491
376
|
case 25: // DECTCEM - Cursor Visible
|
|
492
377
|
curVisible = set
|
|
493
378
|
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
379
|
case 66: // DECNKM - Application Keypad
|
|
505
380
|
applicationKeypad = set
|
|
506
381
|
break
|
|
507
|
-
case 1000: // Mouse tracking (basic)
|
|
508
|
-
case 1002: // Mouse tracking (button events)
|
|
509
|
-
case 1003: // Mouse tracking (all events)
|
|
510
|
-
mouseTracking = set
|
|
511
|
-
break
|
|
512
|
-
case 1004: // Focus tracking
|
|
513
|
-
focusTracking = set
|
|
514
|
-
break
|
|
515
|
-
case 1049: // Alternate screen buffer + save/restore cursor
|
|
516
|
-
if (set && !useAltScreen) {
|
|
517
|
-
savedCurX = curX
|
|
518
|
-
savedCurY = curY
|
|
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)
|
|
539
|
-
insertMode = set
|
|
540
|
-
break
|
|
541
382
|
}
|
|
542
383
|
}
|
|
543
384
|
}
|
|
@@ -590,65 +431,12 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
590
431
|
}
|
|
591
432
|
}
|
|
592
433
|
|
|
593
|
-
function handleInsertLines(count: number): void {
|
|
594
|
-
if (curY < scrollTop || curY > scrollBottom) return
|
|
595
|
-
for (let i = 0; i < count; i++) {
|
|
596
|
-
scrollDown(curY, scrollBottom)
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function handleDeleteLines(count: number): void {
|
|
601
|
-
if (curY < scrollTop || curY > scrollBottom) return
|
|
602
|
-
for (let i = 0; i < count; i++) {
|
|
603
|
-
scrollUp(curY, scrollBottom)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function handleDeleteChars(count: number): void {
|
|
608
|
-
const row = grid[curY]
|
|
609
|
-
if (!row) return
|
|
610
|
-
for (let i = 0; i < count; i++) {
|
|
611
|
-
if (curX < cols) {
|
|
612
|
-
row.splice(curX, 1)
|
|
613
|
-
row.push(emptyCell())
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function handleInsertChars(count: number): void {
|
|
619
|
-
const row = grid[curY]
|
|
620
|
-
if (!row) return
|
|
621
|
-
for (let i = 0; i < count; i++) {
|
|
622
|
-
row.splice(curX, 0, emptyCell())
|
|
623
|
-
row.pop()
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function handleEraseChars(count: number): void {
|
|
628
|
-
const row = grid[curY]
|
|
629
|
-
if (!row) return
|
|
630
|
-
for (let i = 0; i < count && curX + i < cols; i++) {
|
|
631
|
-
row[curX + i] = emptyCell()
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
434
|
// ── SGR (Select Graphic Rendition) ──
|
|
435
|
+
// VT100 is monochrome: only bold (1), underline (4), blink (5), reverse (7).
|
|
436
|
+
// Color codes are silently ignored for forward compatibility.
|
|
636
437
|
|
|
637
438
|
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
|
-
}
|
|
439
|
+
const params = rawParams.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
652
440
|
|
|
653
441
|
if (params.length === 0 || (params.length === 1 && params[0] === 0)) {
|
|
654
442
|
attrs = resetAttrs()
|
|
@@ -665,172 +453,44 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
665
453
|
case 1:
|
|
666
454
|
attrs.bold = true
|
|
667
455
|
break
|
|
668
|
-
case
|
|
669
|
-
attrs.
|
|
670
|
-
break
|
|
671
|
-
case 3:
|
|
672
|
-
attrs.italic = true
|
|
456
|
+
case 4:
|
|
457
|
+
attrs.underline = true
|
|
673
458
|
break
|
|
674
|
-
case
|
|
675
|
-
|
|
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
|
-
}
|
|
459
|
+
case 5: // Blink
|
|
460
|
+
attrs.blink = true
|
|
705
461
|
break
|
|
706
|
-
}
|
|
707
462
|
case 7:
|
|
708
463
|
attrs.inverse = true
|
|
709
464
|
break
|
|
710
|
-
case
|
|
711
|
-
attrs.hidden = true
|
|
712
|
-
break
|
|
713
|
-
case 9:
|
|
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)
|
|
465
|
+
case 22: // Normal intensity (turn off bold)
|
|
720
466
|
attrs.bold = false
|
|
721
|
-
attrs.faint = false
|
|
722
|
-
break
|
|
723
|
-
case 23:
|
|
724
|
-
attrs.italic = false
|
|
725
467
|
break
|
|
726
468
|
case 24:
|
|
727
|
-
attrs.underline =
|
|
469
|
+
attrs.underline = false
|
|
470
|
+
break
|
|
471
|
+
case 25: // Blink off
|
|
472
|
+
attrs.blink = false
|
|
728
473
|
break
|
|
729
474
|
case 27:
|
|
730
475
|
attrs.inverse = false
|
|
731
476
|
break
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
case
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
case 31:
|
|
741
|
-
case 32:
|
|
742
|
-
case 33:
|
|
743
|
-
case 34:
|
|
744
|
-
case 35:
|
|
745
|
-
case 36:
|
|
746
|
-
case 37:
|
|
747
|
-
attrs.fg = { ...PALETTE_256[code - 30]! }
|
|
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
|
|
477
|
+
// Color codes silently ignored — VT100 is monochrome
|
|
478
|
+
// Skip extended color sequences to consume params correctly
|
|
479
|
+
case 38:
|
|
480
|
+
case 48:
|
|
481
|
+
if (i + 1 < params.length && params[i + 1] === 2) {
|
|
482
|
+
i += 4 // skip 38;2;R;G;B or 48;2;R;G;B
|
|
483
|
+
} else if (i + 1 < params.length && params[i + 1] === 5) {
|
|
484
|
+
i += 2 // skip 38;5;N or 48;5;N
|
|
755
485
|
}
|
|
756
486
|
break
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
attrs.fg = null
|
|
760
|
-
break
|
|
761
|
-
// Background colors 40-47
|
|
762
|
-
case 40:
|
|
763
|
-
case 41:
|
|
764
|
-
case 42:
|
|
765
|
-
case 43:
|
|
766
|
-
case 44:
|
|
767
|
-
case 45:
|
|
768
|
-
case 46:
|
|
769
|
-
case 47:
|
|
770
|
-
attrs.bg = { ...PALETTE_256[code - 40]! }
|
|
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
|
-
}
|
|
779
|
-
break
|
|
780
|
-
}
|
|
781
|
-
case 49: // Default background
|
|
782
|
-
attrs.bg = null
|
|
783
|
-
break
|
|
784
|
-
// Bright foreground 90-97
|
|
785
|
-
case 90:
|
|
786
|
-
case 91:
|
|
787
|
-
case 92:
|
|
788
|
-
case 93:
|
|
789
|
-
case 94:
|
|
790
|
-
case 95:
|
|
791
|
-
case 96:
|
|
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]! }
|
|
805
|
-
break
|
|
487
|
+
// All other SGR codes (colors 30-37, 40-47, 39, 49, 8, 28, etc.)
|
|
488
|
+
// are silently ignored for forward compatibility
|
|
806
489
|
}
|
|
807
490
|
i++
|
|
808
491
|
}
|
|
809
492
|
}
|
|
810
493
|
|
|
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
494
|
// ── OSC handler ──
|
|
835
495
|
|
|
836
496
|
function handleOSC(oscString: string): void {
|
|
@@ -883,16 +543,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
883
543
|
// CR - Carriage Return
|
|
884
544
|
curX = 0
|
|
885
545
|
} 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)
|
|
546
|
+
writeChar(ch)
|
|
896
547
|
}
|
|
897
548
|
break
|
|
898
549
|
|
|
@@ -957,6 +608,14 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
957
608
|
scrollUp(scrollTop, scrollBottom)
|
|
958
609
|
}
|
|
959
610
|
parserState = "ground"
|
|
611
|
+
} else if (ch === "=") {
|
|
612
|
+
// DECKPAM - Application Keypad Mode
|
|
613
|
+
applicationKeypad = true
|
|
614
|
+
parserState = "ground"
|
|
615
|
+
} else if (ch === ">") {
|
|
616
|
+
// DECKPNM - Normal Keypad Mode
|
|
617
|
+
applicationKeypad = false
|
|
618
|
+
parserState = "ground"
|
|
960
619
|
} else {
|
|
961
620
|
// Unknown escape — return to ground
|
|
962
621
|
parserState = "ground"
|
|
@@ -1017,9 +676,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1017
676
|
}
|
|
1018
677
|
|
|
1019
678
|
function fullReset(): void {
|
|
1020
|
-
|
|
1021
|
-
altGrid = makeGrid(cols, rows)
|
|
1022
|
-
grid = mainGrid
|
|
679
|
+
grid = makeGrid(cols, rows)
|
|
1023
680
|
scrollback = []
|
|
1024
681
|
curX = 0
|
|
1025
682
|
curY = 0
|
|
@@ -1029,15 +686,10 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1029
686
|
savedState = { curX: 0, curY: 0, attrs: resetAttrs(), originMode: false, autoWrap: true }
|
|
1030
687
|
attrs = resetAttrs()
|
|
1031
688
|
title = ""
|
|
1032
|
-
useAltScreen = false
|
|
1033
|
-
bracketedPaste = false
|
|
1034
689
|
applicationCursor = false
|
|
1035
690
|
applicationKeypad = false
|
|
1036
691
|
autoWrap = true
|
|
1037
|
-
mouseTracking = false
|
|
1038
|
-
focusTracking = false
|
|
1039
692
|
originMode = false
|
|
1040
|
-
insertMode = false
|
|
1041
693
|
reverseVideo = false
|
|
1042
694
|
scrollTop = 0
|
|
1043
695
|
scrollBottom = rows - 1
|
|
@@ -1048,16 +700,12 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1048
700
|
}
|
|
1049
701
|
|
|
1050
702
|
function resize(newCols: number, newRows: number): void {
|
|
1051
|
-
const
|
|
1052
|
-
const newAlt = makeGrid(newCols, newRows)
|
|
703
|
+
const newGrid = makeGrid(newCols, newRows)
|
|
1053
704
|
|
|
1054
|
-
// Copy content from old
|
|
1055
|
-
copyGrid(
|
|
1056
|
-
copyGrid(altGrid, newAlt, Math.min(cols, newCols), Math.min(rows, newRows))
|
|
705
|
+
// Copy content from old grid
|
|
706
|
+
copyGrid(grid, newGrid, Math.min(cols, newCols), Math.min(rows, newRows))
|
|
1057
707
|
|
|
1058
|
-
|
|
1059
|
-
altGrid = newAlt
|
|
1060
|
-
grid = useAltScreen ? altGrid : mainGrid
|
|
708
|
+
grid = newGrid
|
|
1061
709
|
cols = newCols
|
|
1062
710
|
rows = newRows
|
|
1063
711
|
scrollTop = 0
|
|
@@ -1108,13 +756,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1108
756
|
let line = ""
|
|
1109
757
|
for (let i = 0; i < row.length; i++) {
|
|
1110
758
|
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
|
-
}
|
|
759
|
+
if (cell.char === "") {
|
|
1118
760
|
line += " "
|
|
1119
761
|
} else {
|
|
1120
762
|
line += cell.char
|
|
@@ -1137,7 +779,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1137
779
|
for (let col = colStart; col < colEnd; col++) {
|
|
1138
780
|
const cell = r[col]
|
|
1139
781
|
if (!cell) continue
|
|
1140
|
-
if (cell.char === "" && col > 0 && r[col - 1]?.wide) continue // Skip spacer
|
|
1141
782
|
line += cell.char || " "
|
|
1142
783
|
}
|
|
1143
784
|
parts.push(line.replace(/\s+$/, ""))
|
|
@@ -1148,26 +789,16 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
1148
789
|
|
|
1149
790
|
function getMode(mode: string): boolean {
|
|
1150
791
|
switch (mode) {
|
|
1151
|
-
case "altScreen":
|
|
1152
|
-
return useAltScreen
|
|
1153
792
|
case "cursorVisible":
|
|
1154
793
|
return curVisible
|
|
1155
|
-
case "bracketedPaste":
|
|
1156
|
-
return bracketedPaste
|
|
1157
794
|
case "applicationCursor":
|
|
1158
795
|
return applicationCursor
|
|
1159
796
|
case "applicationKeypad":
|
|
1160
797
|
return applicationKeypad
|
|
1161
798
|
case "autoWrap":
|
|
1162
799
|
return autoWrap
|
|
1163
|
-
case "mouseTracking":
|
|
1164
|
-
return mouseTracking
|
|
1165
|
-
case "focusTracking":
|
|
1166
|
-
return focusTracking
|
|
1167
800
|
case "originMode":
|
|
1168
801
|
return originMode
|
|
1169
|
-
case "insertMode":
|
|
1170
|
-
return insertMode
|
|
1171
802
|
case "reverseVideo":
|
|
1172
803
|
return reverseVideo
|
|
1173
804
|
default:
|