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.
- package/package.json +1 -1
- package/src/screen.ts +168 -6
package/package.json
CHANGED
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 (
|
|
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
|
-
//
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
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) => {
|