vt100.js 0.2.3 → 0.3.1

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.
Files changed (3) hide show
  1. package/README.md +13 -14
  2. package/package.json +3 -3
  3. package/src/screen.ts +16 -188
package/README.md CHANGED
@@ -6,15 +6,17 @@ Part of the [vterm](https://github.com/beorn/vterm) monorepo.
6
6
 
7
7
  ## Features
8
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.)
9
+ - Strict DEC VT100 (1978) emulation — monochrome display
10
+ - SGR attributes: bold, underline (boolean), blink, inverse
11
+ - Cursor movement (CUP, CUU, CUD, CUF, CUB, CHA, CNL, CPL, HVP, VPA)
12
+ - Cursor save/restore (DECSC/DECRC)
13
+ - Erase display/line (ED/EL)
14
14
  - Scroll regions (DECSTBM) with content preservation
15
+ - Scroll up/down (SU/SD)
15
16
  - Scrollback buffer with configurable limit
16
- - Insert/delete characters and lines
17
- - Wide character support (CJK, emoji)
17
+ - Screen modes (auto-wrap, origin mode, cursor visibility)
18
+ - DEC special graphics charset
19
+ - DA1/DSR/CPR device responses
18
20
  - Zero dependencies — works in Bun, Node.js, and browsers
19
21
 
20
22
  ## Install
@@ -70,16 +72,13 @@ console.log(screen.getCursorPosition()) // { x: 18, y: 0 }
70
72
  ```typescript
71
73
  interface ScreenCell {
72
74
  char: string
73
- fg: CellColor | null // { r, g, b }
74
- bg: CellColor | null
75
+ fg: CellColor | null // always null (VT100 is monochrome)
76
+ bg: CellColor | null // always null (VT100 is monochrome)
75
77
  bold: boolean
76
- faint: boolean
77
- italic: boolean
78
- underline: "none" | "single" | "double" | "curly" | "dotted" | "dashed"
79
- strikethrough: boolean
78
+ underline: boolean
79
+ blink: boolean
80
80
  inverse: boolean
81
81
  hidden: boolean
82
- wide: boolean
83
82
  }
84
83
  ```
85
84
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vt100.js",
3
- "version": "0.2.3",
4
- "description": "Pure TypeScript VT100 terminal emulator. Zero dependencies, headless, fast.",
3
+ "version": "0.3.1",
4
+ "description": "VT100 terminal emulator monochrome, cursor, scroll regions. Pure TypeScript, zero dependencies.",
5
5
  "keywords": [
6
6
  "ansi",
7
7
  "headless",
@@ -14,7 +14,7 @@
14
14
  "homepage": "https://github.com/beorn/vterm/tree/main/packages/vt100",
15
15
  "bugs": "https://github.com/beorn/vterm/issues",
16
16
  "license": "MIT",
17
- "author": "Beorn",
17
+ "author": "Bjørn Stabell",
18
18
  "repository": {
19
19
  "type": "git",
20
20
  "url": "https://github.com/beorn/vterm.git",
package/src/screen.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Pure TypeScript VT100/VT220 terminal emulator.
2
+ * Pure TypeScript VT100 terminal emulator.
3
3
  *
4
- * Inspired by the Rust vt100 crate parses a terminal byte stream and
5
- * maintains an in-memory screen representation with per-cell attributes,
6
- * cursor tracking, and mode state.
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. 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
+ * 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/VT220)
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
- case 28: // Reveal (turn off hidden/conceal)
584
- attrs.hidden = false
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
- // 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
- }
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: