vterm.js 0.2.0 → 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 +23 -20
  2. package/package.json +2 -2
  3. package/src/screen.ts +376 -51
package/README.md CHANGED
@@ -152,26 +152,29 @@ interface ScreenCell {
152
152
 
153
153
  ## vs vt100.js
154
154
 
155
- | Feature | vt100.js | vterm.js |
156
- | -------------------------------------------- | -------- | -------- |
157
- | SGR basics (bold, italic, underline, colors) | Yes | Yes |
158
- | Underline styles (curly, dotted, dashed) | Yes | Yes |
159
- | Underline color | No | Yes |
160
- | Blink, overline | No | Yes |
161
- | Cursor shapes (DECSCUSR) | No | Yes |
162
- | OSC 8 hyperlinks | No | Yes |
163
- | DA1/DA2/DA3 responses | No | Yes |
164
- | DSR/DECRPM responses | No | Yes |
165
- | Mouse tracking | No | Yes |
166
- | Focus tracking | No | Yes |
167
- | Synchronized output | No | Yes |
168
- | DEC Special Graphics | No | Yes |
169
- | REP (repeat character) | No | Yes |
170
- | DECSTR (soft reset) | No | Yes |
171
- | DCS sequences | No | Yes |
172
- | Package size | Smaller | Larger |
173
-
174
- **Use vt100.js** when you want fast and simple — it covers ~90% of real-world terminal usage.
155
+ | Feature | vt100.js | vterm.js |
156
+ | ----------------------------------------- | -------- | -------- |
157
+ | Bold, underline (boolean), blink, inverse | Yes | Yes |
158
+ | Colors (8/256/truecolor) | No | Yes |
159
+ | Italic, faint, strikethrough, overline | No | Yes |
160
+ | Underline styles (curly, dotted, dashed) | No | Yes |
161
+ | Underline color | No | Yes |
162
+ | Wide characters (CJK, emoji) | No | Yes |
163
+ | Cursor shapes (DECSCUSR) | No | Yes |
164
+ | Insert/delete characters and lines | No | Yes |
165
+ | OSC 8 hyperlinks | No | Yes |
166
+ | DA1/DA2/DA3 responses | DA1 only | Yes |
167
+ | DSR/DECRPM responses | No | Yes |
168
+ | Mouse tracking | No | Yes |
169
+ | Focus tracking | No | Yes |
170
+ | Synchronized output | No | Yes |
171
+ | DEC Special Graphics | Yes | Yes |
172
+ | REP (repeat character) | No | Yes |
173
+ | DECSTR (soft reset) | No | Yes |
174
+ | DCS sequences | No | Yes |
175
+ | Package size | Smaller | Larger |
176
+
177
+ **Use vt100.js** for strict VT100 conformance — monochrome, no colors, no insert/delete. Fast and minimal.
175
178
 
176
179
  **Use vterm.js** when you need everything — 100% coverage of the [terminfo.dev](https://terminfo.dev) feature matrix.
177
180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vterm.js",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Modern terminal emulator — full VT/ECMA-48/xterm standards coverage. Pure TypeScript, zero dependencies.",
5
5
  "keywords": [
6
6
  "ansi",
@@ -17,7 +17,7 @@
17
17
  "homepage": "https://github.com/beorn/vterm/tree/main/packages/vterm",
18
18
  "bugs": "https://github.com/beorn/vterm/issues",
19
19
  "license": "MIT",
20
- "author": "Beorn",
20
+ "author": "Bjørn Stabell",
21
21
  "repository": {
22
22
  "type": "git",
23
23
  "url": "https://github.com/beorn/vterm.git",
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,16 +379,40 @@ 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)
386
399
  let scrollTop = 0
387
400
  let scrollBottom = rows - 1
388
401
 
402
+ // Left/right margin mode (DECLRMM, DECSET ?69)
403
+ let leftRightMarginMode = false
404
+ let leftMargin = 0
405
+ let rightMargin = cols - 1
406
+
407
+ // Color scheme reporting (mode 2031)
408
+ let colorSchemeReporting = false
409
+
410
+ // Text scale (OSC 66)
411
+ let textScale = 1
412
+
413
+ // Advanced clipboard (OSC 5522 — Kitty clipboard protocol)
414
+ let advancedClipboard = ""
415
+
389
416
  // Viewport scroll offset
390
417
  let viewportOffset = 0
391
418
 
@@ -395,10 +422,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
395
422
  // Clipboard (OSC 52)
396
423
  let clipboard = ""
397
424
 
398
- // Semantic prompt zones (OSC 133)
425
+ // Current working directory (OSC 7)
426
+ let cwd = ""
427
+
428
+ // Notifications (OSC 9)
429
+ let notifications: string[] = []
430
+
431
+ // Semantic prompt zones (OSC 133 / OSC 633)
399
432
  let semanticZones: SemanticZone[] = []
400
433
 
401
434
  // Sixel graphics (DCS q)
435
+ // Headless design: we parse sixel data and store it for consumers (e.g. a GUI
436
+ // renderer could use getSixelImages() to display them). We don't decode pixels
437
+ // because there's no framebuffer to render into — but preserving the data means
438
+ // a host application gets full fidelity without re-parsing the stream.
402
439
  let hasSixel = false
403
440
  let sixelImages: SixelImage[] = []
404
441
 
@@ -483,28 +520,65 @@ export function createScreen(options: ScreenOptions = {}): Screen {
483
520
  // ── Scrolling ──
484
521
 
485
522
  function scrollUp(top: number, bottom: number): void {
486
- // Move top row to scrollback (only if main screen & top of screen)
487
- if (grid === mainGrid && top === 0) {
488
- scrollback.push(grid[0]!)
489
- if (scrollback.length > scrollbackLimit * 2) {
490
- scrollback.splice(0, scrollback.length - scrollbackLimit)
523
+ if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
524
+ // Scroll only within the left/right margin columns
525
+ const lm = leftMargin
526
+ const rm = rightMargin
527
+ for (let r = top; r < bottom; r++) {
528
+ const srcRow = grid[r + 1]!
529
+ const dstRow = grid[r]!
530
+ for (let c = lm; c <= rm && c < cols; c++) {
531
+ dstRow[c] = srcRow[c]!
532
+ }
491
533
  }
534
+ // Clear the bottom row within margins
535
+ const bottomRow = grid[bottom]!
536
+ for (let c = lm; c <= rm && c < cols; c++) {
537
+ bottomRow[c] = EMPTY_CELL
538
+ }
539
+ } else {
540
+ // Full-width scroll
541
+ // Move top row to scrollback (only if main screen & top of screen)
542
+ if (grid === mainGrid && top === 0) {
543
+ scrollback.push(grid[0]!)
544
+ if (scrollback.length > scrollbackLimit * 2) {
545
+ scrollback.splice(0, scrollback.length - scrollbackLimit)
546
+ }
547
+ }
548
+ for (let i = top; i < bottom; i++) {
549
+ grid[i] = grid[i + 1]!
550
+ softWrapped[i] = softWrapped[i + 1]!
551
+ }
552
+ grid[bottom] = makeRow(cols)
553
+ softWrapped[bottom] = false
492
554
  }
493
- for (let i = top; i < bottom; i++) {
494
- grid[i] = grid[i + 1]!
495
- softWrapped[i] = softWrapped[i + 1]!
496
- }
497
- grid[bottom] = makeRow(cols)
498
- softWrapped[bottom] = false
499
555
  }
500
556
 
501
557
  function scrollDown(top: number, bottom: number): void {
502
- for (let i = bottom; i > top; i--) {
503
- grid[i] = grid[i - 1]!
504
- softWrapped[i] = softWrapped[i - 1]!
558
+ if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
559
+ // Scroll only within the left/right margin columns
560
+ const lm = leftMargin
561
+ const rm = rightMargin
562
+ for (let r = bottom; r > top; r--) {
563
+ const srcRow = grid[r - 1]!
564
+ const dstRow = grid[r]!
565
+ for (let c = lm; c <= rm && c < cols; c++) {
566
+ dstRow[c] = srcRow[c]!
567
+ }
568
+ }
569
+ // Clear the top row within margins
570
+ const topRow = grid[top]!
571
+ for (let c = lm; c <= rm && c < cols; c++) {
572
+ topRow[c] = EMPTY_CELL
573
+ }
574
+ } else {
575
+ for (let i = bottom; i > top; i--) {
576
+ grid[i] = grid[i - 1]!
577
+ softWrapped[i] = softWrapped[i - 1]!
578
+ }
579
+ grid[top] = makeRow(cols)
580
+ softWrapped[top] = false
505
581
  }
506
- grid[top] = makeRow(cols)
507
- softWrapped[top] = false
508
582
  }
509
583
 
510
584
  // ── Character writing ──
@@ -647,30 +721,37 @@ export function createScreen(options: ScreenOptions = {}): Screen {
647
721
  }
648
722
 
649
723
  function writeCharCore(ch: string, wide: boolean): void {
724
+ // Guard: cannot write to a zero-dimension grid
725
+ if (cols <= 0 || rows <= 0) return
726
+
650
727
  const charWidth = wide ? 2 : 1
728
+ const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : cols
729
+ const wrapReturn = leftRightMarginMode ? leftMargin : 0
651
730
 
652
- // Handle autowrap at end of line
653
- if (curX + charWidth > cols) {
731
+ // Handle autowrap at end of line (or right margin)
732
+ if (curX + charWidth > wrapBoundary) {
654
733
  if (autoWrap) {
655
734
  // Mark this row as soft-wrapped (auto-wrap caused the line break)
656
735
  softWrapped[curY] = true
657
- curX = 0
736
+ curX = wrapReturn
658
737
  curY++
659
738
  if (curY > scrollBottom) {
660
739
  curY = scrollBottom
661
740
  scrollUp(scrollTop, scrollBottom)
662
741
  }
663
742
  } else {
664
- curX = cols - charWidth
743
+ curX = wrapBoundary - charWidth
665
744
  }
666
745
  }
667
746
 
668
747
  // Insert mode: shift existing characters right before writing
669
748
  if (insertMode) {
670
749
  const row = grid[curY]!
750
+ const insertEnd = leftRightMarginMode ? rightMargin + 1 : cols
671
751
  for (let i = 0; i < charWidth; i++) {
752
+ // Shift cells right within margin, dropping the cell at the right edge
753
+ row.splice(insertEnd - 1, 1)
672
754
  row.splice(curX, 0, EMPTY_CELL)
673
- row.pop()
674
755
  }
675
756
  }
676
757
 
@@ -792,9 +873,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
792
873
  break
793
874
  case "H": // CUP - Cursor Position
794
875
  case "f": // HVP - same as CUP
795
- curY = (parts[0] ?? 1) - 1
796
- curX = (parts[1] ?? 1) - 1
797
- clampCursor()
876
+ if (originMode) {
877
+ // DECOM: positions are relative to scroll region
878
+ curY = scrollTop + (parts[0] ?? 1) - 1
879
+ curX = (parts[1] ?? 1) - 1
880
+ // Clamp to scroll region bounds
881
+ if (curY < scrollTop) curY = scrollTop
882
+ if (curY > scrollBottom) curY = scrollBottom
883
+ if (curX < 0) curX = 0
884
+ if (curX >= cols) curX = cols - 1
885
+ } else {
886
+ curY = (parts[0] ?? 1) - 1
887
+ curX = (parts[1] ?? 1) - 1
888
+ clampCursor()
889
+ }
798
890
  break
799
891
  case "J": // ED - Erase in Display
800
892
  handleEraseDisplay(parts[0] ?? 0)
@@ -868,6 +960,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
868
960
  case "c": // DA1 - Primary Device Attributes
869
961
  if (onResponse) {
870
962
  if (params === "" || params === "0") {
963
+ // VT200 (62) + sixel (4). We include sixel because we parse and
964
+ // preserve sixel data via getSixelImages() — applications checking
965
+ // DA1 for sixel support before sending image data will work correctly.
871
966
  onResponse("\x1b[?62;4c")
872
967
  }
873
968
  }
@@ -882,9 +977,26 @@ export function createScreen(options: ScreenOptions = {}): Screen {
882
977
  if (code === 4) insertMode = false
883
978
  }
884
979
  break
885
- case "s": // SCP - Save Cursor Position
886
- savedCurX = curX
887
- savedCurY = curY
980
+ case "s": // SCP - Save Cursor Position / DECSLRM when left/right margin mode active
981
+ if (leftRightMarginMode && intermediates === "") {
982
+ // DECSLRM - Set Left and Right Margins (1-based params)
983
+ const pl = parts[0] ?? 0
984
+ const pr = parts[1] ?? 0
985
+ leftMargin = pl > 0 ? pl - 1 : 0
986
+ rightMargin = pr > 0 ? pr - 1 : cols - 1
987
+ if (leftMargin >= rightMargin) {
988
+ leftMargin = 0
989
+ rightMargin = cols - 1
990
+ }
991
+ if (leftMargin >= cols) leftMargin = 0
992
+ if (rightMargin >= cols) rightMargin = cols - 1
993
+ // Move cursor to home (top-left of margin area if origin mode, else absolute home)
994
+ curX = originMode ? leftMargin : 0
995
+ curY = originMode ? scrollTop : 0
996
+ } else {
997
+ savedCurX = curX
998
+ savedCurY = curY
999
+ }
888
1000
  break
889
1001
  case "u": // RCP - Restore Cursor Position
890
1002
  curX = savedCurX
@@ -929,6 +1041,18 @@ export function createScreen(options: ScreenOptions = {}): Screen {
929
1041
  }
930
1042
  }
931
1043
 
1044
+ function handleCSIEq(params: string, _intermediates: string, finalByte: string): void {
1045
+ // CSI = sequences
1046
+ if (finalByte === "c") {
1047
+ // DA3 - Tertiary Device Attributes
1048
+ if (onResponse) {
1049
+ if (params === "" || params === "0") {
1050
+ onResponse("\x1bP!|00000000\x1b\\")
1051
+ }
1052
+ }
1053
+ }
1054
+ }
1055
+
932
1056
  function handleCSIPrivate(params: string, intermediates: string, finalByte: string): void {
933
1057
  const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
934
1058
 
@@ -983,6 +1107,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
983
1107
  case 2026:
984
1108
  value = syncOutput ? 1 : 2
985
1109
  break
1110
+ case 69:
1111
+ value = leftRightMarginMode ? 1 : 2
1112
+ break
1113
+ case 2031:
1114
+ value = colorSchemeReporting ? 1 : 2
1115
+ break
986
1116
  case 5:
987
1117
  value = reverseVideo ? 1 : 2
988
1118
  break
@@ -995,6 +1125,32 @@ export function createScreen(options: ScreenOptions = {}): Screen {
995
1125
  return
996
1126
  }
997
1127
 
1128
+ // Private DSR: CSI ? Ps n
1129
+ if (finalByte === "n") {
1130
+ if (onResponse) {
1131
+ const ps = parts[0] ?? 0
1132
+ if (ps === 6) {
1133
+ // DECXCPR — Extended Cursor Position Report
1134
+ onResponse(`\x1b[?${curY + 1};${curX + 1}n`)
1135
+ } else if (ps === 997) {
1136
+ // Color scheme report: 1 = dark, 2 = light
1137
+ onResponse("\x1b[?997;1n")
1138
+ }
1139
+ }
1140
+ return
1141
+ }
1142
+
1143
+ // DECSED / DECSEL — selective erase (CSI ? J / CSI ? K)
1144
+ // We treat selective erase same as normal erase (no DECSCA protection tracking)
1145
+ if (finalByte === "J") {
1146
+ handleEraseDisplay(parts[0] ?? 0)
1147
+ return
1148
+ }
1149
+ if (finalByte === "K") {
1150
+ handleEraseLine(parts[0] ?? 0)
1151
+ return
1152
+ }
1153
+
998
1154
  const set = finalByte === "h"
999
1155
 
1000
1156
  for (const code of parts) {
@@ -1032,17 +1188,14 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1032
1188
  case 66: // DECNKM - Application Keypad
1033
1189
  applicationKeypad = set
1034
1190
  break
1191
+ case 9: // X10 mouse
1035
1192
  case 1000: // Mouse tracking (basic)
1036
- mouseTracking = set
1037
- mouseTrackingMode = set ? 1000 : 0
1038
- break
1039
1193
  case 1002: // Mouse tracking (button events)
1040
- mouseTracking = set
1041
- mouseTrackingMode = set ? 1002 : 0
1042
- break
1043
1194
  case 1003: // Mouse tracking (all events)
1195
+ case 1015: // urxvt mouse encoding
1196
+ case 1016: // SGR pixel mouse
1044
1197
  mouseTracking = set
1045
- mouseTrackingMode = set ? 1003 : 0
1198
+ mouseTrackingMode = set ? code : 0
1046
1199
  break
1047
1200
  case 1004: // Focus tracking
1048
1201
  focusTracking = set
@@ -1070,12 +1223,23 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1070
1223
  clampCursor()
1071
1224
  }
1072
1225
  break
1226
+ case 69: // DECLRMM - Left/Right Margin Mode
1227
+ leftRightMarginMode = set
1228
+ if (!set) {
1229
+ // Reset margins when mode is disabled
1230
+ leftMargin = 0
1231
+ rightMargin = cols - 1
1232
+ }
1233
+ break
1073
1234
  case 2004: // Bracketed paste
1074
1235
  bracketedPaste = set
1075
1236
  break
1076
1237
  case 2026: // Synchronized output
1077
1238
  syncOutput = set
1078
1239
  break
1240
+ case 2031: // Color scheme reporting
1241
+ colorSchemeReporting = set
1242
+ break
1079
1243
  }
1080
1244
  }
1081
1245
  }
@@ -1109,15 +1273,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1109
1273
  }
1110
1274
 
1111
1275
  function handleEraseLine(mode: number): void {
1276
+ const eraseRight = leftRightMarginMode ? rightMargin : cols - 1
1277
+ const eraseLeft = leftRightMarginMode ? leftMargin : 0
1112
1278
  switch (mode) {
1113
- case 0: // Erase from cursor to end of line
1114
- eraseCells(curY, curX, curY, cols - 1)
1279
+ case 0: // Erase from cursor to end of line (or right margin)
1280
+ eraseCells(curY, curX, curY, eraseRight)
1115
1281
  break
1116
- case 1: // Erase from start to cursor
1117
- eraseCells(curY, 0, curY, curX)
1282
+ case 1: // Erase from start (or left margin) to cursor
1283
+ eraseCells(curY, eraseLeft, curY, curX)
1118
1284
  break
1119
- case 2: // Erase entire line
1120
- eraseCells(curY, 0, curY, cols - 1)
1285
+ case 2: // Erase entire line (within margins if active)
1286
+ eraseCells(curY, eraseLeft, curY, eraseRight)
1121
1287
  break
1122
1288
  }
1123
1289
  }
@@ -1126,7 +1292,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1126
1292
  const r = grid[row]
1127
1293
  if (!r) return
1128
1294
  for (let col = startCol; col <= endCol && col < cols; col++) {
1129
- r[col] = emptyCell()
1295
+ const cell = emptyCell()
1296
+ // Fill erased cells with the current background color
1297
+ if (attrs.bg) {
1298
+ cell.bg = { ...attrs.bg }
1299
+ }
1300
+ r[col] = cell
1130
1301
  }
1131
1302
  }
1132
1303
 
@@ -1147,10 +1318,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1147
1318
  function handleDeleteChars(count: number): void {
1148
1319
  const row = grid[curY]
1149
1320
  if (!row) return
1150
- for (let i = 0; i < count; i++) {
1151
- if (curX < cols) {
1152
- row.splice(curX, 1)
1153
- row.push(emptyCell())
1321
+ if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
1322
+ // Delete within margin bounds: shift left, insert blanks at right margin
1323
+ for (let i = 0; i < count; i++) {
1324
+ if (curX <= rightMargin) {
1325
+ row.splice(curX, 1)
1326
+ row.splice(rightMargin, 0, emptyCell())
1327
+ }
1328
+ }
1329
+ } else {
1330
+ for (let i = 0; i < count; i++) {
1331
+ if (curX < cols) {
1332
+ row.splice(curX, 1)
1333
+ row.push(emptyCell())
1334
+ }
1154
1335
  }
1155
1336
  }
1156
1337
  }
@@ -1158,9 +1339,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1158
1339
  function handleInsertChars(count: number): void {
1159
1340
  const row = grid[curY]
1160
1341
  if (!row) return
1161
- for (let i = 0; i < count; i++) {
1162
- row.splice(curX, 0, emptyCell())
1163
- row.pop()
1342
+ if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
1343
+ // Insert within margin bounds: shift right, drop chars at right margin
1344
+ for (let i = 0; i < count; i++) {
1345
+ row.splice(rightMargin, 1)
1346
+ row.splice(curX, 0, emptyCell())
1347
+ }
1348
+ } else {
1349
+ for (let i = 0; i < count; i++) {
1350
+ row.splice(curX, 0, emptyCell())
1351
+ row.pop()
1352
+ }
1164
1353
  }
1165
1354
  }
1166
1355
 
@@ -1473,6 +1662,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1473
1662
  }
1474
1663
  case 1: // Set icon name (ignore)
1475
1664
  break
1665
+ case 7: // Current working directory: OSC 7 ; file://host/path ST
1666
+ cwd = value
1667
+ break
1476
1668
  case 8: {
1477
1669
  // Hyperlink: OSC 8 ; params ; url ST
1478
1670
  // Format: 8;params;url or 8;;url or 8;; (close)
@@ -1520,6 +1712,76 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1520
1712
  }
1521
1713
  break
1522
1714
  }
1715
+ case 9: // Notifications: OSC 9 ; message ST (iTerm2/ConEmu convention)
1716
+ notifications.push(value)
1717
+ break
1718
+ case 633: {
1719
+ // VS Code shell integration (OSC 633 is a superset of OSC 133)
1720
+ // Maps to the same semantic zone model as FinalTerm markers.
1721
+ const marker = value.charAt(0)
1722
+ switch (marker) {
1723
+ case "A":
1724
+ semanticZones.push({ type: "prompt", startRow: curY, startCol: curX })
1725
+ break
1726
+ case "B":
1727
+ semanticZones.push({ type: "command", startRow: curY, startCol: curX })
1728
+ break
1729
+ case "C":
1730
+ semanticZones.push({ type: "output", startRow: curY, startCol: curX })
1731
+ break
1732
+ case "D":
1733
+ break
1734
+ }
1735
+ break
1736
+ }
1737
+ case 66: {
1738
+ // OSC 66 — Text Sizing
1739
+ if (value === "?" && onResponse) {
1740
+ // Query: respond with current text scale
1741
+ onResponse(`\x1b]66;s=${textScale}\x1b\\`)
1742
+ } else {
1743
+ // Parse key=value pairs (e.g., "s=2")
1744
+ const kvPairs = value.split(";")
1745
+ for (const pair of kvPairs) {
1746
+ const [k, v] = pair.split("=")
1747
+ if (k === "s" && v !== undefined) {
1748
+ const n = parseInt(v, 10)
1749
+ if (!isNaN(n) && n > 0) {
1750
+ textScale = n
1751
+ }
1752
+ }
1753
+ }
1754
+ }
1755
+ break
1756
+ }
1757
+ case 5522: {
1758
+ // OSC 5522 — Advanced Clipboard (Kitty clipboard protocol)
1759
+ if (value === "?" && onResponse) {
1760
+ // Query: respond with stored clipboard data
1761
+ const encoded = btoa(advancedClipboard)
1762
+ onResponse(`\x1b]5522;${encoded}\x1b\\`)
1763
+ } else {
1764
+ // Store clipboard data (base64-encoded)
1765
+ try {
1766
+ advancedClipboard = atob(value)
1767
+ } catch {
1768
+ advancedClipboard = ""
1769
+ }
1770
+ }
1771
+ break
1772
+ }
1773
+ case 1337: {
1774
+ // iTerm2 proprietary sequences
1775
+ if (value === "ReportCellSize" && onResponse) {
1776
+ // Report default 8x16 pixel cell size (standard monospace ratio)
1777
+ onResponse("\x1b]1337;ReportCellSize=16;8\x1b\\")
1778
+ } else if (value === "RequestCapabilities" && onResponse) {
1779
+ // Report empty capabilities (protocol supported, no iTerm2-specific features)
1780
+ onResponse("\x1b]1337;Capabilities=\x1b\\")
1781
+ }
1782
+ // Inline images (File=...) are silently consumed — no pixel framebuffer
1783
+ break
1784
+ }
1523
1785
  }
