vterm.js 0.3.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 +219 -56
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.3.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
@@ -399,6 +399,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
399
399
  let scrollTop = 0
400
400
  let scrollBottom = rows - 1
401
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
+
402
416
  // Viewport scroll offset
403
417
  let viewportOffset = 0
404
418
 
@@ -506,28 +520,65 @@ export function createScreen(options: ScreenOptions = {}): Screen {
506
520
  // ── Scrolling ──
507
521
 
508
522
  function scrollUp(top: number, bottom: number): void {
509
- // Move top row to scrollback (only if main screen & top of screen)
510
- if (grid === mainGrid && top === 0) {
511
- scrollback.push(grid[0]!)
512
- if (scrollback.length > scrollbackLimit * 2) {
513
- 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
+ }
514
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
515
554
  }
516
- for (let i = top; i < bottom; i++) {
517
- grid[i] = grid[i + 1]!
518
- softWrapped[i] = softWrapped[i + 1]!
519
- }
520
- grid[bottom] = makeRow(cols)
521
- softWrapped[bottom] = false
522
555
  }
523
556
 
524
557
  function scrollDown(top: number, bottom: number): void {
525
- for (let i = bottom; i > top; i--) {
526
- grid[i] = grid[i - 1]!
527
- 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
528
581
  }
529
- grid[top] = makeRow(cols)
530
- softWrapped[top] = false
531
582
  }
532
583
 
533
584
  // ── Character writing ──
@@ -670,30 +721,37 @@ export function createScreen(options: ScreenOptions = {}): Screen {
670
721
  }
671
722
 
672
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
+
673
727
  const charWidth = wide ? 2 : 1
728
+ const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : cols
729
+ const wrapReturn = leftRightMarginMode ? leftMargin : 0
674
730
 
675
- // Handle autowrap at end of line
676
- if (curX + charWidth > cols) {
731
+ // Handle autowrap at end of line (or right margin)
732
+ if (curX + charWidth > wrapBoundary) {
677
733
  if (autoWrap) {
678
734
  // Mark this row as soft-wrapped (auto-wrap caused the line break)
679
735
  softWrapped[curY] = true
680
- curX = 0
736
+ curX = wrapReturn
681
737
  curY++
682
738
  if (curY > scrollBottom) {
683
739
  curY = scrollBottom
684
740
  scrollUp(scrollTop, scrollBottom)
685
741
  }
686
742
  } else {
687
- curX = cols - charWidth
743
+ curX = wrapBoundary - charWidth
688
744
  }
689
745
  }
690
746
 
691
747
  // Insert mode: shift existing characters right before writing
692
748
  if (insertMode) {
693
749
  const row = grid[curY]!
750
+ const insertEnd = leftRightMarginMode ? rightMargin + 1 : cols
694
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)
695
754
  row.splice(curX, 0, EMPTY_CELL)
696
- row.pop()
697
755
  }
698
756
  }
699
757
 
@@ -919,9 +977,26 @@ export function createScreen(options: ScreenOptions = {}): Screen {
919
977
  if (code === 4) insertMode = false
920
978
  }
921
979
  break
922
- case "s": // SCP - Save Cursor Position
923
- savedCurX = curX
924
- 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
+ }
925
1000
  break
926
1001
  case "u": // RCP - Restore Cursor Position
927
1002
  curX = savedCurX
@@ -1032,6 +1107,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1032
1107
  case 2026:
1033
1108
  value = syncOutput ? 1 : 2
1034
1109
  break
1110
+ case 69:
1111
+ value = leftRightMarginMode ? 1 : 2
1112
+ break
1113
+ case 2031:
1114
+ value = colorSchemeReporting ? 1 : 2
1115
+ break
1035
1116
  case 5:
1036
1117
  value = reverseVideo ? 1 : 2
1037
1118
  break
@@ -1044,6 +1125,21 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1044
1125
  return
1045
1126
  }
1046
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
+
1047
1143
  // DECSED / DECSEL — selective erase (CSI ? J / CSI ? K)
1048
1144
  // We treat selective erase same as normal erase (no DECSCA protection tracking)
1049
1145
  if (finalByte === "J") {
@@ -1092,29 +1188,14 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1092
1188
  case 66: // DECNKM - Application Keypad
1093
1189
  applicationKeypad = set
1094
1190
  break
1095
- case 9: // X10 mouse tracking
1096
- mouseTracking = set
1097
- mouseTrackingMode = set ? 9 : 0
1098
- break
1191
+ case 9: // X10 mouse
1099
1192
  case 1000: // Mouse tracking (basic)
1100
- mouseTracking = set
1101
- mouseTrackingMode = set ? 1000 : 0
1102
- break
1103
1193
  case 1002: // Mouse tracking (button events)
1104
- mouseTracking = set
1105
- mouseTrackingMode = set ? 1002 : 0
1106
- break
1107
1194
  case 1003: // Mouse tracking (all events)
1108
- mouseTracking = set
1109
- mouseTrackingMode = set ? 1003 : 0
1110
- break
1111
1195
  case 1015: // urxvt mouse encoding
1112
- mouseTracking = set
1113
- mouseTrackingMode = set ? 1015 : 0
1114
- break
1115
1196
  case 1016: // SGR pixel mouse
1116
1197
  mouseTracking = set
1117
- mouseTrackingMode = set ? 1016 : 0
1198
+ mouseTrackingMode = set ? code : 0
1118
1199
  break
1119
1200
  case 1004: // Focus tracking
1120
1201
  focusTracking = set
@@ -1142,12 +1223,23 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1142
1223
  clampCursor()
1143
1224
  }
