vterm.js 0.7.1 → 0.8.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/README.md CHANGED
@@ -20,7 +20,7 @@ Part of the [vterm](https://github.com/beorn/vterm) monorepo.
20
20
  - **Synchronized output** — mode 2026
21
21
  - **Application cursor keys & keypad**
22
22
  - **Kitty keyboard protocol** — progressive-enhancement flags with a push/pop/set stack (`CSI u`)
23
- - **OSC sequences** — window title (OSC 0/2), hyperlinks (OSC 8), clipboard (OSC 52), colors
23
+ - **OSC sequences** — independent icon/window labels (OSC 0 sets both, OSC 1 icon, OSC 2 window), hyperlinks (OSC 8), clipboard (OSC 52), colors
24
24
  - **DCS sequences** — consumed and ignored, XTVERSION response
25
25
  - **Device attributes** — DA1/DA2/DA3 responses
26
26
  - **Device status reports** — DSR responses
@@ -140,6 +140,8 @@ console.log(restored.getText()) // "Hello, Bold Green World!" — text and style
140
140
 
141
141
  **Soft-wrap history.** `Snapshot` carries `scrollbackSoftWrapped: boolean[]` alongside `scrollback` — bit `i` true means scrollback row `i` wraps into row `i+1` (one logical line split across rows); an absent array (older stored v1 snapshots) is treated as all-false. Phase-1 history re-links a wrapped row by emitting it at full width with no line break, so the receiver's own autowrap re-records the bit as the row scrolls back.
142
142
 
143
+ **Icon/window labels.** `Snapshot.title` remains the window title. New v1 snapshots include optional `iconLabel`, including empty strings; the binary codec preserves whether that field is present. Only `restore()` defaults legacy absence to `title` to preserve old CSI20 behavior; historical OSC0 versus OSC2 intent cannot be recovered. An own `undefined`, `null` or other non-string value is rejected. Older readers ignore the new JSON-header key and lose independent icon semantics. `getTitle()` and `title$` remain window-only. `includeTitle:true` emits a nonempty window title with OSC2, preserving the receiver's independent icon instead of the former OSC0 side effect. Default title omission and empty-title omission remain; ANSI replay does not transfer the icon label.
144
+
143
145
  ### Ops and taps
144
146
 
145
147
  `apply(op)` is the single public write entry — the same seam a session journal records and replays. An op is coarse, serializable data; **bytes stay the canonical encoding** (an `output` op is just bytes, deterministically re-parsed on replay — vterm never reifies a per-VT-action op):
package/dist/index.d.mts CHANGED
@@ -85,6 +85,13 @@ interface ScreenOptions {
85
85
  cols?: number;
86
86
  rows?: number;
87
87
  scrollbackLimit?: number;
88
+ /**
89
+ * The embedder renders or relays kitty graphics images, so this screen may
90
+ * claim them: graphics queries answer OK and the kittyGraphics mode is true.
91
+ * Default false — this screen stores no image, so it refuses queries and
92
+ * reports no capability (#31020).
93
+ */
94
+ kittyGraphicsImages?: boolean;
88
95
  /** Callback for DA1/DA2/DSR responses — write these back to the PTY */
89
96
  onResponse?: (data: string) => void;
90
97
  /**
@@ -246,6 +253,8 @@ interface Snapshot {
246
253
  };
247
254
  tabStops: number[];
248
255
  title: string;
256
+ /** Independent icon label. Legacy absence restores from title; a supplied value must be a string. */
257
+ iconLabel?: string;
249
258
  clipboard: string;
250
259
  cwd: string;
251
260
  notifications: string[];
@@ -572,7 +581,7 @@ declare function createScreen(options?: ScreenOptions): Screen;
572
581
  interface SerializeOptions {
573
582
  /** Emit scrollback history rows before the screen paint. Default: true. */
574
583
  includeScrollback?: boolean;
575
- /** Emit the window title (OSC 0) when non-empty. Default: false. */
584
+ /** Emit only the window title (OSC 2) when non-empty, preserving the receiver's icon label. Default: false. */
576
585
  includeTitle?: boolean;
577
586
  /** Emit OSC 8 hyperlinks for cell and pending-pen urls. Default: true. */
578
587
  hyperlinks?: boolean;
package/dist/index.mjs CHANGED
@@ -478,6 +478,7 @@ function createScreen(options = {}) {
478
478
  let rows = options.rows ?? 24;
479
479
  let scrollbackLimit = options.scrollbackLimit ?? 1e3;
480
480
  const onResponse = options.onResponse;
481
+ const kittyGraphicsImages = options.kittyGraphicsImages ?? false;
481
482
  const maxStringSequenceLength = options.maxStringSequenceLength ?? 16777216;
482
483
  if (!Number.isSafeInteger(maxStringSequenceLength) || maxStringSequenceLength < 0) throw new RangeError("maxStringSequenceLength must be a non-negative safe integer");
483
484
  let mainGrid = makeGrid(cols, rows);
@@ -503,6 +504,7 @@ function createScreen(options = {}) {
503
504
  let savedCurY = 0;
504
505
  let attrs = resetAttrs();
505
506
  let title = "";
507
+ let iconLabel = "";
506
508
  let useAltScreen = false;
507
509
  let bracketedPaste = false;
508
510
  let applicationCursor = false;
@@ -519,7 +521,6 @@ function createScreen(options = {}) {
519
521
  let syncOutput = false;
520
522
  let kittyKeyboardFlags = 0;
521
523
  let kittyKeyboardStack = [];
522
- let hasKittyGraphics = false;
523
524
  let scrollTop = 0;
524
525
  let scrollBottom = rows - 1;
525
526
  let leftRightMarginMode = false;
@@ -716,6 +717,7 @@ function createScreen(options = {}) {
716
717
  if (!Array.isArray(value.tabStops)) throw new TypeError("Invalid vterm snapshot tabStops");
717
718
  for (const stop of value.tabStops) assertInteger("tabStops", stop);
718
719
  assertString("title", value.title);
720
+ if (Object.hasOwn(value, "iconLabel")) assertString("iconLabel", value.iconLabel);
719
721
  assertString("clipboard", value.clipboard);
720
722
  assertString("cwd", value.cwd);
721
723
  if (!Array.isArray(value.notifications) || value.notifications.some((entry) => typeof entry !== "string")) throw new TypeError("Invalid vterm snapshot notifications");
@@ -782,9 +784,6 @@ function createScreen(options = {}) {
782
784
  return 0;
783
785
  }
784
786
  let textScale = 1;
785
- const CELL_W_PX = 8;
786
- const CELL_H_PX = 17;
787
- const FONT_ASCENT_PX = 14;
788
787
  let fontSize = 12;
789
788
  let fontWindowSize = 12;
790
789
  let locale = "en_US.UTF-8";
@@ -1465,20 +1464,18 @@ function createScreen(options = {}) {
1465
1464
  case "t": {
1466
1465
  const op = parts[0] ?? 0;
1467
1466
  if (!onResponse) break;
1468
- const CELL_H = 16;
1469
- const CELL_W = 8;
1470
1467
  switch (op) {
1471
1468
  case 14:
1472
- onResponse(`\x1b[4;${rows * CELL_H};${cols * CELL_W}t`);
1469
+ onResponse("\x1B[4;0;0t");
1473
1470
  break;
1474
1471
  case 16:
1475
- onResponse(`\x1b[6;${CELL_H};${CELL_W}t`);
1472
+ onResponse("\x1B[6;0;0t");
1476
1473
  break;
1477
1474
  case 18:
1478
1475
  onResponse(`\x1b[8;${rows};${cols}t`);
1479
1476
  break;
1480
1477
  case 20:
1481
- onResponse(`\x1b]L${title}\x1b\\`);
1478
+ onResponse(`\x1b]L${iconLabel}\x1b\\`);
1482
1479
  break;
1483
1480
  case 21: onResponse(`\x1b]l${title}\x1b\\`);
1484
1481
  }
@@ -2302,6 +2299,9 @@ function createScreen(options = {}) {
2302
2299
  const value = semicolonIdx === -1 ? "" : oscString.substring(semicolonIdx + 1);
2303
2300
  switch (code) {
2304
2301
  case 0:
2302
+ iconLabel = value;
2303
+ title = value;
2304
+ break;
2305
2305
  case 2:
2306
2306
  title = value;
2307
2307
  break;
@@ -2328,7 +2328,9 @@ function createScreen(options = {}) {
2328
2328
  });
2329
2329
  }
2330
2330
  break;
2331
- case 1: break;
2331
+ case 1:
2332
+ iconLabel = value;
2333
+ break;
2332
2334
  case 7:
2333
2335
  cwd = value;
2334
2336
  break;
@@ -2660,7 +2662,7 @@ function createScreen(options = {}) {
2660
2662
  break;
2661
2663
  }
2662
2664
  case 776:
2663
- if (onResponse) onResponse(`\x1b]776;${CELL_W_PX};${CELL_H_PX};${FONT_ASCENT_PX}\x1b\\`);
2665
+ if (onResponse) onResponse("\x1B]776;0;0;0\x1B\\");
2664
2666
  break;
2665
2667
  case 7770:
2666
2668
  if (value === "?") {
@@ -2698,7 +2700,7 @@ function createScreen(options = {}) {
2698
2700
  advancedClipboard = "";
2699
2701
  }
2700
2702
  break;
2701
- case 1337: if (value === "ReportCellSize" && onResponse) onResponse("\x1B]1337;ReportCellSize=16;8\x1B\\");
2703
+ case 1337: if (value === "ReportCellSize" && onResponse) onResponse("\x1B]1337;ReportCellSize=0;0\x1B\\");
2702
2704
  else if (value === "RequestCapabilities" && onResponse) onResponse("\x1B]1337;Capabilities=\x1B\\");
2703
2705
  }
2704
2706
  }
@@ -2728,7 +2730,6 @@ function createScreen(options = {}) {
2728
2730
  }
2729
2731
  function handleAPC(data) {
2730
2732
  if (!data.startsWith("G")) return;
2731
- hasKittyGraphics = true;
2732
2733
  const semicolonIdx = data.indexOf(";");
2733
2734
  const kvPart = semicolonIdx >= 0 ? data.substring(1, semicolonIdx) : data.substring(1);
2734
2735
  const params = {};
@@ -2736,7 +2737,11 @@ function createScreen(options = {}) {
2736
2737
  const eqIdx = pair.indexOf("=");
2737
2738
  if (eqIdx >= 0) params[pair.substring(0, eqIdx)] = pair.substring(eqIdx + 1);
2738
2739
  }
2739
- if (params.a === "q" && onResponse) onResponse(`\x1b_Gi=${params.i ?? "0"};OK\x1b\\`);
2740
+ if (params.a === "q" && onResponse) {
2741
+ if (kittyGraphicsImages) {
2742
+ if (params.q !== "1" && params.q !== "2") onResponse(`\x1b_Gi=${params.i ?? "0"};OK\x1b\\`);
2743
+ } else if (params.q !== "2") onResponse(`\x1b_Gi=${params.i ?? "0"};ENOTSUP\x1b\\`);
2744
+ }
2740
2745
  }
2741
2746
  function softReset() {
2742
2747
  insertMode = false;
@@ -2791,6 +2796,7 @@ function createScreen(options = {}) {
2791
2796
  };
2792
2797
  attrs = resetAttrs();
2793
2798
  title = "";
2799
+ iconLabel = "";
2794
2800
  useAltScreen = false;
2795
2801
  bracketedPaste = false;
2796
2802
  applicationCursor = false;
@@ -2807,7 +2813,6 @@ function createScreen(options = {}) {
2807
2813
  syncOutput = false;
2808
2814
  kittyKeyboardFlags = 0;
2809
2815
  kittyKeyboardStack = [];
2810
- hasKittyGraphics = false;
2811
2816
  hasSixel = false;
2812
2817
  sixelImages = [];
2813
2818
  scrollTop = 0;
@@ -3531,7 +3536,7 @@ function createScreen(options = {}) {
3531
3536
  syncOutput,
3532
3537
  kittyKeyboardFlags,
3533
3538
  kittyKeyboardStack: [...kittyKeyboardStack],
3534
- kittyGraphics: hasKittyGraphics,
3539
+ kittyGraphics: kittyGraphicsImages,
3535
3540
  colorSchemeReporting,
3536
3541
  decColumn: decColumnMode,
3537
3542
  altScroll: altScrollMode,
@@ -3550,6 +3555,7 @@ function createScreen(options = {}) {
3550
3555
  },
3551
3556
  tabStops: [...tabStops].sort((a, b) => a - b),
3552
3557
  title,
3558
+ iconLabel,
3553
3559
  clipboard,
3554
3560
  cwd,
3555
3561
  notifications: [...notifications],
@@ -3635,7 +3641,6 @@ function createScreen(options = {}) {
3635
3641
  syncOutput = snapshotValue.modes.syncOutput;
3636
3642
  kittyKeyboardFlags = snapshotValue.modes.kittyKeyboardFlags;
3637
3643
  kittyKeyboardStack = [...snapshotValue.modes.kittyKeyboardStack];
3638
- hasKittyGraphics = snapshotValue.modes.kittyGraphics;
3639
3644
  colorSchemeReporting = snapshotValue.modes.colorSchemeReporting;
3640
3645
  decColumnMode = snapshotValue.modes.decColumn;
3641
3646
  altScrollMode = snapshotValue.modes.altScroll;
@@ -3650,6 +3655,7 @@ function createScreen(options = {}) {
3650
3655
  colorStack.push(...snapshotValue.colors.stack.map(cloneColorStateSnapshot));
3651
3656
  tabStops = new Set(snapshotValue.tabStops);
3652
3657
  title = snapshotValue.title;
3658
+ iconLabel = Object.hasOwn(snapshotValue, "iconLabel") ? snapshotValue.iconLabel : snapshotValue.title;
3653
3659
  clipboard = snapshotValue.clipboard;
3654
3660
  cwd = snapshotValue.cwd;
3655
3661
  notifications = [...snapshotValue.notifications];
@@ -3798,7 +3804,7 @@ function createScreen(options = {}) {
3798
3804
  case "leftRightMargin": return leftRightMarginMode;
3799
3805
  case "colorSchemeReporting": return colorSchemeReporting;
3800
3806
  case "kittyKeyboard": return kittyKeyboardFlags > 0;
3801
- case "kittyGraphics": return hasKittyGraphics;
3807
+ case "kittyGraphics": return kittyGraphicsImages;
3802
3808
  case "sixel": return hasSixel;
3803
3809
  default: return false;
3804
3810
  }
@@ -3876,7 +3882,7 @@ function createScreen(options = {}) {
3876
3882
  leftRightMargin: leftRightMarginMode,
3877
3883
  colorSchemeReporting,
3878
3884
  kittyKeyboard: kittyKeyboardFlags > 0,
3879
- kittyGraphics: hasKittyGraphics,
3885
+ kittyGraphics: kittyGraphicsImages,
3880
3886
  sixel: hasSixel
3881
3887
  };
3882
3888
  }
@@ -4321,7 +4327,7 @@ function serializeSnapshot(snapshot, options = {}) {
4321
4327
  const def = SERIALIZE_DEFAULT_PALETTE[i];
4322
4328
  if (cur !== void 0 && def !== void 0 && !serializeColorEq(cur, def)) out.push(`\x1b]4;${String(i)};${serializeColorSpec(cur)}\x1b\\`);
4323
4329
  }
4324
- if (options.includeTitle === true && snapshot.title.length > 0) out.push(`\x1b]0;${snapshot.title}\x1b\\`);
4330
+ if (options.includeTitle === true && snapshot.title.length > 0) out.push(`\x1b]2;${snapshot.title}\x1b\\`);
4325
4331
  out.push("\x1B[?7l", "\x1B[H\x1B[2J");
4326
4332
  const grid = snapshot.activeBuffer === "alt" ? snapshot.alt.grid : snapshot.main.grid;
4327
4333
  for (let r = 0; r < rows; r++) {
@@ -4819,6 +4825,7 @@ function readGridSection(r, softWrapNullable, expected, strings, colors, styles)
4819
4825
  * deterministic for identical input and decodes back to a deep-equal snapshot.
4820
4826
  */
4821
4827
  function encodeScreenSnapshotBinary(snapshot) {
4828
+ if (Object.hasOwn(snapshot, "iconLabel") && typeof snapshot.iconLabel !== "string") throw new TypeError("vterm snapshot codec: invalid iconLabel");
4822
4829
  const w = createWriter();
4823
4830
  w.u8(VERSION);
4824
4831
  const header = {
@@ -4835,6 +4842,7 @@ function encodeScreenSnapshotBinary(snapshot) {
4835
4842
  colors: snapshot.colors,
4836
4843
  tabStops: snapshot.tabStops,
4837
4844
  title: snapshot.title,
4845
+ ...Object.hasOwn(snapshot, "iconLabel") ? { iconLabel: snapshot.iconLabel } : {},
4838
4846
  clipboard: snapshot.clipboard,
4839
4847
  cwd: snapshot.cwd,
4840
4848
  notifications: snapshot.notifications,
@@ -4868,6 +4876,7 @@ function decodeScreenSnapshotBinary(bytes) {
4868
4876
  if (version !== VERSION) throw new Error(`vterm snapshot codec: unsupported version byte ${version} (expected ${VERSION})`);
4869
4877
  const headerLen = r.varint();
4870
4878
  const header = JSON.parse(decoder.decode(r.take(headerLen)));
4879
+ if (Object.hasOwn(header, "iconLabel") && typeof header.iconLabel !== "string") throw new TypeError("vterm snapshot codec: invalid iconLabel");
4871
4880
  const strings = readStringTable(r);
4872
4881
  const colors = readColorTable(r);
4873
4882
  const styles = readStyleTable(r);
@@ -4903,6 +4912,7 @@ function decodeScreenSnapshotBinary(bytes) {
4903
4912
  colors: header.colors,
4904
4913
  tabStops: header.tabStops,
4905
4914
  title: header.title,
4915
+ ...Object.hasOwn(header, "iconLabel") ? { iconLabel: header.iconLabel } : {},
4906
4916
  clipboard: header.clipboard,
4907
4917
  cwd: header.cwd,
4908
4918
  notifications: header.notifications,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vterm.js",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Modern terminal emulator — full VT/ECMA-48/xterm standards coverage. Pure TypeScript, zero dependencies.",
5
5
  "keywords": [
6
6
  "ansi",