vterm.js 0.2.0 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/screen.ts +168 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vterm.js",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Modern terminal emulator — full VT/ECMA-48/xterm standards coverage. Pure TypeScript, zero dependencies.",
5
5
  "keywords": [
6
6
  "ansi",
package/src/screen.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * - Scrollback buffer with configurable limit
21
21
  * - Wide character support (CJK, emoji ZWJ, regional indicators, VS-16)
22
22
  * - OSC sequences (title, hyperlinks, clipboard, colors)
23
- * - DCS sequences (consumed and ignored, XTVERSION response)
23
+ * - DCS sequences (XTVERSION, DECRQSS, XTGETTCAP, Sixel)
24
24
  * - APC sequences (Kitty graphics protocol — parsed, query responses)
25
25
  * - DA1/DA2/DA3 device attribute responses
26
26
  * - DSR (device status report) responses
@@ -101,6 +101,9 @@ export interface Screen {
101
101
 
102
102
  getTitle(): string
103
103
  getMode(mode: string): boolean
104
+ getClipboard(): string
105
+ getCwd(): string
106
+ getNotifications(): string[]
104
107
 
105
108
  getScrollbackLength(): number
106
109
  getViewportOffset(): number
@@ -376,10 +379,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
376
379
  let syncOutput = false
377
380
 
378
381
  // Kitty keyboard protocol
382
+ // Headless design: we implement the full push/pop/query state machine so that
383
+ // applications can negotiate keyboard encoding modes. The flags are stored and
384
+ // queryable (CSI ? u responds correctly), and a host can read them to decide
385
+ // how to encode keypresses. The actual key encoding lives in the host layer
386
+ // (e.g. termless's encodeKeyToAnsi), not here — separation of concerns.
379
387
  let kittyKeyboardFlags = 0
380
388
  let kittyKeyboardStack: number[] = []
381
389
 
382
390
  // Kitty graphics protocol
391
+ // Headless design: we parse the APC G protocol and respond to queries so that
392
+ // applications relying on the query→response handshake work correctly. We don't
393
+ // store image data because there's no pixel framebuffer — unlike sixel where we
394
+ // preserve the raw data for consumers, kitty graphics payloads are chunked and
395
+ // stateful, making storage without rendering impractical.
383
396
  let hasKittyGraphics = false
384
397
 
385
398
  // Scroll region (inclusive, 0-based)
@@ -395,10 +408,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
395
408
  // Clipboard (OSC 52)
396
409
  let clipboard = ""
397
410
 
398
- // Semantic prompt zones (OSC 133)
411
+ // Current working directory (OSC 7)
412
+ let cwd = ""
413
+
414
+ // Notifications (OSC 9)
415
+ let notifications: string[] = []
416
+
417
+ // Semantic prompt zones (OSC 133 / OSC 633)
399
418
  let semanticZones: SemanticZone[] = []
400
419
 
401
420
  // Sixel graphics (DCS q)
421
+ // Headless design: we parse sixel data and store it for consumers (e.g. a GUI
422
+ // renderer could use getSixelImages() to display them). We don't decode pixels
423
+ // because there's no framebuffer to render into — but preserving the data means
424
+ // a host application gets full fidelity without re-parsing the stream.
402
425
  let hasSixel = false
403
426
  let sixelImages: SixelImage[] = []
404
427
 
@@ -792,9 +815,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
792
815
  break
793
816
  case "H": // CUP - Cursor Position
794
817
  case "f": // HVP - same as CUP
795
- curY = (parts[0] ?? 1) - 1
796
- curX = (parts[1] ?? 1) - 1
797
- clampCursor()
818
+ if (originMode) {
819
+ // DECOM: positions are relative to scroll region
820
+ curY = scrollTop + (parts[0] ?? 1) - 1
821
+ curX = (parts[1] ?? 1) - 1
822
+ // Clamp to scroll region bounds
823
+ if (curY < scrollTop) curY = scrollTop
824
+ if (curY > scrollBottom) curY = scrollBottom
825
+ if (curX < 0) curX = 0
826
+ if (curX >= cols) curX = cols - 1
827
+ } else {
828
+ curY = (parts[0] ?? 1) - 1
829
+ curX = (parts[1] ?? 1) - 1
830
+ clampCursor()
831
+ }
798
832
  break
799
833
  case "J": // ED - Erase in Display
800
834
  handleEraseDisplay(parts[0] ?? 0)
@@ -868,6 +902,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
868
902
  case "c": // DA1 - Primary Device Attributes
869
903
  if (onResponse) {
870
904
  if (params === "" || params === "0") {
905
+ // VT200 (62) + sixel (4). We include sixel because we parse and
906
+ // preserve sixel data via getSixelImages() — applications checking
907
+ // DA1 for sixel support before sending image data will work correctly.
871
908
  onResponse("\x1b[?62;4c")
872
909
  }
873
910
  }
@@ -929,6 +966,18 @@ export function createScreen(options: ScreenOptions = {}): Screen {
929
966
  }
930
967
  }
931
968
 
969
+ function handleCSIEq(params: string, _intermediates: string, finalByte: string): void {
970
+ // CSI = sequences
971
+ if (finalByte === "c") {
972
+ // DA3 - Tertiary Device Attributes
973
+ if (onResponse) {
974
+ if (params === "" || params === "0") {
975
+ onResponse("\x1bP!|00000000\x1b\\")
976
+ }
977
+ }
978
+ }
979
+ }
980
+
932
981
  function handleCSIPrivate(params: string, intermediates: string, finalByte: string): void {
933
982
  const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
934
983
 
@@ -995,6 +1044,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
995
1044
  return
996
1045
  }
997
1046
 
1047
+ // DECSED / DECSEL — selective erase (CSI ? J / CSI ? K)
1048
+ // We treat selective erase same as normal erase (no DECSCA protection tracking)
1049
+ if (finalByte === "J") {
1050
+ handleEraseDisplay(parts[0] ?? 0)
1051
+ return
1052
+ }
1053
+ if (finalByte === "K") {
1054
+ handleEraseLine(parts[0] ?? 0)
1055
+ return
1056
+ }
1057
+
998
1058
  const set = finalByte === "h"
999
1059
 
1000
1060
  for (const code of parts) {
@@ -1032,6 +1092,10 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1032
1092
  case 66: // DECNKM - Application Keypad
1033
1093
  applicationKeypad = set
1034
1094
  break
1095
+ case 9: // X10 mouse tracking
1096
+ mouseTracking = set
1097
+ mouseTrackingMode = set ? 9 : 0
1098
+ break
1035
1099
  case 1000: // Mouse tracking (basic)
1036
1100
  mouseTracking = set
1037
1101
  mouseTrackingMode = set ? 1000 : 0
@@ -1044,6 +1108,14 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1044
1108
  mouseTracking = set
1045
1109
  mouseTrackingMode = set ? 1003 : 0
1046
1110
  break
1111
+ case 1015: // urxvt mouse encoding
1112
+ mouseTracking = set
1113
+ mouseTrackingMode = set ? 1015 : 0
1114
+ break
1115
+ case 1016: // SGR pixel mouse
1116
+ mouseTracking = set
1117
+ mouseTrackingMode = set ? 1016 : 0
1118
+ break
1047
1119
  case 1004: // Focus tracking
1048
1120
  focusTracking = set
1049
1121
  break
@@ -1126,7 +1198,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1126
1198
  const r = grid[row]
1127
1199
  if (!r) return
1128
1200
  for (let col = startCol; col <= endCol && col < cols; col++) {
1129
- r[col] = emptyCell()
1201
+ const cell = emptyCell()
1202
+ // Fill erased cells with the current background color
1203
+ if (attrs.bg) {
1204
+ cell.bg = { ...attrs.bg }
1205
+ }
1206
+ r[col] = cell
1130
1207
  }
1131
1208
  }
1132
1209
 
@@ -1473,6 +1550,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1473
1550
  }
1474
1551
  case 1: // Set icon name (ignore)
1475
1552
  break
1553
+ case 7: // Current working directory: OSC 7 ; file://host/path ST
1554
+ cwd = value
1555
+ break
1476
1556
  case 8: {
1477
1557
  // Hyperlink: OSC 8 ; params ; url ST
1478
1558
  // Format: 8;params;url or 8;;url or 8;; (close)
@@ -1520,6 +1600,40 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1520
1600
  }
1521
1601
  break
1522
1602
  }
1603
+ case 9: // Notifications: OSC 9 ; message ST (iTerm2/ConEmu convention)
1604
+ notifications.push(value)
1605
+ break
1606
+ case 633: {
1607
+ // VS Code shell integration (OSC 633 is a superset of OSC 133)
1608
+ // Maps to the same semantic zone model as FinalTerm markers.
1609
+ const marker = value.charAt(0)
1610
+ switch (marker) {
1611
+ case "A":
1612
+ semanticZones.push({ type: "prompt", startRow: curY, startCol: curX })
1613
+ break
1614
+ case "B":
1615
+ semanticZones.push({ type: "command", startRow: curY, startCol: curX })
1616
+ break
1617
+ case "C":
1618
+ semanticZones.push({ type: "output", startRow: curY, startCol: curX })
1619
+ break
1620
+ case "D":
1621
+ break
1622
+ }
1623
+ break
1624
+ }
1625
+ case 1337: {
1626
+ // iTerm2 proprietary sequences
1627
+ if (value === "ReportCellSize" && onResponse) {
1628
+ // Report default 8x16 pixel cell size (standard monospace ratio)
1629
+ onResponse("\x1b]1337;ReportCellSize=16;8\x1b\\")
1630
+ } else if (value === "RequestCapabilities" && onResponse) {
1631
+ // Report empty capabilities (protocol supported, no iTerm2-specific features)
1632
+ onResponse("\x1b]1337;Capabilities=\x1b\\")
1633
+ }
1634
+ // Inline images (File=...) are silently consumed — no pixel framebuffer
1635
+ break
1636
+ }
1523
1637
  }