1524
1786
  }
1525
1787
 
@@ -1538,6 +1800,35 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1538
1800
  return
1539
1801
  }
1540
1802
 
1803
+ // DECRQSS: DCS $ q Pt ST → response DCS Ps $ r Pt ST
1804
+ if (data.startsWith("$q") && onResponse) {
1805
+ const pt = data.substring(2)
1806
+ if (pt === '"p') {
1807
+ // DECSCL - Conformance level: VT200 mode, 8-bit controls
1808
+ onResponse('\x1bP1$r62;1"p\x1b\\')
1809
+ } else {
1810
+ // Not recognized
1811
+ onResponse("\x1bP0$r\x1b\\")
1812
+ }
1813
+ return
1814
+ }
1815
+
1816
+ // XTGETTCAP: DCS + q hex ST → response DCS 1 + r hex = hexvalue ST
1817
+ if (data.startsWith("+q") && onResponse) {
1818
+ const hexName = data.substring(2)
1819
+ if (hexName === "544e") {
1820
+ // "TN" = terminal name → "vterm"
1821
+ const hexValue = Array.from(new TextEncoder().encode("vterm"))
1822
+ .map((b) => b.toString(16).padStart(2, "0"))
1823
+ .join("")
1824
+ onResponse(`\x1bP1+r544e=${hexValue}\x1b\\`)
1825
+ } else {
1826
+ // Unknown capability
1827
+ onResponse("\x1bP0+r\x1b\\")
1828
+ }
1829
+ return
1830
+ }
1831
+
1541
1832
  // Other DCS sequences are consumed and ignored
