@vibecook/ghosttea-react 0.11.1 → 0.13.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 (90) hide show
  1. package/README.md +86 -4
  2. package/dist/TerminalSurface.d.ts +53 -0
  3. package/dist/TerminalSurface.d.ts.map +1 -1
  4. package/dist/TerminalSurface.js +106 -12
  5. package/dist/TerminalSurface.js.map +1 -1
  6. package/dist/appearance/AppearanceSettings.d.ts +5 -2
  7. package/dist/appearance/AppearanceSettings.d.ts.map +1 -1
  8. package/dist/appearance/AppearanceSettings.js +14 -3
  9. package/dist/appearance/AppearanceSettings.js.map +1 -1
  10. package/dist/appearance/BackdropBlurControl.d.ts +5 -0
  11. package/dist/appearance/BackdropBlurControl.d.ts.map +1 -0
  12. package/dist/appearance/BackdropBlurControl.js +42 -0
  13. package/dist/appearance/BackdropBlurControl.js.map +1 -0
  14. package/dist/appearance/types.d.ts +10 -0
  15. package/dist/appearance/types.d.ts.map +1 -1
  16. package/dist/catalog-budget.d.ts +2 -2
  17. package/dist/catalog-budget.d.ts.map +1 -1
  18. package/dist/catalog-budget.js +2 -2
  19. package/dist/catalog-budget.js.map +1 -1
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +2 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/link-input.d.ts +18 -0
  28. package/dist/link-input.d.ts.map +1 -0
  29. package/dist/link-input.js +27 -0
  30. package/dist/link-input.js.map +1 -0
  31. package/dist/renderers/background-geometry.d.ts +16 -0
  32. package/dist/renderers/background-geometry.d.ts.map +1 -0
  33. package/dist/renderers/background-geometry.js +20 -0
  34. package/dist/renderers/background-geometry.js.map +1 -0
  35. package/dist/renderers/glyph-atlas.d.ts +43 -0
  36. package/dist/renderers/glyph-atlas.d.ts.map +1 -0
  37. package/dist/renderers/glyph-atlas.js +175 -0
  38. package/dist/renderers/glyph-atlas.js.map +1 -0
  39. package/dist/renderers/light-adaptation.d.ts +36 -0
  40. package/dist/renderers/light-adaptation.d.ts.map +1 -0
  41. package/dist/renderers/light-adaptation.js +332 -0
  42. package/dist/renderers/light-adaptation.js.map +1 -0
  43. package/dist/renderers/types.d.ts +23 -0
  44. package/dist/renderers/types.d.ts.map +1 -1
  45. package/dist/renderers/types.js.map +1 -1
  46. package/dist/renderers/webgpu-renderer.d.ts +4 -0
  47. package/dist/renderers/webgpu-renderer.d.ts.map +1 -1
  48. package/dist/renderers/webgpu-renderer.js +331 -239
  49. package/dist/renderers/webgpu-renderer.js.map +1 -1
  50. package/dist/routed-control.d.ts +26 -2
  51. package/dist/routed-control.d.ts.map +1 -1
  52. package/dist/routed-control.js +145 -27
  53. package/dist/routed-control.js.map +1 -1
  54. package/dist/routed-frames.d.ts +1 -1
  55. package/dist/routed-frames.d.ts.map +1 -1
  56. package/dist/routed-frames.js +3 -7
  57. package/dist/routed-frames.js.map +1 -1
  58. package/dist/row-damage.d.ts +21 -0
  59. package/dist/row-damage.d.ts.map +1 -0
  60. package/dist/row-damage.js +76 -0
  61. package/dist/row-damage.js.map +1 -0
  62. package/dist/runtime.d.ts +97 -3
  63. package/dist/runtime.d.ts.map +1 -1
  64. package/dist/runtime.js +489 -49
  65. package/dist/runtime.js.map +1 -1
  66. package/dist/styles.css +24 -0
  67. package/dist/terminal-render.worker.js +979 -297
  68. package/dist/terminal-render.worker.js.map +4 -4
  69. package/dist/use-terminal-links.d.ts +17 -0
  70. package/dist/use-terminal-links.d.ts.map +1 -0
  71. package/dist/use-terminal-links.js +101 -0
  72. package/dist/use-terminal-links.js.map +1 -0
  73. package/dist/worker-messages.d.ts +27 -1
  74. package/dist/worker-messages.d.ts.map +1 -1
  75. package/dist/workspace/Workspace.d.ts +19 -3
  76. package/dist/workspace/Workspace.d.ts.map +1 -1
  77. package/dist/workspace/Workspace.js +53 -20
  78. package/dist/workspace/Workspace.js.map +1 -1
  79. package/dist/workspace/index.d.ts +2 -2
  80. package/dist/workspace/index.d.ts.map +1 -1
  81. package/dist/workspace/index.js.map +1 -1
  82. package/dist/workspace/pane-focus.d.ts +8 -0
  83. package/dist/workspace/pane-focus.d.ts.map +1 -0
  84. package/dist/workspace/pane-focus.js +24 -0
  85. package/dist/workspace/pane-focus.js.map +1 -0
  86. package/dist/workspace/pane-layout.d.ts +5 -2
  87. package/dist/workspace/pane-layout.d.ts.map +1 -1
  88. package/dist/workspace/pane-layout.js +45 -6
  89. package/dist/workspace/pane-layout.js.map +1 -1
  90. package/package.json +4 -4
@@ -22,6 +22,8 @@ var SectionKind;
22
22
  SectionKind2[SectionKind2["ViewportMetadata"] = 9] = "ViewportMetadata";
23
23
  SectionKind2[SectionKind2["AccessibilityText"] = 10] = "AccessibilityText";
24
24
  SectionKind2[SectionKind2["ClipboardWrite"] = 11] = "ClipboardWrite";
25
+ SectionKind2[SectionKind2["LinkTargets"] = 12] = "LinkTargets";
26
+ SectionKind2[SectionKind2["StylePalette"] = 13] = "StylePalette";
25
27
  })(SectionKind || (SectionKind = {}));
26
28
  var GlyphFormat;
27
29
  (function(GlyphFormat2) {
@@ -40,14 +42,15 @@ function assertRange(condition, message) {
40
42
  throw new RangeError(`Invalid terminal frame: ${message}`);
41
43
  }
42
44
  function decodeFrame(buffer) {
43
- assertRange(buffer.byteLength >= FRAME_HEADER_BYTES, "truncated header");
44
- assertRange(buffer.byteLength <= MAX_FRAME_BYTES, "packet exceeds limit");
45
- const view = new DataView(buffer);
45
+ const bytes = buffer instanceof Uint8Array ? new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) : new Uint8Array(buffer);
46
+ assertRange(bytes.byteLength >= FRAME_HEADER_BYTES, "truncated header");
47
+ assertRange(bytes.byteLength <= MAX_FRAME_BYTES, "packet exceeds limit");
48
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
46
49
  assertRange(view.getUint32(0, true) === FRAME_MAGIC, "bad magic");
47
50
  assertRange(view.getUint16(4, true) === FRAME_PROTOCOL_VERSION, "unsupported protocol version");
48
51
  const sectionCount = view.getUint16(60, true);
49
52
  const tableEnd = FRAME_HEADER_BYTES + sectionCount * SECTION_HEADER_BYTES;
50
- assertRange(tableEnd <= buffer.byteLength, "truncated section table");
53
+ assertRange(tableEnd <= bytes.byteLength, "truncated section table");
51
54
  const sections = [];
52
55
  for (let index = 0; index < sectionCount; index += 1) {
53
56
  const base = FRAME_HEADER_BYTES + index * SECTION_HEADER_BYTES;
@@ -57,8 +60,8 @@ function decodeFrame(buffer) {
57
60
  const length = view.getUint32(base + 8, true);
58
61
  const itemCount = view.getUint32(base + 12, true);
59
62
  const end = offset + length;
60
- assertRange(end >= offset && offset >= tableEnd && end <= buffer.byteLength, "section out of bounds");
61
- sections.push({ kind, flags, itemCount, bytes: new Uint8Array(buffer, offset, length) });
63
+ assertRange(end >= offset && offset >= tableEnd && end <= bytes.byteLength, "section out of bounds");
64
+ sections.push({ kind, flags, itemCount, bytes: bytes.subarray(offset, end) });
62
65
  }
63
66
  return {
64
67
  protocolVersion: view.getUint16(4, true),
@@ -151,14 +154,14 @@ function decodeGlyphDefinitions(section) {
151
154
  bearingX: view.getInt16(offset - 12, true),
152
155
  bearingY: view.getInt16(offset - 10, true),
153
156
  format,
154
- pixels: section.bytes.slice(offset, offset + pixelLength)
157
+ pixels: new Uint8Array(section.bytes.subarray(offset, offset + pixelLength))
155
158
  });
156
159
  offset += pixelLength;
157
160
  }
158
161
  assertRange(offset === section.bytes.byteLength, "trailing glyph definition bytes");
159
162
  return definitions;
160
163
  }
161
- function decodeStyleDefinitions(section) {
164
+ function decodeStyleDefinitions(section, paletteSection) {
162
165
  assertRange(section.kind === SectionKind.StyleDefinitions, "wrong style definition section kind");
163
166
  assertRange(section.bytes.byteLength >= 4, "truncated style definition header");
164
167
  const view = new DataView(section.bytes.buffer, section.bytes.byteOffset, section.bytes.byteLength);
@@ -187,7 +190,39 @@ function decodeStyleDefinitions(section) {
187
190
  ...backgroundKind === 1 ? { background: [view.getUint8(offset + 11), view.getUint8(offset + 12), view.getUint8(offset + 13)] } : {}
188
191
  });
189
192
  }
190
- return styles;
193
+ if (!paletteSection)
194
+ return styles;
195
+ const palette = new Map(decodeStylePalette(paletteSection).map((entry) => [entry.styleId, entry]));
196
+ return styles.map((style) => {
197
+ const entry = palette.get(style.id);
198
+ if (!entry)
199
+ return style;
200
+ return {
201
+ ...style,
202
+ ...entry.foreground !== void 0 && style.foreground ? { foregroundPalette: entry.foreground } : {},
203
+ ...entry.background !== void 0 && style.background ? { backgroundPalette: entry.background } : {}
204
+ };
205
+ });
206
+ }
207
+ function decodeStylePalette(section) {
208
+ assertRange(section.kind === SectionKind.StylePalette, "wrong style palette section kind");
209
+ assertRange(section.bytes.byteLength >= 4, "truncated style palette header");
210
+ const view = new DataView(section.bytes.buffer, section.bytes.byteOffset, section.bytes.byteLength);
211
+ const count = view.getUint32(0, true);
212
+ assertRange(count === section.itemCount, "style palette count mismatch");
213
+ assertRange(section.bytes.byteLength === 4 + count * 8, "invalid style palette length");
214
+ const entries = [];
215
+ for (let index = 0; index < count; index += 1) {
216
+ const offset = 4 + index * 8;
217
+ const flags = view.getUint8(offset + 4);
218
+ assertRange((flags & ~3) === 0, "invalid style palette flags");
219
+ entries.push({
220
+ styleId: view.getUint32(offset, true),
221
+ ...flags & 1 ? { foreground: view.getUint8(offset + 5) } : {},
222
+ ...flags & 2 ? { background: view.getUint8(offset + 6) } : {}
223
+ });
224
+ }
225
+ return entries;
191
226
  }
