vt100.js 0.2.3 → 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/screen.ts +16 -188
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/screen.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure TypeScript VT100
|
|
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
|
-
* DA1/DSR responses, and more. No truecolor, no 256 colors, no wide chars —
|
|
11
|
-
* those belong in vterm.js.
|
|
10
|
+
* Zero dependencies. For colors and VT220 features, use vt220.js.
|
|
11
|
+
* For truecolor, 256 colors, and wide chars, use vterm.js.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
// ═══════════════════════════════════════════════════════
|
|
@@ -48,21 +48,6 @@ function emptyCell(): ScreenCell {
|
|
|
48
48
|
return { ...EMPTY_CELL }
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// ═══════════════════════════════════════════════════════
|
|
52
|
-
// ANSI 8-color palette (standard VT100/VT220 colors)
|
|
53
|
-
// ═══════════════════════════════════════════════════════
|
|
54
|
-
|
|
55
|
-
const ANSI_8: readonly CellColor[] = [
|
|
56
|
-
{ r: 0x00, g: 0x00, b: 0x00 }, // 0 Black
|
|
57
|
-
{ r: 0x80, g: 0x00, b: 0x00 }, // 1 Red
|
|
58
|
-
{ r: 0x00, g: 0x80, b: 0x00 }, // 2 Green
|
|
59
|
-
{ r: 0x80, g: 0x80, b: 0x00 }, // 3 Yellow
|
|
60
|
-
{ r: 0x00, g: 0x00, b: 0x80 }, // 4 Blue
|
|
61
|
-
{ r: 0x80, g: 0x00, b: 0x80 }, // 5 Magenta
|
|
62
|
-
{ r: 0x00, g: 0x80, b: 0x80 }, // 6 Cyan
|
|
63
|
-
{ r: 0xc0, g: 0xc0, b: 0xc0 }, // 7 White
|
|
64
|
-
]
|
|
65
|
-
|
|
66
51
|
// ═══════════════════════════════════════════════════════
|
|
67
52
|
// Screen
|
|
68
53
|
// ═══════════════════════════════════════════════════════
|
|
@@ -110,7 +95,7 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
110
95
|
const scrollbackLimit = opts.scrollbackLimit ?? 1000
|
|
111
96
|
const onResponse = opts.onResponse
|
|
112
97
|
|
|
113
|
-
// Main screen buffer (no alternate screen in VT100
|
|
98
|
+
// Main screen buffer (no alternate screen in VT100)
|
|
114
99
|
let grid: ScreenCell[][] = makeGrid(cols, rows)
|
|
115
100
|
let scrollback: ScreenCell[][] = []
|
|
116
101
|
|
|
@@ -146,7 +131,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
146
131
|
let applicationKeypad = false
|
|
147
132
|
let autoWrap = true
|
|
148
133
|
let originMode = false
|
|
149
|
-
let insertMode = false
|
|
150
134
|
let reverseVideo = false
|
|
151
135
|
|
|
152
136
|
// Scroll region (inclusive, 0-based)
|
|
@@ -241,13 +225,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
241
225
|
}
|
|
242
226
|
}
|
|
243
227
|
|
|
244
|
-
// Insert mode: shift existing characters right before writing
|
|
245
|
-
if (insertMode) {
|
|
246
|
-
const row = grid[curY]!
|
|
247
|
-
row.splice(curX, 0, EMPTY_CELL)
|
|
248
|
-
row.pop()
|
|
249
|
-
}
|
|
250
|
-
|
|
251
228
|
// Copy-on-write: if cell is the shared EMPTY_CELL sentinel, create a fresh object
|
|
252
229
|
const row = grid[curY]!
|
|
253
230
|
let cell = row[curX]!
|
|
@@ -315,21 +292,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
315
292
|
case "K": // EL - Erase in Line
|
|
316
293
|
handleEraseLine(parts[0] ?? 0)
|
|
317
294
|
break
|
|
318
|
-
case "L": // IL - Insert Lines
|
|
319
|
-
handleInsertLines(Math.max(parts[0] ?? 1, 1))
|
|
320
|
-
break
|
|
321
|
-
case "M": // DL - Delete Lines
|
|
322
|
-
handleDeleteLines(Math.max(parts[0] ?? 1, 1))
|
|
323
|
-
break
|
|
324
|
-
case "P": // DCH - Delete Characters
|
|
325
|
-
handleDeleteChars(Math.max(parts[0] ?? 1, 1))
|
|
326
|
-
break
|
|
327
|
-
case "@": // ICH - Insert Characters
|
|
328
|
-
handleInsertChars(Math.max(parts[0] ?? 1, 1))
|
|
329
|
-
break
|
|
330
|
-
case "X": // ECH - Erase Characters
|
|
331
|
-
handleEraseChars(Math.max(parts[0] ?? 1, 1))
|
|
332
|
-
break
|
|
333
295
|
case "S": // SU - Scroll Up
|
|
334
296
|
for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) {
|
|
335
297
|
scrollUp(scrollTop, scrollBottom)
|
|
@@ -393,14 +355,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
393
355
|
}
|
|
394
356
|
}
|
|
395
357
|
|
|
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
|
-
|
|
404
358
|
function handleCSIPrivate(params: string, finalByte: string): void {
|
|
405
359
|
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
406
360
|
const set = finalByte === "h"
|
|
@@ -410,9 +364,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
410
364
|
case 1: // DECCKM - Application Cursor
|
|
411
365
|
applicationCursor = set
|
|
412
366
|
break
|
|
413
|
-
case 4: // IRM - Insert Mode (via DEC private)
|
|
414
|
-
insertMode = set
|
|
415
|
-
break
|
|
416
367
|
case 5: // DECSCNM - Reverse Video
|
|
417
368
|
reverseVideo = set
|
|
418
369
|
break
|
|
@@ -432,20 +383,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
432
383
|
}
|
|
433
384
|
}
|
|
434
385
|
|
|
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
|
|
443
|
-
insertMode = set
|
|
444
|
-
break
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
386
|
function handleEraseDisplay(mode: number): void {
|
|
450
387
|
switch (mode) {
|
|
451
388
|
case 0: // Erase from cursor to end
|
|
@@ -494,49 +431,9 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
494
431
|
}
|
|
495
432
|
}
|
|
496
433
|
|
|
497
|
-
function handleInsertLines(count: number): void {
|
|
498
|
-
if (curY < scrollTop || curY > scrollBottom) return
|
|
499
|
-
for (let i = 0; i < count; i++) {
|
|
500
|
-
scrollDown(curY, scrollBottom)
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function handleDeleteLines(count: number): void {
|
|
505
|
-
if (curY < scrollTop || curY > scrollBottom) return
|
|
506
|
-
for (let i = 0; i < count; i++) {
|
|
507
|
-
scrollUp(curY, scrollBottom)
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function handleDeleteChars(count: number): void {
|
|
512
|
-
const row = grid[curY]
|
|
513
|
-
if (!row) return
|
|
514
|
-
for (let i = 0; i < count; i++) {
|
|
515
|
-
if (curX < cols) {
|
|
516
|
-
row.splice(curX, 1)
|
|
517
|
-
row.push(emptyCell())
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function handleInsertChars(count: number): void {
|
|
523
|
-
const row = grid[curY]
|
|
524
|
-
if (!row) return
|
|
525
|
-
for (let i = 0; i < count; i++) {
|
|
526
|
-
row.splice(curX, 0, emptyCell())
|
|
527
|
-
row.pop()
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function handleEraseChars(count: number): void {
|
|
532
|
-
const row = grid[curY]
|
|
533
|
-
if (!row) return
|
|
534
|
-
for (let i = 0; i < count && curX + i < cols; i++) {
|
|
535
|
-
row[curX + i] = emptyCell()
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
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.
|
|
540
437
|
|
|
541
438
|
function handleSGR(rawParams: string): void {
|
|
542
439
|
const params = rawParams.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
@@ -565,9 +462,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
565
462
|
case 7:
|
|
566
463
|
attrs.inverse = true
|
|
567
464
|
break
|
|
568
|
-
case 8: // Hidden/conceal
|
|
569
|
-
attrs.hidden = true
|
|
570
|
-
break
|
|
571
465
|
case 22: // Normal intensity (turn off bold)
|
|
572
466
|
attrs.bold = false
|
|
573
467
|
break
|
|
@@ -580,38 +474,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
580
474
|
case 27:
|
|
581
475
|
attrs.inverse = false
|
|
582
476
|
break
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
break
|
|
586
|
-
// Foreground colors 30-37
|
|
587
|
-
case 30:
|
|
588
|
-
case 31:
|
|
589
|
-
case 32:
|
|
590
|
-
case 33:
|
|
591
|
-
case 34:
|
|
592
|
-
case 35:
|
|
593
|
-
case 36:
|
|
594
|
-
case 37:
|
|
595
|
-
attrs.fg = { ...ANSI_8[code - 30]! }
|
|
596
|
-
break
|
|
597
|
-
case 39: // Default foreground
|
|
598
|
-
attrs.fg = null
|
|
599
|
-
break
|
|
600
|
-
// Background colors 40-47
|
|
601
|
-
case 40:
|
|
602
|
-
case 41:
|
|
603
|
-
case 42:
|
|
604
|
-
case 43:
|
|
605
|
-
case 44:
|
|
606
|
-
case 45:
|
|
607
|
-
case 46:
|
|
608
|
-
case 47:
|
|
609
|
-
attrs.bg = { ...ANSI_8[code - 40]! }
|
|
610
|
-
break
|
|
611
|
-
case 49: // Default background
|
|
612
|
-
attrs.bg = null
|
|
613
|
-
break
|
|
614
|
-
// Skip extended color sequences (not supported, but must consume params)
|
|
477
|
+
// Color codes silently ignored — VT100 is monochrome
|
|
478
|
+
// Skip extended color sequences to consume params correctly
|
|
615
479
|
case 38:
|
|
616
480
|
case 48:
|
|
617
481
|
if (i + 1 < params.length && params[i + 1] === 2) {
|
|
@@ -620,6 +484,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
620
484
|
i += 2 // skip 38;5;N or 48;5;N
|
|
621
485
|
}
|
|
622
486
|
break
|
|
487
|
+
// All other SGR codes (colors 30-37, 40-47, 39, 49, 8, 28, etc.)
|
|
488
|
+
// are silently ignored for forward compatibility
|
|
623
489
|
}
|
|
624
490
|
i++
|
|
625
491
|
}
|
|
@@ -761,27 +627,8 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
761
627
|
// Final byte — dispatch CSI
|
|
762
628
|
if (escBuf.startsWith("?")) {
|
|
763
629
|
handleCSIPrivate(escBuf.substring(1), ch)
|
|
764
|
-
} else if (ch === "h" || ch === "l") {
|
|
765
|
-
// Standard set/reset mode (non-private)
|
|
766
|
-
handleSetResetMode(escBuf, ch)
|
|
767
630
|
} else {
|
|
768
|
-
|
|
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
|
-
}
|
|
631
|
+
handleCSI(escBuf, ch)
|
|
785
632
|
}
|
|
786
633
|
parserState = "ground"
|
|
787
634
|
} else if (escBuf.length >= 256) {
|
|
@@ -843,7 +690,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
843
690
|
applicationKeypad = false
|
|
844
691
|
autoWrap = true
|
|
845
692
|
originMode = false
|
|
846
|
-
insertMode = false
|
|
847
693
|
reverseVideo = false
|
|
848
694
|
scrollTop = 0
|
|
849
695
|
scrollBottom = rows - 1
|
|
@@ -853,22 +699,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
853
699
|
oscBuf = ""
|
|
854
700
|
}
|
|
855
701
|
|
|
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
|
-
|
|
872
702
|
function resize(newCols: number, newRows: number): void {
|
|
873
703
|
const newGrid = makeGrid(newCols, newRows)
|
|
874
704
|
|
|
@@ -969,8 +799,6 @@ export function createScreen(opts: ScreenOptions): Screen {
|
|
|
969
799
|
return autoWrap
|
|
970
800
|
case "originMode":
|
|
971
801
|
return originMode
|
|
972
|
-
case "insertMode":
|
|
973
|
-
return insertMode
|
|
974
802
|
case "reverseVideo":
|
|
975
803
|
return reverseVideo
|
|
976
804
|
default:
|