1542
1833
  }
1543
1834
 
@@ -1587,6 +1878,11 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1587
1878
  scrollTop = 0
1588
1879
  scrollBottom = rows - 1
1589
1880
 
1881
+ // Reset left/right margins
1882
+ leftRightMarginMode = false
1883
+ leftMargin = 0
1884
+ rightMargin = cols - 1
1885
+
1590
1886
  // Reset attributes
1591
1887
  attrs = resetAttrs()
1592
1888
 
@@ -1635,9 +1931,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1635
1931
  sixelImages = []
1636
1932
  scrollTop = 0
1637
1933
  scrollBottom = rows - 1
1934
+ leftRightMarginMode = false
1935
+ leftMargin = 0
1936
+ rightMargin = cols - 1
1937
+ colorSchemeReporting = false
1938
+ textScale = 1
1939
+ advancedClipboard = ""
1638
1940
  viewportOffset = 0
1639
1941
  charsetG0 = false
1640
1942
  clipboard = ""
1943
+ cwd = ""
1944
+ notifications = []
1641
1945
  lastChar = ""
1642
1946
  pendingRegionalIndicator = null
1643
1947
  afterZWJ = false
@@ -1777,6 +2081,14 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1777
2081
  } else if (ch === ")") {
1778
2082
  // Designate G1 character set (ignored, just consume next byte)
1779
2083
  parserState = "escape_charset"
2084
+ } else if (ch === "=") {
2085
+ // DECKPAM - Application Keypad Mode
2086
+ applicationKeypad = true
2087
+ parserState = "ground"
2088
+ } else if (ch === ">") {
2089
+ // DECKPNM - Normal Keypad Mode
2090
+ applicationKeypad = false
2091
+ parserState = "ground"
1780
2092
  } else if (ch === "_") {
1781
2093
  // APC - Application Program Command
1782
2094
  parserState = "apc"
@@ -1826,6 +2138,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1826
2138
  handleCSIGt(paramPart.substring(1), intermediatePart, ch)
1827
2139
  } else if (paramPart.startsWith("<")) {
1828
2140
  handleCSILt(paramPart.substring(1), intermediatePart, ch)
2141
+ } else if (paramPart.startsWith("=")) {
2142
+ handleCSIEq(paramPart.substring(1), intermediatePart, ch)
1829
2143
  } else {
1830
2144
  handleCSI(paramPart, intermediatePart, ch)
1831
2145
  }