1144
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
1145
1234
  case 2004: // Bracketed paste
1146
1235
  bracketedPaste = set
1147
1236
  break
1148
1237
  case 2026: // Synchronized output
1149
1238
  syncOutput = set
1150
1239
  break
1240
+ case 2031: // Color scheme reporting
1241
+ colorSchemeReporting = set
1242
+ break
1151
1243
  }
1152
1244
  }
1153
1245
  }
@@ -1181,15 +1273,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1181
1273
  }
1182
1274
 
1183
1275
  function handleEraseLine(mode: number): void {
1276
+ const eraseRight = leftRightMarginMode ? rightMargin : cols - 1
1277
+ const eraseLeft = leftRightMarginMode ? leftMargin : 0
1184
1278
  switch (mode) {
1185
- case 0: // Erase from cursor to end of line
1186
- 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)
1187
1281
  break
1188
- case 1: // Erase from start to cursor
1189
- eraseCells(curY, 0, curY, curX)
1282
+ case 1: // Erase from start (or left margin) to cursor
1283
+ eraseCells(curY, eraseLeft, curY, curX)
1190
1284
  break
1191
- case 2: // Erase entire line
1192
- eraseCells(curY, 0, curY, cols - 1)
1285
+ case 2: // Erase entire line (within margins if active)
1286
+ eraseCells(curY, eraseLeft, curY, eraseRight)
1193
1287
  break
1194
1288
  }
1195
1289
  }
@@ -1224,10 +1318,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1224
1318
  function handleDeleteChars(count: number): void {
1225
1319
  const row = grid[curY]
1226
1320
  if (!row) return
1227
- for (let i = 0; i < count; i++) {
1228
- if (curX < cols) {
1229
- row.splice(curX, 1)
1230
- 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
+ }
1231
1335
  }
1232
1336
  }
1233
1337
  }
@@ -1235,9 +1339,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1235
1339
  function handleInsertChars(count: number): void {
1236
1340
  const row = grid[curY]
1237
1341
  if (!row) return
1238
- for (let i = 0; i < count; i++) {
1239
- row.splice(curX, 0, emptyCell())
1240
- 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
+ }
1241
1353
  }
1242
1354
  }
1243
1355
 
@@ -1622,6 +1734,42 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1622
1734
  }
1623
1735
  break
1624
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
+ }
1625
1773
  case 1337: {
1626
1774
  // iTerm2 proprietary sequences
1627
1775
  if (value === "ReportCellSize" && onResponse) {
@@ -1730,6 +1878,11 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1730
1878
  scrollTop = 0
1731
1879
  scrollBottom = rows - 1
1732
1880
 
1881
+ // Reset left/right margins
1882
+ leftRightMarginMode = false
1883
+ leftMargin = 0
1884
+ rightMargin = cols - 1
1885
+
1733
1886
  // Reset attributes
1734
1887
  attrs = resetAttrs()
1735
1888
 
@@ -1778,6 +1931,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
1778
1931
  sixelImages = []
1779
1932
  scrollTop = 0
1780
1933
  scrollBottom = rows - 1
1934
+ leftRightMarginMode = false
1935
+ leftMargin = 0
1936
+ rightMargin = cols - 1
1937
+ colorSchemeReporting = false
1938
+ textScale = 1
1939
+ advancedClipboard = ""
1781
1940
  viewportOffset = 0
1782
1941
  charsetG0 = false
1783
1942
  clipboard = ""
@@ -2209,6 +2368,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2209
2368
  rows = newRows
2210
2369
  scrollTop = 0
2211
2370
  scrollBottom = rows - 1
2371
+ leftMargin = 0
2372
+ rightMargin = cols - 1
2212
2373
  clampCursor()
2213
2374
  }
2214
2375
 
@@ -2306,7 +2467,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2306
2467
  case "pixelMouse":
2307
2468
  return mouseTrackingMode === 1016
2308
2469
  case "leftRightMargin":
2309
- return false // Not implemented yet
2470
+ return leftRightMarginMode
2471
+ case "colorSchemeReporting":
2472
+ return colorSchemeReporting
2310
2473
  case "kittyKeyboard":
2311
2474
  return kittyKeyboardFlags > 0
2312
2475
  case "kittyGraphics":
@@ -2319,7 +2482,7 @@ export function createScreen(options: ScreenOptions = {}): Screen {
2319
2482
  }
2320
2483
 
2321
2484
  // Suppress unused variable warnings
2322
- void [mouseTrackingMode]
2485
+ void [mouseTrackingMode, textScale, advancedClipboard]
2323
2486
 
2324
2487
  return {
2325
2488
  get cols() {