192
227
  function decodeCursorState(section) {
193
228
  assertRange(section.kind === SectionKind.CursorState, "wrong cursor section kind");
@@ -230,6 +265,39 @@ function decodeClipboardWrite(section) {
230
265
  assertRange(length === section.bytes.byteLength - 4, "clipboard length mismatch");
231
266
  return new TextDecoder("utf-8", { fatal: true }).decode(section.bytes.subarray(4));
232
267
  }
268
+ function decodeLinkTargets(section, cols, rows) {
269
+ const bytes = section.bytes;
270
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
271
+ assertRange(bytes.length >= 4, "truncated link count");
272
+ const count = view.getUint32(0, true);
273
+ assertRange(count === section.itemCount && count <= cols * rows, "invalid link count");
274
+ const links = [];
275
+ const decoder = new TextDecoder("utf-8", { fatal: true });
276
+ let offset = 4;
277
+ for (let index = 0; index < count; index++) {
278
+ assertRange(offset + 8 <= bytes.length, "truncated link header");
279
+ const length = view.getUint32(offset, true);
280
+ const spanCount = view.getUint16(offset + 4, true);
281
+ const flags = view.getUint16(offset + 6, true);
282
+ offset += 8;
283
+ assertRange(length > 0 && length <= 8192 && spanCount > 0 && spanCount <= rows && flags <= 1, "invalid link header");
284
+ assertRange(offset + length + spanCount * 6 <= bytes.length, "truncated link target");
285
+ const uri = decoder.decode(bytes.subarray(offset, offset + length));
286
+ offset += length;
287
+ const spans = [];
288
+ for (let spanIndex = 0; spanIndex < spanCount; spanIndex++) {
289
+ const row = view.getUint16(offset, true);
290
+ const startColumn = view.getUint16(offset + 2, true);
291
+ const endColumn = view.getUint16(offset + 4, true);
292
+ assertRange(row < rows && startColumn < endColumn && endColumn <= cols, "link span exceeds viewport");
293
+ spans.push({ row, startColumn, endColumn });
294
+ offset += 6;
295
+ }
296
+ links.push({ uri, explicit: flags === 1, spans });
297
+ }
298
+ assertRange(offset === bytes.length, "trailing link data");
299
+ return links;
300
+ }
233
301
 
234
302
  // src/performance.ts
235
303
  function emptyRenderMetrics() {
@@ -422,6 +490,18 @@ function graphemeCellWidth(grapheme) {
422
490
  return /\p{Extended_Pictographic}/u.test(grapheme) || codepoint >= 4352 && codepoint <= 4447 || codepoint >= 11904 && codepoint <= 42191 || codepoint >= 44032 && codepoint <= 55203 || codepoint >= 63744 && codepoint <= 64255 || codepoint >= 65040 && codepoint <= 65135 || codepoint >= 65280 && codepoint <= 65376 || codepoint >= 127744 && codepoint <= 129791 || codepoint >= 131072 && codepoint <= 262141 ? 2 : 1;
423
491
  }
424
492
 
493
+ // src/renderers/background-geometry.ts
494
+ function backgroundRunBounds(run, row, cols, rows, scale, viewportWidth, viewportHeight) {
495
+ if (cols <= 0 || row < 0 || row >= rows || run.cellSpan <= 0 || run.cellStart >= cols) return null;
496
+ const end = run.cellStart + run.cellSpan;
497
+ const x = run.cellStart === 0 ? 0 : (ORIGIN_X + run.cellStart * CELL_WIDTH) * scale;
498
+ const y = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
499
+ const right = end >= cols ? viewportWidth : Math.min(viewportWidth, (ORIGIN_X + end * CELL_WIDTH) * scale);
500
+ const bottom = row === rows - 1 ? viewportHeight : Math.min(viewportHeight, (ORIGIN_Y + (row + 1) * LINE_HEIGHT) * scale);
501
+ if (right <= x || bottom <= y) return null;
502
+ return { x, y, width: right - x, height: bottom - y };
503
+ }
504
+
425
505
  // src/renderers/render-damage.ts
426
506
  function rowsForDamage(rowCount, damage, sceneValid) {
427
507
  const full = !sceneValid || !damage || damage.full;
@@ -642,8 +722,418 @@ fn sparks(position: vec2f, uv: vec2f) -> vec4f {
642
722
  `
643
723
  );
644
724
 
725
+ // src/renderers/light-adaptation.ts
726
+ var clamp = (value, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, value));
727
+ var toLinear = (v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
728
+ var toGamma = (v) => v <= 31308e-7 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055;
729
+ function oklab([r8, g8, b8]) {
730
+ const r = toLinear(r8 / 255);
731
+ const g = toLinear(g8 / 255);
732
+ const b = toLinear(b8 / 255);
733
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
734
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
735
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
736
+ return [
737
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
738
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
739
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
740
+ ];
741
+ }
742
+ function oklabToLinear([L, a, b]) {
743
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
744
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
745
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
746
+ return [
747
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
748
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
749
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
750
+ ];
751
+ }
752
+ var inGamut = (linear) => linear.every((v) => v >= -1e-4 && v <= 1 + 1e-4);
753
+ var fromLch = (L, C, h) => [L, C * Math.cos(h), C * Math.sin(h)];
754
+ var toRgb = (lab) => oklabToLinear(lab).map((v) => Math.round(clamp(toGamma(clamp(v))) * 255));
755
+ function gamutMap(L, C, h) {
756
+ L = clamp(L);
757
+ if (inGamut(oklabToLinear(fromLch(L, C, h)))) return toRgb(fromLch(L, C, h));
758
+ let lo = 0;
759
+ let hi = C;
760
+ for (let i = 0; i < 20; i += 1) {
761
+ const mid = (lo + hi) / 2;
762
+ if (inGamut(oklabToLinear(fromLch(L, mid, h)))) lo = mid;
763
+ else hi = mid;
764
+ }
765
+ return toRgb(fromLch(L, lo, h));
766
+ }
767
+ function maxChroma(L, h) {
768
+ let lo = 0;
769
+ let hi = 0.4;
770
+ for (let i = 0; i < 16; i += 1) {
771
+ const mid = (lo + hi) / 2;
772
+ if (inGamut(oklabToLinear(fromLch(L, mid, h)))) lo = mid;
773
+ else hi = mid;
774
+ }
775
+ return lo;
776
+ }
777
+ var K1 = 0.206;
778
+ var K2 = 0.03;
779
+ var K3 = (1 + K1) / (1 + K2);
780
+ var toe = (L) => 0.5 * (K3 * L - K1 + Math.sqrt((K3 * L - K1) ** 2 + 4 * K2 * K3 * L));
781
+ var toeInv = (Lr) => (Lr * Lr + K1 * Lr) / (K3 * (Lr + K2));
782
+ function apcaY([r, g, b]) {
783
+ const y = 0.2126729 * (r / 255) ** 2.4 + 0.7151522 * (g / 255) ** 2.4 + 0.072175 * (b / 255) ** 2.4;
784
+ return y < 0.022 ? y + (0.022 - y) ** 1.414 : y;
785
+ }
786
+ function apca(text, background) {
787
+ const yt = apcaY(text);
788
+ const yb = apcaY(background);
789
+ if (Math.abs(yb - yt) < 5e-4) return 0;
790
+ if (yb > yt) {
791
+ const sapc2 = (yb ** 0.56 - yt ** 0.57) * 1.14;
792
+ return sapc2 < 0.1 ? 0 : (sapc2 - 0.027) * 100;
793
+ }
794
+ const sapc = (yb ** 0.65 - yt ** 0.62) * 1.14;
795
+ return sapc > -0.1 ? 0 : (sapc + 0.027) * 100;
796
+ }
797
+ var smoothstep = (e0, e1, x) => {
798
+ const t = clamp((x - e0) / (e1 - e0));
799
+ return t * t * (3 - 2 * t);
800
+ };
801
+ var toRgba = ([r, g, b]) => [r / 255, g / 255, b / 255, 1];
802
+ var fromRgba = (color) => [
803
+ Math.round(clamp(color[0]) * 255),
804
+ Math.round(clamp(color[1]) * 255),
805
+ Math.round(clamp(color[2]) * 255)
806
+ ];
807
+ var keyOf = ([r, g, b]) => r << 16 | g << 8 | b;
808
+ var luminance = ([r, g, b]) => 0.2126 * toLinear(r / 255) + 0.7152 * toLinear(g / 255) + 0.0722 * toLinear(b / 255);
809
+ var contrastRatio = (a, b) => {
810
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
811
+ return (hi + 0.05) / (lo + 0.05);
812
+ };
813
+ function isLightBackground(color) {
814
+ const y = luminance(color);
815
+ return (y + 0.05) / 0.05 > 1.05 / (y + 0.05);
816
+ }
817
+ var contrasted = /* @__PURE__ */ new Map();
818
+ function ensureContrast(text, background, ratio) {
819
+ if (!(ratio > 1)) return text;
820
+ const fg = fromRgba(text);
821
+ const bg = fromRgba(background);
822
+ const cacheKey = `${keyOf(fg)}/${keyOf(bg)}/${ratio}`;
823
+ let result = contrasted.get(cacheKey);
824
+ if (result === void 0) {
825
+ if (contrastRatio(fg, bg) >= ratio) result = null;
826
+ else {
827
+ const towardWhite = contrastRatio([255, 255, 255], bg) > contrastRatio([0, 0, 0], bg);
828
+ const [L, a, b] = oklab(fg);
829
+ const C = Math.hypot(a, b);
830
+ const h = Math.atan2(b, a);
831
+ let near = L;
832
+ let far = towardWhite ? 1 : 0;
833
+ let best = towardWhite ? [255, 255, 255] : [0, 0, 0];
834
+ if (contrastRatio(best, bg) >= ratio) {
835
+ for (let i = 0; i < 20; i += 1) {
836
+ const mid = (near + far) / 2;
837
+ const candidate = gamutMap(mid, C, h);
838
+ if (contrastRatio(candidate, bg) >= ratio) {
839
+ far = mid;
840
+ best = candidate;
841
+ } else near = mid;
842
+ }
843
+ }
844
+ result = best;
845
+ }
846
+ if (contrasted.size > 4096) contrasted.clear();
847
+ contrasted.set(cacheKey, result);
848
+ }
849
+ if (result === null) return text;
850
+ return [result[0] / 255, result[1] / 255, result[2] / 255, text[3]];
851
+ }
852
+ function createLightAdaptation(base, paperColor, inkColor) {
853
+ const paper = fromRgba(paperColor);
854
+ const inkRgb = fromRgba(inkColor);
855
+ const baseLab = oklab(base);
856
+ const paperLab = oklab(paper);
857
+ const inkLab = oklab(inkRgb);
858
+ const baseLr = toe(baseLab[0]);
859
+ const paperLr = toe(paperLab[0]);
860
+ const surfaces2 = /* @__PURE__ */ new Map();
861
+ const surfaceRgb = (color) => {
862
+ const cached = surfaces2.get(keyOf(color));
863
+ if (cached) return cached;
864
+ const [L, a, b] = oklab(color);
865
+ let Lr = clamp(paperLr - (toe(L) - baseLr));
866
+ const ap = a - baseLab[1] + paperLab[1];
867
+ const bp = b - baseLab[2] + paperLab[2];
868
+ const C = Math.hypot(ap, bp);
869
+ const h = Math.atan2(bp, ap);
870
+ if (C > 0.015) {
871
+ const want = Math.min(C, 0.045);
872
+ const stop = Lr - 0.15;
873
+ while (Lr > stop && maxChroma(toeInv(Lr), h) < want) Lr -= 5e-3;
874
+ }
875
+ const result = gamutMap(toeInv(Lr), C, h);
876
+ surfaces2.set(keyOf(color), result);
877
+ return result;
878
+ };
879
+ const solveLc = (want, C, h, background) => {
880
+ const backgroundL = oklab(background)[0];
881
+ let lo = want > 0 ? 0 : backgroundL;
882
+ let hi = want > 0 ? backgroundL : 1;
883
+ let best = gamutMap(want > 0 ? 0 : 1, C, h);
884
+ for (let i = 0; i < 18; i += 1) {
885
+ const mid = (lo + hi) / 2;
886
+ const candidate = gamutMap(mid, C, h);
887
+ const lc = apca(candidate, background);
888
+ const enough = want > 0 ? lc >= want : lc <= want;
889
+ if (enough) best = candidate;
890
+ if (want > 0 === enough) lo = mid;
891
+ else hi = mid;
892
+ }
893
+ return best;
894
+ };
895
+ const inks = /* @__PURE__ */ new Map();
896
+ const ink = (color, sourceBackground, paletteIndex) => {
897
+ const themeColor = paletteIndex !== void 0 && CHROMATIC_ANSI.has(paletteIndex);
898
+ const cacheKey = `${keyOf(color)}/${keyOf(sourceBackground)}${themeColor ? "/p" : ""}`;
899
+ const cached = inks.get(cacheKey);
900
+ if (cached) return cached;
901
+ const mapped = surfaceRgb(sourceBackground);
902
+ if (themeColor) {
903
+ const legibility = Math.min(45, Math.abs(apca(color, paper)));
904
+ if (Math.abs(apca(color, mapped)) >= legibility) {
905
+ const rgba2 = toRgba(color);
906
+ inks.set(cacheKey, rgba2);
907
+ return rgba2;
908
+ }
909
+ }
910
+ const [L, a0, b0] = oklab(color);
911
+ const C0 = Math.hypot(a0, b0);
912
+ let a = a0;
913
+ let b = b0;
914
+ if (C0 < 0.03) {
915
+ const w2 = (1 - C0 / 0.03) * 0.7;
916
+ a = a0 * (1 - w2) + inkLab[1] * w2;
917
+ b = b0 * (1 - w2) + inkLab[2] * w2;
918
+ }
919
+ const C = Math.hypot(a, b);
920
+ const h = Math.atan2(b, a);
921
+ const delta = toe(L) - toe(oklab(sourceBackground)[0]);
922
+ const sign = delta >= 0 ? -1 : 1;
923
+ const Lr = clamp(toe(oklab(mapped)[0]) + sign * Math.max(Math.abs(delta), 0.06));
924
+ let result = gamutMap(toeInv(Lr), C, h);
925
+ const w = smoothstep(0.03, 0.08, C0);
926
+ if (w > 0) {
927
+ const source = apca(color, sourceBackground);
928
+ const lightOnDark = source < 0 || source === 0 && delta >= 0;
929
+ const magnitude = clamp(Math.abs(source) * 0.5 + 28, 45, 70);
930
+ const boosted = C0 + w * clamp(0.12 - C0, 0, C0 * 0.8);
931
+ const accent = solveLc(lightOnDark ? magnitude : -magnitude, boosted, Math.atan2(b0, a0), mapped);
932
+ if (w >= 1) result = accent;
933
+ else {
934
+ const p = oklab(result);
935
+ const q = oklab(accent);
936
+ result = toRgb([p[0] * (1 - w) + q[0] * w, p[1] * (1 - w) + q[1] * w, p[2] * (1 - w) + q[2] * w]);
937
+ }
938
+ }
939
+ const rgba = toRgba(result);
940
+ inks.set(cacheKey, rgba);
941
+ return rgba;
942
+ };
943
+ return {
944
+ key: [base, paper, inkRgb].map((color) => keyOf(color).toString(16)).join("/"),
945
+ base,
946
+ surface: (color) => toRgba(surfaceRgb(color)),
947
+ ink
948
+ };
949
+ }
950
+ var CHROMATIC_ANSI = /* @__PURE__ */ new Set([1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14]);
951
+ var ENGAGE_SHARE = 0.6;
952
+ var RELEASE_SHARE = 0.35;
953
+ function darkPaintedBase(view, engaged) {
954
+ if ((view.theme.lightAdaptation ?? "off") === "off") return null;
955
+ if (!isLightBackground(fromRgba(view.theme.background))) return null;
956
+ const rows = Math.max(view.rows.length, view.nativeStyleRows.length);
957
+ const total = rows * view.cols;
958
+ if (total === 0) return null;
959
+ const areas = /* @__PURE__ */ new Map();
960
+ let dark = 0;
961
+ const darkness = /* @__PURE__ */ new Map();
962
+ for (const runs of view.nativeStyleRows) {
963
+ for (const run of runs) {
964
+ const style = view.styleDefinitions.get(run.styleId);
965
+ const background = style?.inverse ? style.foreground : style?.background;
966
+ if (!background) continue;
967
+ const key = keyOf(background);
968
+ let isDark = darkness.get(key);
969
+ if (isDark === void 0) {
970
+ isDark = !isLightBackground(background);
971
+ darkness.set(key, isDark);
972
+ }
973
+ if (!isDark) continue;
974
+ dark += run.cellSpan;
975
+ areas.set(key, (areas.get(key) ?? 0) + run.cellSpan);
976
+ }
977
+ }
978
+ if (dark / total < (engaged ? RELEASE_SHARE : ENGAGE_SHARE)) return null;
979
+ let bestKey = 0;
980
+ let bestArea = -1;
981
+ for (const [key, area] of areas) {
982
+ if (area > bestArea) {
983
+ bestKey = key;
984
+ bestArea = area;
985
+ }
986
+ }
987
+ return [bestKey >> 16 & 255, bestKey >> 8 & 255, bestKey & 255];
988
+ }
989
+
990
+ // src/renderers/glyph-atlas.ts
991
+ var AtlasCapacityError = class extends Error {
992
+ };
993
+ var AtlasBatchFullError = class extends Error {
994
+ };
995
+ var GlyphAtlasPages = class {
996
+ constructor(device, layout, label, format, maxPages, size = 2048) {
997
+ this.device = device;
998
+ this.layout = layout;
999
+ this.label = label;
1000
+ this.format = format;
1001
+ this.maxPages = maxPages;
1002
+ this.size = size;
1003
+ this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
1004
+ }
1005
+ #pages = [];
1006
+ #sampler;
1007
+ #current;
1008
+ #generation = 0;
1009
+ #clock = 0;
1010
+ get generation() {
1011
+ return this.#current?.generation ?? 0;
1012
+ }
1013
+ get bindGroup() {
1014
+ if (!this.#current) throw new Error(`${this.label} has not been prepared`);
1015
+ return this.#current.bindGroup;
1016
+ }
1017
+ prepare(input) {
1018
+ const glyphs = /* @__PURE__ */ new Map();
1019
+ for (const glyph of input) glyphs.set(glyph.key, glyph);
1020
+ if (glyphs.size === 0) return;
1021
+ let selected;
1022
+ let selectedPlan;
1023
+ let bestHits = -1;
1024
+ for (const page of this.#pages) {
1025
+ const plan2 = this.#plan(glyphs.values(), page);
1026
+ if (!plan2) continue;
1027
+ let hits = 0;
1028
+ for (const key of glyphs.keys()) if (page.cache.has(key)) hits += 1;
1029
+ if (hits > bestHits || hits === bestHits && page.used > (selected?.used ?? -1)) {
1030
+ selected = page;
1031
+ selectedPlan = plan2;
1032
+ bestHits = hits;
1033
+ }
1034
+ }
1035
+ if (!selected) {
1036
+ selectedPlan = this.#plan(glyphs.values());
1037
+ if (!selectedPlan) {
1038
+ throw new AtlasCapacityError(`${this.label} cannot fit the glyphs visible in one frame`);
1039
+ }
1040
+ if (this.#pages.length === this.maxPages) {
1041
+ const recyclable = this.#pages.filter((page) => !page.pinned).sort((a, b) => a.used - b.used)[0];
1042
+ if (!recyclable) throw new AtlasBatchFullError(this.label);
1043
+ recyclable.texture.destroy();
1044
+ this.#pages.splice(this.#pages.indexOf(recyclable), 1);
1045
+ }
1046
+ const texture = this.device.createTexture({
1047
+ label: this.label,
1048
+ size: [this.size, this.size],
1049
+ format: this.format,
1050
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
1051
+ });
1052
+ selected = {
1053
+ texture,
1054
+ bindGroup: this.device.createBindGroup({
1055
+ label: `${this.label} bindings`,
1056
+ layout: this.layout,
1057
+ entries: [
1058
+ { binding: 0, resource: texture.createView() },
1059
+ { binding: 1, resource: this.#sampler }
1060
+ ]
1061
+ }),
1062
+ cache: /* @__PURE__ */ new Map(),
1063
+ slots: /* @__PURE__ */ new Map(),
1064
+ generation: ++this.#generation,
1065
+ used: 0,
1066
+ pinned: false,
1067
+ x: 1,
1068
+ y: 1,
1069
+ rowHeight: 0
1070
+ };
1071
+ this.#pages.push(selected);
1072
+ }
1073
+ const plan = selectedPlan;
1074
+ for (const [key, slot] of plan.slots) selected.slots.set(key, slot);
1075
+ selected.x = plan.x;
1076
+ selected.y = plan.y;
1077
+ selected.rowHeight = plan.rowHeight;
1078
+ selected.used = ++this.#clock;
1079
+ this.#current = selected;
1080
+ }
1081
+ pin() {
1082
+ if (this.#current) this.#current.pinned = true;
1083
+ }
1084
+ #plan(glyphs, page) {
1085
+ const slots = /* @__PURE__ */ new Map();
1086
+ let x = page?.x ?? 1;
1087
+ let y = page?.y ?? 1;
1088
+ let rowHeight = page?.rowHeight ?? 0;
1089
+ for (const { key, width, height } of glyphs) {
1090
+ if (page?.slots.has(key)) continue;
1091
+ if (width + 2 > this.size || height + 2 > this.size) return void 0;
1092
+ if (x + width + 1 > this.size) {
1093
+ x = 1;
1094
+ y += rowHeight + 1;
1095
+ rowHeight = 0;
1096
+ }
1097
+ if (y + height + 1 > this.size) return void 0;
1098
+ slots.set(key, { origin: [x, y], width, height });
1099
+ x += width + 1;
1100
+ rowHeight = Math.max(rowHeight, height);
1101
+ }
1102
+ return { slots, x, y, rowHeight };
1103
+ }
1104
+ get(key) {
1105
+ return this.#current?.cache.get(key);
1106
+ }
1107
+ insert(key, width, height, cells) {
1108
+ const page = this.#current;
1109
+ if (!page) throw new Error(`${this.label} has not been prepared`);
1110
+ const slot = page.slots.get(key);
1111
+ if (!slot || slot.width !== width || slot.height !== height) {
1112
+ throw new Error(`${this.label} glyph does not match its prepared reservation`);
1113
+ }
1114
+ const origin = slot.origin;
1115
+ const [x, y] = origin;
1116
+ const location = {
1117
+ u0: x / this.size,
1118
+ v0: y / this.size,
1119
+ u1: (x + width) / this.size,
1120
+ v1: (y + height) / this.size,
1121
+ cells
1122
+ };
1123
+ page.cache.set(key, location);
1124
+ return { location, texture: page.texture, origin };
1125
+ }
1126
+ releaseBatch() {
1127
+ for (const page of this.#pages) page.pinned = false;
1128
+ }
1129
+ destroy() {
1130
+ for (const page of this.#pages) page.texture.destroy();
1131
+ this.#pages.length = 0;
1132
+ this.#current = void 0;
1133
+ }
1134
+ };
1135
+
645
1136
  // src/renderers/webgpu-renderer.ts
646
- var ATLAS_SIZE = 2048;
647
1137
  var GEOMETRY_CACHE_LIMIT = 8;
648
1138
  var FONT_STACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
649
1139
  var EFFECT_MODES = {
@@ -824,88 +1314,37 @@ var DynamicVertexBuffer = class {
824
1314
  var FallbackGlyphAtlas = class {
825
1315
  constructor(device, bindGroupLayout) {
826
1316
  this.device = device;
827
- this.#texture = device.createTexture({
828
- label: "terminal glyph atlas",
829
- size: [ATLAS_SIZE, ATLAS_SIZE],
830
- format: "rgba8unorm",
831
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
832
- });
833
- this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
834
- this.#bindGroup = device.createBindGroup({
835
- label: "terminal glyph atlas bindings",
836
- layout: bindGroupLayout,
837
- entries: [
838
- { binding: 0, resource: this.#texture.createView() },
839
- { binding: 1, resource: this.#sampler }
840
- ]
841
- });
1317
+ this.pages = new GlyphAtlasPages(device, bindGroupLayout, "terminal diagnostic glyph atlas", "rgba8unorm", 1);
842
1318
  }
843
- #texture;
844
- #sampler;
845
- #bindGroup;
846
- #cache = /* @__PURE__ */ new Map();
1319
+ pages;
847
1320
  #scratch = new OffscreenCanvas(1, 1);
848
- #x = 1;
849
- #y = 1;
850
- #rowHeight = 0;
851
1321
  #uploadBytes = 0;
852
1322
  #uploadCalls = 0;
853
- #generation = 0;
854
1323
  get bindGroup() {
855
- return this.#bindGroup;
1324
+ return this.pages.bindGroup;
856
1325
  }
857
1326
  get generation() {
858
- return this.#generation;
1327
+ return this.pages.generation;
859
1328
  }
860
1329
  prepare(values, dpr) {
861
1330
  const scale = Math.max(1, Math.min(3, dpr));
862
- const all = [...new Set(values)].filter((value) => value.trim().length > 0);
863
- const missing = all.filter((value) => !this.#cache.has(`${scale.toFixed(2)}:${value}`));
864
- if (this.#fits(missing, scale, this.#x, this.#y, this.#rowHeight)) return;
865
- this.#cache.clear();
866
- this.#generation += 1;
867
- this.#x = 1;
868
- this.#y = 1;
869
- this.#rowHeight = 0;
870
- if (!this.#fits(all, scale, this.#x, this.#y, this.#rowHeight)) {
871
- throw new Error("The diagnostic glyph atlas cannot fit the text visible in one frame");
872
- }
873
- }
874
- #fits(values, scale, startX, startY, startRowHeight) {
875
- let x = startX;
876
- let y = startY;
877
- let rowHeight = startRowHeight;
878
- for (const value of values) {
879
- const width = Math.max(1, Math.ceil(CELL_WIDTH * graphemeCellWidth(value) * scale));
880
- const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
881
- if (x + width + 1 > ATLAS_SIZE) {
882
- x = 1;
883
- y += rowHeight + 1;
884
- rowHeight = 0;
885
- }
886
- if (y + height + 1 > ATLAS_SIZE) return false;
887
- x += width + 1;
888
- rowHeight = Math.max(rowHeight, height);
889
- }
890
- return true;
1331
+ this.pages.prepare(
1332
+ [...new Set(values)].filter((value) => value.trim().length > 0).map((value) => ({
1333
+ key: `${scale.toFixed(2)}:${value}`,
1334
+ width: Math.max(1, Math.ceil(CELL_WIDTH * graphemeCellWidth(value) * scale)),
1335
+ height: Math.max(1, Math.ceil(LINE_HEIGHT * scale))
1336
+ }))
1337
+ );
891
1338
  }
892
1339
  glyph(grapheme, dpr) {
893
1340
  if (grapheme.trim().length === 0) return void 0;
894
1341
  const scale = Math.max(1, Math.min(3, dpr));
895
1342
  const cells = graphemeCellWidth(grapheme);
896
1343
  const key = `${scale.toFixed(2)}:${grapheme}`;
897
- const cached = this.#cache.get(key);
1344
+ const cached = this.pages.get(key);
898
1345
  if (cached) return cached;
899
1346
  const width = Math.max(1, Math.ceil(CELL_WIDTH * cells * scale));
900
1347
  const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
901
- if (this.#x + width + 1 > ATLAS_SIZE) {
902
- this.#x = 1;
903
- this.#y += this.#rowHeight + 1;
904
- this.#rowHeight = 0;
905
- }
906
- if (this.#y + height + 1 > ATLAS_SIZE) {
907
- throw new Error("Temporary glyph atlas exhausted");
908
- }
909
1348
  this.#scratch.width = width;
910
1349
  this.#scratch.height = height;
911
1350
  const context = this.#scratch.getContext("2d", { willReadFrequently: true });
@@ -916,28 +1355,17 @@ var FallbackGlyphAtlas = class {
916
1355
  context.textBaseline = "top";
917
1356
  context.fillText(grapheme, 0, Math.max(0, Math.floor((height - 15 * scale) / 2)));
918
1357
  const image = context.getImageData(0, 0, width, height);
919
- this.device.queue.writeTexture(
920
- { texture: this.#texture, origin: [this.#x, this.#y] },
921
- image.data,
922
- { bytesPerRow: width * 4, rowsPerImage: height },
923
- [width, height]
924
- );
1358
+ const { location, texture, origin } = this.pages.insert(key, width, height, cells);
1359
+ this.device.queue.writeTexture({ texture, origin }, image.data, { bytesPerRow: width * 4, rowsPerImage: height }, [
1360
+ width,
1361
+ height
1362
+ ]);
925
1363
  this.#uploadBytes += image.data.byteLength;
926
1364
  this.#uploadCalls += 1;
927
- const location = {
928
- u0: this.#x / ATLAS_SIZE,
929
- v0: this.#y / ATLAS_SIZE,
930
- u1: (this.#x + width) / ATLAS_SIZE,
931
- v1: (this.#y + height) / ATLAS_SIZE,
932
- cells
933
- };
934
- this.#cache.set(key, location);
935
- this.#x += width + 1;
936
- this.#rowHeight = Math.max(this.#rowHeight, height);
937
1365
  return location;
938
1366
  }
939
1367
  destroy() {
940
- this.#texture.destroy();
1368
+ this.pages.destroy();
941
1369
  }
942
1370
  uploadMetrics() {
943
1371
  return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
@@ -948,106 +1376,44 @@ var NativeGlyphAtlas = class {
948
1376
  this.device = device;
949
1377
  this.label = label;
950
1378
  this.format = format;
951
- this.#texture = device.createTexture({
952
- label,
953
- size: [ATLAS_SIZE, ATLAS_SIZE],
954
- format,
955
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
956
- });
957
- this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
958
- this.#bindGroup = device.createBindGroup({
959
- label: `${label} bindings`,
960
- layout: bindGroupLayout,
961
- entries: [
962
- { binding: 0, resource: this.#texture.createView() },
963
- { binding: 1, resource: this.#sampler }
964
- ]
965
- });
1379
+ this.pages = new GlyphAtlasPages(device, bindGroupLayout, label, format, format === "r8unorm" ? 4 : 2);
966
1380
  }
967
- #texture;
968
- #sampler;
969
- #bindGroup;
970
- #cache = /* @__PURE__ */ new Map();
971
- #x = 1;
972
- #y = 1;
973
- #rowHeight = 0;
1381
+ pages;
974
1382
  #uploadBytes = 0;
975
1383
  #uploadCalls = 0;
976
- #generation = 0;
977
1384
  get bindGroup() {
978
- return this.#bindGroup;
1385
+ return this.pages.bindGroup;
979
1386
  }
980
1387
  get generation() {
981
- return this.#generation;
1388
+ return this.pages.generation;
982
1389
  }
983
1390
  prepare(definitions) {
984
- const all = /* @__PURE__ */ new Map();
985
- const missing = /* @__PURE__ */ new Map();
986
- for (const definition of definitions) {
987
- all.set(definition.id, definition);
988
- if (!this.#cache.has(definition.id)) missing.set(definition.id, definition);
989
- }
990
- if (this.#fits(missing.values(), this.#x, this.#y, this.#rowHeight)) return;
991
- this.#cache.clear();
992
- this.#generation += 1;
993
- this.#x = 1;
994
- this.#y = 1;
995
- this.#rowHeight = 0;
996
- if (!this.#fits(all.values(), this.#x, this.#y, this.#rowHeight)) {
997
- throw new Error(`${this.label} cannot fit the glyphs visible in one frame`);
998
- }
999
- }
1000
- #fits(definitions, startX, startY, startRowHeight) {
1001
- let x = startX;
1002
- let y = startY;
1003
- let rowHeight = startRowHeight;
1004
- for (const { width, height } of definitions) {
1005
- if (width + 2 > ATLAS_SIZE || height + 2 > ATLAS_SIZE) return false;
1006
- if (x + width + 1 > ATLAS_SIZE) {
1007
- x = 1;
1008
- y += rowHeight + 1;
1009
- rowHeight = 0;
1010
- }
1011
- if (y + height + 1 > ATLAS_SIZE) return false;
1012
- x += width + 1;
1013
- rowHeight = Math.max(rowHeight, height);
1014
- }
1015
- return true;
1391
+ this.pages.prepare(
1392
+ Array.from(definitions, (definition) => ({
1393
+ key: definition.id,
1394
+ width: definition.width,
1395
+ height: definition.height
1396
+ }))
1397
+ );
1016
1398
  }
1017
1399
  glyph(definition) {
1018
- const cached = this.#cache.get(definition.id);
1400
+ const cached = this.pages.get(definition.id);
1019
1401
  if (cached) return cached;
1020
1402
  const { width, height } = definition;
1021
- if (this.#x + width + 1 > ATLAS_SIZE) {
1022
- this.#x = 1;
1023
- this.#y += this.#rowHeight + 1;
1024
- this.#rowHeight = 0;
1025
- }
1026
- if (this.#y + height + 1 > ATLAS_SIZE) throw new Error(`${this.label} exhausted`);
1027
- const pixels = definition.pixels;
1403
+ const { location, texture, origin } = this.pages.insert(definition.id, width, height, 0);
1028
1404
  const bytesPerPixel = this.format === "r8unorm" ? 1 : 4;
1029
1405
  this.device.queue.writeTexture(
1030
- { texture: this.#texture, origin: [this.#x, this.#y] },
1031
- pixels,
1406
+ { texture, origin },
1407
+ definition.pixels,
1032
1408
  { bytesPerRow: width * bytesPerPixel, rowsPerImage: height },
1033
1409
  [width, height]
1034
1410
  );
1035
- this.#uploadBytes += pixels.byteLength;
1411
+ this.#uploadBytes += definition.pixels.byteLength;
1036
1412
  this.#uploadCalls += 1;
1037
- const location = {
1038
- u0: this.#x / ATLAS_SIZE,
1039
- v0: this.#y / ATLAS_SIZE,
1040
- u1: (this.#x + width) / ATLAS_SIZE,
1041
- v1: (this.#y + height) / ATLAS_SIZE,
1042
- cells: 0
1043
- };
1044
- this.#cache.set(definition.id, location);
1045
- this.#x += width + 1;
1046
- this.#rowHeight = Math.max(this.#rowHeight, height);
1047
1413
  return location;
1048
1414
  }
1049
1415
  destroy() {
1050
- this.#texture.destroy();
1416
+ this.pages.destroy();
1051
1417
  }
1052
1418
  uploadMetrics() {
1053
1419
  return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
@@ -1074,11 +1440,12 @@ function createCachedVertexBuffer(device, label, data) {
1074
1440
  device.queue.writeBuffer(buffer, 0, data);
1075
1441
  return buffer;
1076
1442
  }
1077
- function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
1443
+ function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations, adaptationKey) {
1078
1444
  const selection = view.selection ? `${view.selection.anchor.row},${view.selection.anchor.column},${view.selection.focus.row},${view.selection.focus.column}` : "-";
1079
1445
  return [
1080
1446
  view.sessionEpoch.toString(),
1081
1447
  view.layoutEpoch.toString(),
1448
+ view.cols,
1082
1449
  hasNativeRows ? "native" : "fallback",
1083
1450
  rows.map((row) => `${row}:${view.rowRevisions[row]?.toString() ?? "-"}`).join(","),
1084
1451
  [
@@ -1090,6 +1457,8 @@ function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
1090
1457
  ...view.theme.selectionForeground
1091
1458
  ].join(","),
1092
1459
  view.theme.backgroundOpacityCells ? "opacity-cells" : "opaque-cells",
1460
+ view.theme.minimumContrast ?? 1,
1461
+ adaptationKey,
1093
1462
  selection,
1094
1463
  effectiveCursorStyle(view) === CursorStyle.Block ? `${view.cursor.x},${view.cursor.y}` : "-",
1095
1464
  atlasGenerations.join(",")
@@ -1304,21 +1673,45 @@ function pushBlockCursorBackground(vertices, view, renderRows, scale, viewportWi
1304
1673
  viewportHeight
1305
1674
  );
1306
1675
  }
1307
- function resolveStyle(style, theme) {
1676
+ function resolveStyle(style, theme, adaptation = null) {
1308
1677
  let foreground = style?.foreground ? rgb(style.foreground) : theme.foreground;
1309
1678
  let background = style?.background ? rgb(style.background) : null;
1310
- if (style?.inverse) {
1679
+ let fill = foreground;
1680
+ if (adaptation) {
1681
+ const source = { foreground: style?.foreground, background: style?.background };
1682
+ if (style?.inverse) {
1683
+ background = source.foreground ? adaptation.surface(source.foreground) : theme.foreground;
1684
+ foreground = source.background ? adaptation.ink(source.background, source.foreground ?? adaptation.base, style?.backgroundPalette) : theme.background;
1685
+ fill = foreground;
1686
+ } else {
1687
+ foreground = source.foreground ? adaptation.ink(source.foreground, source.background ?? adaptation.base, style?.foregroundPalette) : theme.foreground;
1688
+ background = source.background ? adaptation.surface(source.background) : null;
1689
+ fill = source.foreground ? adaptation.surface(source.foreground) : theme.foreground;
1690
+ }
1691
+ } else if (style?.inverse) {
1311
1692
  const originalForeground = foreground;
1312
1693
  foreground = background ?? theme.background;
1313
1694
  background = originalForeground;
1695
+ fill = foreground;
1314
1696
  }
1315
1697
  if (background && theme.backgroundOpacityCells) {
1316
1698
  background = [background[0], background[1], background[2], theme.background[3]];
1317
1699
  }
1318
- if (style?.faint) foreground = [foreground[0], foreground[1], foreground[2], foreground[3] * 0.55];
1700
+ let text = ensureContrast(
1701
+ foreground,
1702
+ over(background ?? theme.background, theme.background),
1703
+ theme.minimumContrast ?? 1
1704
+ );
1705
+ if (style?.faint) {
1706
+ foreground = [foreground[0], foreground[1], foreground[2], foreground[3] * 0.55];
1707
+ fill = [fill[0], fill[1], fill[2], fill[3] * 0.55];
1708
+ text = [text[0], text[1], text[2], text[3] * 0.55];
1709
+ }
1319
1710
  return {
1320
1711
  foreground,
1321
1712
  background,
1713
+ fill,
1714
+ text,
1322
1715
  underline: style?.underline ?? false,
1323
1716
  strikethrough: style?.strikethrough ?? false,
1324
1717
  invisible: style?.invisible ?? false
@@ -1621,7 +2014,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1621
2014
  #monoAtlas;
1622
2015
  #colorAtlas;
1623
2016
  #fallbackAtlas;
2017
+ #adaptations = /* @__PURE__ */ new Map();
1624
2018
  #performanceMeasurementEnabled = false;
2019
+ #submission = { queueSubmits: 0, surfaceIds: [] };
2020
+ #scrollScratch;
1625
2021
  static async create(onLost) {
1626
2022
  if (!navigator.gpu) throw new Error("WebGPU is unavailable");
1627
2023
  const adapter = await navigator.gpu.requestAdapter();
@@ -1661,7 +2057,11 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1661
2057
  cursorBuffer: new DynamicVertexBuffer(this.device, `cursor ${id}`),
1662
2058
  geometryCache: /* @__PURE__ */ new Map(),
1663
2059
  geometryCandidates: /* @__PURE__ */ new Map(),
1664
- sceneValid: false
2060
+ sceneValid: false,
2061
+ adaptation: null,
2062
+ sceneHadSelection: false,
2063
+ sceneHadOverhang: false,
2064
+ sceneCursorRow: null
1665
2065
  };
1666
2066
  this.#surfaces.set(id, surface2);
1667
2067
  this.#configure(surface2);
@@ -1680,6 +2080,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1680
2080
  surface2.cursorBuffer.destroy();
1681
2081
  clearGeometryCache(surface2);
1682
2082
  this.#surfaces.delete(id);
2083
+ if (this.#surfaces.size === 0) {
2084
+ this.#scrollScratch?.destroy();
2085
+ this.#scrollScratch = void 0;
2086
+ }
1683
2087
  }
1684
2088
  resize(id, size) {
1685
2089
  const surface2 = this.#surfaces.get(id);
@@ -1708,7 +2112,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1708
2112
  label: "terminal scene before Ghostty custom shaders",
1709
2113
  size: [Math.max(1, width), Math.max(1, height)],
1710
2114
  format: this.format,
1711
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
2115
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST
1712
2116
  });
1713
2117
  }
1714
2118
  #createEffectTexture(width, height, suffix) {
@@ -1818,32 +2222,186 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1818
2222
  drawCalls: effectPassCount
1819
2223
  };
1820
2224
  }
2225
+ /**
2226
+ * Engage, re-key, or release light adaptation for a pane. Any change remaps
2227
+ * every color, so the persistent scene and cached geometry are invalidated.
2228
+ */
2229
+ #prepareAdaptation(surface2, view) {
2230
+ const base = darkPaintedBase(view, surface2.adaptation !== null);
2231
+ let next = null;
2232
+ if (base) {
2233
+ const key = `${base.join(",")}|${view.theme.background.join(",")}|${view.theme.foreground.join(",")}`;
2234
+ next = this.#adaptations.get(key) ?? null;
2235
+ if (!next) {
2236
+ next = createLightAdaptation(base, view.theme.background, view.theme.foreground);
2237
+ this.#adaptations.set(key, next);
2238
+ if (this.#adaptations.size > 8) this.#adaptations.delete(this.#adaptations.keys().next().value);
2239
+ }
2240
+ }
2241
+ if ((next?.key ?? null) === (surface2.adaptation?.key ?? null)) return;
2242
+ surface2.adaptation = next;
2243
+ surface2.sceneValid = false;
2244
+ clearGeometryCache(surface2);
2245
+ }
2246
+ submission() {
2247
+ return this.#submission;
2248
+ }
2249
+ #releaseAtlasBatch() {
2250
+ this.#monoAtlas.pages.releaseBatch();
2251
+ this.#colorAtlas.pages.releaseBatch();
2252
+ this.#fallbackAtlas.pages.releaseBatch();
2253
+ }
2254
+ #prepareAtlases(view, renderRows, dpr) {
2255
+ const mono = /* @__PURE__ */ new Map();
2256
+ const color = /* @__PURE__ */ new Map();
2257
+ for (const row of renderRows) {
2258
+ for (const glyph of view.nativeRows[row] ?? []) {
2259
+ const definition = view.glyphDefinitions.get(glyph.glyphId);
2260
+ if (definition) (definition.format === GlyphFormat.Alpha8 ? mono : color).set(definition.id, definition);
2261
+ }
2262
+ }
2263
+ this.#monoAtlas.prepare(mono.values());
2264
+ this.#colorAtlas.prepare(color.values());
2265
+ const fallback = !view.nativeRows.some((row) => row.length > 0);
2266
+ if (fallback) {
2267
+ this.#fallbackAtlas.prepare(
2268
+ renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")),
2269
+ dpr
2270
+ );
2271
+ }
2272
+ if (mono.size > 0) this.#monoAtlas.pages.pin();
2273
+ if (color.size > 0) this.#colorAtlas.pages.pin();
2274
+ if (fallback && renderRows.length > 0) this.#fallbackAtlas.pages.pin();
2275
+ }
1821
2276
  render(id, view) {
1822
- const surface2 = this.#surfaces.get(id);
1823
- if (!surface2) return;
1824
- const encoder = this.device.createCommandEncoder({ label: `terminal frame ${id}` });
1825
- const metrics = view.damage?.geometryChanged === false ? this.#encodeCachedRender(id, view, encoder) : this.#encodeRender(id, view, encoder);
1826
- this.device.queue.submit([encoder.finish()]);
1827
- if (metrics) metrics.queueSubmits = 1;
1828
- return metrics;
2277
+ return this.renderBatch([{ id, view }])[0];
2278
+ }
2279
+ #canCopyScroll(surface2, view) {
2280
+ const shift = view.damage?.scrollRows ?? 0;
2281
+ const top = ORIGIN_Y * surface2.dpr;
2282
+ const line = LINE_HEIGHT * surface2.dpr;
2283
+ const count = view.rows.length;
2284
+ return surface2.sceneValid && !view.damage?.full && Number.isSafeInteger(shift) && shift !== 0 && Math.abs(shift) < count && !view.selection && !surface2.sceneHadSelection && Number.isInteger(top) && Number.isInteger(line) && top + count * line <= surface2.canvas.height;
2285
+ }
2286
+ #prepareScrollScratch(entries) {
2287
+ let width = 0, height = 0;
2288
+ for (const { id, view } of entries) {
2289
+ const surface2 = this.#surfaces.get(id);
2290
+ if (!surface2 || !this.#canCopyScroll(surface2, view)) continue;
2291
+ width = Math.max(width, surface2.canvas.width);
2292
+ height = Math.max(height, view.rows.length * LINE_HEIGHT * surface2.dpr);
2293
+ }
2294
+ if (width === 0) return false;
2295
+ if (width * height * 4 > 64 * 1024 * 1024) return false;
2296
+ if (!this.#scrollScratch || this.#scrollScratch.width < width || this.#scrollScratch.height < height) {
2297
+ this.#scrollScratch?.destroy();
2298
+ this.#scrollScratch = this.device.createTexture({
2299
+ label: "terminal shared scroll staging",
2300
+ size: [width, height],
2301
+ format: this.format,
2302
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST
2303
+ });
2304
+ }
2305
+ return true;
2306
+ }
2307
+ #copyScroll(surface2, view, encoder) {
2308
+ const dy = view.damage.scrollRows * LINE_HEIGHT * surface2.dpr;
2309
+ const top = ORIGIN_Y * surface2.dpr;
2310
+ const size = [surface2.canvas.width, view.rows.length * LINE_HEIGHT * surface2.dpr - Math.abs(dy)];
2311
+ encoder.copyTextureToTexture(
2312
+ { texture: surface2.sceneTexture, origin: [0, top + Math.max(0, dy)] },
2313
+ { texture: this.#scrollScratch },
2314
+ size
2315
+ );
2316
+ encoder.copyTextureToTexture(
2317
+ { texture: this.#scrollScratch },
2318
+ { texture: surface2.sceneTexture, origin: [0, top + Math.max(0, -dy)] },
2319
+ size
2320
+ );
1829
2321
  }
1830
2322
  renderBatch(entries) {
1831
- const active = entries.filter(({ id }) => this.#surfaces.has(id));
1832
- if (active.length === 0) return entries.map(() => void 0);
1833
- const encoder = this.device.createCommandEncoder({ label: `terminal frame batch (${active.length} panes)` });
2323
+ this.#submission = { queueSubmits: 0, surfaceIds: [] };
2324
+ this.#releaseAtlasBatch();
2325
+ const prepared = entries.map(({ id, view }) => {
2326
+ const surface2 = this.#surfaces.get(id);
2327
+ if (surface2) this.#prepareAdaptation(surface2, view);
2328
+ const overhang = view.damage?.geometryChanged === false && surface2?.sceneValid ? surface2.sceneHadOverhang : view.nativeRows.some((row) => row.some((glyph) => glyph.y < 0 || glyph.y + glyph.height > LINE_HEIGHT));
2329
+ if (view.damage && !view.damage.full && (view.damage.scrollRows || view.damage.rows.size > 0) && (overhang || surface2?.sceneHadOverhang)) {
2330
+ view = { ...view, damage: { full: true, rows: /* @__PURE__ */ new Set(), geometryChanged: true } };
2331
+ }
2332
+ return { id, view, overhang };
2333
+ });
2334
+ const canStageScroll = this.#prepareScrollScratch(prepared);
2335
+ let encoder;
2336
+ let pendingIds = [];
1834
2337
  const byId = /* @__PURE__ */ new Map();
1835
- for (const { id, view } of active) {
1836
- byId.set(
1837
- id,
1838
- view.damage?.geometryChanged === false ? this.#encodeCachedRender(id, view, encoder) : this.#encodeRender(id, view, encoder)
1839
- );
2338
+ const submit = () => {
2339
+ if (encoder && pendingIds.length > 0) {
2340
+ this.device.queue.submit([encoder.finish()]);
2341
+ this.#submission.queueSubmits += 1;
2342
+ this.#submission.surfaceIds.push(...pendingIds);
2343
+ const first = pendingIds.map((id) => byId.get(id)).find((metric) => metric !== void 0);
2344
+ if (first) first.queueSubmits += 1;
2345
+ }
2346
+ encoder = void 0;
2347
+ pendingIds = [];
2348
+ this.#releaseAtlasBatch();
2349
+ };
2350
+ try {
2351
+ for (const { id, view: originalView, overhang } of prepared) {
2352
+ const surface2 = this.#surfaces.get(id);
2353
+ if (!surface2) continue;
2354
+ let view = originalView;
2355
+ const copyScroll = canStageScroll && this.#canCopyScroll(surface2, view);
2356
+ if (view.damage?.scrollRows) {
2357
+ if (copyScroll) {
2358
+ const rows2 = new Set(view.damage.rows);
2359
+ const shift = view.damage.scrollRows;
2360
+ rows2.add(0);
2361
+ rows2.add(view.rows.length - 1);
2362
+ for (let row = 0; row < view.rows.length; row++) {
2363
+ if (row + shift < 0 || row + shift >= view.rows.length) rows2.add(row);
2364
+ }
2365
+ if (surface2.sceneCursorRow !== null) rows2.add(surface2.sceneCursorRow - shift);
2366
+ if (effectiveCursorStyle(view) !== null) rows2.add(view.cursor.y);
2367
+ view = { ...view, damage: { ...view.damage, rows: rows2, geometryChanged: true } };
2368
+ } else {
2369
+ view = { ...view, damage: { full: true, rows: /* @__PURE__ */ new Set(), geometryChanged: true } };
2370
+ }
2371
+ }
2372
+ const count = Math.max(view.rows.length, view.nativeRows.length, view.nativeStyleRows.length);
2373
+ const { rows } = rowsForDamage(count, view.damage, surface2.sceneValid);
2374
+ try {
2375
+ try {
2376
+ this.#prepareAtlases(view, rows, surface2.dpr);
2377
+ } catch (error) {
2378
+ if (!(error instanceof AtlasBatchFullError) || pendingIds.length === 0) throw error;
2379
+ submit();
2380
+ this.#prepareAtlases(view, rows, surface2.dpr);
2381
+ }
2382
+ } catch (error) {
2383
+ if (!(error instanceof AtlasCapacityError)) throw error;
2384
+ console.error(`[terminal-renderer] cannot render ${id}`, error);
2385
+ continue;
2386
+ }
2387
+ encoder ??= this.device.createCommandEncoder({ label: "terminal frame batch" });
2388
+ if (copyScroll) this.#copyScroll(surface2, view, encoder);
2389
+ byId.set(
2390
+ id,
2391
+ view.damage?.geometryChanged === false ? this.#encodeCachedRender(id, view, encoder) : this.#encodeRender(id, view, encoder)
2392
+ );
2393
+ surface2.sceneHadSelection = view.selection !== null;
2394
+ surface2.sceneHadOverhang = overhang;
2395
+ surface2.sceneCursorRow = effectiveCursorStyle(view) === null ? null : view.cursor.y;
2396
+ pendingIds.push(id);
2397
+ }
2398
+ submit();
2399
+ } finally {
2400
+ this.#releaseAtlasBatch();
1840
2401
  }
1841
- this.device.queue.submit([encoder.finish()]);
1842
- const firstMetrics = [...byId.values()].find((metrics) => metrics !== void 0);
1843
- if (firstMetrics) firstMetrics.queueSubmits = 1;
1844
2402
  return entries.map(({ id }) => byId.get(id));
1845
2403
  }
1846
- #buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight) {
2404
+ #buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight, adaptation) {
1847
2405
  const rectangleVertices = [];
1848
2406
  const glyphVertices = [];
1849
2407
  const colorGlyphVertices = [];
@@ -1852,24 +2410,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1852
2410
  const styleFor = (styleId) => {
1853
2411
  const cached = resolvedStyles.get(styleId);
1854
2412
  if (cached) return cached;
1855
- const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
2413
+ const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme, adaptation);
1856
2414
  resolvedStyles.set(styleId, resolved);
1857
2415
  return resolved;
1858
2416
  };
1859
- const monoDefinitions = /* @__PURE__ */ new Map();
1860
- const colorDefinitions = /* @__PURE__ */ new Map();
1861
- for (const row of renderRows) {
1862
- for (const glyph of view.nativeRows[row] ?? []) {
1863
- const definition = view.glyphDefinitions.get(glyph.glyphId);
1864
- if (!definition) continue;
1865
- (definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(
1866
- definition.id,
1867
- definition
1868
- );
1869
- }
1870
- }
1871
- this.#monoAtlas.prepare(monoDefinitions.values());
1872
- this.#colorAtlas.prepare(colorDefinitions.values());
1873
2417
  if (!fullRedraw) {
1874
2418
  for (const row of renderRows) {
1875
2419
  const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
@@ -1891,12 +2435,14 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1891
2435
  for (const run of view.nativeStyleRows[row] ?? []) {
1892
2436
  const style = styleFor(run.styleId);
1893
2437
  if (!style.background) continue;
2438
+ const bounds = backgroundRunBounds(run, row, view.cols, rowCount, scale, viewportWidth, viewportHeight);
2439
+ if (!bounds) continue;
1894
2440
  pushRectangle(
1895
2441
  rectangleVertices,
1896
- (ORIGIN_X + run.cellStart * CELL_WIDTH) * scale,
1897
- (ORIGIN_Y + row * LINE_HEIGHT) * scale,
1898
- run.cellSpan * CELL_WIDTH * scale,
1899
- LINE_HEIGHT * scale,
2442
+ bounds.x,
2443
+ bounds.y,
2444
+ bounds.width,
2445
+ bounds.height,
1900
2446
  style.background,
1901
2447
  viewportWidth,
1902
2448
  viewportHeight
@@ -1925,12 +2471,6 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1925
2471
  }
1926
2472
  pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
1927
2473
  const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
1928
- if (!hasNativeRows) {
1929
- this.#fallbackAtlas.prepare(
1930
- renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")),
1931
- scale
1932
- );
1933
- }
1934
2474
  if (hasNativeRows) {
1935
2475
  for (const row of renderRows) {
1936
2476
  const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
@@ -1961,20 +2501,22 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1961
2501
  if (blockElement) {
1962
2502
  let backdrop = style.background ?? view.theme.background;
1963
2503
  if (isSelected) backdrop = view.theme.selection;
2504
+ const blockForeground = foreground === style.foreground ? style.fill : foreground;
1964
2505
  if (pushBlockElement(
1965
2506
  rectangleVertices,
1966
2507
  blockElement,
1967
2508
  instance.cellStart,
1968
2509
  row,
1969
2510
  scale,
1970
- over(foreground, backdrop),
2511
+ over(blockForeground, backdrop),
1971
2512
  viewportWidth,
1972
2513
  viewportHeight
1973
2514
  ))
1974
2515
  continue;
1975
2516
  }
1976
- const atlas = definition.format === GlyphFormat.Alpha8 ? this.#monoAtlas : this.#colorAtlas;
1977
- const vertices = definition.format === GlyphFormat.Alpha8 ? glyphVertices : colorGlyphVertices;
2517
+ const mono = definition.format === GlyphFormat.Alpha8;
2518
+ const atlas = mono ? this.#monoAtlas : this.#colorAtlas;
2519
+ const vertices = mono ? glyphVertices : colorGlyphVertices;
1978
2520
  pushGlyph(
1979
2521
  vertices,
1980
2522
  (ORIGIN_X + instance.x) * scale,
@@ -1982,7 +2524,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1982
2524
  instance.width * scale,
1983
2525
  instance.height * scale,
1984
2526
  atlas.glyph(definition),
1985
- foreground,
2527
+ mono && foreground === style.foreground ? style.text : foreground,
1986
2528
  viewportWidth,
1987
2529
  viewportHeight
1988
2530
  );
@@ -2027,7 +2569,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2027
2569
  Math.round(rowTop + 16 * scale),
2028
2570
  width,
2029
2571
  stroke,
2030
- style.foreground,
2572
+ style.text,
2031
2573
  viewportWidth,
2032
2574
  viewportHeight
2033
2575
  );
@@ -2039,7 +2581,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2039
2581
  Math.round(rowTop + 9 * scale),
2040
2582
  width,
2041
2583
  stroke,
2042
- style.foreground,
2584
+ style.text,
2043
2585
  viewportWidth,
2044
2586
  viewportHeight
2045
2587
  );
@@ -2084,24 +2626,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2084
2626
  const styleFor = (styleId) => {
2085
2627
  const cached = resolvedStyles.get(styleId);
2086
2628
  if (cached) return cached;
2087
- const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
2629
+ const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme, surface2.adaptation);
2088
2630
  resolvedStyles.set(styleId, resolved);
2089
2631
  return resolved;
2090
2632
  };
2091
- const monoDefinitions = /* @__PURE__ */ new Map();
2092
- const colorDefinitions = /* @__PURE__ */ new Map();
2093
- for (const row of renderRows) {
2094
- for (const glyph of view.nativeRows[row] ?? []) {
2095
- const definition = view.glyphDefinitions.get(glyph.glyphId);
2096
- if (!definition) continue;
2097
- (definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(
2098
- definition.id,
2099
- definition
2100
- );
2101
- }
2102
- }
2103
- this.#monoAtlas.prepare(monoDefinitions.values());
2104
- this.#colorAtlas.prepare(colorDefinitions.values());
2105
2633
  if (!fullRedraw) {
2106
2634
  for (const row of renderRows) {
2107
2635
  const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
@@ -2123,12 +2651,14 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2123
2651
  for (const run of view.nativeStyleRows[row] ?? []) {
2124
2652
  const style = styleFor(run.styleId);
2125
2653
  if (!style.background) continue;
2654
+ const bounds = backgroundRunBounds(run, row, view.cols, rowCount, scale, viewportWidth, viewportHeight);
2655
+ if (!bounds) continue;
2126
2656
  pushRectangle(
2127
2657
  rectangleVertices,
2128
- (ORIGIN_X + run.cellStart * CELL_WIDTH) * scale,
2129
- (ORIGIN_Y + row * LINE_HEIGHT) * scale,
2130
- run.cellSpan * CELL_WIDTH * scale,
2131
- LINE_HEIGHT * scale,
2658
+ bounds.x,
2659
+ bounds.y,
2660
+ bounds.width,
2661
+ bounds.height,
2132
2662
  style.background,
2133
2663
  viewportWidth,
2134
2664
  viewportHeight
@@ -2158,12 +2688,6 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2158
2688
  pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
2159
2689
  const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
2160
2690
  const hasNativeRows = view.nativeRows.some((row) => row.length > 0);
2161
- if (!hasNativeRows) {
2162
- this.#fallbackAtlas.prepare(
2163
- renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")),
2164
- scale
2165
- );
2166
- }
2167
2691
  if (hasNativeRows) {
2168
2692
  for (const row of renderRows) {
2169
2693
  const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
@@ -2195,7 +2719,8 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2195
2719
  if (blockElement) {
2196
2720
  let backdrop = style.background ?? view.theme.background;
2197
2721
  if (isSelected) backdrop = view.theme.selection;
2198
- const blockColor = over(foreground, backdrop);
2722
+ const blockForeground = foreground === style.foreground ? style.fill : foreground;
2723
+ const blockColor = over(blockForeground, backdrop);
2199
2724
  if (pushBlockElement(
2200
2725
  rectangleVertices,
2201
2726
  blockElement,
@@ -2208,8 +2733,9 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2208
2733
  ))
2209
2734
  continue;
2210
2735
  }
2211
- const atlas = definition.format === GlyphFormat.Alpha8 ? this.#monoAtlas : this.#colorAtlas;
2212
- const vertices = definition.format === GlyphFormat.Alpha8 ? glyphVertices : colorGlyphVertices;
2736
+ const mono = definition.format === GlyphFormat.Alpha8;
2737
+ const atlas = mono ? this.#monoAtlas : this.#colorAtlas;
2738
+ const vertices = mono ? glyphVertices : colorGlyphVertices;
2213
2739
  pushGlyph(
2214
2740
  vertices,
2215
2741
  (ORIGIN_X + instance.x) * scale,
@@ -2217,7 +2743,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2217
2743
  instance.width * scale,
2218
2744
  instance.height * scale,
2219
2745
  atlas.glyph(definition),
2220
- foreground,
2746
+ mono && foreground === style.foreground ? style.text : foreground,
2221
2747
  viewportWidth,
2222
2748
  viewportHeight
2223
2749
  );
@@ -2262,7 +2788,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2262
2788
  Math.round(rowTop + 16 * scale),
2263
2789
  width,
2264
2790
  stroke,
2265
- style.foreground,
2791
+ style.text,
2266
2792
  viewportWidth,
2267
2793
  viewportHeight
2268
2794
  );
@@ -2274,7 +2800,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2274
2800
  Math.round(rowTop + 9 * scale),
2275
2801
  width,
2276
2802
  stroke,
2277
- style.foreground,
2803
+ style.text,
2278
2804
  viewportWidth,
2279
2805
  viewportHeight
2280
2806
  );
@@ -2464,7 +2990,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2464
2990
  this.#colorAtlas.generation,
2465
2991
  this.#fallbackAtlas.generation
2466
2992
  ];
2467
- let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
2993
+ let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations(), surface2.adaptation?.key ?? "-");
2468
2994
  const cached = surface2.geometryCache.get(key);
2469
2995
  const cacheHit = cached !== void 0;
2470
2996
  if (cached) {
@@ -2482,9 +3008,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2482
3008
  hasNativeRows,
2483
3009
  scale,
2484
3010
  viewportWidth,
2485
- viewportHeight
3011
+ viewportHeight,
3012
+ surface2.adaptation
2486
3013
  );
2487
- key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
3014
+ key = geometryCacheKey(view, damage.rows, hasNativeRows, generations(), surface2.adaptation?.key ?? "-");
2488
3015
  const promote = admitted && key === candidateKey;
2489
3016
  geometry = {
2490
3017
  rectangleBuffer: promote ? createCachedVertexBuffer(this.device, `cached rectangles ${id}`, cpu.rectangleData) : surface2.rectangleBuffer.write(cpu.rectangleData),
@@ -2642,13 +3169,72 @@ function cursorActivityChangesPixels(cursor, focused, cursorBlinkVisible) {
2642
3169
  return focused && cursor.visible && cursor.blinking && !cursorBlinkVisible;
2643
3170
  }
2644
3171
 
3172
+ // src/row-damage.ts
3173
+ function sameRow(before, from, after, to) {
3174
+ if (from < 0 || from >= before.rows.length || before.rows[from] !== after.rows[to]) return false;
3175
+ const a = before.nativeRows[from] ?? [], b = after.nativeRows[to] ?? [];
3176
+ if (a.length !== b.length) return false;
3177
+ for (let i = 0; i < a.length; i++) {
3178
+ const x = a[i], y = b[i];
3179
+ if (x.glyphId !== y.glyphId || x.styleId !== y.styleId || x.x !== y.x || x.y !== y.y || x.width !== y.width || x.height !== y.height || x.cellStart !== y.cellStart || x.cellSpan !== y.cellSpan)
3180
+ return false;
3181
+ }
3182
+ const as = before.nativeStyleRows[from] ?? [], bs = after.nativeStyleRows[to] ?? [];
3183
+ if (as.length !== bs.length) return false;
3184
+ return as.every(
3185
+ (x, i) => x.styleId === bs[i].styleId && x.cellStart === bs[i].cellStart && x.cellSpan === bs[i].cellSpan
3186
+ );
3187
+ }
3188
+ function rowDamage(before, after) {
3189
+ const positions = /* @__PURE__ */ new Map();
3190
+ for (let row = 0; row < before.rows.length; row++) {
3191
+ const text = before.rows[row];
3192
+ positions.set(text, positions.has(text) ? -1 : row);
3193
+ }
3194
+ const votes = /* @__PURE__ */ new Map();
3195
+ for (let row = 0; row < after.rows.length; row++) {
3196
+ const from = positions.get(after.rows[row]);
3197
+ if (from !== void 0 && from >= 0) {
3198
+ const delta = from - row;
3199
+ votes.set(delta, (votes.get(delta) ?? 0) + 1);
3200
+ }
3201
+ }
3202
+ let scrollRows = 0, best = votes.get(0) ?? 0;
3203
+ for (const [delta, count] of votes)
3204
+ if (count > best) {
3205
+ best = count;
3206
+ scrollRows = delta;
3207
+ }
3208
+ const changed = (delta) => after.rows.flatMap((_, row) => sameRow(before, row + delta, after, row) ? [] : [row]);
3209
+ if (scrollRows !== 0) {
3210
+ const rows = changed(scrollRows);
3211
+ if (after.rows.length - rows.length >= Math.ceil(after.rows.length / 2)) return { scrollRows, rows };
3212
+ }
3213
+ return { scrollRows: 0, rows: changed(0) };
3214
+ }
3215
+ function shiftDamage(rows, shift, rowCount) {
3216
+ const moved = [...rows].map((row) => row - shift).filter((row) => row >= 0 && row < rowCount);
3217
+ rows.clear();
3218
+ for (const row of moved) rows.add(row);
3219
+ }
3220
+ function selectionDamageRows(before, after, count) {
3221
+ const rows = /* @__PURE__ */ new Set();
3222
+ for (const selection of [before, after]) {
3223
+ if (!selection) continue;
3224
+ const start = Math.max(0, Math.min(selection.anchor.row, selection.focus.row));
3225
+ const end = Math.min(count - 1, Math.max(selection.anchor.row, selection.focus.row));
3226
+ for (let row = start; row <= end; row++) rows.add(row);
3227
+ }
3228
+ return [...rows];
3229
+ }
3230
+
2645
3231
  // src/catalog-budget.ts
2646
3232
  function catalogAdmission(resetsCatalog, alreadyUsingFallback, fits) {
2647
3233
  if (alreadyUsingFallback && !resetsCatalog) return "text-fallback";
2648
3234
  if (fits) return "install";
2649
3235
  return resetsCatalog ? "text-fallback" : "request-full";
2650
3236
  }
2651
- function glyphCatalogFits(sessionDefinitions, sessionPixelBytes, sharedDefinitions, sharedPixelBytes, definitions, budget) {
3237
+ function glyphCatalogFits(sessionDefinitions, sessionPixelBytes, sharedDefinitions, sharedPixelBytes, definitions, budget, sharedKey = (id) => id) {
2652
3238
  if (definitions.length === 0) return true;
2653
3239
  let nextDefinitionCount = sessionDefinitions.size;
2654
3240
  let nextSharedDefinitionCount = sharedDefinitions.size;
@@ -2658,7 +3244,7 @@ function glyphCatalogFits(sessionDefinitions, sessionPixelBytes, sharedDefinitio
2658
3244
  for (const definition of definitions) {
2659
3245
  if (sessionDefinitions.has(definition.id) || pending.has(definition.id)) continue;
2660
3246
  pending.add(definition.id);
2661
- const shared = sharedDefinitions.get(definition.id);
3247
+ const shared = sharedDefinitions.get(sharedKey(definition.id));
2662
3248
  const pixelBytes = shared?.definition.pixels.byteLength ?? definition.pixels.byteLength;
2663
3249
  nextDefinitionCount += 1;
2664
3250
  nextSessionPixelBytes += pixelBytes;
@@ -3033,10 +3619,6 @@ var socketClosing = 2;
3033
3619
  function exactStamp(left, right) {
3034
3620
  return left !== void 0 && compareRoutedSceneContent(left, right) === 0;
3035
3621
  }
3036
- function copyArrayBuffer(bytes) {
3037
- const copy = bytes.slice();
3038
- return copy.buffer;
3039
- }
3040
3622
  function capacitiesFitWithin(accepted, advertised) {
3041
3623
  return accepted.connectionCreditBytes <= advertised.connectionCreditBytes && accepted.perActivationCreditBytes <= advertised.perActivationCreditBytes && accepted.stagingBytesPerSession <= advertised.stagingBytesPerSession && accepted.stagingBytesTotal <= advertised.stagingBytesTotal && accepted.maxConcurrentActivations <= advertised.maxConcurrentActivations && accepted.maxConcurrentSeeds <= advertised.maxConcurrentSeeds;
3042
3624
  }
@@ -3344,13 +3926,13 @@ var RoutedFramesTransport = class {
3344
3926
  }
3345
3927
  }
3346
3928
  #incremental(connection, activation, header, payload, chargedBytes) {
3347
- if (payload.byteLength > connection.accepted.protocolLimits.maxPresentationChunkBytes || !header.resultContent || header.baseContent === void 0 || header.baseContent !== null && !exactStamp(activation.appliedContent, header.baseContent)) {
3929
+ if (payload.byteLength > connection.accepted.protocolLimits.maxUrgentPresentationUnitBytes || !header.resultContent || header.baseContent === void 0 || header.baseContent !== null && !exactStamp(activation.appliedContent, header.baseContent)) {
3348
3930
  this.#protocolFailure(connection, header.activationId, chargedBytes, "incremental base mismatch");
3349
3931
  return;
3350
3932
  }
3351
3933
  try {
3352
3934
  this.#emit({ type: "frames-state", activationId: header.activationId, state: "applying" });
3353
- this.#applyFrame(copyArrayBuffer(payload), activation.identity);
3935
+ this.#applyFrame(payload, activation.identity, void 0, header.resultContent.sceneEpoch.cellBootId);
3354
3936
  activation.appliedContent = header.resultContent;
3355
3937
  this.#returnCredit(connection, header.activationId, chargedBytes);
3356
3938
  this.#sceneApplied(connection, activation);
@@ -3430,7 +4012,12 @@ var RoutedFramesTransport = class {
3430
4012
  }
3431
4013
  try {
3432
4014
  this.#emit({ type: "frames-state", activationId: header.activationId, state: "applying" });
3433
- this.#applyFrame(staging.bytes.buffer, activation.identity, staging.targetLayout);
4015
+ this.#applyFrame(
4016
+ staging.bytes.buffer,
4017
+ activation.identity,
4018
+ staging.targetLayout,
4019
+ staging.resultContent.sceneEpoch.cellBootId
4020
+ );
3434
4021
  activation.appliedContent = staging.resultContent;
3435
4022
  this.#releaseTransfer(activation);
3436
4023
  this.#sceneApplied(connection, activation);
@@ -3635,7 +4222,10 @@ var MAX_PERFORMANCE_SAMPLES = 1e5;
3635
4222
  var FRAME_CREDIT_BATCH_BYTES = 4 * 1024 * 1024;
3636
4223
  var FRAME_CREDIT_MAX_DELAY_MS = 8;
3637
4224
  var snapshots = /* @__PURE__ */ new Map();
4225
+ var drawnSessions = /* @__PURE__ */ new Set();
4226
+ var routedMouseTracking = /* @__PURE__ */ new Map();
3638
4227
  var sharedGlyphDefinitions = /* @__PURE__ */ new Map();
4228
+ var nextRendererGlyphId = 1;
3639
4229
  var sharedGlyphPixelBytes = 0;
3640
4230
  var retainedStyleDefinitions = 0;
3641
4231
  var surfaces = /* @__PURE__ */ new Map();
@@ -3826,10 +4416,12 @@ function postToRenderer(message) {
3826
4416
  }
3827
4417
  function emptySessionSnapshot() {
3828
4418
  return {
4419
+ cols: 0,
3829
4420
  rows: [],
3830
4421
  nativeRows: [],
3831
4422
  nativeStyleRows: [],
3832
4423
  glyphDefinitions: /* @__PURE__ */ new Map(),
4424
+ glyphNamespace: "",
3833
4425
  glyphPixelBytes: 0,
3834
4426
  styleDefinitions: /* @__PURE__ */ new Map(),
3835
4427
  rowRevisions: [],
@@ -3839,7 +4431,8 @@ function emptySessionSnapshot() {
3839
4431
  sequence: 0n,
3840
4432
  awaitingResync: false,
3841
4433
  catalogFallback: false,
3842
- scrollbar: null
4434
+ scrollbar: null,
4435
+ linkSignature: ""
3843
4436
  };
3844
4437
  }
3845
4438
  function snapshot(sessionHandle) {
@@ -3852,29 +4445,34 @@ function snapshot(sessionHandle) {
3852
4445
  }
3853
4446
  function clearSessionCatalog(session) {
3854
4447
  for (const glyphId of session.glyphDefinitions.keys()) {
3855
- const shared = sharedGlyphDefinitions.get(glyphId);
4448
+ const key = `${session.glyphNamespace}:${glyphId}`;
4449
+ const shared = sharedGlyphDefinitions.get(key);
3856
4450
  if (!shared) continue;
3857
4451
  shared.references -= 1;
3858
4452
  if (shared.references <= 0) {
3859
- sharedGlyphDefinitions.delete(glyphId);
4453
+ sharedGlyphDefinitions.delete(key);
3860
4454
  sharedGlyphPixelBytes -= shared.definition.pixels.byteLength;
3861
4455
  }
3862
4456
  }
3863
- session.glyphDefinitions.clear();
4457
+ session.glyphDefinitions = /* @__PURE__ */ new Map();
3864
4458
  session.glyphPixelBytes = 0;
3865
4459
  retainedStyleDefinitions -= session.styleDefinitions.size;
3866
4460
  session.styleDefinitions.clear();
3867
4461
  }
3868
- function installGlyphDefinitions(session, definitions) {
4462
+ function installGlyphDefinitions(session, definitions, previousDefinitions) {
3869
4463
  for (const decoded of definitions) {
3870
4464
  if (session.glyphDefinitions.has(decoded.id)) continue;
3871
- let shared = sharedGlyphDefinitions.get(decoded.id);
4465
+ const key = `${session.glyphNamespace}:${decoded.id}`;
4466
+ let shared = sharedGlyphDefinitions.get(key);
3872
4467
  const pixelBytes = shared?.definition.pixels.byteLength ?? decoded.pixels.byteLength;
3873
4468
  if (shared) {
3874
4469
  shared.references += 1;
3875
4470
  } else {
3876
- shared = { definition: decoded, references: 1 };
3877
- sharedGlyphDefinitions.set(decoded.id, shared);
4471
+ shared = {
4472
+ definition: { ...decoded, id: previousDefinitions?.get(decoded.id)?.id ?? nextRendererGlyphId++ },
4473
+ references: 1
4474
+ };
4475
+ sharedGlyphDefinitions.set(key, shared);
3878
4476
  sharedGlyphPixelBytes += decoded.pixels.byteLength;
3879
4477
  }
3880
4478
  session.glyphDefinitions.set(decoded.id, shared.definition);
@@ -3892,6 +4490,10 @@ function dropSessionSnapshot(sessionHandle) {
3892
4490
  const session = snapshots.get(sessionHandle);
3893
4491
  if (session) clearSessionCatalog(session);
3894
4492
  snapshots.delete(sessionHandle);
4493
+ drawnSessions.delete(sessionHandle);
4494
+ }
4495
+ function rowShowsContent(row) {
4496
+ return row.glyphs.length > 0 || /\S/.test(row.text);
3895
4497
  }
3896
4498
  function defaults(sessionHandle) {
3897
4499
  let value = presentationDefaults.get(sessionHandle);
@@ -3941,6 +4543,7 @@ function deleteSurface(surfaceId) {
3941
4543
  }
3942
4544
  function renderView(session, presentation) {
3943
4545
  return {
4546
+ cols: session.cols,
3944
4547
  rows: session.rows,
3945
4548
  nativeRows: session.nativeRows,
3946
4549
  nativeStyleRows: session.nativeStyleRows,
@@ -4015,6 +4618,7 @@ async function recoverDevice(info) {
4015
4618
  try {
4016
4619
  const next = await rendererPromise;
4017
4620
  renderer = next;
4621
+ next.setPerformanceMeasurementEnabled?.(performanceMeasurement !== void 0);
4018
4622
  recoveryAttempts = 0;
4019
4623
  recovering = false;
4020
4624
  for (const [id, mount2] of mounts) {
@@ -4065,7 +4669,10 @@ function scheduleShaderAnimation() {
4065
4669
  }
4066
4670
  async function flush() {
4067
4671
  const backend = await ensureRenderer();
4068
- const ids = [...dirty].filter((id) => !occluded.has(id));
4672
+ const ids = [...dirty].filter((id) => {
4673
+ const size = mounts.get(id)?.size;
4674
+ return !occluded.has(id) && size !== void 0 && size.width > 0 && size.height > 0;
4675
+ });
4069
4676
  if (performanceMeasurement && ids.length > 0) {
4070
4677
  performanceMeasurement.scheduling.flushes += 1;
4071
4678
  appendPerformanceSample(performanceMeasurement.scheduling.panesPerFlush, ids.length);
@@ -4085,14 +4692,23 @@ async function flush() {
4085
4692
  const active = performanceMeasurement;
4086
4693
  const beforeRender = active ? performance.now() : 0;
4087
4694
  const metrics = backend.renderBatch ? backend.renderBatch(entries) : entries.map(({ id, view }) => backend.render(id, view));
4088
- for (const metric of metrics) recordLifetimeRenderMetrics(metric ?? emptyRenderMetrics());
4695
+ const submission = backend.submission?.();
4696
+ if (submission) {
4697
+ lifetimeRenderer.queueSubmits += submission.queueSubmits;
4698
+ lifetimeRenderer.presents += submission.surfaceIds.length;
4699
+ } else {
4700
+ for (const metric of metrics) recordLifetimeRenderMetrics(metric ?? emptyRenderMetrics());
4701
+ }
4702
+ const submitted = submission ? new Set(submission.surfaceIds) : void 0;
4089
4703
  const renderedAt = active ? performance.now() : 0;
4090
4704
  for (const { id } of entries) {
4705
+ if (submitted && !submitted.has(id)) continue;
4091
4706
  const damage = surfaces.get(id)?.damage;
4092
4707
  if (!damage) continue;
4093
4708
  damage.full = false;
4094
4709
  damage.rows.clear();
4095
4710
  damage.geometryChanged = false;
4711
+ delete damage.scrollRows;
4096
4712
  }
4097
4713
  if (active && performanceMeasurement === active) {
4098
4714
  active.scheduling.renderCalls += entries.length;
@@ -4131,6 +4747,7 @@ function invalidateFull(id) {
4131
4747
  damage.full = true;
4132
4748
  damage.rows.clear();
4133
4749
  damage.geometryChanged = true;
4750
+ delete damage.scrollRows;
4134
4751
  markDirty(id);
4135
4752
  }
4136
4753
  function invalidateRows(id, rows, geometryChanged = false) {
@@ -4152,15 +4769,35 @@ function invalidateRows(id, rows, geometryChanged = false) {
4152
4769
  function invalidateSessionFull(sessionHandle) {
4153
4770
  for (const surfaceId of surfaceIdsForSession(sessionHandle)) invalidateFull(surfaceId);
4154
4771
  }
4772
+ function invalidateScroll(id, shift, rows, rowCount) {
4773
+ const value = surfaces.get(id);
4774
+ if (!value) return;
4775
+ const damage = value.damage;
4776
+ if (!partialRenderingEnabled || damage.full || value.selection || Math.abs((damage.scrollRows ?? 0) + shift) >= rowCount) {
4777
+ invalidateFull(id);
4778
+ return;
4779
+ }
4780
+ shiftDamage(damage.rows, shift, rowCount);
4781
+ damage.scrollRows = (damage.scrollRows ?? 0) + shift;
4782
+ invalidateRows(id, rows, true);
4783
+ }
4784
+ function changeSelection(id, selection) {
4785
+ const value = surfaces.get(id);
4786
+ if (!value || value.selection === null && selection === null) return;
4787
+ const rows = selectionDamageRows(value.selection, selection, snapshots.get(value.sessionHandle)?.rows.length ?? 0);
4788
+ value.selection = selection;
4789
+ if (value.damage.scrollRows) invalidateFull(id);
4790
+ else invalidateRows(id, rows, true);
4791
+ }
4155
4792
  function invalidateSessionRows(sessionHandle, rows, geometryChanged = false) {
4156
4793
  const damagedRows = [...rows];
4157
4794
  for (const surfaceId of surfaceIdsForSession(sessionHandle)) invalidateRows(surfaceId, damagedRows, geometryChanged);
4158
4795
  }
4159
- async function mount(surfaceId, sessionHandle, canvas) {
4796
+ async function mount(surfaceId, sessionHandle, canvas, size) {
4160
4797
  if (mounts.has(surfaceId)) renderer?.unmount(surfaceId);
4161
4798
  snapshot(sessionHandle);
4162
4799
  surface(surfaceId, sessionHandle);
4163
- const entry = { canvas, size: { width: 1, height: 1, dpr: 1 }, sessionHandle };
4800
+ const entry = { canvas, size, sessionHandle };
4164
4801
  mounts.set(surfaceId, entry);
4165
4802
  const backend = await ensureRenderer();
4166
4803
  if (mounts.get(surfaceId) !== entry) return;
@@ -4169,7 +4806,7 @@ async function mount(surfaceId, sessionHandle, canvas) {
4169
4806
  invalidateFull(surfaceId);
4170
4807
  scheduleShaderAnimation();
4171
4808
  }
4172
- function applyFrame(packet, expectedIdentity, expectedLayout) {
4809
+ function applyFrame(packet, expectedIdentity, expectedLayout, glyphNamespace) {
4173
4810
  const active = performanceMeasurement;
4174
4811
  const applyStarted = active ? performance.now() : 0;
4175
4812
  lifetimeFrames.received += 1;
@@ -4266,11 +4903,15 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4266
4903
  throw new Error("routed transfer row section is not a full snapshot");
4267
4904
  }
4268
4905
  const glyphDefinitions = glyphSection ? decodeGlyphDefinitions(glyphSection) : NO_GLYPH_DEFINITIONS;
4269
- const styleDefinitions = styleSection ? decodeStyleDefinitions(styleSection) : NO_STYLE_DEFINITIONS;
4906
+ const paletteSection = frame.sections.find((candidate) => candidate.kind === SectionKind.StylePalette);
4907
+ const styleDefinitions = styleSection ? decodeStyleDefinitions(styleSection, paletteSection) : NO_STYLE_DEFINITIONS;
4270
4908
  const replacements = decodeRowReplacements(rowSection);
4271
4909
  for (const replacement of replacements) {
4272
4910
  if (replacement.row >= frame.rows) throw new RangeError("Row replacement exceeds viewport");
4273
4911
  }
4912
+ const firstDrawn = !drawnSessions.has(id) && replacements.some(rowShowsContent);
4913
+ const linkSection = frame.sections.find((candidate) => candidate.kind === SectionKind.LinkTargets);
4914
+ const links = linkSection ? decodeLinkTargets(linkSection, frame.cols, frame.rows) : [];
4274
4915
  const nextCursor = decodeCursorState(cursorSection);
4275
4916
  const clipboardText = clipboardSection ? decodeClipboardWrite(clipboardSection) : void 0;
4276
4917
  let scrollbar;
@@ -4289,13 +4930,21 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4289
4930
  throw new Error("transfer scrollback layout does not match TRF1");
4290
4931
  }
4291
4932
  if (active) active.frames.glyphDefinitions += glyphDefinitions.length;
4292
- const resetsCatalog = replacingScene || changedSession || completingResync || catalogReset;
4933
+ const namespace = glyphNamespace === void 0 ? `session:${id}:${frame.sessionEpoch}` : `cell:${glyphNamespace}`;
4934
+ const resetsCatalog = replacingScene || changedSession || completingResync || catalogReset || previous.glyphNamespace !== namespace;
4935
+ const styleChanged = styleDefinitions.some((definition) => {
4936
+ const old = previous.styleDefinitions.get(definition.id);
4937
+ return old !== void 0 && JSON.stringify(old) !== JSON.stringify(definition);
4938
+ });
4939
+ const canReuseRows = !resetsCatalog && !styleChanged && previous.cols === frame.cols && previous.rows.length === frame.rows && previous.layoutEpoch === frame.layoutEpoch;
4293
4940
  const wasCatalogFallback = previous.catalogFallback;
4294
4941
  if (resetsCatalog) {
4942
+ const previousDefinitions = previous.glyphNamespace === namespace ? previous.glyphDefinitions : void 0;
4295
4943
  previous.rows = [];
4296
4944
  previous.nativeRows = [];
4297
4945
  previous.nativeStyleRows = [];
4298
4946
  clearSessionCatalog(previous);
4947
+ previous.glyphNamespace = namespace;
4299
4948
  previous.rowRevisions = [];
4300
4949
  const catalogFits = glyphCatalogFits(
4301
4950
  previous.glyphDefinitions,
@@ -4303,7 +4952,8 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4303
4952
  sharedGlyphDefinitions,
4304
4953
  sharedGlyphPixelBytes,
4305
4954
  glyphDefinitions,
4306
- GLYPH_CATALOG_BUDGET
4955
+ GLYPH_CATALOG_BUDGET,
4956
+ (glyphId) => `${namespace}:${glyphId}`
4307
4957
  ) && definitionCatalogFits(
4308
4958
  previous.styleDefinitions,
4309
4959
  styleDefinitions,
@@ -4314,7 +4964,7 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4314
4964
  const admission = catalogAdmission(true, wasCatalogFallback, catalogFits);
4315
4965
  previous.catalogFallback = admission === "text-fallback";
4316
4966
  if (admission === "install") {
4317
- installGlyphDefinitions(previous, glyphDefinitions);
4967
+ installGlyphDefinitions(previous, glyphDefinitions, previousDefinitions);
4318
4968
  installStyleDefinitions(previous, styleDefinitions);
4319
4969
  } else if (!wasCatalogFallback) {
4320
4970
  postToRenderer({
@@ -4330,7 +4980,8 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4330
4980
  sharedGlyphDefinitions,
4331
4981
  sharedGlyphPixelBytes,
4332
4982
  glyphDefinitions,
4333
- GLYPH_CATALOG_BUDGET
4983
+ GLYPH_CATALOG_BUDGET,
4984
+ (glyphId) => `${namespace}:${glyphId}`
4334
4985
  ) && definitionCatalogFits(
4335
4986
  previous.styleDefinitions,
4336
4987
  styleDefinitions,
@@ -4370,7 +5021,7 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4370
5021
  const nativeRows = fullRows ? Array.from({ length: frame.rows }, () => []) : previous.nativeRows.slice();
4371
5022
  const nativeStyleRows = fullRows ? Array.from({ length: frame.rows }, () => []) : previous.nativeStyleRows.slice();
4372
5023
  const rowRevisions = fullRows ? Array(frame.rows).fill(0n) : previous.rowRevisions.slice();
4373
- const damagedRows = [];
5024
+ let damagedRows = [];
4374
5025
  if (active) active.frames.rowsDecoded += replacements.length;
4375
5026
  for (const replacement of replacements) {
4376
5027
  if (replacement.revision < (rowRevisions[replacement.row] ?? 0n)) continue;
@@ -4380,11 +5031,24 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4380
5031
  rowRevisions[replacement.row] = replacement.revision;
4381
5032
  damagedRows.push(replacement.row);
4382
5033
  }
5034
+ let scrollRows = 0;
5035
+ if (fullRows && canReuseRows) {
5036
+ const damage = rowDamage(previous, { rows, nativeRows, nativeStyleRows });
5037
+ scrollRows = damage.scrollRows;
5038
+ damagedRows = damage.rows;
5039
+ const changed = new Set(damagedRows);
5040
+ for (let row = 0; row < rows.length; row++) {
5041
+ if (changed.has(row)) continue;
5042
+ nativeRows[row] = previous.nativeRows[row + scrollRows];
5043
+ nativeStyleRows[row] = previous.nativeStyleRows[row + scrollRows];
5044
+ }
5045
+ }
5046
+ previous.cols = frame.cols;
4383
5047
  previous.rows = rows;
4384
5048
  previous.nativeRows = nativeRows;
4385
5049
  previous.nativeStyleRows = nativeStyleRows;
4386
5050
  previous.rowRevisions = rowRevisions;
4387
- const geometryChanged = damagedRows.length > 0;
5051
+ const geometryChanged = damagedRows.length > 0 || scrollRows !== 0;
4388
5052
  const previousCursor = previous.cursor;
4389
5053
  const cursorChanged = nextCursor.x !== previousCursor.x || nextCursor.y !== previousCursor.y || nextCursor.visible !== previousCursor.visible || nextCursor.style !== previousCursor.style || nextCursor.blinking !== previousCursor.blinking;
4390
5054
  previous.cursor = nextCursor;
@@ -4396,15 +5060,22 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4396
5060
  snapshots.set(id, previous);
4397
5061
  if (installedScene) clearSessionCatalog(installedScene);
4398
5062
  }
5063
+ const linkSignature = JSON.stringify(links);
5064
+ if (previous.linkSignature !== linkSignature || changedSession || completingResync) {
5065
+ previous.linkSignature = linkSignature;
5066
+ postToRenderer({ type: "link-targets", sessionHandle: id, links });
5067
+ }
4399
5068
  if (clipboardText !== void 0) postToRenderer({ type: "clipboard-write", text: clipboardText });
4400
5069
  if (scrollbarChanged && scrollbar) postToRenderer({ type: "scrollbar-state", sessionHandle: id, scrollbar });
4401
5070
  if (completingResync) postToRenderer({ type: "frame-resync-complete", sessionHandle: id });
4402
- const requiresFullRedraw = fullRows || changedSession || completingResync;
4403
- if (!requiresFullRedraw && cursorChanged) damagedRows.push(previousCursor.y, nextCursor.y);
5071
+ const requiresFullRedraw = !canReuseRows;
5072
+ if (!requiresFullRedraw && (cursorChanged || scrollRows !== 0))
5073
+ damagedRows.push(previousCursor.y - scrollRows, nextCursor.y);
4404
5074
  const hasRowDamage = damagedRows.length > 0;
4405
5075
  for (const surfaceId of surfaceIdsForSession(id)) {
4406
5076
  if (cursorChanged) scheduleCursorBlink(surfaceId, true);
4407
5077
  if (requiresFullRedraw) invalidateFull(surfaceId);
5078
+ else if (scrollRows !== 0) invalidateScroll(surfaceId, scrollRows, damagedRows, rows.length);
4408
5079
  else if (hasRowDamage) invalidateRows(surfaceId, damagedRows, geometryChanged);
4409
5080
  }
4410
5081
  postToRenderer({
@@ -4417,6 +5088,17 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4417
5088
  // recovered screen apart from a partial update of the stale one.
4418
5089
  fullSnapshot: fullFrame
4419
5090
  });
5091
+ if (expectedIdentity) {
5092
+ const mouseTracking = (frame.flags & FrameFlag.MouseTracking) !== 0;
5093
+ if (routedMouseTracking.get(id) !== mouseTracking) {
5094
+ routedMouseTracking.set(id, mouseTracking);
5095
+ postToRenderer({ type: "terminal-modes", sessionHandle: id, mouseTracking });
5096
+ }
5097
+ }
5098
+ if (firstDrawn) {
5099
+ drawnSessions.add(id);
5100
+ postToRenderer({ type: "session-drawn", sessionHandle: id });
5101
+ }
4420
5102
  lifetimeFrames.applies += 1;
4421
5103
  counters.applies += 1;
4422
5104
  if (active) appendPerformanceSample(active.samples.frameApplyMs, performance.now() - applyStarted);
@@ -4428,8 +5110,8 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4428
5110
  };
4429
5111
  }
4430
5112
  var routedFrames = new RoutedFramesTransport({
4431
- applyFrame: (packet, identity, expectedLayout) => {
4432
- const applied = applyFrame(packet, identity, expectedLayout);
5113
+ applyFrame: (packet, identity, expectedLayout, glyphNamespace) => {
5114
+ const applied = applyFrame(packet, identity, expectedLayout, glyphNamespace);
4433
5115
  if (!applied) throw new Error("routed TRF1 frame was not applied");
4434
5116
  return applied;
4435
5117
  },
@@ -4443,7 +5125,7 @@ self.onmessage = (event) => {
4443
5125
  const message = event.data;
4444
5126
  try {
4445
5127
  if (message.type === "mount") {
4446
- void mount(message.surfaceId, message.sessionHandle, message.canvas).catch((error) => {
5128
+ void mount(message.surfaceId, message.sessionHandle, message.canvas, message.size).catch((error) => {
4447
5129
  console.error("[terminal-renderer] mount failed", error);
4448
5130
  });
4449
5131
  } else if (message.type === "renderer-config") {
@@ -4526,13 +5208,12 @@ self.onmessage = (event) => {
4526
5208
  scheduleShaderAnimation();
4527
5209
  } else if (message.type === "selection") {
4528
5210
  if (message.surfaceId) {
4529
- surface(message.surfaceId, message.sessionHandle).selection = message.selection;
4530
- invalidateFull(message.surfaceId);
5211
+ surface(message.surfaceId, message.sessionHandle);
5212
+ changeSelection(message.surfaceId, message.selection);
4531
5213
  } else {
4532
5214
  defaults(message.sessionHandle).selection = message.selection;
4533
5215
  for (const surfaceId of surfaceIdsForSession(message.sessionHandle)) {
4534
- surfaces.get(surfaceId).selection = message.selection;
4535
- invalidateFull(surfaceId);
5216
+ changeSelection(surfaceId, message.selection);
4536
5217
  }
4537
5218
  }
4538
5219
  } else if (message.type === "visibility") {
@@ -4598,6 +5279,7 @@ self.onmessage = (event) => {
4598
5279
  } else if (message.type === "performance-counters") {
4599
5280
  postToRenderer({ type: "performance-counters", requestId: message.requestId, snapshot: counterSnapshot() });
4600
5281
  } else if (message.type === "routed-frames-attach") {
5282
+ routedMouseTracking.delete(message.request.sessionHandle);
4601
5283
  routedFrames.attach(message.request);
4602
5284
  } else if (message.type === "routed-frames-detach") {
4603
5285
  routedFrames.detach(message.activationId);