@vibecook/ghosttea-react 0.12.0 → 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 (67) hide show
  1. package/README.md +59 -4
  2. package/dist/TerminalSurface.d.ts +53 -0
  3. package/dist/TerminalSurface.d.ts.map +1 -1
  4. package/dist/TerminalSurface.js +76 -9
  5. package/dist/TerminalSurface.js.map +1 -1
  6. package/dist/appearance/AppearanceSettings.d.ts +2 -0
  7. package/dist/appearance/AppearanceSettings.d.ts.map +1 -1
  8. package/dist/appearance/AppearanceSettings.js +12 -2
  9. package/dist/appearance/AppearanceSettings.js.map +1 -1
  10. package/dist/appearance/types.d.ts +4 -0
  11. package/dist/appearance/types.d.ts.map +1 -1
  12. package/dist/catalog-budget.d.ts +2 -2
  13. package/dist/catalog-budget.d.ts.map +1 -1
  14. package/dist/catalog-budget.js +2 -2
  15. package/dist/catalog-budget.js.map +1 -1
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +2 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/renderers/glyph-atlas.d.ts +43 -0
  24. package/dist/renderers/glyph-atlas.d.ts.map +1 -0
  25. package/dist/renderers/glyph-atlas.js +175 -0
  26. package/dist/renderers/glyph-atlas.js.map +1 -0
  27. package/dist/renderers/light-adaptation.d.ts +36 -0
  28. package/dist/renderers/light-adaptation.d.ts.map +1 -0
  29. package/dist/renderers/light-adaptation.js +332 -0
  30. package/dist/renderers/light-adaptation.js.map +1 -0
  31. package/dist/renderers/types.d.ts +21 -0
  32. package/dist/renderers/types.d.ts.map +1 -1
  33. package/dist/renderers/types.js.map +1 -1
  34. package/dist/renderers/webgpu-renderer.d.ts +4 -0
  35. package/dist/renderers/webgpu-renderer.d.ts.map +1 -1
  36. package/dist/renderers/webgpu-renderer.js +321 -237
  37. package/dist/renderers/webgpu-renderer.js.map +1 -1
  38. package/dist/routed-control.d.ts +26 -2
  39. package/dist/routed-control.d.ts.map +1 -1
  40. package/dist/routed-control.js +145 -27
  41. package/dist/routed-control.js.map +1 -1
  42. package/dist/routed-frames.d.ts +1 -1
  43. package/dist/routed-frames.d.ts.map +1 -1
  44. package/dist/routed-frames.js +3 -7
  45. package/dist/routed-frames.js.map +1 -1
  46. package/dist/row-damage.d.ts +21 -0
  47. package/dist/row-damage.d.ts.map +1 -0
  48. package/dist/row-damage.js +76 -0
  49. package/dist/row-damage.js.map +1 -0
  50. package/dist/runtime.d.ts +92 -3
  51. package/dist/runtime.d.ts.map +1 -1
  52. package/dist/runtime.js +439 -43
  53. package/dist/runtime.js.map +1 -1
  54. package/dist/styles.css +11 -0
  55. package/dist/terminal-render.worker.js +908 -288
  56. package/dist/terminal-render.worker.js.map +4 -4
  57. package/dist/worker-messages.d.ts +22 -1
  58. package/dist/worker-messages.d.ts.map +1 -1
  59. package/dist/workspace/Workspace.d.ts +15 -2
  60. package/dist/workspace/Workspace.d.ts.map +1 -1
  61. package/dist/workspace/Workspace.js +36 -12
  62. package/dist/workspace/Workspace.js.map +1 -1
  63. package/dist/workspace/pane-layout.d.ts +3 -1
  64. package/dist/workspace/pane-layout.d.ts.map +1 -1
  65. package/dist/workspace/pane-layout.js +16 -4
  66. package/dist/workspace/pane-layout.js.map +1 -1
  67. package/package.json +4 -4
@@ -23,6 +23,7 @@ var SectionKind;
23
23
  SectionKind2[SectionKind2["AccessibilityText"] = 10] = "AccessibilityText";
24
24
  SectionKind2[SectionKind2["ClipboardWrite"] = 11] = "ClipboardWrite";
25
25
  SectionKind2[SectionKind2["LinkTargets"] = 12] = "LinkTargets";
26
+ SectionKind2[SectionKind2["StylePalette"] = 13] = "StylePalette";
26
27
  })(SectionKind || (SectionKind = {}));
27
28
  var GlyphFormat;
28
29
  (function(GlyphFormat2) {
@@ -41,14 +42,15 @@ function assertRange(condition, message) {
41
42
  throw new RangeError(`Invalid terminal frame: ${message}`);
42
43
  }
43
44
  function decodeFrame(buffer) {
44
- assertRange(buffer.byteLength >= FRAME_HEADER_BYTES, "truncated header");
45
- assertRange(buffer.byteLength <= MAX_FRAME_BYTES, "packet exceeds limit");
46
- 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);
47
49
  assertRange(view.getUint32(0, true) === FRAME_MAGIC, "bad magic");
48
50
  assertRange(view.getUint16(4, true) === FRAME_PROTOCOL_VERSION, "unsupported protocol version");
49
51
  const sectionCount = view.getUint16(60, true);
50
52
  const tableEnd = FRAME_HEADER_BYTES + sectionCount * SECTION_HEADER_BYTES;
51
- assertRange(tableEnd <= buffer.byteLength, "truncated section table");
53
+ assertRange(tableEnd <= bytes.byteLength, "truncated section table");
52
54
  const sections = [];
53
55
  for (let index = 0; index < sectionCount; index += 1) {
54
56
  const base = FRAME_HEADER_BYTES + index * SECTION_HEADER_BYTES;
@@ -58,8 +60,8 @@ function decodeFrame(buffer) {
58
60
  const length = view.getUint32(base + 8, true);
59
61
  const itemCount = view.getUint32(base + 12, true);
60
62
  const end = offset + length;
61
- assertRange(end >= offset && offset >= tableEnd && end <= buffer.byteLength, "section out of bounds");
62
- 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) });
63
65
  }
64
66
  return {
65
67
  protocolVersion: view.getUint16(4, true),
@@ -152,14 +154,14 @@ function decodeGlyphDefinitions(section) {
152
154
  bearingX: view.getInt16(offset - 12, true),
153
155
  bearingY: view.getInt16(offset - 10, true),
154
156
  format,
155
- pixels: section.bytes.slice(offset, offset + pixelLength)
157
+ pixels: new Uint8Array(section.bytes.subarray(offset, offset + pixelLength))
156
158
  });
157
159
  offset += pixelLength;
158
160
  }
159
161
  assertRange(offset === section.bytes.byteLength, "trailing glyph definition bytes");
160
162
  return definitions;
161
163
  }