1524
1638
  }
1525
1639
 
@@ -1538,6 +1652,35 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1538
1652
  return
1539
1653
  }
1540
1654
 
1655
+ // DECRQSS: DCS $ q Pt ST → response DCS Ps $ r Pt ST
1656
+ if (data.startsWith("$q") && onResponse) {
1657
+ const pt = data.substring(2)
1658
+ if (pt === '"p') {
1659
+ // DECSCL - Conformance level: VT200 mode, 8-bit controls
1660
+ onResponse('\x1bP1$r62;1"p\x1b\\')
1661
+ } else {
1662
+ // Not recognized
1663
+ onResponse("\x1bP0$r\x1b\\")
1664
+ }
1665
+ return
1666
+ }
1667
+
1668
+ // XTGETTCAP: DCS + q hex ST → response DCS 1 + r hex = hexvalue ST
1669
+ if (data.startsWith("+q") && onResponse) {
1670
+ const hexName = data.substring(2)
1671
+ if (hexName === "544e") {
1672
+ // "TN" = terminal name → "vterm"
1673
+ const hexValue = Array.from(new TextEncoder().encode("vterm"))
1674
+ .map((b) => b.toString(16).padStart(2, "0"))
1675
+ .join("")
1676
+ onResponse(`\x1bP1+r544e=${hexValue}\x1b\\`)
1677
+ } else {
1678
+ // Unknown capability
1679
+ onResponse("\x1bP0+r\x1b\\")
1680
+ }
1681
+ return
1682
+ }
1683
+
1541
1684
  // Other DCS sequences are consumed and ignored
1542
1685
  }
1543
1686
 
@@ -1638,6 +1781,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1638
1781
  viewportOffset = 0
1639
1782
  charsetG0 = false
1640
1783
  clipboard = ""
1784
+ cwd = ""
1785
+ notifications = []
1641
1786
  lastChar = ""
1642
1787
  pendingRegionalIndicator = null
1643
1788
  afterZWJ = false
@@ -1777,6 +1922,14 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1777
1922
  } else if (ch === ")") {
1778
1923
  // Designate G1 character set (ignored, just consume next byte)
1779
1924
  parserState = "escape_charset"
1925
+ } else if (ch === "=") {
1926
+ // DECKPAM - Application Keypad Mode
1927
+ applicationKeypad = true
1928
+ parserState = "ground"
1929
+ } else if (ch === ">") {
1930
+ // DECKPNM - Normal Keypad Mode
1931
+ applicationKeypad = false
1932
+ parserState = "ground"
1780
1933
  } else if (ch === "_") {
1781
1934
  // APC - Application Program Command
1782
1935
  parserState = "apc"
@@ -1826,6 +1979,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1826
1979
  handleCSIGt(paramPart.substring(1), intermediatePart, ch)
1827
1980
  } else if (paramPart.startsWith("<")) {
1828
1981
  handleCSILt(paramPart.substring(1), intermediatePart, ch)
1982
+ } else if (paramPart.startsWith("=")) {
1983
+ handleCSIEq(paramPart.substring(1), intermediatePart, ch)
1829
1984
  } else {
1830
1985
  handleCSI(paramPart, intermediatePart, ch)
1831
1986
  }
@@ -2148,6 +2303,10 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2148
2303
  return syncOutput
2149
2304
  case "sgrMouse":
2150
2305
  return sgrMouse
2306
+ case "pixelMouse":
2307
+ return mouseTrackingMode === 1016
2308
+ case "leftRightMargin":
2309
+ return false // Not implemented yet
2151
2310
  case "kittyKeyboard":
2152
2311
  return kittyKeyboardFlags > 0
2153
2312
  case "kittyGraphics":
@@ -2182,6 +2341,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2182
2341
  getCursorBlinking: () => cursorBlinking,
2183
2342
  getTitle: () => title,
2184
2343
  getMode,
2344
+ getClipboard: () => clipboard,
2345
+ getCwd: () => cwd,
2346
+ getNotifications: () => [...notifications],
2185
2347
  getScrollbackLength: () => scrollback.length,
2186
2348
  getViewportOffset: () => viewportOffset,
2187
2349
  scrollViewport: (delta: number) => {