@@ -2054,6 +2368,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2054
2368
  rows = newRows
2055
2369
  scrollTop = 0
2056
2370
  scrollBottom = rows - 1
2371
+ leftMargin = 0
2372
+ rightMargin = cols - 1
2057
2373
  clampCursor()
2058
2374
  }
2059
2375
 
@@ -2148,6 +2464,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2148
2464
  return syncOutput
2149
2465
  case "sgrMouse":
2150
2466
  return sgrMouse
2467
+ case "pixelMouse":
2468
+ return mouseTrackingMode === 1016
2469
+ case "leftRightMargin":
2470
+ return leftRightMarginMode
2471
+ case "colorSchemeReporting":
2472
+ return colorSchemeReporting
2151
2473
  case "kittyKeyboard":
2152
2474
  return kittyKeyboardFlags > 0
2153
2475
  case "kittyGraphics":
@@ -2160,7 +2482,7 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2160
2482
  }
2161
2483
 
2162
2484
  // Suppress unused variable warnings
2163
- void [mouseTrackingMode]
2485
+ void [mouseTrackingMode, textScale, advancedClipboard]
2164
2486
 
2165
2487
  return {
2166
2488
  get cols() {
@@ -2182,6 +2504,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2182
2504
  getCursorBlinking: () => cursorBlinking,
2183
2505
  getTitle: () => title,
2184
2506
  getMode,
2507
+ getClipboard: () => clipboard,
2508
+ getCwd: () => cwd,
2509
+ getNotifications: () => [...notifications],
2185
2510
  getScrollbackLength: () => scrollback.length,
2186
2511
  getViewportOffset: () => viewportOffset,
2187
2512
  scrollViewport: (delta: number) => {