162
- function decodeStyleDefinitions(section) {
164
+ function decodeStyleDefinitions(section, paletteSection) {
163
165
  assertRange(section.kind === SectionKind.StyleDefinitions, "wrong style definition section kind");
164
166
  assertRange(section.bytes.byteLength >= 4, "truncated style definition header");
165
167
  const view = new DataView(section.bytes.buffer, section.bytes.byteOffset, section.bytes.byteLength);
@@ -188,7 +190,39 @@ function decodeStyleDefinitions(section) {
188
190
  ...backgroundKind === 1 ? { background: [view.getUint8(offset + 11), view.getUint8(offset + 12), view.getUint8(offset + 13)] } : {}
189
191
  });
190
192
  }
191
- 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;
192
226
  }
193
227
  function decodeCursorState(section) {
194
228
  assertRange(section.kind === SectionKind.CursorState, "wrong cursor section kind");
@@ -688,8 +722,418 @@ fn sparks(position: vec2f, uv: vec2f) -> vec4f {
688
722
  `
689
723
  );
690
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
+
691
1136
  // src/renderers/webgpu-renderer.ts
692
- var ATLAS_SIZE = 2048;
693
1137
  var GEOMETRY_CACHE_LIMIT = 8;
694
1138
  var FONT_STACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
695
1139
  var EFFECT_MODES = {
@@ -870,88 +1314,37 @@ var DynamicVertexBuffer = class {
870
1314
  var FallbackGlyphAtlas = class {
871
1315
  constructor(device, bindGroupLayout) {
872
1316
  this.device = device;
873
- this.#texture = device.createTexture({
874
- label: "terminal glyph atlas",
875
- size: [ATLAS_SIZE, ATLAS_SIZE],
876
- format: "rgba8unorm",
877
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
878
- });
879
- this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
880
- this.#bindGroup = device.createBindGroup({
881
- label: "terminal glyph atlas bindings",
882
- layout: bindGroupLayout,
883
- entries: [
884
- { binding: 0, resource: this.#texture.createView() },
885
- { binding: 1, resource: this.#sampler }
886
- ]
887
- });
1317
+ this.pages = new GlyphAtlasPages(device, bindGroupLayout, "terminal diagnostic glyph atlas", "rgba8unorm", 1);
888
1318
  }
889
- #texture;
890
- #sampler;
891
- #bindGroup;
892
- #cache = /* @__PURE__ */ new Map();
1319
+ pages;
893
1320
  #scratch = new OffscreenCanvas(1, 1);
894
- #x = 1;
895
- #y = 1;
896
- #rowHeight = 0;
897
1321
  #uploadBytes = 0;
898
1322
  #uploadCalls = 0;
899
- #generation = 0;
900
1323
  get bindGroup() {
901
- return this.#bindGroup;
1324
+ return this.pages.bindGroup;
902
1325
  }
903
1326
  get generation() {
904
- return this.#generation;
1327
+ return this.pages.generation;
905
1328
  }
906
1329
  prepare(values, dpr) {
907
1330
  const scale = Math.max(1, Math.min(3, dpr));
908
- const all = [...new Set(values)].filter((value) => value.trim().length > 0);
909
- const missing = all.filter((value) => !this.#cache.has(`${scale.toFixed(2)}:${value}`));
910
- if (this.#fits(missing, scale, this.#x, this.#y, this.#rowHeight)) return;
911
- this.#cache.clear();
912
- this.#generation += 1;
913
- this.#x = 1;
914
- this.#y = 1;
915
- this.#rowHeight = 0;
916
- if (!this.#fits(all, scale, this.#x, this.#y, this.#rowHeight)) {
917
- throw new Error("The diagnostic glyph atlas cannot fit the text visible in one frame");
918
- }
919
- }
920
- #fits(values, scale, startX, startY, startRowHeight) {
921
- let x = startX;
922
- let y = startY;
923
- let rowHeight = startRowHeight;
924
- for (const value of values) {
925
- const width = Math.max(1, Math.ceil(CELL_WIDTH * graphemeCellWidth(value) * scale));
926
- const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
927
- if (x + width + 1 > ATLAS_SIZE) {
928
- x = 1;
929
- y += rowHeight + 1;
930
- rowHeight = 0;
931
- }
932
- if (y + height + 1 > ATLAS_SIZE) return false;
933
- x += width + 1;
934
- rowHeight = Math.max(rowHeight, height);
935
- }
936
- 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
+ );
937
1338
  }
938
1339
  glyph(grapheme, dpr) {
939
1340
  if (grapheme.trim().length === 0) return void 0;
940
1341
  const scale = Math.max(1, Math.min(3, dpr));
941
1342
  const cells = graphemeCellWidth(grapheme);
942
1343
  const key = `${scale.toFixed(2)}:${grapheme}`;
943
- const cached = this.#cache.get(key);
1344
+ const cached = this.pages.get(key);
944
1345
  if (cached) return cached;
945
1346
  const width = Math.max(1, Math.ceil(CELL_WIDTH * cells * scale));
946
1347
  const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
947
- if (this.#x + width + 1 > ATLAS_SIZE) {
948
- this.#x = 1;
949
- this.#y += this.#rowHeight + 1;
950
- this.#rowHeight = 0;
951
- }
952
- if (this.#y + height + 1 > ATLAS_SIZE) {
953
- throw new Error("Temporary glyph atlas exhausted");
954
- }
955
1348
  this.#scratch.width = width;
956
1349
  this.#scratch.height = height;
957
1350
  const context = this.#scratch.getContext("2d", { willReadFrequently: true });
@@ -962,28 +1355,17 @@ var FallbackGlyphAtlas = class {
962
1355
  context.textBaseline = "top";
963
1356
  context.fillText(grapheme, 0, Math.max(0, Math.floor((height - 15 * scale) / 2)));
964
1357
  const image = context.getImageData(0, 0, width, height);
965
- this.device.queue.writeTexture(
966
- { texture: this.#texture, origin: [this.#x, this.#y] },
967
- image.data,
968
- { bytesPerRow: width * 4, rowsPerImage: height },
969
- [width, height]
970
- );
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
+ ]);
971
1363
  this.#uploadBytes += image.data.byteLength;
972
1364
  this.#uploadCalls += 1;
973
- const location = {
974
- u0: this.#x / ATLAS_SIZE,
975
- v0: this.#y / ATLAS_SIZE,
976
- u1: (this.#x + width) / ATLAS_SIZE,
977
- v1: (this.#y + height) / ATLAS_SIZE,
978
- cells
979
- };
980
- this.#cache.set(key, location);
981
- this.#x += width + 1;
982
- this.#rowHeight = Math.max(this.#rowHeight, height);
983
1365
  return location;
984
1366
  }
985
1367
  destroy() {
986
- this.#texture.destroy();
1368
+ this.pages.destroy();
987
1369
  }
988
1370
  uploadMetrics() {
989
1371
  return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
@@ -994,106 +1376,44 @@ var NativeGlyphAtlas = class {
994
1376
  this.device = device;
995
1377
  this.label = label;
996
1378
  this.format = format;
997
- this.#texture = device.createTexture({
998
- label,
999
- size: [ATLAS_SIZE, ATLAS_SIZE],
1000
- format,
1001
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
1002
- });
1003
- this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
1004
- this.#bindGroup = device.createBindGroup({
1005
- label: `${label} bindings`,
1006
- layout: bindGroupLayout,
1007
- entries: [
1008
- { binding: 0, resource: this.#texture.createView() },
1009
- { binding: 1, resource: this.#sampler }
1010
- ]
1011
- });
1379
+ this.pages = new GlyphAtlasPages(device, bindGroupLayout, label, format, format === "r8unorm" ? 4 : 2);
1012
1380
  }
1013
- #texture;
1014
- #sampler;
1015
- #bindGroup;
1016
- #cache = /* @__PURE__ */ new Map();
1017
- #x = 1;
1018
- #y = 1;
1019
- #rowHeight = 0;
1381
+ pages;
1020
1382
  #uploadBytes = 0;
1021
1383
  #uploadCalls = 0;
1022
- #generation = 0;
1023
1384
  get bindGroup() {
1024
- return this.#bindGroup;
1385
+ return this.pages.bindGroup;
1025
1386
  }
1026
1387
  get generation() {
1027
- return this.#generation;
1388
+ return this.pages.generation;
1028
1389
  }
1029
1390
  prepare(definitions) {
1030
- const all = /* @__PURE__ */ new Map();
1031
- const missing = /* @__PURE__ */ new Map();
1032
- for (const definition of definitions) {
1033
- all.set(definition.id, definition);
1034
- if (!this.#cache.has(definition.id)) missing.set(definition.id, definition);
1035
- }
1036
- if (this.#fits(missing.values(), this.#x, this.#y, this.#rowHeight)) return;
1037
- this.#cache.clear();
1038
- this.#generation += 1;
1039
- this.#x = 1;
1040
- this.#y = 1;
1041
- this.#rowHeight = 0;
1042
- if (!this.#fits(all.values(), this.#x, this.#y, this.#rowHeight)) {
1043
- throw new Error(`${this.label} cannot fit the glyphs visible in one frame`);
1044
- }
1045
- }
1046
- #fits(definitions, startX, startY, startRowHeight) {
1047
- let x = startX;
1048
- let y = startY;
1049
- let rowHeight = startRowHeight;
1050
- for (const { width, height } of definitions) {
1051
- if (width + 2 > ATLAS_SIZE || height + 2 > ATLAS_SIZE) return false;
1052
- if (x + width + 1 > ATLAS_SIZE) {
1053
- x = 1;
1054
- y += rowHeight + 1;
1055
- rowHeight = 0;
1056
- }
1057
- if (y + height + 1 > ATLAS_SIZE) return false;
1058
- x += width + 1;
1059
- rowHeight = Math.max(rowHeight, height);
1060
- }
1061
- 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
+ );
1062
1398
  }
1063
1399
  glyph(definition) {
1064
- const cached = this.#cache.get(definition.id);
1400
+ const cached = this.pages.get(definition.id);
1065
1401
  if (cached) return cached;
1066
1402
  const { width, height } = definition;
1067
- if (this.#x + width + 1 > ATLAS_SIZE) {
1068
- this.#x = 1;
1069
- this.#y += this.#rowHeight + 1;
1070
- this.#rowHeight = 0;
1071
- }
1072
- if (this.#y + height + 1 > ATLAS_SIZE) throw new Error(`${this.label} exhausted`);
1073
- const pixels = definition.pixels;
1403
+ const { location, texture, origin } = this.pages.insert(definition.id, width, height, 0);
1074
1404
  const bytesPerPixel = this.format === "r8unorm" ? 1 : 4;
1075
1405
  this.device.queue.writeTexture(
1076
- { texture: this.#texture, origin: [this.#x, this.#y] },
1077
- pixels,
1406
+ { texture, origin },
1407
+ definition.pixels,
1078
1408
  { bytesPerRow: width * bytesPerPixel, rowsPerImage: height },
1079
1409
  [width, height]
1080
1410
  );
1081
- this.#uploadBytes += pixels.byteLength;
1411
+ this.#uploadBytes += definition.pixels.byteLength;
1082
1412
  this.#uploadCalls += 1;
1083
- const location = {
1084
- u0: this.#x / ATLAS_SIZE,
1085
- v0: this.#y / ATLAS_SIZE,
1086
- u1: (this.#x + width) / ATLAS_SIZE,
1087
- v1: (this.#y + height) / ATLAS_SIZE,
1088
- cells: 0
1089
- };
1090
- this.#cache.set(definition.id, location);
1091
- this.#x += width + 1;
1092
- this.#rowHeight = Math.max(this.#rowHeight, height);
1093
1413
  return location;
1094
1414
  }
1095
1415
  destroy() {
1096
- this.#texture.destroy();
1416
+ this.pages.destroy();
1097
1417
  }
1098
1418
  uploadMetrics() {
1099
1419
  return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
@@ -1120,7 +1440,7 @@ function createCachedVertexBuffer(device, label, data) {
1120
1440
  device.queue.writeBuffer(buffer, 0, data);
1121
1441
  return buffer;
1122
1442
  }
1123
- function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
1443
+ function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations, adaptationKey) {
1124
1444
  const selection = view.selection ? `${view.selection.anchor.row},${view.selection.anchor.column},${view.selection.focus.row},${view.selection.focus.column}` : "-";
1125
1445
  return [
1126
1446
  view.sessionEpoch.toString(),
@@ -1137,6 +1457,8 @@ function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
1137
1457
  ...view.theme.selectionForeground
1138
1458
  ].join(","),
1139
1459
  view.theme.backgroundOpacityCells ? "opacity-cells" : "opaque-cells",
1460
+ view.theme.minimumContrast ?? 1,
1461
+ adaptationKey,
1140
1462
  selection,
1141
1463
  effectiveCursorStyle(view) === CursorStyle.Block ? `${view.cursor.x},${view.cursor.y}` : "-",
1142
1464
  atlasGenerations.join(",")
@@ -1351,21 +1673,45 @@ function pushBlockCursorBackground(vertices, view, renderRows, scale, viewportWi
1351
1673
  viewportHeight
1352
1674
  );
1353
1675
  }
1354
- function resolveStyle(style, theme) {
1676
+ function resolveStyle(style, theme, adaptation = null) {
1355
1677
  let foreground = style?.foreground ? rgb(style.foreground) : theme.foreground;
1356
1678
  let background = style?.background ? rgb(style.background) : null;
1357
- 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) {
1358
1692
  const originalForeground = foreground;
1359
1693
  foreground = background ?? theme.background;
1360
1694
  background = originalForeground;
1695
+ fill = foreground;
1361
1696
  }
1362
1697
  if (background && theme.backgroundOpacityCells) {
1363
1698
  background = [background[0], background[1], background[2], theme.background[3]];
1364
1699
  }
1365
- 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
+ }
1366
1710
  return {
1367
1711
  foreground,
1368
1712
  background,
1713
+ fill,
1714
+ text,
1369
1715
  underline: style?.underline ?? false,
1370
1716
  strikethrough: style?.strikethrough ?? false,
1371
1717
  invisible: style?.invisible ?? false
@@ -1668,7 +2014,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1668
2014
  #monoAtlas;
1669
2015
  #colorAtlas;
1670
2016
  #fallbackAtlas;
2017
+ #adaptations = /* @__PURE__ */ new Map();
1671
2018
  #performanceMeasurementEnabled = false;
2019
+ #submission = { queueSubmits: 0, surfaceIds: [] };
2020
+ #scrollScratch;
1672
2021
  static async create(onLost) {
1673
2022
  if (!navigator.gpu) throw new Error("WebGPU is unavailable");
1674
2023
  const adapter = await navigator.gpu.requestAdapter();
@@ -1708,7 +2057,11 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1708
2057
  cursorBuffer: new DynamicVertexBuffer(this.device, `cursor ${id}`),
1709
2058
  geometryCache: /* @__PURE__ */ new Map(),
1710
2059
  geometryCandidates: /* @__PURE__ */ new Map(),
1711
- sceneValid: false
2060
+ sceneValid: false,
2061
+ adaptation: null,
2062
+ sceneHadSelection: false,
2063
+ sceneHadOverhang: false,
2064
+ sceneCursorRow: null
1712
2065
  };
1713
2066
  this.#surfaces.set(id, surface2);
1714
2067
  this.#configure(surface2);
@@ -1727,6 +2080,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1727
2080
  surface2.cursorBuffer.destroy();
1728
2081
  clearGeometryCache(surface2);
1729
2082
  this.#surfaces.delete(id);
2083
+ if (this.#surfaces.size === 0) {
2084
+ this.#scrollScratch?.destroy();
2085
+ this.#scrollScratch = void 0;
2086
+ }
1730
2087
  }
1731
2088
  resize(id, size) {
1732
2089
  const surface2 = this.#surfaces.get(id);
@@ -1755,7 +2112,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1755
2112
  label: "terminal scene before Ghostty custom shaders",
1756
2113
  size: [Math.max(1, width), Math.max(1, height)],
1757
2114
  format: this.format,
1758
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
2115
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST
1759
2116
  });
1760
2117
  }
1761
2118
  #createEffectTexture(width, height, suffix) {
@@ -1865,32 +2222,186 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1865
2222
  drawCalls: effectPassCount
1866
2223
  };
1867
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
+ }
1868
2276
  render(id, view) {
1869
- const surface2 = this.#surfaces.get(id);
1870
- if (!surface2) return;
1871
- const encoder = this.device.createCommandEncoder({ label: `terminal frame ${id}` });
1872
- const metrics = view.damage?.geometryChanged === false ? this.#encodeCachedRender(id, view, encoder) : this.#encodeRender(id, view, encoder);
1873
- this.device.queue.submit([encoder.finish()]);
1874
- if (metrics) metrics.queueSubmits = 1;
1875
- 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
+ );
1876
2321
  }
1877
2322
  renderBatch(entries) {
1878
- const active = entries.filter(({ id }) => this.#surfaces.has(id));
1879
- if (active.length === 0) return entries.map(() => void 0);
1880
- 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 = [];
1881
2337
  const byId = /* @__PURE__ */ new Map();
1882
- for (const { id, view } of active) {
1883
- byId.set(
1884
- id,
1885
- view.damage?.geometryChanged === false ? this.#encodeCachedRender(id, view, encoder) : this.#encodeRender(id, view, encoder)
1886
- );
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();
1887
2401
  }
1888
- this.device.queue.submit([encoder.finish()]);
1889
- const firstMetrics = [...byId.values()].find((metrics) => metrics !== void 0);
1890
- if (firstMetrics) firstMetrics.queueSubmits = 1;
1891
2402
  return entries.map(({ id }) => byId.get(id));
1892
2403
  }
1893
- #buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight) {
2404
+ #buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight, adaptation) {
1894
2405
  const rectangleVertices = [];
1895
2406
  const glyphVertices = [];
1896
2407
  const colorGlyphVertices = [];
@@ -1899,24 +2410,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1899
2410
  const styleFor = (styleId) => {
1900
2411
  const cached = resolvedStyles.get(styleId);
1901
2412
  if (cached) return cached;
1902
- const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
2413
+ const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme, adaptation);
1903
2414
  resolvedStyles.set(styleId, resolved);
1904
2415
  return resolved;
1905
2416
  };
1906
- const monoDefinitions = /* @__PURE__ */ new Map();
1907
- const colorDefinitions = /* @__PURE__ */ new Map();
1908
- for (const row of renderRows) {
1909
- for (const glyph of view.nativeRows[row] ?? []) {
1910
- const definition = view.glyphDefinitions.get(glyph.glyphId);
1911
- if (!definition) continue;
1912
- (definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(
1913
- definition.id,
1914
- definition
1915
- );
1916
- }
1917
- }
1918
- this.#monoAtlas.prepare(monoDefinitions.values());
1919
- this.#colorAtlas.prepare(colorDefinitions.values());
1920
2417
  if (!fullRedraw) {
1921
2418
  for (const row of renderRows) {
1922
2419
  const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
@@ -1974,12 +2471,6 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1974
2471
  }
1975
2472
  pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
1976
2473
  const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
1977
- if (!hasNativeRows) {
1978
- this.#fallbackAtlas.prepare(
1979
- renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")),
1980
- scale
1981
- );
1982
- }
1983
2474
  if (hasNativeRows) {
1984
2475
  for (const row of renderRows) {
1985
2476
  const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
@@ -2010,20 +2501,22 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2010
2501
  if (blockElement) {
2011
2502
  let backdrop = style.background ?? view.theme.background;
2012
2503
  if (isSelected) backdrop = view.theme.selection;
2504
+ const blockForeground = foreground === style.foreground ? style.fill : foreground;
2013
2505
  if (pushBlockElement(
2014
2506
  rectangleVertices,
2015
2507
  blockElement,
2016
2508
  instance.cellStart,
2017
2509
  row,
2018
2510
  scale,
2019
- over(foreground, backdrop),
2511
+ over(blockForeground, backdrop),
2020
2512
  viewportWidth,
2021
2513
  viewportHeight
2022
2514
  ))
2023
2515
  continue;
2024
2516
  }
2025
- const atlas = definition.format === GlyphFormat.Alpha8 ? this.#monoAtlas : this.#colorAtlas;
2026
- 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;
2027
2520
  pushGlyph(
2028
2521
  vertices,
2029
2522
  (ORIGIN_X + instance.x) * scale,
@@ -2031,7 +2524,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2031
2524
  instance.width * scale,
2032
2525
  instance.height * scale,
2033
2526
  atlas.glyph(definition),
2034
- foreground,
2527
+ mono && foreground === style.foreground ? style.text : foreground,
2035
2528
  viewportWidth,
2036
2529
  viewportHeight
2037
2530
  );
@@ -2076,7 +2569,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2076
2569
  Math.round(rowTop + 16 * scale),
2077
2570
  width,
2078
2571
  stroke,
2079
- style.foreground,
2572
+ style.text,
2080
2573
  viewportWidth,
2081
2574
  viewportHeight
2082
2575
  );
@@ -2088,7 +2581,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2088
2581
  Math.round(rowTop + 9 * scale),
2089
2582
  width,
2090
2583
  stroke,
2091
- style.foreground,
2584
+ style.text,
2092
2585
  viewportWidth,
2093
2586
  viewportHeight
2094
2587
  );
@@ -2133,24 +2626,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2133
2626
  const styleFor = (styleId) => {
2134
2627
  const cached = resolvedStyles.get(styleId);
2135
2628
  if (cached) return cached;
2136
- const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
2629
+ const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme, surface2.adaptation);
2137
2630
  resolvedStyles.set(styleId, resolved);
2138
2631
  return resolved;
2139
2632
  };
2140
- const monoDefinitions = /* @__PURE__ */ new Map();
2141
- const colorDefinitions = /* @__PURE__ */ new Map();
2142
- for (const row of renderRows) {
2143
- for (const glyph of view.nativeRows[row] ?? []) {
2144
- const definition = view.glyphDefinitions.get(glyph.glyphId);
2145
- if (!definition) continue;
2146
- (definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(
2147
- definition.id,
2148
- definition
2149
- );
2150
- }
2151
- }
2152
- this.#monoAtlas.prepare(monoDefinitions.values());
2153
- this.#colorAtlas.prepare(colorDefinitions.values());
2154
2633
  if (!fullRedraw) {
2155
2634
  for (const row of renderRows) {
2156
2635
  const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
@@ -2209,12 +2688,6 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2209
2688
  pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
2210
2689
  const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
2211
2690
  const hasNativeRows = view.nativeRows.some((row) => row.length > 0);
2212
- if (!hasNativeRows) {
2213
- this.#fallbackAtlas.prepare(
2214
- renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")),
2215
- scale
2216
- );
2217
- }
2218
2691
  if (hasNativeRows) {
2219
2692
  for (const row of renderRows) {
2220
2693
  const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
@@ -2246,7 +2719,8 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2246
2719
  if (blockElement) {
2247
2720
  let backdrop = style.background ?? view.theme.background;
2248
2721
  if (isSelected) backdrop = view.theme.selection;
2249
- const blockColor = over(foreground, backdrop);
2722
+ const blockForeground = foreground === style.foreground ? style.fill : foreground;
2723
+ const blockColor = over(blockForeground, backdrop);
2250
2724
  if (pushBlockElement(
2251
2725
  rectangleVertices,
2252
2726
  blockElement,
@@ -2259,8 +2733,9 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2259
2733
  ))
2260
2734
  continue;
2261
2735
  }
2262
- const atlas = definition.format === GlyphFormat.Alpha8 ? this.#monoAtlas : this.#colorAtlas;
2263
- 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;
2264
2739
  pushGlyph(
2265
2740
  vertices,
2266
2741
  (ORIGIN_X + instance.x) * scale,
@@ -2268,7 +2743,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2268
2743
  instance.width * scale,
2269
2744
  instance.height * scale,
2270
2745
  atlas.glyph(definition),
2271
- foreground,
2746
+ mono && foreground === style.foreground ? style.text : foreground,
2272
2747
  viewportWidth,
2273
2748
  viewportHeight
2274
2749
  );
@@ -2313,7 +2788,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2313
2788
  Math.round(rowTop + 16 * scale),
2314
2789
  width,
2315
2790
  stroke,
2316
- style.foreground,
2791
+ style.text,
2317
2792
  viewportWidth,
2318
2793
  viewportHeight
2319
2794
  );
@@ -2325,7 +2800,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2325
2800
  Math.round(rowTop + 9 * scale),
2326
2801
  width,
2327
2802
  stroke,
2328
- style.foreground,
2803
+ style.text,
2329
2804
  viewportWidth,
2330
2805
  viewportHeight
2331
2806
  );
@@ -2515,7 +2990,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2515
2990
  this.#colorAtlas.generation,
2516
2991
  this.#fallbackAtlas.generation
2517
2992
  ];
2518
- let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
2993
+ let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations(), surface2.adaptation?.key ?? "-");
2519
2994
  const cached = surface2.geometryCache.get(key);
2520
2995
  const cacheHit = cached !== void 0;
2521
2996
  if (cached) {
@@ -2533,9 +3008,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2533
3008
  hasNativeRows,
2534
3009
  scale,
2535
3010
  viewportWidth,
2536
- viewportHeight
3011
+ viewportHeight,
3012
+ surface2.adaptation
2537
3013
  );
2538
- key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
3014
+ key = geometryCacheKey(view, damage.rows, hasNativeRows, generations(), surface2.adaptation?.key ?? "-");
2539
3015
  const promote = admitted && key === candidateKey;
2540
3016
  geometry = {
2541
3017
  rectangleBuffer: promote ? createCachedVertexBuffer(this.device, `cached rectangles ${id}`, cpu.rectangleData) : surface2.rectangleBuffer.write(cpu.rectangleData),
@@ -2693,13 +3169,72 @@ function cursorActivityChangesPixels(cursor, focused, cursorBlinkVisible) {
2693
3169
  return focused && cursor.visible && cursor.blinking && !cursorBlinkVisible;
2694
3170
  }
2695
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
+
2696
3231
  // src/catalog-budget.ts
2697
3232
  function catalogAdmission(resetsCatalog, alreadyUsingFallback, fits) {
2698
3233
  if (alreadyUsingFallback && !resetsCatalog) return "text-fallback";
2699
3234
  if (fits) return "install";
2700
3235
  return resetsCatalog ? "text-fallback" : "request-full";
2701
3236
  }
2702
- function glyphCatalogFits(sessionDefinitions, sessionPixelBytes, sharedDefinitions, sharedPixelBytes, definitions, budget) {
3237
+ function glyphCatalogFits(sessionDefinitions, sessionPixelBytes, sharedDefinitions, sharedPixelBytes, definitions, budget, sharedKey = (id) => id) {
2703
3238
  if (definitions.length === 0) return true;
2704
3239
  let nextDefinitionCount = sessionDefinitions.size;
2705
3240
  let nextSharedDefinitionCount = sharedDefinitions.size;
@@ -2709,7 +3244,7 @@ function glyphCatalogFits(sessionDefinitions, sessionPixelBytes, sharedDefinitio
2709
3244
  for (const definition of definitions) {
2710
3245
  if (sessionDefinitions.has(definition.id) || pending.has(definition.id)) continue;
2711
3246
  pending.add(definition.id);
2712
- const shared = sharedDefinitions.get(definition.id);
3247
+ const shared = sharedDefinitions.get(sharedKey(definition.id));
2713
3248
  const pixelBytes = shared?.definition.pixels.byteLength ?? definition.pixels.byteLength;
2714
3249
  nextDefinitionCount += 1;
2715
3250
  nextSessionPixelBytes += pixelBytes;
@@ -3084,10 +3619,6 @@ var socketClosing = 2;
3084
3619
  function exactStamp(left, right) {
3085
3620
  return left !== void 0 && compareRoutedSceneContent(left, right) === 0;
3086
3621
  }
3087
- function copyArrayBuffer(bytes) {
3088
- const copy = bytes.slice();
3089
- return copy.buffer;
3090
- }
3091
3622
  function capacitiesFitWithin(accepted, advertised) {
3092
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;
3093
3624
  }
@@ -3395,13 +3926,13 @@ var RoutedFramesTransport = class {
3395
3926
  }
3396
3927
  }
3397
3928
  #incremental(connection, activation, header, payload, chargedBytes) {
3398
- 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)) {
3399
3930
  this.#protocolFailure(connection, header.activationId, chargedBytes, "incremental base mismatch");
3400
3931
  return;
3401
3932
  }
3402
3933
  try {
3403
3934
  this.#emit({ type: "frames-state", activationId: header.activationId, state: "applying" });
3404
- this.#applyFrame(copyArrayBuffer(payload), activation.identity);
3935
+ this.#applyFrame(payload, activation.identity, void 0, header.resultContent.sceneEpoch.cellBootId);
3405
3936
  activation.appliedContent = header.resultContent;
3406
3937
  this.#returnCredit(connection, header.activationId, chargedBytes);
3407
3938
  this.#sceneApplied(connection, activation);
@@ -3481,7 +4012,12 @@ var RoutedFramesTransport = class {
3481
4012
  }
3482
4013
  try {
3483
4014
  this.#emit({ type: "frames-state", activationId: header.activationId, state: "applying" });
3484
- 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
+ );
3485
4021
  activation.appliedContent = staging.resultContent;
3486
4022
  this.#releaseTransfer(activation);
3487
4023
  this.#sceneApplied(connection, activation);
@@ -3686,7 +4222,10 @@ var MAX_PERFORMANCE_SAMPLES = 1e5;
3686
4222
  var FRAME_CREDIT_BATCH_BYTES = 4 * 1024 * 1024;
3687
4223
  var FRAME_CREDIT_MAX_DELAY_MS = 8;
3688
4224
  var snapshots = /* @__PURE__ */ new Map();
4225
+ var drawnSessions = /* @__PURE__ */ new Set();
4226
+ var routedMouseTracking = /* @__PURE__ */ new Map();
3689
4227
  var sharedGlyphDefinitions = /* @__PURE__ */ new Map();
4228
+ var nextRendererGlyphId = 1;
3690
4229
  var sharedGlyphPixelBytes = 0;
3691
4230
  var retainedStyleDefinitions = 0;
3692
4231
  var surfaces = /* @__PURE__ */ new Map();
@@ -3882,6 +4421,7 @@ function emptySessionSnapshot() {
3882
4421
  nativeRows: [],
3883
4422
  nativeStyleRows: [],
3884
4423
  glyphDefinitions: /* @__PURE__ */ new Map(),
4424
+ glyphNamespace: "",
3885
4425
  glyphPixelBytes: 0,
3886
4426
  styleDefinitions: /* @__PURE__ */ new Map(),
3887
4427
  rowRevisions: [],
@@ -3905,29 +4445,34 @@ function snapshot(sessionHandle) {
3905
4445
  }
3906
4446
  function clearSessionCatalog(session) {
3907
4447
  for (const glyphId of session.glyphDefinitions.keys()) {
3908
- const shared = sharedGlyphDefinitions.get(glyphId);
4448
+ const key = `${session.glyphNamespace}:${glyphId}`;
4449
+ const shared = sharedGlyphDefinitions.get(key);
3909
4450
  if (!shared) continue;
3910
4451
  shared.references -= 1;
3911
4452
  if (shared.references <= 0) {
3912
- sharedGlyphDefinitions.delete(glyphId);
4453
+ sharedGlyphDefinitions.delete(key);
3913
4454
  sharedGlyphPixelBytes -= shared.definition.pixels.byteLength;
3914
4455
  }
3915
4456
  }
3916
- session.glyphDefinitions.clear();
4457
+ session.glyphDefinitions = /* @__PURE__ */ new Map();
3917
4458
  session.glyphPixelBytes = 0;
3918
4459
  retainedStyleDefinitions -= session.styleDefinitions.size;
3919
4460
  session.styleDefinitions.clear();
3920
4461
  }
3921
- function installGlyphDefinitions(session, definitions) {
4462
+ function installGlyphDefinitions(session, definitions, previousDefinitions) {
3922
4463
  for (const decoded of definitions) {
3923
4464
  if (session.glyphDefinitions.has(decoded.id)) continue;
3924
- let shared = sharedGlyphDefinitions.get(decoded.id);
4465
+ const key = `${session.glyphNamespace}:${decoded.id}`;
4466
+ let shared = sharedGlyphDefinitions.get(key);
3925
4467
  const pixelBytes = shared?.definition.pixels.byteLength ?? decoded.pixels.byteLength;
3926
4468
  if (shared) {
3927
4469
  shared.references += 1;
3928
4470
  } else {
3929
- shared = { definition: decoded, references: 1 };
3930
- 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);
3931
4476
  sharedGlyphPixelBytes += decoded.pixels.byteLength;
3932
4477
  }
3933
4478
  session.glyphDefinitions.set(decoded.id, shared.definition);
@@ -3945,6 +4490,10 @@ function dropSessionSnapshot(sessionHandle) {
3945
4490
  const session = snapshots.get(sessionHandle);
3946
4491
  if (session) clearSessionCatalog(session);
3947
4492
  snapshots.delete(sessionHandle);
4493
+ drawnSessions.delete(sessionHandle);
4494
+ }
4495
+ function rowShowsContent(row) {
4496
+ return row.glyphs.length > 0 || /\S/.test(row.text);
3948
4497
  }
3949
4498
  function defaults(sessionHandle) {
3950
4499
  let value = presentationDefaults.get(sessionHandle);
@@ -4069,6 +4618,7 @@ async function recoverDevice(info) {
4069
4618
  try {
4070
4619
  const next = await rendererPromise;
4071
4620
  renderer = next;
4621
+ next.setPerformanceMeasurementEnabled?.(performanceMeasurement !== void 0);
4072
4622
  recoveryAttempts = 0;
4073
4623
  recovering = false;
4074
4624
  for (const [id, mount2] of mounts) {
@@ -4119,7 +4669,10 @@ function scheduleShaderAnimation() {
4119
4669
  }
4120
4670
  async function flush() {
4121
4671
  const backend = await ensureRenderer();
4122
- 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
+ });
4123
4676
  if (performanceMeasurement && ids.length > 0) {
4124
4677
  performanceMeasurement.scheduling.flushes += 1;
4125
4678
  appendPerformanceSample(performanceMeasurement.scheduling.panesPerFlush, ids.length);
@@ -4139,14 +4692,23 @@ async function flush() {
4139
4692
  const active = performanceMeasurement;
4140
4693
  const beforeRender = active ? performance.now() : 0;
4141
4694
  const metrics = backend.renderBatch ? backend.renderBatch(entries) : entries.map(({ id, view }) => backend.render(id, view));
4142
- 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;
4143
4703
  const renderedAt = active ? performance.now() : 0;
4144
4704
  for (const { id } of entries) {
4705
+ if (submitted && !submitted.has(id)) continue;
4145
4706
  const damage = surfaces.get(id)?.damage;
4146
4707
  if (!damage) continue;
4147
4708
  damage.full = false;
4148
4709
  damage.rows.clear();
4149
4710
  damage.geometryChanged = false;
4711
+ delete damage.scrollRows;
4150
4712
  }
4151
4713
  if (active && performanceMeasurement === active) {
4152
4714
  active.scheduling.renderCalls += entries.length;
@@ -4185,6 +4747,7 @@ function invalidateFull(id) {
4185
4747
  damage.full = true;
4186
4748
  damage.rows.clear();
4187
4749
  damage.geometryChanged = true;
4750
+ delete damage.scrollRows;
4188
4751
  markDirty(id);
4189
4752
  }
4190
4753
  function invalidateRows(id, rows, geometryChanged = false) {
@@ -4206,15 +4769,35 @@ function invalidateRows(id, rows, geometryChanged = false) {
4206
4769
  function invalidateSessionFull(sessionHandle) {
4207
4770
  for (const surfaceId of surfaceIdsForSession(sessionHandle)) invalidateFull(surfaceId);
4208
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
+ }
4209
4792
  function invalidateSessionRows(sessionHandle, rows, geometryChanged = false) {
4210
4793
  const damagedRows = [...rows];
4211
4794
  for (const surfaceId of surfaceIdsForSession(sessionHandle)) invalidateRows(surfaceId, damagedRows, geometryChanged);
4212
4795
  }
4213
- async function mount(surfaceId, sessionHandle, canvas) {
4796
+ async function mount(surfaceId, sessionHandle, canvas, size) {
4214
4797
  if (mounts.has(surfaceId)) renderer?.unmount(surfaceId);
4215
4798
  snapshot(sessionHandle);
4216
4799
  surface(surfaceId, sessionHandle);
4217
- const entry = { canvas, size: { width: 1, height: 1, dpr: 1 }, sessionHandle };
4800
+ const entry = { canvas, size, sessionHandle };
4218
4801
  mounts.set(surfaceId, entry);
4219
4802
  const backend = await ensureRenderer();
4220
4803
  if (mounts.get(surfaceId) !== entry) return;
@@ -4223,7 +4806,7 @@ async function mount(surfaceId, sessionHandle, canvas) {
4223
4806
  invalidateFull(surfaceId);
4224
4807
  scheduleShaderAnimation();
4225
4808
  }
4226
- function applyFrame(packet, expectedIdentity, expectedLayout) {
4809
+ function applyFrame(packet, expectedIdentity, expectedLayout, glyphNamespace) {
4227
4810
  const active = performanceMeasurement;
4228
4811
  const applyStarted = active ? performance.now() : 0;
4229
4812
  lifetimeFrames.received += 1;
@@ -4320,11 +4903,13 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4320
4903
  throw new Error("routed transfer row section is not a full snapshot");
4321
4904
  }
4322
4905
  const glyphDefinitions = glyphSection ? decodeGlyphDefinitions(glyphSection) : NO_GLYPH_DEFINITIONS;
4323
- 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;
4324
4908
  const replacements = decodeRowReplacements(rowSection);
4325
4909
  for (const replacement of replacements) {
4326
4910
  if (replacement.row >= frame.rows) throw new RangeError("Row replacement exceeds viewport");
4327
4911
  }
4912
+ const firstDrawn = !drawnSessions.has(id) && replacements.some(rowShowsContent);
4328
4913
  const linkSection = frame.sections.find((candidate) => candidate.kind === SectionKind.LinkTargets);
4329
4914
  const links = linkSection ? decodeLinkTargets(linkSection, frame.cols, frame.rows) : [];
4330
4915
  const nextCursor = decodeCursorState(cursorSection);
@@ -4345,13 +4930,21 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4345
4930
  throw new Error("transfer scrollback layout does not match TRF1");
4346
4931
  }
4347
4932
  if (active) active.frames.glyphDefinitions += glyphDefinitions.length;
4348
- 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;
4349
4940
  const wasCatalogFallback = previous.catalogFallback;
4350
4941
  if (resetsCatalog) {
4942
+ const previousDefinitions = previous.glyphNamespace === namespace ? previous.glyphDefinitions : void 0;
4351
4943
  previous.rows = [];
4352
4944
  previous.nativeRows = [];
4353
4945
  previous.nativeStyleRows = [];
4354
4946
  clearSessionCatalog(previous);
4947
+ previous.glyphNamespace = namespace;
4355
4948
  previous.rowRevisions = [];
4356
4949
  const catalogFits = glyphCatalogFits(
4357
4950
  previous.glyphDefinitions,
@@ -4359,7 +4952,8 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4359
4952
  sharedGlyphDefinitions,
4360
4953
  sharedGlyphPixelBytes,
4361
4954
  glyphDefinitions,
4362
- GLYPH_CATALOG_BUDGET
4955
+ GLYPH_CATALOG_BUDGET,
4956
+ (glyphId) => `${namespace}:${glyphId}`
4363
4957
  ) && definitionCatalogFits(
4364
4958
  previous.styleDefinitions,
4365
4959
  styleDefinitions,
@@ -4370,7 +4964,7 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4370
4964
  const admission = catalogAdmission(true, wasCatalogFallback, catalogFits);
4371
4965
  previous.catalogFallback = admission === "text-fallback";
4372
4966
  if (admission === "install") {
4373
- installGlyphDefinitions(previous, glyphDefinitions);
4967
+ installGlyphDefinitions(previous, glyphDefinitions, previousDefinitions);
4374
4968
  installStyleDefinitions(previous, styleDefinitions);
4375
4969
  } else if (!wasCatalogFallback) {
4376
4970
  postToRenderer({
@@ -4386,7 +4980,8 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4386
4980
  sharedGlyphDefinitions,
4387
4981
  sharedGlyphPixelBytes,
4388
4982
  glyphDefinitions,
4389
- GLYPH_CATALOG_BUDGET
4983
+ GLYPH_CATALOG_BUDGET,
4984
+ (glyphId) => `${namespace}:${glyphId}`
4390
4985
  ) && definitionCatalogFits(
4391
4986
  previous.styleDefinitions,
4392
4987
  styleDefinitions,
@@ -4426,7 +5021,7 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4426
5021
  const nativeRows = fullRows ? Array.from({ length: frame.rows }, () => []) : previous.nativeRows.slice();
4427
5022
  const nativeStyleRows = fullRows ? Array.from({ length: frame.rows }, () => []) : previous.nativeStyleRows.slice();
4428
5023
  const rowRevisions = fullRows ? Array(frame.rows).fill(0n) : previous.rowRevisions.slice();
4429
- const damagedRows = [];
5024
+ let damagedRows = [];
4430
5025
  if (active) active.frames.rowsDecoded += replacements.length;
4431
5026
  for (const replacement of replacements) {
4432
5027
  if (replacement.revision < (rowRevisions[replacement.row] ?? 0n)) continue;
@@ -4436,12 +5031,24 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4436
5031
  rowRevisions[replacement.row] = replacement.revision;
4437
5032
  damagedRows.push(replacement.row);
4438
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
+ }
4439
5046
  previous.cols = frame.cols;
4440
5047
  previous.rows = rows;
4441
5048
  previous.nativeRows = nativeRows;
4442
5049
  previous.nativeStyleRows = nativeStyleRows;
4443
5050
  previous.rowRevisions = rowRevisions;
4444
- const geometryChanged = damagedRows.length > 0;
5051
+ const geometryChanged = damagedRows.length > 0 || scrollRows !== 0;
4445
5052
  const previousCursor = previous.cursor;
4446
5053
  const cursorChanged = nextCursor.x !== previousCursor.x || nextCursor.y !== previousCursor.y || nextCursor.visible !== previousCursor.visible || nextCursor.style !== previousCursor.style || nextCursor.blinking !== previousCursor.blinking;
4447
5054
  previous.cursor = nextCursor;
@@ -4461,12 +5068,14 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4461
5068
  if (clipboardText !== void 0) postToRenderer({ type: "clipboard-write", text: clipboardText });
4462
5069
  if (scrollbarChanged && scrollbar) postToRenderer({ type: "scrollbar-state", sessionHandle: id, scrollbar });
4463
5070
  if (completingResync) postToRenderer({ type: "frame-resync-complete", sessionHandle: id });
4464
- const requiresFullRedraw = fullRows || changedSession || completingResync;
4465
- 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);
4466
5074
  const hasRowDamage = damagedRows.length > 0;
4467
5075
  for (const surfaceId of surfaceIdsForSession(id)) {
4468
5076
  if (cursorChanged) scheduleCursorBlink(surfaceId, true);
4469
5077
  if (requiresFullRedraw) invalidateFull(surfaceId);
5078
+ else if (scrollRows !== 0) invalidateScroll(surfaceId, scrollRows, damagedRows, rows.length);
4470
5079
  else if (hasRowDamage) invalidateRows(surfaceId, damagedRows, geometryChanged);
4471
5080
  }
4472
5081
  postToRenderer({
@@ -4479,6 +5088,17 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4479
5088
  // recovered screen apart from a partial update of the stale one.
4480
5089
  fullSnapshot: fullFrame
4481
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
+ }
4482
5102
  lifetimeFrames.applies += 1;
4483
5103
  counters.applies += 1;
4484
5104
  if (active) appendPerformanceSample(active.samples.frameApplyMs, performance.now() - applyStarted);
@@ -4490,8 +5110,8 @@ function applyFrame(packet, expectedIdentity, expectedLayout) {
4490
5110
  };
4491
5111
  }
4492
5112
  var routedFrames = new RoutedFramesTransport({
4493
- applyFrame: (packet, identity, expectedLayout) => {
4494
- const applied = applyFrame(packet, identity, expectedLayout);
5113
+ applyFrame: (packet, identity, expectedLayout, glyphNamespace) => {
5114
+ const applied = applyFrame(packet, identity, expectedLayout, glyphNamespace);
4495
5115
  if (!applied) throw new Error("routed TRF1 frame was not applied");
4496
5116
  return applied;
4497
5117
  },
@@ -4505,7 +5125,7 @@ self.onmessage = (event) => {
4505
5125
  const message = event.data;
4506
5126
  try {
4507
5127
  if (message.type === "mount") {
4508
- void mount(message.surfaceId, message.sessionHandle, message.canvas).catch((error) => {
5128
+ void mount(message.surfaceId, message.sessionHandle, message.canvas, message.size).catch((error) => {
4509
5129
  console.error("[terminal-renderer] mount failed", error);
4510
5130
  });
4511
5131
  } else if (message.type === "renderer-config") {
@@ -4588,13 +5208,12 @@ self.onmessage = (event) => {
4588
5208
  scheduleShaderAnimation();
4589
5209
  } else if (message.type === "selection") {
4590
5210
  if (message.surfaceId) {
4591
- surface(message.surfaceId, message.sessionHandle).selection = message.selection;
4592
- invalidateFull(message.surfaceId);
5211
+ surface(message.surfaceId, message.sessionHandle);
5212
+ changeSelection(message.surfaceId, message.selection);
4593
5213
  } else {
4594
5214
  defaults(message.sessionHandle).selection = message.selection;
4595
5215
  for (const surfaceId of surfaceIdsForSession(message.sessionHandle)) {
4596
- surfaces.get(surfaceId).selection = message.selection;
4597
- invalidateFull(surfaceId);
5216
+ changeSelection(surfaceId, message.selection);
4598
5217
  }
4599
5218
  }
4600
5219
  } else if (message.type === "visibility") {
@@ -4660,6 +5279,7 @@ self.onmessage = (event) => {
4660
5279
  } else if (message.type === "performance-counters") {
4661
5280
  postToRenderer({ type: "performance-counters", requestId: message.requestId, snapshot: counterSnapshot() });
4662
5281
  } else if (message.type === "routed-frames-attach") {
5282
+ routedMouseTracking.delete(message.request.sessionHandle);
4663
5283
  routedFrames.attach(message.request);
4664
5284
  } else if (message.type === "routed-frames-detach") {
4665
5285
  routedFrames.detach(message.activationId);