@neuroplastio/xterm-addon-hotty 0.1.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.
@@ -0,0 +1,2906 @@
1
+ // src/hostcss.ts
2
+ var DEFAULT_FG = "#ffffff";
3
+ var DEFAULT_BG = "#000000";
4
+ var ANSI_KEYS = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "brightBlack", "brightRed", "brightGreen", "brightYellow", "brightBlue", "brightMagenta", "brightCyan", "brightWhite"];
5
+ var DEFAULT_ANSI = ["#2e3436", "#cc0000", "#4e9a06", "#c4a000", "#3465a4", "#75507b", "#06989a", "#d3d7cf", "#555753", "#ef2929", "#8ae234", "#fce94f", "#729fcf", "#ad7fa8", "#34e2e2", "#eeeeec"];
6
+ function palette(theme) {
7
+ const fg = theme?.foreground ?? DEFAULT_FG;
8
+ const bg = theme?.background ?? DEFAULT_BG;
9
+ const ansi = ANSI_KEYS.map((k, i) => theme?.[k] ?? DEFAULT_ANSI[i]);
10
+ return { fg, bg, ansi, dark: luminance(bg) < 0.5 };
11
+ }
12
+ function hostCss(theme, m) {
13
+ const p = palette(theme);
14
+ const lines = [
15
+ "@layer hotty-host {",
16
+ ":root {",
17
+ ` color-scheme: ${p.dark ? "dark" : "light"};`,
18
+ ` --hotty-fg: ${p.fg};`,
19
+ ` --hotty-bg: ${p.bg};`,
20
+ ...p.ansi.map((c, i) => ` --hotty-ansi-${i}: ${c};`),
21
+ // The terminal's blue: plain blue is hard to read on a dark background.
22
+ ` --hotty-accent: var(--hotty-ansi-${p.dark ? 12 : 4});`,
23
+ ` --hotty-cell-w: ${m.cellW}px;`,
24
+ ` --hotty-cell-h: ${m.cellH}px;`,
25
+ ` --hotty-font: ${fontStack(m.fontFamily)};`,
26
+ " font-family: var(--hotty-font);",
27
+ ` font-size: ${m.fontSize}px;`,
28
+ // An explicit line-height, so that `1rlh` is one terminal row.
29
+ ` line-height: ${m.cellH}px;`,
30
+ " color: var(--hotty-fg);",
31
+ " background: var(--hotty-bg);",
32
+ " accent-color: var(--hotty-accent);",
33
+ "}",
34
+ "body { margin: 0; }",
35
+ // Controls in the terminal's colours (SPEC §8), not the browser's: ansi-8
36
+ // is the dim grey of borders, buttons, placeholders and what is disabled.
37
+ // :where() keeps the base rule below the state rules after it, such as
38
+ // :disabled.
39
+ ":where(input:not([type=checkbox], [type=radio], [type=range]), textarea,",
40
+ " select, button) {",
41
+ " color: var(--hotty-fg);",
42
+ " background: var(--hotty-bg);",
43
+ " border: 1px solid var(--hotty-ansi-8);",
44
+ "}",
45
+ "button:enabled, input:is([type=button], [type=submit], [type=reset]):enabled {",
46
+ " background: var(--hotty-ansi-8);",
47
+ "}",
48
+ "::placeholder, :disabled { color: var(--hotty-ansi-8); }",
49
+ ":focus-visible { outline: 1px solid var(--hotty-accent); }",
50
+ ":any-link { color: var(--hotty-accent); }",
51
+ "::selection { color: var(--hotty-bg); background: var(--hotty-accent); }",
52
+ // An element that opts in to drags selects no text, whatever the
53
+ // document's CSS (SPEC §9.1, §11): important in the host's layer, the
54
+ // first, wins over every rule of the document's.
55
+ "[data-on~=drag], [data-on~=drag] * { -webkit-user-select: none !important; user-select: none !important; }",
56
+ // The rows a document needs are measured with no scrollbar of the root's
57
+ // in the way (`r=auto`, `fit`, SPEC §5.2).
58
+ `:root[${MEASURE}] { overflow: hidden !important; }`,
59
+ "}"
60
+ ];
61
+ return lines.join("\n");
62
+ }
63
+ var MEASURE = "data-xterm-hotty-measure";
64
+ var CLIP = "data-xterm-hotty-clip";
65
+ var UNCLIPPED = "data-xterm-hotty-unclipped";
66
+ function scrollCss(axes, pageScrolls) {
67
+ if (!axes) {
68
+ return [
69
+ "@layer hotty-host {",
70
+ `:root { overflow: hidden; touch-action: ${pageScrolls ? "manipulation" : "none"}; }`,
71
+ "* { scrollbar-width: none !important; }",
72
+ "::-webkit-scrollbar { display: none !important; }",
73
+ "}"
74
+ ].join("\n");
75
+ }
76
+ const pan = pageScrolls ? "manipulation" : axes === 3 ? "pan-x pan-y" : axes === 1 ? "pan-y" : "pan-x";
77
+ const lines = ["@layer hotty-host {", `:root { touch-action: ${pan}; }`];
78
+ if (axes !== 3) {
79
+ const clipped = axes === 1 ? "overflow-x" : "overflow-y";
80
+ lines.push(`:root, :root:not([${UNCLIPPED}]) [${CLIP}] { ${clipped}: hidden !important; }`);
81
+ }
82
+ lines.push("}");
83
+ return lines.join("\n");
84
+ }
85
+ function fontStack(family) {
86
+ const f = family.trim();
87
+ if (!f) return "monospace";
88
+ return /monospace\s*$/.test(f) ? f : `${f}, monospace`;
89
+ }
90
+ function luminance(color) {
91
+ let hex = color.trim().replace(/^#/, "");
92
+ if (hex.length === 3) hex = hex.replace(/./g, (c) => c + c);
93
+ if (!/^[0-9a-f]{6}/i.test(hex)) return 0;
94
+ const [r, g, b] = [0, 2, 4].map((i) => {
95
+ const c = parseInt(hex.slice(i, i + 2), 16) / 255;
96
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
97
+ });
98
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
99
+ }
100
+
101
+ // src/delta.ts
102
+ var DeltaError = class extends Error {
103
+ code;
104
+ constructor(code, detail) {
105
+ super(detail);
106
+ this.code = code;
107
+ }
108
+ };
109
+ var OPS = ["morph", "inner", "replace", "append", "prepend", "before", "after", "remove", "attr", "unattr", "text", "var"];
110
+ var Deltas = class {
111
+ doc;
112
+ resolver;
113
+ constructor(doc, resolver) {
114
+ this.doc = doc;
115
+ this.resolver = resolver;
116
+ }
117
+ /** Parses a fragment inert, cleaned and resolved. */
118
+ parse(html) {
119
+ const t = this.doc.createElement("template");
120
+ t.innerHTML = html;
121
+ this.resolver.adopt(t.content);
122
+ return Array.from(t.content.childNodes);
123
+ }
124
+ apply(op, target, key, payload) {
125
+ const find = () => {
126
+ if (!target) throw new DeltaError("EINVAL", `op=${op} needs t=<element id>`);
127
+ const el = this.doc.getElementById(target);
128
+ if (!el) throw new DeltaError("ENOTARGET", target);
129
+ return el;
130
+ };
131
+ const needKey = () => {
132
+ if (!key) throw new DeltaError("EINVAL", `op=${op} needs k=<name>`);
133
+ return key;
134
+ };
135
+ switch (op) {
136
+ case "":
137
+ case "morph": {
138
+ if (target === void 0) {
139
+ const missing = [];
140
+ for (const k of this.parse(payload)) {
141
+ if (k.nodeType !== 1) continue;
142
+ const id = k.getAttribute("id");
143
+ const old = id ? this.doc.getElementById(id) : null;
144
+ if (old) this.morphNode(old, k);
145
+ else missing.push(id ?? "(element without id)");
146
+ }
147
+ if (missing.length) throw new DeltaError("ENOTARGET", missing.join(","));
148
+ return;
149
+ }
150
+ const t = find();
151
+ const frag = this.parse(payload);
152
+ const elements = frag.filter((n) => n.nodeType === 1);
153
+ if (elements.length === 1) {
154
+ this.morphNode(t, elements[0]);
155
+ } else {
156
+ t.before(...frag);
157
+ t.remove();
158
+ }
159
+ return;
160
+ }
161
+ case "inner":
162
+ this.morphChildren(find(), this.parse(payload));
163
+ return;
164
+ case "replace": {
165
+ const t = find();
166
+ t.before(...this.parse(payload));
167
+ t.remove();
168
+ return;
169
+ }
170
+ case "append":
171
+ case "prepend": {
172
+ const t = find();
173
+ const fresh = [];
174
+ for (const k of this.parse(payload)) {
175
+ const id = k.nodeType === 1 ? k.getAttribute("id") : null;
176
+ const old = id ? this.doc.getElementById(id) : null;
177
+ if (old && old.parentNode === t) this.morphNode(old, k);
178
+ else fresh.push(k);
179
+ }
180
+ if (op === "append") t.append(...fresh);
181
+ else t.prepend(...fresh);
182
+ return;
183
+ }
184
+ case "before":
185
+ find().before(...this.parse(payload));
186
+ return;
187
+ case "after":
188
+ find().after(...this.parse(payload));
189
+ return;
190
+ case "remove":
191
+ find().remove();
192
+ return;
193
+ case "attr": {
194
+ const t = find();
195
+ const name = needKey();
196
+ this.resolver.set(t, name, payload);
197
+ syncProperty(t, name, payload, this.doc.activeElement === t);
198
+ return;
199
+ }
200
+ case "unattr": {
201
+ const t = find();
202
+ const name = needKey();
203
+ this.resolver.remove(t, name);
204
+ syncProperty(t, name, null, this.doc.activeElement === t);
205
+ return;
206
+ }
207
+ case "text": {
208
+ const t = find();
209
+ if (t.localName === "style") this.resolver.setCss(t, payload);
210
+ else if (t.childNodes.length === 1 && t.firstChild.nodeType === 3) t.firstChild.data = payload;
211
+ else t.textContent = payload;
212
+ return;
213
+ }
214
+ case "var": {
215
+ const t = find();
216
+ const k = needKey();
217
+ const name = k.startsWith("--") ? k : `--${k}`;
218
+ t.style?.setProperty(name, payload);
219
+ return;
220
+ }
221
+ default:
222
+ throw new DeltaError("EINVAL", `unknown op=${op}`);
223
+ }
224
+ }
225
+ /** Makes `old` (live) look like `neu` (parsed), keeping `old` where possible. Returns the node now in place. */
226
+ morphNode(old, neu) {
227
+ if (old.nodeType === 3 && neu.nodeType === 3) {
228
+ if (old.data !== neu.data) old.data = neu.data;
229
+ return old;
230
+ }
231
+ if (old.nodeType === 8 && neu.nodeType === 8) return old;
232
+ if (old.nodeType === 1 && neu.nodeType === 1) {
233
+ const a = old;
234
+ const b = neu;
235
+ const ia = a.getAttribute("id");
236
+ const ib = b.getAttribute("id");
237
+ if (a.localName === b.localName && a.namespaceURI === b.namespaceURI && (ia === ib || ia === null || ib === null)) {
238
+ this.syncAttributes(a, b);
239
+ if (a.localName === "style") {
240
+ const css = this.resolver.cssOf(b);
241
+ if (this.resolver.cssOf(a) !== css) this.resolver.setCss(a, css);
242
+ } else {
243
+ const kids = a.localName === "template" ? b.content.childNodes : b.childNodes;
244
+ this.morphChildren(a.localName === "template" ? a.content : a, Array.from(kids));
245
+ }
246
+ return a;
247
+ }
248
+ }
249
+ old.replaceWith(neu);
250
+ return neu;
251
+ }
252
+ syncAttributes(old, neu) {
253
+ const oldAttrs = this.resolver.attributes(old);
254
+ const newAttrs = this.resolver.attributes(neu);
255
+ const focused = this.doc.activeElement === old;
256
+ for (const [name, value] of newAttrs) {
257
+ if (focused && name === "value") continue;
258
+ const had = oldAttrs.find(([n]) => n === name);
259
+ if (!had || had[1] !== value) {
260
+ this.resolver.set(old, name, value);
261
+ syncProperty(old, name, value, focused);
262
+ }
263
+ }
264
+ for (const [name] of oldAttrs) {
265
+ if (!newAttrs.some(([n]) => n === name)) {
266
+ this.resolver.remove(old, name);
267
+ syncProperty(old, name, null, focused);
268
+ }
269
+ }
270
+ }
271
+ /** Morphs the children of `parent` (live) into `kids` (parsed). */
272
+ morphChildren(parent, kids) {
273
+ const oldKids = Array.from(parent.childNodes);
274
+ const idOf = (n) => n.nodeType === 1 ? n.getAttribute("id") : null;
275
+ const newIds = new Set(kids.map(idOf).filter((x) => x !== null));
276
+ const used = /* @__PURE__ */ new Set();
277
+ const plan = [];
278
+ let cursor = 0;
279
+ for (const nk of kids) {
280
+ const id = idOf(nk);
281
+ let match = null;
282
+ if (id !== null) {
283
+ match = oldKids.find((ok) => !used.has(ok) && idOf(ok) === id) ?? null;
284
+ } else {
285
+ for (let i = cursor; i < oldKids.length; i++) {
286
+ const ok = oldKids[i];
287
+ if (used.has(ok)) continue;
288
+ const okId = idOf(ok);
289
+ let same = false;
290
+ if (okId !== null && newIds.has(okId)) same = false;
291
+ else if (ok.nodeType === 1 && nk.nodeType === 1)
292
+ same = okId === null && ok.localName === nk.localName && ok.namespaceURI === nk.namespaceURI;
293
+ else same = ok.nodeType === nk.nodeType && (ok.nodeType === 3 || ok.nodeType === 8);
294
+ if (same) {
295
+ match = ok;
296
+ cursor = i + 1;
297
+ break;
298
+ }
299
+ }
300
+ }
301
+ if (match) used.add(match);
302
+ plan.push([match, nk]);
303
+ }
304
+ for (const ok of oldKids) if (!used.has(ok)) ok.remove();
305
+ plan.forEach(([keep, nk], i) => {
306
+ const want = keep ? this.morphNode(keep, nk) : nk;
307
+ const current = parent.childNodes[i] ?? null;
308
+ if (current !== want) move(parent, want, current);
309
+ });
310
+ }
311
+ };
312
+ function move(parent, node, before) {
313
+ const p = parent;
314
+ if (node.parentNode === parent && typeof p.moveBefore === "function") {
315
+ try {
316
+ p.moveBefore(node, before);
317
+ return;
318
+ } catch {
319
+ }
320
+ }
321
+ parent.insertBefore(node, before);
322
+ }
323
+ function syncProperty(el, name, value, focused) {
324
+ if (focused) return;
325
+ const n = name.toLowerCase();
326
+ if (n === "value" && "value" in el && (el.localName === "input" || el.localName === "textarea" || el.localName === "select")) {
327
+ el.value = value ?? "";
328
+ } else if (n === "checked" && el.localName === "input") {
329
+ el.checked = value !== null;
330
+ } else if (n === "selected" && el.localName === "option") {
331
+ el.selected = value !== null;
332
+ }
333
+ }
334
+
335
+ // src/network.ts
336
+ var DIRECTIVES = ["img-src", "media-src", "font-src", "style-src"];
337
+ function source(s) {
338
+ const v = s.trim().toLowerCase();
339
+ if (v === "https:") return v;
340
+ let u;
341
+ try {
342
+ u = new URL(v);
343
+ } catch {
344
+ return null;
345
+ }
346
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
347
+ if (u.pathname !== "/" && u.pathname !== "" || u.search || u.hash || u.username || u.password) return null;
348
+ if (!/^https?:\/\/[^/]+\/?$/.test(v)) return null;
349
+ return u.origin;
350
+ }
351
+ function clean(p) {
352
+ const out = {};
353
+ for (const d of DIRECTIVES) {
354
+ const srcs = (p?.[d] ?? []).map(source).filter((s) => s !== null);
355
+ if (srcs.length) out[d] = [...new Set(srcs)];
356
+ }
357
+ return out;
358
+ }
359
+ function parse(content) {
360
+ const p = {};
361
+ for (const part of content.split(";")) {
362
+ const [name, ...srcs] = part.trim().split(/\s+/);
363
+ const d = (name ?? "").toLowerCase();
364
+ if (DIRECTIVES.includes(d)) p[d] = [...p[d] ?? [], ...srcs];
365
+ }
366
+ return clean(p);
367
+ }
368
+ function matches(src, url) {
369
+ if (src === "https:") return url.protocol === "https:";
370
+ return url.origin === src;
371
+ }
372
+ function intersect(host, doc) {
373
+ const out = {};
374
+ for (const d of DIRECTIVES) {
375
+ const h = host[d] ?? [];
376
+ const res = /* @__PURE__ */ new Set();
377
+ for (const s of doc[d] ?? []) {
378
+ if (s === "https:") {
379
+ for (const hs of h) if (hs === "https:" || hs.startsWith("https://")) res.add(hs);
380
+ } else if (h.some((hs) => matches(hs, new URL(s)))) {
381
+ res.add(s);
382
+ }
383
+ }
384
+ if (res.size) out[d] = [...res];
385
+ }
386
+ return out;
387
+ }
388
+ function allows(p, d, url) {
389
+ let u;
390
+ try {
391
+ u = new URL(url);
392
+ } catch {
393
+ return false;
394
+ }
395
+ return (p[d] ?? []).some((s) => matches(s, u));
396
+ }
397
+ function cspSources(p, d) {
398
+ return (p[d] ?? []).join(" ");
399
+ }
400
+ function directiveFor(tag, attr, rel = "") {
401
+ if (attr === "srcset") return "img-src";
402
+ if (attr === "poster") return "img-src";
403
+ if (attr === "src") {
404
+ if (tag === "img" || tag === "input" || tag === "image") return "img-src";
405
+ if (tag === "video" || tag === "audio" || tag === "source" || tag === "track") return "media-src";
406
+ }
407
+ if ((attr === "href" || attr === "xlink:href") && (tag === "image" || tag === "use" || tag === "feimage")) return "img-src";
408
+ if (attr === "href" && tag === "link" && rel.split(/\s+/).includes("stylesheet")) return "style-src";
409
+ return null;
410
+ }
411
+
412
+ // src/resources.ts
413
+ var INVALID = "about:invalid";
414
+ var NO_BASE = "https://hotty.invalid/";
415
+ function network(value, ctx, d) {
416
+ if (!ctx) return INVALID;
417
+ let abs;
418
+ try {
419
+ abs = new URL(value, ctx.base).href;
420
+ } catch {
421
+ return INVALID;
422
+ }
423
+ return !abs.startsWith(NO_BASE) && allows(ctx.policy, d, abs) ? abs : INVALID;
424
+ }
425
+ var URL_ATTRS = /* @__PURE__ */ new Set(["src", "href", "poster", "background", "data", "action", "formaction", "xlink:href", "srcset", "cite", "longdesc", "lowsrc", "manifest", "codebase", "usemap", "ping"]);
426
+ function isUrlAttr(name) {
427
+ return URL_ATTRS.has(name.toLowerCase());
428
+ }
429
+ var Store = class {
430
+ entries = /* @__PURE__ */ new Map();
431
+ used = 0;
432
+ quota;
433
+ /** Called with the names whose URL changed, so surfaces can re-resolve. */
434
+ onChange = () => {
435
+ };
436
+ constructor(quota = 64 << 20) {
437
+ this.quota = quota;
438
+ }
439
+ put(name, mime, bytes) {
440
+ const old = this.entries.get(name);
441
+ const next = this.used - (old?.bytes.length ?? 0) + bytes.length;
442
+ if (next > this.quota) throw new Error(`resource store over quota (${next} > ${this.quota} bytes)`);
443
+ this.used = next;
444
+ this.entries.set(name, { mime, bytes, url: null, deps: /* @__PURE__ */ new Set() });
445
+ this.changed(name, old);
446
+ }
447
+ remove(name) {
448
+ const old = this.entries.get(name);
449
+ if (!old) return false;
450
+ this.used -= old.bytes.length;
451
+ this.entries.delete(name);
452
+ this.changed(name, old);
453
+ return true;
454
+ }
455
+ /** The `blob:` URL for a resource, or `null` if there is none (yet). */
456
+ url(name) {
457
+ const e = this.entries.get(name);
458
+ if (!e) return null;
459
+ if (e.url === null) {
460
+ let bytes = e.bytes;
461
+ if (e.mime.startsWith("text/css")) {
462
+ e.deps.clear();
463
+ const css = new TextDecoder().decode(e.bytes);
464
+ bytes = new TextEncoder().encode(this.rewriteCss(css, e.deps));
465
+ }
466
+ e.url = URL.createObjectURL(new Blob([bytes], { type: e.mime }));
467
+ }
468
+ return e.url;
469
+ }
470
+ /** CSS text with every reference resolved; `names` collects the resources.
471
+ * Without a context (a stylesheet resource, shared by surfaces with
472
+ * different policies) nothing from the network is allowed. */
473
+ rewriteCss(css, names, ctx) {
474
+ const one = (raw, d) => {
475
+ const v = raw.trim();
476
+ if (/^cid:/i.test(v)) {
477
+ names.add(v.slice(4));
478
+ return this.url(v.slice(4)) ?? INVALID;
479
+ }
480
+ if (/^data:/i.test(v) || /^blob:/i.test(v) || v.startsWith("#")) return raw;
481
+ for (const dir of d) {
482
+ const r = network(v, ctx, dir);
483
+ if (r !== INVALID) return r;
484
+ }
485
+ return INVALID;
486
+ };
487
+ return css.replace(/@import\s+(?:url\(\s*)?(['"])([^'"]*)\1\s*\)?/gi, (_m, _q, u) => `@import "${one(u, ["style-src"])}"`).replace(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi, (_m, _q, u) => `url("${one(u, ["img-src", "font-src"])}")`);
488
+ }
489
+ /** Resolves one attribute value; `names` collects the resources it names.
490
+ * `d` is the directive that covers the attribute, if it fetches. */
491
+ resolveUrl(value, names, ctx, d) {
492
+ const v = value.trim();
493
+ if (/^cid:/i.test(v)) {
494
+ const name = v.slice(4);
495
+ names.add(name);
496
+ return this.url(name) ?? INVALID;
497
+ }
498
+ if (/^data:/i.test(v) || /^blob:/i.test(v) || v === "" || v.startsWith("#")) return value;
499
+ if (d) return network(v, ctx, d);
500
+ return /^[a-z][a-z0-9+.-]*:/i.test(v) ? INVALID : value;
501
+ }
502
+ changed(name, old) {
503
+ const names = /* @__PURE__ */ new Set([name]);
504
+ for (const [other, e] of this.entries) {
505
+ if (e.deps.has(name)) {
506
+ if (e.url) URL.revokeObjectURL(e.url);
507
+ e.url = null;
508
+ names.add(other);
509
+ }
510
+ }
511
+ this.onChange(names);
512
+ if (old?.url) URL.revokeObjectURL(old.url);
513
+ }
514
+ };
515
+
516
+ // src/touch.ts
517
+ var DRAG_SLOP = 8;
518
+ var FLING_DECAY = 0.996;
519
+ var REST_MS = 80;
520
+ var Touch = class {
521
+ /**
522
+ * `exclusive`: every touch here is this Touch's (the cells: xterm.js's own
523
+ * touch handling, which listens on the document, never sees it). Otherwise
524
+ * (a surface) a touch is left alone until it moves. `engage`, if given,
525
+ * says on a touch's start whether it is this Touch's at all: one it
526
+ * declines is the browser's, from start to end.
527
+ */
528
+ constructor(target, win, host, exclusive = false, engage) {
529
+ this.win = win;
530
+ this.host = host;
531
+ this.exclusive = exclusive;
532
+ this.engage = engage;
533
+ const on = (type, f, passive) => {
534
+ const l = (e) => f(e);
535
+ target.addEventListener(type, l, { capture: true, passive });
536
+ this.off.push(() => target.removeEventListener(type, l, { capture: true }));
537
+ };
538
+ on("touchstart", (e) => this.start(e), true);
539
+ on("touchmove", (e) => this.move(e), false);
540
+ on("touchend", (e) => this.end(e), !exclusive);
541
+ on("touchcancel", (e) => {
542
+ this.keep(e);
543
+ this.touch = null;
544
+ }, true);
545
+ }
546
+ win;
547
+ host;
548
+ exclusive;
549
+ engage;
550
+ touch = null;
551
+ fling = 0;
552
+ off = [];
553
+ dispose() {
554
+ this.stopFling();
555
+ for (const f of this.off) f();
556
+ }
557
+ keep(e) {
558
+ if (this.exclusive) e.stopPropagation();
559
+ }
560
+ start(e) {
561
+ this.keep(e);
562
+ this.stopFling();
563
+ const t = e.touches.length === 1 ? e.touches[0] : void 0;
564
+ if (!t || this.engage && !this.engage(e)) {
565
+ this.touch = null;
566
+ return;
567
+ }
568
+ const [x, y] = this.host.toPage(t.clientX, t.clientY);
569
+ this.touch = { id: t.identifier, x0: x, y0: y, x, y, t: e.timeStamp, t0: e.timeStamp, vx: 0, vy: 0, dragging: false };
570
+ }
571
+ move(e) {
572
+ this.keep(e);
573
+ const s = this.touch;
574
+ if (!s) return;
575
+ if (e.touches.length !== 1) {
576
+ this.touch = null;
577
+ return;
578
+ }
579
+ const t = [...e.changedTouches].find((c) => c.identifier === s.id);
580
+ if (!t) return;
581
+ const [x, y] = this.host.toPage(t.clientX, t.clientY);
582
+ if (!s.dragging && Math.hypot(x - s.x0, y - s.y0) < DRAG_SLOP) return;
583
+ if (!s.dragging && this.host.claim && !this.host.claim(s.x0 - x, s.y0 - y, e)) {
584
+ this.touch = null;
585
+ return;
586
+ }
587
+ s.dragging = true;
588
+ e.preventDefault();
589
+ const dx = x - s.x;
590
+ const dy = y - s.y;
591
+ const dt = Math.max(1, e.timeStamp - s.t);
592
+ s.vx = 0.7 * (dx / dt) + 0.3 * s.vx;
593
+ s.vy = 0.7 * (dy / dt) + 0.3 * s.vy;
594
+ s.x = x;
595
+ s.y = y;
596
+ s.t = e.timeStamp;
597
+ this.host.scroll(-dx, -dy, x, y);
598
+ }
599
+ end(e) {
600
+ this.keep(e);
601
+ const s = this.touch;
602
+ if (!s || ![...e.changedTouches].some((c) => c.identifier === s.id)) return;
603
+ this.touch = null;
604
+ if (s.dragging) {
605
+ if (e.timeStamp - s.t < REST_MS) this.startFling(-s.vx, -s.vy, s.x, s.y);
606
+ return;
607
+ }
608
+ if (this.host.tap && e.timeStamp - s.t0 < 500) {
609
+ e.preventDefault();
610
+ this.host.tap(s.x, s.y);
611
+ }
612
+ }
613
+ startFling(vx, vy, x, y) {
614
+ let last = this.win.performance.now();
615
+ const step = (now) => {
616
+ const dt = now - last;
617
+ last = now;
618
+ const decay = Math.pow(FLING_DECAY, dt);
619
+ vx *= decay;
620
+ vy *= decay;
621
+ if (Math.abs(vx) < 0.02 && Math.abs(vy) < 0.02) {
622
+ this.fling = 0;
623
+ return;
624
+ }
625
+ this.host.scroll(vx * dt, vy * dt, x, y);
626
+ this.fling = this.win.requestAnimationFrame(step);
627
+ };
628
+ this.fling = this.win.requestAnimationFrame(step);
629
+ }
630
+ stopFling() {
631
+ if (this.fling) this.win.cancelAnimationFrame(this.fling);
632
+ this.fling = 0;
633
+ }
634
+ };
635
+
636
+ // src/resolver.ts
637
+ var DROP = /* @__PURE__ */ new Set(["script", "iframe", "frame", "frameset", "object", "embed", "applet", "base", "meta", "portal", "fencedframe"]);
638
+ var DROP_ATTRS = /* @__PURE__ */ new Set(["srcdoc", "autofocus", "http-equiv", "ping", "nonce", "integrity"]);
639
+ var Resolver = class {
640
+ /** The program's value of each attribute the resolver rewrote. */
641
+ orig = /* @__PURE__ */ new WeakMap();
642
+ /** The program's text of each `<style>` whose CSS names a resource. */
643
+ css = /* @__PURE__ */ new WeakMap();
644
+ /** Elements that name resources, and which. */
645
+ refs = /* @__PURE__ */ new Map();
646
+ /** Attributes the host set and the program did not write (a detached
647
+ * surface's `disabled`, SPEC §5.5): live, never reported. */
648
+ own = /* @__PURE__ */ new WeakMap();
649
+ store;
650
+ /** The document's base URL and effective network policy (set per a=doc). */
651
+ ctx;
652
+ constructor(store) {
653
+ this.store = store;
654
+ }
655
+ /** Cleans and resolves an inert subtree, in place. */
656
+ adopt(root) {
657
+ const walk = (node) => {
658
+ if (node.nodeType === 1) {
659
+ const el = node;
660
+ if (!this.allowed(el)) {
661
+ el.remove();
662
+ return;
663
+ }
664
+ this.cleanAttributes(el);
665
+ if (el.localName === "style") this.setCss(el, el.textContent ?? "");
666
+ }
667
+ for (const child of Array.from(node.childNodes)) walk(child);
668
+ if (node.nodeType === 1 && node.localName === "template") {
669
+ walk(node.content);
670
+ }
671
+ };
672
+ walk(root);
673
+ }
674
+ allowed(el) {
675
+ const name = el.localName;
676
+ if (DROP.has(name)) return false;
677
+ if (name === "link") {
678
+ const rel = (el.getAttribute("rel") ?? "").toLowerCase().split(/\s+/);
679
+ return rel.includes("stylesheet");
680
+ }
681
+ return true;
682
+ }
683
+ cleanAttributes(el) {
684
+ for (const a of Array.from(el.attributes)) {
685
+ const n = a.name.toLowerCase();
686
+ if (n.startsWith("on") || DROP_ATTRS.has(n)) {
687
+ el.removeAttribute(a.name);
688
+ } else if (isUrlAttr(n) || n === "style") {
689
+ this.set(el, a.name, a.value);
690
+ }
691
+ }
692
+ }
693
+ /** An attribute as the program wrote it. */
694
+ get(el, name) {
695
+ if (this.own.get(el)?.has(name)) return null;
696
+ return this.orig.get(el)?.get(name) ?? el.getAttribute(name);
697
+ }
698
+ /** Every attribute as the program wrote it, in order. */
699
+ attributes(el) {
700
+ const o = this.orig.get(el);
701
+ const own = this.own.get(el);
702
+ const out = [];
703
+ for (const a of Array.from(el.attributes)) if (!own?.has(a.name)) out.push([a.name, o?.get(a.name) ?? a.value]);
704
+ return out;
705
+ }
706
+ /** Sets an attribute of the host's own, which the program does not see. */
707
+ setOwn(el, name, value) {
708
+ el.setAttribute(name, value);
709
+ let own = this.own.get(el);
710
+ if (!own) this.own.set(el, own = /* @__PURE__ */ new Set());
711
+ own.add(name);
712
+ }
713
+ /** Removes an attribute, if it is the host's own. */
714
+ removeOwn(el, name) {
715
+ if (!this.own.get(el)?.delete(name)) return;
716
+ el.removeAttribute(name);
717
+ }
718
+ /** Sets an attribute to the program's value, resolving URLs and CSS. */
719
+ set(el, name, value) {
720
+ const n = name.toLowerCase();
721
+ if (n.startsWith("on") || DROP_ATTRS.has(n)) return;
722
+ const names = /* @__PURE__ */ new Set();
723
+ let live = value;
724
+ if (n === "style") live = this.store.rewriteCss(value, names, this.ctx);
725
+ else if (isUrlAttr(n)) {
726
+ const d = directiveFor(el.localName, n, el.getAttribute("rel") ?? "");
727
+ live = n === "srcset" ? this.srcset(value, names) : this.store.resolveUrl(value, names, this.ctx, d);
728
+ }
729
+ if ((el.localName === "a" || el.localName === "area") && n === "href") live = value;
730
+ try {
731
+ el.setAttribute(name, live);
732
+ } catch {
733
+ return;
734
+ }
735
+ this.own.get(el)?.delete(n);
736
+ this.remember(el, name, value, live, names);
737
+ }
738
+ remove(el, name) {
739
+ el.removeAttribute(name);
740
+ this.orig.get(el)?.delete(name);
741
+ this.own.get(el)?.delete(name.toLowerCase());
742
+ }
743
+ /** A `<style>`'s CSS as the program wrote it. */
744
+ cssOf(el) {
745
+ return this.css.get(el) ?? el.textContent ?? "";
746
+ }
747
+ setCss(el, text2) {
748
+ const names = /* @__PURE__ */ new Set();
749
+ const live = this.store.rewriteCss(text2, names, this.ctx);
750
+ if (el.textContent !== live) el.textContent = live;
751
+ if (live !== text2) this.css.set(el, text2);
752
+ else this.css.delete(el);
753
+ if (names.size > 0) this.track(el, names);
754
+ }
755
+ /** Re-resolves everything that names one of `names` (a resource changed);
756
+ * whether anything did. */
757
+ refresh(names) {
758
+ let any = false;
759
+ for (const [el, uses] of Array.from(this.refs)) {
760
+ if (!el.isConnected) {
761
+ this.refs.delete(el);
762
+ continue;
763
+ }
764
+ if (![...uses].some((u) => names.has(u))) continue;
765
+ any = true;
766
+ this.refs.delete(el);
767
+ const o = this.orig.get(el);
768
+ if (o) for (const [attr, value] of Array.from(o)) this.set(el, attr, value);
769
+ const css = this.css.get(el);
770
+ if (css !== void 0) this.setCss(el, css);
771
+ }
772
+ return any;
773
+ }
774
+ srcset(value, names) {
775
+ return value.split(",").map((part) => {
776
+ const [url, ...rest] = part.trim().split(/\s+/);
777
+ return [this.store.resolveUrl(url ?? "", names, this.ctx, "img-src"), ...rest].join(" ");
778
+ }).join(", ");
779
+ }
780
+ remember(el, name, value, live, names) {
781
+ let o = this.orig.get(el);
782
+ if (value !== live) {
783
+ if (!o) this.orig.set(el, o = /* @__PURE__ */ new Map());
784
+ o.set(name, value);
785
+ } else {
786
+ o?.delete(name);
787
+ }
788
+ if (names.size > 0) this.track(el, names);
789
+ }
790
+ track(el, names) {
791
+ const uses = this.refs.get(el) ?? /* @__PURE__ */ new Set();
792
+ for (const n of names) uses.add(n);
793
+ this.refs.set(el, uses);
794
+ }
795
+ };
796
+
797
+ // src/surface.ts
798
+ function csp(host) {
799
+ const with_ = (fixed, d) => [fixed, cspSources(host, d)].filter(Boolean).join(" ");
800
+ return [
801
+ "default-src 'none'",
802
+ with_("img-src data: blob:", "img-src"),
803
+ with_("media-src data: blob:", "media-src"),
804
+ with_("font-src data: blob:", "font-src"),
805
+ with_("style-src 'unsafe-inline' blob:", "style-src"),
806
+ "form-action 'none'",
807
+ "base-uri http: https:"
808
+ ].join("; ");
809
+ }
810
+ function baseOf(doc) {
811
+ const href = doc.querySelector("base[href]")?.getAttribute("href") ?? "";
812
+ try {
813
+ const u = new URL(href);
814
+ if (u.protocol === "http:" || u.protocol === "https:") return u.href;
815
+ } catch {
816
+ }
817
+ return NO_BASE;
818
+ }
819
+ var LINE_PX = 40;
820
+ var PAGE_FRACTION = 0.875;
821
+ var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "email", "password", "search", "tel", "url", "number", "date", "datetime-local", "month", "time", "week"]);
822
+ var CONTROLS = "input, select, textarea, button";
823
+ var HYPERLINK = "data-xterm-hotty-hyperlink";
824
+ var DETACHED_CSS = `@layer hotty-host {
825
+ * { cursor: auto !important; }
826
+ a[href], a[href] * { cursor: text !important; }
827
+ a[href][${HYPERLINK}], a[href][${HYPERLINK}] * { cursor: pointer !important; }
828
+ }`;
829
+ var Surface = class {
830
+ name;
831
+ box;
832
+ frame;
833
+ doc;
834
+ resolver;
835
+ deltas;
836
+ cols = 80;
837
+ rows = 24;
838
+ /** The part of the surface on screen, in cells (SPEC §5.2). */
839
+ win = { x: 0, y: 0, w: 80, h: 24 };
840
+ autoRows = false;
841
+ host;
842
+ hostStyle;
843
+ /** Set while the program moves focus, so no `focus` event echoes back. */
844
+ programFocus = false;
845
+ /** The document's base URL (SPEC §7.3). */
846
+ base = NO_BASE;
847
+ keyboard = false;
848
+ /** Detached (SPEC §5.5): nothing in the surface reaches the program. */
849
+ detachedState = false;
850
+ /** A press is on something that takes no focus (SPEC §10.1): the focus
851
+ * the browser gives the frame for it is not the surface's. */
852
+ pressNothing = false;
853
+ /** The element a press focuses, until the browser is done with it. */
854
+ pressed = null;
855
+ /** A press of another button than the primary one, until the browser is
856
+ * done with it: it neither takes nor gives back the keyboard. */
857
+ auxPress = false;
858
+ settling = null;
859
+ /** A drag under way (SPEC §9.1). */
860
+ drag = null;
861
+ /** A drag just ended at a release: it reported its own click, so the
862
+ * browser's, which follows in the same task, is not reported. */
863
+ dragReleased = false;
864
+ /** The placement asked for presses (`p=1`, SPEC §5.2). */
865
+ presses = false;
866
+ /** A mouse's or a pen's pointerdown came first: the mousedown that
867
+ * follows is the same press, not a tap's. */
868
+ pointerPress = false;
869
+ /** The pointer of a press with Alt held (SPEC §9.2), until its release:
870
+ * the gesture is the program's. */
871
+ altPointer = null;
872
+ /** Such a gesture just ended at a release: the browser's click that
873
+ * follows in the same task is not the surface's. */
874
+ altReleased = false;
875
+ /** One cell, in CSS pixels: a drag's detail counts cells (SPEC §9.1). */
876
+ cellW = 9;
877
+ cellH = 17;
878
+ css = "";
879
+ /** Keys whose keydown went to the program, so their keyup follows. */
880
+ forwarded = /* @__PURE__ */ new Set();
881
+ /** The rows the program heard last, while the placement asked for `fit`
882
+ * (`f=1`, SPEC §5.2); null when it did not. */
883
+ fitRows = null;
884
+ /** The height may have changed since the last check. */
885
+ fitDirty = false;
886
+ /** The frame a check waits for. */
887
+ fitFrame = 0;
888
+ fitObserver = null;
889
+ /** The axes the document asked to scroll along (`scroll`, SPEC §5.1): 1
890
+ * vertically, 2 horizontally, 3 both; 0, nothing scrolls. */
891
+ axes = 0;
892
+ /** Where the touch drag under way goes, once it has a direction. */
893
+ touchRoute = "terminal";
894
+ /** The box the wheel gesture under way scrolls, if it went to the
895
+ * document: the gesture moves no other (`scrollWheel`). */
896
+ wheelBox = null;
897
+ /** Some element may carry `CLIP`. */
898
+ clipping = false;
899
+ constructor(name, host, hostCss2, scheme) {
900
+ this.name = name;
901
+ this.host = host;
902
+ this.box = document.createElement("div");
903
+ this.box.className = "hotty-surface";
904
+ this.box.dataset.surface = name;
905
+ Object.assign(this.box.style, {
906
+ position: "absolute",
907
+ left: "0px",
908
+ top: "0px",
909
+ overflow: "hidden",
910
+ pointerEvents: "auto",
911
+ visibility: "hidden"
912
+ });
913
+ this.frame = document.createElement("iframe");
914
+ this.frame.setAttribute("sandbox", "allow-same-origin allow-forms");
915
+ this.frame.setAttribute("title", `HOTTY surface ${name}`);
916
+ Object.assign(this.frame.style, {
917
+ border: "0",
918
+ position: "absolute",
919
+ left: "0px",
920
+ top: "0px",
921
+ width: "100%",
922
+ height: "100%",
923
+ display: "block",
924
+ colorScheme: scheme
925
+ });
926
+ this.box.append(this.frame);
927
+ host.layer.append(this.box);
928
+ const doc = this.frame.contentDocument;
929
+ if (!doc) throw new Error("surface iframe has no document");
930
+ doc.open();
931
+ doc.write(
932
+ `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${csp(host.policy)}"><base href="${NO_BASE}"><style id="hotty-host"></style></head><body></body></html>`
933
+ );
934
+ doc.close();
935
+ this.doc = doc;
936
+ this.hostStyle = doc.getElementById("hotty-host");
937
+ this.css = hostCss2;
938
+ this.restyle();
939
+ this.resolver = new Resolver(host.store);
940
+ this.deltas = new Deltas(doc, this.resolver);
941
+ this.listen();
942
+ }
943
+ setHostCss(css, scheme) {
944
+ this.css = css;
945
+ this.restyle();
946
+ if (this.frame.style.colorScheme !== scheme) this.frame.style.colorScheme = scheme;
947
+ this.refit();
948
+ }
949
+ restyle() {
950
+ const css = [this.css, scrollCss(this.axes, this.host.pageScrolls), ...this.detachedState ? [DETACHED_CSS] : []].join("\n");
951
+ if (this.hostStyle.textContent !== css) this.hostStyle.textContent = css;
952
+ this.clip();
953
+ }
954
+ /** Replaces the whole document (`a=doc`): its head's styles and its body.
955
+ * `detached` (`d=1`) detaches the surface first; without it, the surface
956
+ * is the program's again (SPEC §5.1, §5.5). `axes` (`scroll`) are the
957
+ * axes it scrolls along, 0 for none (§5.3). */
958
+ setDocument(html, detached = false, axes = 0) {
959
+ if (detached) this.detach();
960
+ const scrolled = this.axes !== 0;
961
+ this.axes = axes;
962
+ const parsed = new DOMParser().parseFromString(html, "text/html");
963
+ this.base = baseOf(parsed);
964
+ const request = parse(parsed.querySelector('meta[name="hotty-network" i]')?.getAttribute("content") ?? "");
965
+ this.resolver.ctx = { base: this.base, policy: intersect(this.host.policy, request) };
966
+ const ours = this.doc.head.querySelector("base");
967
+ if (ours && ours.getAttribute("href") !== this.base) ours.setAttribute("href", this.base);
968
+ this.resolver.adopt(parsed.documentElement);
969
+ const head = this.doc.head;
970
+ for (const n of Array.from(head.childNodes)) {
971
+ const el = n;
972
+ const ours2 = el.nodeType === 1 && (el.id === "hotty-host" || el.localName === "meta" || el.localName === "base");
973
+ if (!ours2) n.remove();
974
+ }
975
+ head.append(...Array.from(parsed.head.childNodes));
976
+ this.copyAttributes(parsed.documentElement, this.doc.documentElement);
977
+ this.copyAttributes(parsed.body, this.doc.body);
978
+ this.doc.body.replaceChildren(...Array.from(parsed.body.childNodes));
979
+ this.detachedState = detached;
980
+ this.sync();
981
+ if (scrolled) {
982
+ this.doc.documentElement.scrollTo(0, 0);
983
+ this.doc.body.scrollTo(0, 0);
984
+ }
985
+ this.refit();
986
+ }
987
+ /** A delta (SPEC §6). On a detached surface, the controls it adds are
988
+ * disabled too, and so is one whose `disabled` it removes. */
989
+ delta(op, target, key, payload) {
990
+ try {
991
+ this.deltas.apply(op, target, key, payload);
992
+ } finally {
993
+ if (this.detachedState) this.sync();
994
+ else this.clip();
995
+ this.refit();
996
+ }
997
+ }
998
+ // --- Ownership (SPEC §5.5) -----------------------------------------------
999
+ get detached() {
1000
+ return this.detachedState;
1001
+ }
1002
+ /** `a=detach`: nothing in the surface reaches the program any more. A
1003
+ * surface that has the keyboard gives it back silently, with no `change`
1004
+ * and no `blur`, and its form controls act disabled. */
1005
+ detach() {
1006
+ if (!this.detachedState) {
1007
+ this.detachedState = true;
1008
+ this.dropDrag();
1009
+ const had = this.keyboard || this.frameFocused();
1010
+ this.keyboard = false;
1011
+ this.focusedControl()?.blur();
1012
+ if (had) this.giveBack();
1013
+ }
1014
+ this.sync();
1015
+ }
1016
+ /** Disables the form controls of a detached surface, as if each had the
1017
+ * `disabled` attribute, or enables those it disabled, and marks its
1018
+ * hyperlinks for the host's stylesheet. The attributes are the host's
1019
+ * own: the document reports the program's (SPEC §16). */
1020
+ sync() {
1021
+ const detached = this.detachedState;
1022
+ for (const el of Array.from(this.doc.querySelectorAll(CONTROLS))) {
1023
+ if (!detached) this.resolver.removeOwn(el, "disabled");
1024
+ else if (!el.hasAttribute("disabled")) this.resolver.setOwn(el, "disabled", "");
1025
+ }
1026
+ for (const a of Array.from(this.doc.querySelectorAll("a"))) {
1027
+ if (detached && this.isHyperlink(a)) {
1028
+ if (!a.hasAttribute(HYPERLINK)) this.resolver.setOwn(a, HYPERLINK, "");
1029
+ } else this.resolver.removeOwn(a, HYPERLINK);
1030
+ }
1031
+ this.restyle();
1032
+ }
1033
+ copyAttributes(from, to) {
1034
+ for (const [name] of this.resolver.attributes(to)) {
1035
+ if (!from.hasAttribute(name)) this.resolver.remove(to, name);
1036
+ }
1037
+ for (const [name, value] of this.resolver.attributes(from)) this.resolver.set(to, name, value);
1038
+ }
1039
+ /** Rows the content needs at `cols` columns (`r=auto`). */
1040
+ contentRows(cols, cellW, cellH) {
1041
+ Object.assign(this.box.style, { display: "block", visibility: "hidden" });
1042
+ return this.neededRows(cols, cellW, cellH);
1043
+ }
1044
+ /**
1045
+ * Rows the document needs at `cols` columns: `r=auto`'s, and `fit`'s
1046
+ * (SPEC §5.2). The frame is laid out at the width a placement gives it
1047
+ * and a height of 1px, so the viewport adds nothing, then restored in the
1048
+ * same task: nothing is drawn in between.
1049
+ */
1050
+ neededRows(cols, cellW, cellH) {
1051
+ const f = this.frame.style;
1052
+ const [width, height] = [f.width, f.height];
1053
+ const root = this.doc.documentElement;
1054
+ if (this.axes) this.resolver.setOwn(root, MEASURE, "");
1055
+ Object.assign(f, { width: `${Math.round(cols * cellW)}px`, height: "1px" });
1056
+ const h = root.scrollHeight;
1057
+ Object.assign(f, { width, height });
1058
+ if (this.axes) this.resolver.removeOwn(root, MEASURE);
1059
+ return Math.max(1, Math.min(1e3, Math.ceil(h / cellH)));
1060
+ }
1061
+ /**
1062
+ * In a document that scrolls along one axis only, the elements whose
1063
+ * `overflow` along the other is `auto` or `scroll` are marked (`CLIP`), so
1064
+ * that the host stylesheet clips that axis as `overflow: hidden` does
1065
+ * (SPEC §5.3): no scrollbar, and nothing the user does scrolls it. CSS
1066
+ * cannot select by a computed value, so the addon reads it, with its own
1067
+ * rule off (`UNCLIPPED`), each time the document or its style may have
1068
+ * changed: a document, a delta, the host stylesheet. Style only, no
1069
+ * layout, and only for a document with `scroll` of 1 or 2.
1070
+ */
1071
+ clip() {
1072
+ const prop = this.axes === 1 ? "overflowX" : this.axes === 2 ? "overflowY" : null;
1073
+ if (!prop && !this.clipping) return;
1074
+ const marked = new Set(this.doc.querySelectorAll(`[${CLIP}]`));
1075
+ const want = /* @__PURE__ */ new Set();
1076
+ if (prop) {
1077
+ const root = this.doc.documentElement;
1078
+ const win = this.frame.contentWindow;
1079
+ this.resolver.setOwn(root, UNCLIPPED, "");
1080
+ for (const el of [this.doc.body, ...Array.from(this.doc.body.querySelectorAll("*"))]) {
1081
+ if (/auto|scroll/.test(win.getComputedStyle(el)[prop])) want.add(el);
1082
+ }
1083
+ this.resolver.removeOwn(root, UNCLIPPED);
1084
+ }
1085
+ for (const el of marked) if (!want.has(el)) this.resolver.removeOwn(el, CLIP);
1086
+ for (const el of want) if (!marked.has(el)) this.resolver.setOwn(el, CLIP, "");
1087
+ this.clipping = want.size > 0;
1088
+ }
1089
+ // --- Fit (SPEC §5.2: f=1 on a=place) --------------------------------------
1090
+ /**
1091
+ * `f=1` on the placement: from `rows`, the placement's own, the program
1092
+ * hears `fit` whenever the rows the document needs differ from those it
1093
+ * heard last. `null`: the placement did not ask (or is gone).
1094
+ *
1095
+ * What can change the height asks for a check in the next frame drawn:
1096
+ * a document, a delta, a resource (`refresh`), the host stylesheet (cell
1097
+ * size, font), an image, stylesheet or font loading, and, for whatever
1098
+ * else does (the user opening a `<details>`, say), the root's and the
1099
+ * body's boxes changing size. One check per frame, so one `fit` at most.
1100
+ */
1101
+ setFit(rows) {
1102
+ this.fitRows = rows;
1103
+ if (rows === null) {
1104
+ this.fitObserver?.disconnect();
1105
+ this.fitObserver = null;
1106
+ if (this.fitFrame) this.frame.ownerDocument.defaultView?.cancelAnimationFrame(this.fitFrame);
1107
+ this.fitFrame = 0;
1108
+ this.fitDirty = false;
1109
+ return;
1110
+ }
1111
+ if (!this.fitObserver) {
1112
+ this.fitObserver = new ResizeObserver(() => this.refit());
1113
+ this.fitObserver.observe(this.doc.documentElement);
1114
+ this.fitObserver.observe(this.doc.body);
1115
+ }
1116
+ this.refit();
1117
+ }
1118
+ /** The document's height may have changed: a check in the next frame
1119
+ * drawn. A surface out of view draws none; it checks once shown. */
1120
+ refit() {
1121
+ if (this.fitRows === null) return;
1122
+ this.fitDirty = true;
1123
+ if (this.fitFrame || this.box.style.display === "none") return;
1124
+ this.fitFrame = this.frame.ownerDocument.defaultView.requestAnimationFrame(() => {
1125
+ this.fitFrame = 0;
1126
+ this.checkFit();
1127
+ });
1128
+ }
1129
+ checkFit() {
1130
+ if (this.fitRows === null || !this.fitDirty || this.box.style.display === "none") return;
1131
+ this.fitDirty = false;
1132
+ if (this.detachedState) return;
1133
+ const rows = this.neededRows(this.cols, this.cellW, this.cellH);
1134
+ if (rows === this.fitRows) return;
1135
+ this.fitRows = rows;
1136
+ this.emit("fit", "", { r: rows });
1137
+ }
1138
+ /** A resource changed (`a=res`, `a=del`): what names it resolves again. */
1139
+ refresh(names) {
1140
+ if (this.resolver.refresh(names)) this.refit();
1141
+ }
1142
+ /** The surface is `cols`×`rows` cells, and the box shows `win` of it:
1143
+ * the document keeps the whole size, offset by the window's corner. */
1144
+ setSize(cols, rows, cellW, cellH, win = { x: 0, y: 0, w: cols, h: rows }) {
1145
+ this.cols = cols;
1146
+ this.rows = rows;
1147
+ this.win = win;
1148
+ this.cellW = cellW;
1149
+ this.cellH = cellH;
1150
+ Object.assign(this.frame.style, {
1151
+ left: `${-Math.round(win.x * cellW)}px`,
1152
+ top: `${-win.y * cellH}px`,
1153
+ width: `${Math.round(cols * cellW)}px`,
1154
+ height: `${rows * cellH}px`
1155
+ });
1156
+ this.box.style.width = `${Math.round(win.w * cellW)}px`;
1157
+ this.box.style.height = `${win.h * cellH}px`;
1158
+ }
1159
+ /** The placement's z (SPEC §5.2): CSS z-index on the box, so overlapping
1160
+ * boxes stack by it, and by their order in the layer at the same z. */
1161
+ setZ(z) {
1162
+ this.box.style.zIndex = String(z);
1163
+ }
1164
+ /** Shows the surface with its top-left corner at (x, y) in the screen's pixels. */
1165
+ show(x, y) {
1166
+ this.box.style.left = `${x}px`;
1167
+ this.box.style.top = `${y}px`;
1168
+ this.box.style.visibility = "visible";
1169
+ this.box.style.display = "block";
1170
+ if (this.fitDirty) this.refit();
1171
+ }
1172
+ /** Out of view (its line left the screen): a drag under way ends
1173
+ * (SPEC §9.1). */
1174
+ hide() {
1175
+ this.cancelDrag();
1176
+ this.dropAlt();
1177
+ this.box.style.display = "none";
1178
+ }
1179
+ /** No placement (hidden, or not placed yet): the document stays, and the
1180
+ * browser skips its style, layout and paint until it is placed again. */
1181
+ park() {
1182
+ this.box.style.display = "none";
1183
+ }
1184
+ destroy() {
1185
+ this.setFit(null);
1186
+ this.dropDrag();
1187
+ this.dropAlt();
1188
+ if (this.settling) clearTimeout(this.settling);
1189
+ if (this.keyboard || this.frameFocused()) this.host.focusTerminal();
1190
+ this.box.remove();
1191
+ }
1192
+ // --- Keyboard (PROTOCOL §8, Q-0005) -------------------------------------
1193
+ hasKeyboard() {
1194
+ return this.keyboard;
1195
+ }
1196
+ /** `a=focus`: the surface takes the keyboard, at `target` if given. */
1197
+ focus(target) {
1198
+ const el = target ? this.doc.getElementById(target) : null;
1199
+ if (target && !el) throw new Error(target);
1200
+ this.programFocus = true;
1201
+ try {
1202
+ this.frame.focus();
1203
+ this.frame.contentWindow?.focus();
1204
+ if (el) el.focus();
1205
+ else if (!this.focusedControl()) this.tabbable()[0]?.focus();
1206
+ this.keyboard = true;
1207
+ } finally {
1208
+ setTimeout(() => this.programFocus = false, 0);
1209
+ }
1210
+ }
1211
+ /** `a=blur`, or the keyboard leaving: the edited control commits first. */
1212
+ blur() {
1213
+ this.commit();
1214
+ if (this.keyboard) this.giveBack();
1215
+ }
1216
+ /** The browser's focus goes from the frame to the terminal. The frame lets
1217
+ * go of it first: Firefox can leave the frame focused otherwise. */
1218
+ giveBack() {
1219
+ this.frame.blur();
1220
+ this.host.focusTerminal();
1221
+ }
1222
+ commit() {
1223
+ const active = this.focusedControl();
1224
+ if (active) active.blur();
1225
+ }
1226
+ focusedControl() {
1227
+ const a = this.doc.activeElement;
1228
+ return a && a !== this.doc.body && a !== this.doc.documentElement ? a : null;
1229
+ }
1230
+ /** Whether the browser's focus is in this surface's frame. */
1231
+ frameFocused() {
1232
+ return this.frame.ownerDocument.activeElement === this.frame;
1233
+ }
1234
+ /**
1235
+ * A press (SPEC §10.1). On an element that takes focus, the focus that
1236
+ * follows gives the surface the keyboard (`onFocusIn`). On anything else,
1237
+ * or anywhere in a detached surface, it takes nothing: a surface that had
1238
+ * the keyboard gives it back (its control commits, then `blur`), and the
1239
+ * focus the browser gives the frame anyway goes back to the terminal.
1240
+ */
1241
+ onPress(e) {
1242
+ if (e.type === "pointerdown" && e.pointerType === "touch") return;
1243
+ if (e.button !== 0) {
1244
+ this.auxPress = true;
1245
+ setTimeout(() => this.auxPress = false, 0);
1246
+ return;
1247
+ }
1248
+ const el = this.detachedState ? null : focusTargetAt(e, this.hyper);
1249
+ if (el) {
1250
+ this.pressed = el;
1251
+ this.settle();
1252
+ return;
1253
+ }
1254
+ this.pressNothing = true;
1255
+ if (this.keyboard) {
1256
+ this.commit();
1257
+ this.keyboard = false;
1258
+ this.emit("blur", "");
1259
+ }
1260
+ this.settle();
1261
+ }
1262
+ /**
1263
+ * Once the browser is done with a press (its default action focuses the
1264
+ * frame): an element that takes focus has it, even where the browser does
1265
+ * not focus one on a click (a button on a Mac, say); and focus the
1266
+ * surface does not hold goes back to the terminal. The frame keeps it
1267
+ * until then, so text selection starts as usual.
1268
+ */
1269
+ settle() {
1270
+ if (this.settling) return;
1271
+ this.settling = setTimeout(() => {
1272
+ this.settling = null;
1273
+ const el = this.pressed;
1274
+ this.pressed = null;
1275
+ this.pressNothing = false;
1276
+ if (el && !this.keyboard && !this.detachedState && el.isConnected && this.doc.activeElement !== el) {
1277
+ this.frame.focus();
1278
+ el.focus({ preventScroll: true });
1279
+ }
1280
+ if (!this.keyboard && this.frameFocused()) this.giveBack();
1281
+ }, 0);
1282
+ }
1283
+ /** An element got focus: the surface takes the keyboard if the element is
1284
+ * one that takes focus, and the focus is neither the program's (no echo)
1285
+ * nor a detached surface's. */
1286
+ onFocusIn(e) {
1287
+ if (this.programFocus || this.auxPress) return;
1288
+ if (this.detachedState || this.pressNothing || !isElement(e.target) || !takesFocus(e.target, this.hyper)) {
1289
+ this.settle();
1290
+ return;
1291
+ }
1292
+ if (this.keyboard) return;
1293
+ this.keyboard = true;
1294
+ this.emit("focus", "");
1295
+ }
1296
+ tabbable() {
1297
+ const all = this.doc.querySelectorAll(
1298
+ "a[href], button, input:not([type=hidden]), select, textarea, summary, [tabindex], [contenteditable=''], [contenteditable=true]"
1299
+ );
1300
+ return Array.from(all).filter((el) => el.tabIndex >= 0 && !el.disabled && el.getClientRects().length > 0);
1301
+ }
1302
+ controlKind(el) {
1303
+ if (!el) return "none";
1304
+ const tag = el.localName;
1305
+ if (tag === "textarea" || el.isContentEditable) return "textarea";
1306
+ if (tag === "select") return "select";
1307
+ if (tag === "input") {
1308
+ const type = (el.type || "text").toLowerCase();
1309
+ if (TEXT_TYPES.has(type)) return "text";
1310
+ return "activatable";
1311
+ }
1312
+ if (tag === "button" || tag === "a" || tag === "summary") return "activatable";
1313
+ return "none";
1314
+ }
1315
+ /** Whether the focused element uses the key; if not, the program gets it. */
1316
+ consumes(e, kind) {
1317
+ const plain = !e.ctrlKey && !e.altKey && !e.metaKey;
1318
+ const k = e.key;
1319
+ const printable = [...k].length === 1;
1320
+ if (k === "Escape") return false;
1321
+ switch (kind) {
1322
+ case "text":
1323
+ return (plain || e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) && (printable || ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Home", "End", "Enter"].includes(k));
1324
+ case "textarea":
1325
+ return (plain || e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) && (printable || ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown", "Enter"].includes(k));
1326
+ case "select":
1327
+ return plain && (printable || ["ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown", "Enter", " "].includes(k));
1328
+ case "activatable":
1329
+ return plain && (k === " " || k === "Enter");
1330
+ default:
1331
+ return false;
1332
+ }
1333
+ }
1334
+ onKey(e) {
1335
+ if (e.isComposing || e.defaultPrevented) return;
1336
+ if (!this.keyboard) {
1337
+ this.settle();
1338
+ if (this.host.browserKey(e)) return;
1339
+ this.forward(e);
1340
+ return;
1341
+ }
1342
+ if (e.key === "Tab" && !e.ctrlKey && !e.altKey && !e.metaKey) {
1343
+ const order = this.tabbable();
1344
+ const at = order.indexOf(this.doc.activeElement);
1345
+ const leaving = order.length === 0 || (e.shiftKey ? at <= 0 : at === order.length - 1 || at < 0);
1346
+ if (leaving) {
1347
+ e.preventDefault();
1348
+ this.blur();
1349
+ }
1350
+ return;
1351
+ }
1352
+ if (this.consumes(e, this.controlKind(this.doc.activeElement))) return;
1353
+ if (this.host.browserKey(e)) return;
1354
+ if (this.axes && this.scrollKey(e)) return;
1355
+ this.forward(e);
1356
+ }
1357
+ /**
1358
+ * A key a browser scrolls with, in a document that scrolls (SPEC §5.3):
1359
+ * it scrolls the innermost box, from the focused element outward (from
1360
+ * the root when none is), that can still move that way. Where none can,
1361
+ * the key goes on to the program, as every key the surface does not use
1362
+ * does (§10.2), unless `overscroll-behavior` stops it. The addon scrolls
1363
+ * the box itself: the browser's own action could be the focused
1364
+ * element's instead (a radio button's arrows, a number field's).
1365
+ */
1366
+ scrollKey(e) {
1367
+ const k = keyScroll(e);
1368
+ if (!k) return false;
1369
+ const to = this.scroller(this.focusedControl(), k.axis, k.sign);
1370
+ if (to === "terminal") return false;
1371
+ e.preventDefault();
1372
+ if (to === "stop") return true;
1373
+ const y = k.axis === "y";
1374
+ const by = k.by === "line" ? LINE_PX : k.by === "page" ? Math.max(1, (y ? to.clientHeight : to.clientWidth) * PAGE_FRACTION) : y ? to.scrollHeight : to.scrollWidth;
1375
+ to.scrollBy(y ? { top: k.sign * by } : { left: k.sign * by });
1376
+ return true;
1377
+ }
1378
+ forward(e) {
1379
+ e.preventDefault();
1380
+ e.stopPropagation();
1381
+ this.forwarded.add(e.code);
1382
+ this.host.key(e);
1383
+ }
1384
+ /** The release of a key the program had: the program hears it too, if it
1385
+ * asked the terminal for releases (SPEC §10.3). */
1386
+ onKeyUp(e) {
1387
+ if (!this.forwarded.delete(e.code)) return;
1388
+ e.preventDefault();
1389
+ e.stopPropagation();
1390
+ this.host.key(e);
1391
+ }
1392
+ // --- Drags (SPEC §9.1) ----------------------------------------------------
1393
+ /**
1394
+ * A press of a mouse's or a pen's primary button on an element with
1395
+ * `drag` in its `data-on` (the nearest, from the pressed one outward, and
1396
+ * only if it has an id) starts a drag. The surface then holds the pointer
1397
+ * until the release: captured by its root element, which no delta
1398
+ * replaces, so every move comes here wherever it is (over the cells,
1399
+ * another surface, or outside the page), and nothing else hears it.
1400
+ */
1401
+ onDragDown(e) {
1402
+ this.cancelDrag();
1403
+ if (this.detachedState || e.pointerType === "touch" || e.button !== 0 || !e.isPrimary) return;
1404
+ let start = null;
1405
+ for (const n of e.composedPath()) {
1406
+ if (isElement(n) && listens(n, "drag")) {
1407
+ start = n;
1408
+ break;
1409
+ }
1410
+ }
1411
+ const id = start?.getAttribute("id");
1412
+ if (!id) return;
1413
+ const [c, r] = this.cellOf(e);
1414
+ const keys = keysOf(e);
1415
+ this.drag = { pointer: e.pointerId, start: id, target: id, c, r, keys };
1416
+ this.emit("dragstart", id, { c, r, keys });
1417
+ try {
1418
+ this.doc.documentElement.setPointerCapture(e.pointerId);
1419
+ } catch {
1420
+ }
1421
+ }
1422
+ /** A move during a drag: an event each time its target changes, and
1423
+ * while it has none, each time its cell does. */
1424
+ onDragMove(e) {
1425
+ const d = this.drag;
1426
+ if (!d || e.pointerId !== d.pointer) return;
1427
+ const [c, r] = this.cellOf(e);
1428
+ const target = this.dragTarget(this.elementAt(e, c, r));
1429
+ const keys = keysOf(e);
1430
+ if (target !== d.target || target === "" && (c !== d.c || r !== d.r)) {
1431
+ this.emit("drag", target, { c, r, keys });
1432
+ }
1433
+ Object.assign(d, { target, c, r, keys });
1434
+ }
1435
+ /** The release ends the drag, wherever it is. Where it began, it is the
1436
+ * click it would have been without the drag; anywhere else, no click. */
1437
+ onDragUp(e) {
1438
+ const d = this.drag;
1439
+ if (!d || e.pointerId !== d.pointer || e.button !== 0) return;
1440
+ this.drag = null;
1441
+ const [c, r] = this.cellOf(e);
1442
+ const at = this.elementAt(e, c, r);
1443
+ const target = this.dragTarget(at);
1444
+ this.emit("dragend", target, { c, r, keys: keysOf(e) });
1445
+ this.dragReleased = true;
1446
+ setTimeout(() => this.dragReleased = false, 0);
1447
+ if (target === d.start && at) this.reportClick(ancestry(at));
1448
+ }
1449
+ /** The browser took the pointer away before the release. */
1450
+ lostPointer(e) {
1451
+ if (this.drag && e.pointerId === this.drag.pointer) this.cancelDrag();
1452
+ if (e.pointerId === this.altPointer) this.altPointer = null;
1453
+ }
1454
+ /**
1455
+ * Ends a drag under way without a release (SPEC §9.1: its placement went
1456
+ * away, a new document came, or the pointer was lost): `dragend` with no
1457
+ * target, at the last cell the program heard of.
1458
+ */
1459
+ cancelDrag() {
1460
+ const d = this.drag;
1461
+ if (!d) return;
1462
+ this.dropDrag();
1463
+ this.emit("dragend", "", { c: d.c, r: d.r, keys: d.keys });
1464
+ }
1465
+ /** Ends a drag under way, silently (a detached or deleted surface). */
1466
+ dropDrag() {
1467
+ const d = this.drag;
1468
+ if (!d) return;
1469
+ this.drag = null;
1470
+ try {
1471
+ if (this.doc.documentElement.hasPointerCapture(d.pointer)) this.doc.documentElement.releasePointerCapture(d.pointer);
1472
+ } catch {
1473
+ }
1474
+ }
1475
+ /** The surface's cell under the pointer, from its top left (the frame's
1476
+ * origin), counting on past its edges. */
1477
+ cellOf(e) {
1478
+ return [Math.floor(e.clientX / this.cellW), Math.floor(e.clientY / this.cellH)];
1479
+ }
1480
+ /** The element under the pointer, if it is in the window (SPEC §5.2). */
1481
+ elementAt(e, c, r) {
1482
+ const w = this.win;
1483
+ if (c < w.x || c >= w.x + w.w || r < w.y || r >= w.y + w.h) return null;
1484
+ return this.doc.elementFromPoint(e.clientX, e.clientY);
1485
+ }
1486
+ /** A drag's target: the nearest element with an id and `drag` in its
1487
+ * `data-on`, from `el` outward; "" where there is none. */
1488
+ dragTarget(el) {
1489
+ for (let n = el; n; n = n.parentElement) {
1490
+ const id = n.getAttribute("id");
1491
+ if (id && listens(n, "drag")) return id;
1492
+ }
1493
+ return "";
1494
+ }
1495
+ // --- Presses with Alt (SPEC §9.2) ---------------------------------------
1496
+ /**
1497
+ * A press of a mouse's or a pen's primary button with Alt held is the
1498
+ * program's: the addon replays it on the cells beneath, and the surface
1499
+ * hears nothing of it (no press, drag, click, focus or selection; its
1500
+ * mousedown is cancelled). It holds the pointer until the release, as a
1501
+ * drag does, so every move and the release go to the cells, wherever the
1502
+ * pointer is. Whether it is the program's is decided here, at the press:
1503
+ * Alt let go later changes nothing.
1504
+ */
1505
+ onAltDown(e) {
1506
+ if (e.pointerType === "touch" || e.button !== 0 || !e.altKey || !e.isPrimary) return false;
1507
+ this.cancelDrag();
1508
+ this.dropAlt();
1509
+ this.pointerPress = true;
1510
+ this.altPointer = e.pointerId;
1511
+ this.onLeave(e);
1512
+ try {
1513
+ this.doc.documentElement.setPointerCapture(e.pointerId);
1514
+ } catch {
1515
+ }
1516
+ const [x, y] = this.toPage(e);
1517
+ this.host.cells("down", e, x, y);
1518
+ return true;
1519
+ }
1520
+ onAltMove(e) {
1521
+ if (e.pointerId !== this.altPointer) return false;
1522
+ const [x, y] = this.toPage(e);
1523
+ this.host.cells("move", e, x, y);
1524
+ return true;
1525
+ }
1526
+ onAltUp(e) {
1527
+ if (e.pointerId !== this.altPointer || e.button !== 0) return false;
1528
+ this.dropAlt();
1529
+ this.altReleased = true;
1530
+ setTimeout(() => this.altReleased = false, 0);
1531
+ const [x, y] = this.toPage(e);
1532
+ this.host.cells("up", e, x, y);
1533
+ return true;
1534
+ }
1535
+ /** Ends such a gesture without a release: the frame goes away. */
1536
+ dropAlt() {
1537
+ const p = this.altPointer;
1538
+ if (p === null) return;
1539
+ this.altPointer = null;
1540
+ try {
1541
+ if (this.doc.documentElement.hasPointerCapture(p)) this.doc.documentElement.releasePointerCapture(p);
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ /** A point of the frame's document in the page. */
1546
+ toPage(e) {
1547
+ const r = this.frame.getBoundingClientRect();
1548
+ return [r.left + e.clientX, r.top + e.clientY];
1549
+ }
1550
+ /** `press` (SPEC §9), if the placement asked for it: `t` is the nearest
1551
+ * element with an id. A press on a hyperlink is the terminal's. */
1552
+ reportPress(e) {
1553
+ if (!this.presses) return;
1554
+ const link = this.linkIn(e);
1555
+ if (link && this.isHyperlink(link)) return;
1556
+ for (const n of e.composedPath()) {
1557
+ const id = isElement(n) ? n.getAttribute("id") : null;
1558
+ if (id) {
1559
+ this.emit("press", id, { area: this.areaOf(n) });
1560
+ return;
1561
+ }
1562
+ }
1563
+ this.emit("press", "");
1564
+ }
1565
+ /**
1566
+ * The cells an element's border box covers as the user sees it, scrolled
1567
+ * included (SPEC §9: `area`), counted from the surface's top left cell:
1568
+ * whole, where it is clipped or scrolled away. An edge within half a
1569
+ * device pixel of a cell's is on it: layout rounds positions to fractions
1570
+ * of a pixel, and the user sees no less than a device pixel.
1571
+ */
1572
+ areaOf(el) {
1573
+ const b = el.getBoundingClientRect();
1574
+ const tol = 0.5 / (this.frame.ownerDocument.defaultView?.devicePixelRatio || 1);
1575
+ const c = Math.floor((b.left + tol) / this.cellW);
1576
+ const r = Math.floor((b.top + tol) / this.cellH);
1577
+ const right = Math.ceil((b.right - tol) / this.cellW);
1578
+ const bottom = Math.ceil((b.bottom - tol) / this.cellH);
1579
+ return { c, r, w: Math.max(0, right - c), h: Math.max(0, bottom - r) };
1580
+ }
1581
+ // --- Events for the program (PROTOCOL §8) --------------------------------
1582
+ listen() {
1583
+ const d = this.doc;
1584
+ const win = this.frame.contentWindow;
1585
+ d.addEventListener(
1586
+ "pointerdown",
1587
+ (e) => {
1588
+ if (this.onAltDown(e)) return;
1589
+ this.pointerPress = e.pointerType !== "touch";
1590
+ if (this.pointerPress && e.button === 0 && e.isPrimary) this.reportPress(e);
1591
+ this.onDragDown(e);
1592
+ this.onPress(e);
1593
+ },
1594
+ true
1595
+ );
1596
+ d.addEventListener("pointermove", (e) => this.onAltMove(e) || this.onDragMove(e), true);
1597
+ d.addEventListener("pointerup", (e) => this.onAltUp(e) || this.onDragUp(e), true);
1598
+ d.addEventListener("pointercancel", (e) => this.lostPointer(e), true);
1599
+ d.addEventListener("lostpointercapture", (e) => this.lostPointer(e), true);
1600
+ d.addEventListener(
1601
+ "mousedown",
1602
+ (e) => {
1603
+ if (this.altPointer !== null) {
1604
+ e.preventDefault();
1605
+ e.stopImmediatePropagation();
1606
+ return;
1607
+ }
1608
+ if (!this.pointerPress && e.button === 0) this.reportPress(e);
1609
+ this.pointerPress = false;
1610
+ this.onPress(e);
1611
+ },
1612
+ true
1613
+ );
1614
+ d.addEventListener("focusin", (e) => this.onFocusIn(e), true);
1615
+ d.addEventListener("click", (e) => this.onClick(e), true);
1616
+ d.addEventListener("auxclick", (e) => this.onAuxClick(e), true);
1617
+ d.addEventListener("mouseover", (e) => this.onOver(e), true);
1618
+ d.addEventListener("mouseout", (e) => this.onOut(e), true);
1619
+ d.addEventListener("submit", (e) => this.onSubmit(e), true);
1620
+ d.addEventListener("change", (e) => this.onChange(e), true);
1621
+ d.addEventListener("input", (e) => this.onInput(e), true);
1622
+ d.addEventListener("keydown", (e) => this.onKey(e), true);
1623
+ d.addEventListener("keyup", (e) => this.onKeyUp(e), true);
1624
+ d.addEventListener("dragstart", (e) => e.preventDefault(), true);
1625
+ d.addEventListener("wheel", (e) => this.onWheel(e), { capture: true, passive: false });
1626
+ d.addEventListener("scroll", (e) => this.onScroll(e), true);
1627
+ d.addEventListener("load", () => this.refit(), true);
1628
+ d.addEventListener("error", () => this.refit(), true);
1629
+ d.fonts.addEventListener("loadingdone", () => this.refit());
1630
+ const toPage = (x, y) => {
1631
+ const r = this.frame.getBoundingClientRect();
1632
+ return [r.left + x, r.top + y];
1633
+ };
1634
+ if (this.host.pageScrolls) {
1635
+ new Touch(
1636
+ d,
1637
+ this.frame.ownerDocument.defaultView,
1638
+ { scroll: (dx, dy) => this.host.scrollPage(dx, dy), toPage },
1639
+ false,
1640
+ (e) => !this.axes && this.inScroller(e.target)
1641
+ );
1642
+ } else {
1643
+ new Touch(d, this.frame.ownerDocument.defaultView, {
1644
+ scroll: (dx, dy, x, y) => {
1645
+ if (this.touchRoute === "terminal") this.host.wheel(new WheelEvent("wheel", { deltaX: dx, deltaY: dy, deltaMode: 0, clientX: x, clientY: y }), x, y);
1646
+ },
1647
+ toPage,
1648
+ claim: (dx, dy, e) => {
1649
+ this.touchRoute = this.axes ? this.gestureRoute(e.target, dx, dy) : "terminal";
1650
+ return this.touchRoute !== "doc";
1651
+ }
1652
+ });
1653
+ }
1654
+ win.addEventListener("focus", () => {
1655
+ if (!this.programFocus && !this.auxPress && !this.keyboard) this.settle();
1656
+ });
1657
+ win.addEventListener("blur", () => {
1658
+ this.commit();
1659
+ if (this.keyboard) {
1660
+ this.keyboard = false;
1661
+ this.emit("blur", "");
1662
+ }
1663
+ });
1664
+ }
1665
+ /**
1666
+ * Nothing in a surface scrolls, and a wheel over it is the terminal's
1667
+ * (SPEC §5.3, §9). Wheel events never leave an iframe, so without this
1668
+ * the terminal's scrollback (or, on the alternate screen, the program's
1669
+ * wheel input) would stall under the pointer. Ctrl and the wheel stay the
1670
+ * browser's: its zoom. Where the page scrolls (the `scroll` option), the
1671
+ * wheel is the browser's too: nothing in the frame scrolls, so it scrolls
1672
+ * the page, as over the cells; but over an element it would scroll
1673
+ * instead, the surface scrolls the page itself.
1674
+ */
1675
+ onWheel(e) {
1676
+ if (e.ctrlKey) return;
1677
+ if (this.axes) {
1678
+ this.scrollWheel(e);
1679
+ return;
1680
+ }
1681
+ if (this.host.pageScrolls) {
1682
+ if (!this.inScroller(e.target)) return;
1683
+ e.preventDefault();
1684
+ const px = e.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? 1 : e.deltaMode === WheelEvent.DOM_DELTA_LINE ? 20 : 400;
1685
+ this.host.scrollPage(e.deltaX * px, e.deltaY * px);
1686
+ return;
1687
+ }
1688
+ e.preventDefault();
1689
+ const r = this.frame.getBoundingClientRect();
1690
+ this.host.wheel(e, r.left + e.clientX, r.top + e.clientY);
1691
+ }
1692
+ /** Whether a wheel or a touch at target would scroll an element of the
1693
+ * document, overflowing with `overflow: auto` or `scroll`: the browser
1694
+ * gives the gesture to it before the page, and nothing in a surface
1695
+ * scrolls (SPEC §5.3), so the gesture would go nowhere. */
1696
+ inScroller(target) {
1697
+ for (let n = target && isElement(target) ? target : null; n; n = n.parentElement) {
1698
+ const s = n.ownerDocument.defaultView.getComputedStyle(n);
1699
+ const y = /auto|scroll/.test(s.overflowY) && n.scrollHeight > n.clientHeight;
1700
+ const x = /auto|scroll/.test(s.overflowX) && n.scrollWidth > n.clientWidth;
1701
+ if (x || y) return true;
1702
+ }
1703
+ return false;
1704
+ }
1705
+ /**
1706
+ * A wheel over a document that scrolls (SPEC §5.3). A gesture's first
1707
+ * wheel decides where all of it goes, as a browser latches scrolling to
1708
+ * what it began on: while an element under the pointer can still move
1709
+ * that way, the browser scrolls it; where none can, the gesture goes on
1710
+ * to the terminal, as over the cells beneath (§9), unless
1711
+ * `overscroll-behavior` stops it. A gesture the document took goes no
1712
+ * further when it reaches the end there, as in a browser. Where the page
1713
+ * scrolls (the `scroll` option), the page is the terminal, and the
1714
+ * browser chains to it from the document natively. A wheel the browser
1715
+ * does not let the page cancel is the browser's already.
1716
+ */
1717
+ scrollWheel(e) {
1718
+ if (this.host.pageScrolls || !e.cancelable) return;
1719
+ const route = this.host.wheelGesture(this.name, () => {
1720
+ const to = this.gestureTarget(e.target, e.deltaX, e.deltaY);
1721
+ this.wheelBox = typeof to === "string" ? null : to;
1722
+ return typeof to === "string" ? to : "doc";
1723
+ });
1724
+ const [axis, d] = mainAxis(e.deltaX, e.deltaY);
1725
+ if (route === "doc" && this.wheelBox && this.canMove(this.wheelBox, axis, d)) return;
1726
+ e.preventDefault();
1727
+ if (route !== "terminal") return;
1728
+ const r = this.frame.getBoundingClientRect();
1729
+ this.host.wheel(e, r.left + e.clientX, r.top + e.clientY);
1730
+ }
1731
+ /** Where a gesture that scrolls by dx, dy pixels at `target` goes, by its
1732
+ * main axis (SPEC §5.3). */
1733
+ gestureRoute(target, dx, dy) {
1734
+ const to = this.gestureTarget(target, dx, dy);
1735
+ return typeof to === "string" ? to : "doc";
1736
+ }
1737
+ gestureTarget(target, dx, dy) {
1738
+ const [axis, d] = mainAxis(dx, dy);
1739
+ return this.scroller(target && isElement(target) ? target : null, axis, d);
1740
+ }
1741
+ /** Whether a box that scrolls can still move along `axis`, `d`'s way. */
1742
+ canMove(el, axis, d) {
1743
+ if (!d || !(this.axes & (axis === "y" ? 1 : 2))) return false;
1744
+ const [at, span] = axis === "y" ? [el.scrollTop, el.scrollHeight - el.clientHeight] : [el.scrollLeft, el.scrollWidth - el.clientWidth];
1745
+ const rtl = axis === "x" && this.frame.contentWindow.getComputedStyle(el).direction === "rtl";
1746
+ const [min, max] = rtl ? [-span, 0] : [0, span];
1747
+ return d > 0 ? at < max - 0.5 : at > min + 0.5;
1748
+ }
1749
+ /**
1750
+ * What a scroll along `axis`, `d`'s way, from `from` (an element, or the
1751
+ * root for none) moves in a document that scrolls (SPEC §5.3): the
1752
+ * innermost box, from `from` outward, that the user scrolls along it
1753
+ * (`overflow` `auto` or `scroll`; the root's, unless `hidden` or `clip`),
1754
+ * that overflows there, and that can still move that way. At a box that
1755
+ * cannot, the scroll stops if its `overscroll-behavior` is `contain` or
1756
+ * `none`, and goes on outward otherwise. Past the root, and along an axis
1757
+ * the document did not ask for, it is the terminal's.
1758
+ */
1759
+ scroller(from, axis, d) {
1760
+ if (!d || !(this.axes & (axis === "y" ? 1 : 2))) return "terminal";
1761
+ const win = this.frame.contentWindow;
1762
+ const root = this.doc.documentElement;
1763
+ const body = this.doc.body;
1764
+ const rootStyle = win.getComputedStyle(root);
1765
+ const fromBody = rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
1766
+ const prop = axis === "y" ? "overflowY" : "overflowX";
1767
+ for (let el = from ?? root; el; el = el.parentElement) {
1768
+ if (el === body && fromBody) continue;
1769
+ const s = el === root ? rootStyle : win.getComputedStyle(el);
1770
+ const scrolls = el === root ? !/hidden|clip/.test((fromBody ? win.getComputedStyle(body) : s)[prop]) : /auto|scroll/.test(s[prop]);
1771
+ const span = axis === "y" ? el.scrollHeight - el.clientHeight : el.scrollWidth - el.clientWidth;
1772
+ if (!scrolls || span < 1) continue;
1773
+ if (this.canMove(el, axis, d)) return el;
1774
+ const stops = axis === "y" ? s.overscrollBehaviorY : s.overscrollBehaviorX;
1775
+ if (stops === "contain" || stops === "none") return "stop";
1776
+ }
1777
+ return "terminal";
1778
+ }
1779
+ /** Whatever the browser scrolled along an axis the document did not ask
1780
+ * for (a focused element into view, say) goes back to zero. A text
1781
+ * field's own text follows its caret (SPEC §5.3). */
1782
+ onScroll(e) {
1783
+ if (this.axes === 3) return;
1784
+ const t = e.target;
1785
+ const el = t === this.doc ? this.doc.scrollingElement : t && isElement(t) ? t : null;
1786
+ if (!el || t !== this.doc && (el.localName === "input" || el.localName === "textarea")) return;
1787
+ const x = !(this.axes & 2) && el.scrollLeft !== 0;
1788
+ const y = !(this.axes & 1) && el.scrollTop !== 0;
1789
+ if (x || y) el.scrollTo(x ? 0 : el.scrollLeft, y ? 0 : el.scrollTop);
1790
+ }
1791
+ /** An event for the program (SPEC §9); a detached surface sends none (§5.5). */
1792
+ emit(kind, target, detail) {
1793
+ if (this.detachedState) return;
1794
+ this.host.event(this.name, kind, target, detail);
1795
+ }
1796
+ linkIn(e) {
1797
+ for (const n of e.composedPath()) {
1798
+ if (isElement(n) && n.localName === "a" && n.hasAttribute("href")) return n;
1799
+ }
1800
+ return null;
1801
+ }
1802
+ /** A link's `url` (SPEC §9): its href resolved against the document's
1803
+ * base, or "" under hotty.invalid or when it is no URL. */
1804
+ urlOf(link) {
1805
+ const href = this.resolver.get(link, "href") ?? "";
1806
+ let url = "";
1807
+ try {
1808
+ url = new URL(href, this.base).href;
1809
+ } catch {
1810
+ }
1811
+ return url.startsWith(NO_BASE) ? "" : url;
1812
+ }
1813
+ /** The link's box in the page (the iframe may be offset for a window). */
1814
+ pageBox(el) {
1815
+ const f = this.frame.getBoundingClientRect();
1816
+ const r = el.getBoundingClientRect();
1817
+ return new DOMRect(f.left + r.left, f.top + r.top, r.width, r.height);
1818
+ }
1819
+ /** A hyperlink (SPEC §9): a link with target="_blank" and a `url`. One
1820
+ * without a url is the program's, as any other link. */
1821
+ isHyperlink(link) {
1822
+ return link.getAttribute("target") === "_blank" && this.urlOf(link) !== "";
1823
+ }
1824
+ hyper = (link) => this.isHyperlink(link);
1825
+ /** A link's click (SPEC §9). A hyperlink is the terminal's, as an OSC 8
1826
+ * one is, and not reported; any other link is the program's: an event,
1827
+ * with or without an id. The host opens nothing itself. */
1828
+ onLink(link, e) {
1829
+ e.preventDefault();
1830
+ const url = this.urlOf(link);
1831
+ if (this.isHyperlink(link)) {
1832
+ if (e.button === 0) this.host.hyperlink("activate", e, url, this.pageBox(link));
1833
+ return;
1834
+ }
1835
+ const detail = { href: this.resolver.get(link, "href") ?? "" };
1836
+ if (url) detail.url = url;
1837
+ detail.area = this.areaOf(link);
1838
+ this.emit("click", link.getAttribute("id") ?? "", detail);
1839
+ }
1840
+ /** Other buttons on a link do nothing: no navigation, no new tab. */
1841
+ onAuxClick(e) {
1842
+ if (this.linkIn(e)) e.preventDefault();
1843
+ }
1844
+ hovered = null;
1845
+ /** The pointer onto a hyperlink: the terminal hears it, as it does over
1846
+ * an OSC 8 one. */
1847
+ onOver(e) {
1848
+ if (this.altPointer !== null) return;
1849
+ const link = this.linkIn(e);
1850
+ const hyper = link && this.isHyperlink(link) ? link : null;
1851
+ if (hyper === this.hovered?.link) return;
1852
+ this.onLeave(e);
1853
+ const url = hyper ? this.urlOf(hyper) : "";
1854
+ if (hyper) {
1855
+ this.hovered = { link: hyper, url };
1856
+ this.host.hyperlink("hover", e, url, this.pageBox(hyper));
1857
+ }
1858
+ }
1859
+ onOut(e) {
1860
+ if (this.altPointer !== null) return;
1861
+ const to = e.relatedTarget;
1862
+ if (this.hovered && to && this.hovered.link.contains(to)) return;
1863
+ this.onLeave(e);
1864
+ }
1865
+ onLeave(e) {
1866
+ const h = this.hovered;
1867
+ if (!h) return;
1868
+ this.hovered = null;
1869
+ this.host.hyperlink("leave", e, h.url, this.pageBox(h.link));
1870
+ }
1871
+ onClick(e) {
1872
+ const link = this.linkIn(e);
1873
+ if (this.altReleased) {
1874
+ this.altReleased = false;
1875
+ e.preventDefault();
1876
+ e.stopImmediatePropagation();
1877
+ return;
1878
+ }
1879
+ if (this.dragReleased) {
1880
+ this.dragReleased = false;
1881
+ if (link) e.preventDefault();
1882
+ return;
1883
+ }
1884
+ if (link) {
1885
+ this.onLink(link, e);
1886
+ return;
1887
+ }
1888
+ this.reportClick(e.composedPath());
1889
+ }
1890
+ /** The click of the nearest element on `path` that reports one (SPEC §9). */
1891
+ reportClick(path) {
1892
+ for (const n of path) {
1893
+ if (!isElement(n)) continue;
1894
+ if (n.localName === "a" && n.hasAttribute("href")) {
1895
+ if (this.isHyperlink(n)) return;
1896
+ const url = this.urlOf(n);
1897
+ const detail = { href: this.resolver.get(n, "href") ?? "" };
1898
+ if (url) detail.url = url;
1899
+ detail.area = this.areaOf(n);
1900
+ this.emit("click", n.getAttribute("id") ?? "", detail);
1901
+ return;
1902
+ }
1903
+ const el = n;
1904
+ const tag = el.localName;
1905
+ const type = (el.getAttribute("type") ?? "").toLowerCase();
1906
+ const wants = (el.getAttribute("data-on") ?? "").split(/\s+/).includes("click");
1907
+ const reportable = tag === "button" || tag === "a" || tag === "summary" || tag === "input" && ["button", "submit", "reset"].includes(type) || wants;
1908
+ if (!reportable) continue;
1909
+ const id = el.getAttribute("id");
1910
+ if (id) {
1911
+ const detail = {};
1912
+ const href = this.resolver.get(el, "href");
1913
+ if (href !== null) detail.href = href;
1914
+ const value = this.resolver.get(el, "value");
1915
+ if (value !== null) detail.value = value;
1916
+ detail.area = this.areaOf(el);
1917
+ this.emit("click", id, detail);
1918
+ }
1919
+ break;
1920
+ }
1921
+ }
1922
+ onSubmit(e) {
1923
+ e.preventDefault();
1924
+ const form = e.target;
1925
+ const id = form.getAttribute("id");
1926
+ if (!id) return;
1927
+ const fields = {};
1928
+ const data = new this.frame.contentWindow.FormData(form, e.submitter ?? void 0);
1929
+ for (const [k, v] of data) if (typeof v === "string") fields[k] = v;
1930
+ this.emit("submit", id, fields);
1931
+ }
1932
+ onChange(e) {
1933
+ const el = e.target;
1934
+ const id = el.getAttribute?.("id");
1935
+ if (!id) return;
1936
+ const type = (el.getAttribute("type") ?? "").toLowerCase();
1937
+ if (el.localName === "input" && (type === "checkbox" || type === "radio")) {
1938
+ this.emit("change", id, { checked: el.checked, value: this.resolver.get(el, "value") });
1939
+ } else if ("value" in el) {
1940
+ this.emit("change", id, { value: el.value });
1941
+ }
1942
+ }
1943
+ onInput(e) {
1944
+ const el = e.target;
1945
+ const id = el.getAttribute?.("id");
1946
+ if (!id) return;
1947
+ const type = (el.getAttribute("type") ?? "").toLowerCase();
1948
+ if (type === "checkbox" || type === "radio") return;
1949
+ if ((el.getAttribute("data-on") ?? "").split(/\s+/).includes("input") && "value" in el) {
1950
+ this.emit("input", id, { value: el.value });
1951
+ }
1952
+ }
1953
+ };
1954
+ function isElement(n) {
1955
+ return n.nodeType === 1;
1956
+ }
1957
+ function listens(el, what) {
1958
+ return (el.getAttribute("data-on") ?? "").split(/\s+/).includes(what);
1959
+ }
1960
+ function ancestry(el) {
1961
+ const out = [];
1962
+ for (let n = el; n; n = n.parentElement) out.push(n);
1963
+ return out;
1964
+ }
1965
+ function keysOf(e) {
1966
+ const keys = [];
1967
+ if (e.shiftKey) keys.push("shift");
1968
+ if (e.ctrlKey) keys.push("ctrl");
1969
+ if (e.altKey) keys.push("alt");
1970
+ if (e.metaKey) keys.push("meta");
1971
+ return keys;
1972
+ }
1973
+ function takesFocus(el, hyper) {
1974
+ if (el.matches(":disabled")) return false;
1975
+ switch (el.localName) {
1976
+ case "button":
1977
+ case "select":
1978
+ case "textarea":
1979
+ return true;
1980
+ case "input":
1981
+ return el.type !== "hidden";
1982
+ case "a":
1983
+ case "area":
1984
+ if (el.hasAttribute("href")) return !hyper(el);
1985
+ break;
1986
+ case "summary": {
1987
+ const d = el.parentElement;
1988
+ if (d?.localName === "details" && d.querySelector(":scope > summary") === el) return true;
1989
+ break;
1990
+ }
1991
+ }
1992
+ const ce = el.getAttribute("contenteditable");
1993
+ if (ce !== null && ce.toLowerCase() !== "false" && el.isContentEditable) return true;
1994
+ return el.hasAttribute("tabindex") && el.tabIndex >= 0;
1995
+ }
1996
+ function focusTargetAt(e, hyper) {
1997
+ for (const n of e.composedPath()) {
1998
+ if (!isElement(n)) continue;
1999
+ if (n.localName === "a" && n.hasAttribute("href") && hyper(n)) return null;
2000
+ if (takesFocus(n, hyper)) return n;
2001
+ const control = n.localName === "label" ? n.control : null;
2002
+ if (control) return takesFocus(control, hyper) ? control : null;
2003
+ }
2004
+ return null;
2005
+ }
2006
+ function mainAxis(dx, dy) {
2007
+ return Math.abs(dy) >= Math.abs(dx) ? ["y", dy] : ["x", dx];
2008
+ }
2009
+ function keyScroll(e) {
2010
+ if (e.ctrlKey || e.altKey || e.metaKey) return null;
2011
+ if (e.shiftKey) return e.key === " " ? { axis: "y", sign: -1, by: "page" } : null;
2012
+ switch (e.key) {
2013
+ case "ArrowDown":
2014
+ return { axis: "y", sign: 1, by: "line" };
2015
+ case "ArrowUp":
2016
+ return { axis: "y", sign: -1, by: "line" };
2017
+ case "ArrowRight":
2018
+ return { axis: "x", sign: 1, by: "line" };
2019
+ case "ArrowLeft":
2020
+ return { axis: "x", sign: -1, by: "line" };
2021
+ case "PageDown":
2022
+ case " ":
2023
+ return { axis: "y", sign: 1, by: "page" };
2024
+ case "PageUp":
2025
+ return { axis: "y", sign: -1, by: "page" };
2026
+ case "End":
2027
+ return { axis: "y", sign: 1, by: "end" };
2028
+ case "Home":
2029
+ return { axis: "y", sign: -1, by: "end" };
2030
+ }
2031
+ return null;
2032
+ }
2033
+
2034
+ // src/version.ts
2035
+ var VERSION = "0.1.0";
2036
+ var HOST = "@neuroplastio/xterm-addon-hotty";
2037
+
2038
+ // src/wire.ts
2039
+ var OSC = 7279;
2040
+ var CHUNK = 4096;
2041
+ var MAX_COMMAND = 16 << 20;
2042
+ var Control = class _Control {
2043
+ pairs;
2044
+ constructor(pairs = []) {
2045
+ this.pairs = pairs;
2046
+ }
2047
+ static parse(text2) {
2048
+ const pairs = [];
2049
+ if (text2 === "") return new _Control(pairs);
2050
+ for (const part of text2.split(":")) {
2051
+ const eq = part.indexOf("=");
2052
+ if (eq <= 0) throw new Error(`bad control pair ${JSON.stringify(part)}`);
2053
+ pairs.push([part.slice(0, eq), part.slice(eq + 1)]);
2054
+ }
2055
+ return new _Control(pairs);
2056
+ }
2057
+ get(key) {
2058
+ for (const [k, v] of this.pairs) if (k === key) return v;
2059
+ return void 0;
2060
+ }
2061
+ set(key, value) {
2062
+ const i = this.pairs.findIndex(([k]) => k === key);
2063
+ if (i >= 0) this.pairs[i] = [key, value];
2064
+ else this.pairs.push([key, value]);
2065
+ return this;
2066
+ }
2067
+ delete(key) {
2068
+ const i = this.pairs.findIndex(([k]) => k === key);
2069
+ if (i >= 0) this.pairs.splice(i, 1);
2070
+ return this;
2071
+ }
2072
+ /** A continuation chunk carries nothing but `m` and `q`. */
2073
+ onlyChunkKeys() {
2074
+ return this.pairs.every(([k]) => k === "m" || k === "q");
2075
+ }
2076
+ encode() {
2077
+ return this.pairs.map(([k, v]) => `${k}=${clean2(v)}`).join(":");
2078
+ }
2079
+ };
2080
+ function clean2(value) {
2081
+ return value.replace(/[^\x20-\x7e]|[:;=]/gu, "_");
2082
+ }
2083
+ var Assembler = class {
2084
+ partial = null;
2085
+ /** Feeds the data of one OSC 7279 sequence. */
2086
+ feed(data) {
2087
+ const semi = data.indexOf(";");
2088
+ const ctlText = semi < 0 ? data : data.slice(0, semi);
2089
+ const b64 = semi < 0 ? "" : data.slice(semi + 1);
2090
+ let control;
2091
+ try {
2092
+ control = Control.parse(ctlText);
2093
+ } catch (e) {
2094
+ this.partial = null;
2095
+ return [{ kind: "invalid", reason: String(e.message) }];
2096
+ }
2097
+ const more = control.get("m") === "1";
2098
+ const partial = this.partial;
2099
+ this.partial = null;
2100
+ if (partial && control.onlyChunkKeys()) {
2101
+ partial.b64.push(b64);
2102
+ partial.size += b64.length;
2103
+ if (partial.size > MAX_COMMAND) return [{ kind: "invalid", reason: "command too large" }];
2104
+ if (more) {
2105
+ this.partial = partial;
2106
+ return [{ kind: "partial" }];
2107
+ }
2108
+ return settle([], emit(partial.control, partial.b64.join("")));
2109
+ }
2110
+ const out = [];
2111
+ if (partial) out.push({ kind: "invalid", reason: "chunked command interrupted" });
2112
+ if (more) {
2113
+ this.partial = { control, b64: [b64], size: b64.length };
2114
+ out.push({ kind: "partial" });
2115
+ return out;
2116
+ }
2117
+ return settle(out, emit(control, b64));
2118
+ }
2119
+ };
2120
+ function settle(out, d) {
2121
+ if (d instanceof Promise) return d.then((x) => [...out, x]);
2122
+ out.push(d);
2123
+ return out;
2124
+ }
2125
+ function emit(control, b64) {
2126
+ const fail = (e) => ({ kind: "invalid", reason: `bad payload: ${e.message}` });
2127
+ let bytes;
2128
+ try {
2129
+ bytes = fromBase64(b64);
2130
+ } catch (e) {
2131
+ return fail(e);
2132
+ }
2133
+ const done = (payload) => {
2134
+ control.delete("m").delete("o");
2135
+ return { kind: "command", command: { control, payload } };
2136
+ };
2137
+ if (control.get("o") === "z") return inflate(bytes).then(done, fail);
2138
+ return done(bytes);
2139
+ }
2140
+ function fromBase64(b64) {
2141
+ const s = b64.replace(/\s+/g, "");
2142
+ if (s === "") return new Uint8Array(0);
2143
+ const bin = atob(s);
2144
+ const out = new Uint8Array(bin.length);
2145
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
2146
+ return out;
2147
+ }
2148
+ function toBase64(bytes) {
2149
+ let bin = "";
2150
+ for (let i = 0; i < bytes.length; i += 32768) {
2151
+ bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
2152
+ }
2153
+ return btoa(bin);
2154
+ }
2155
+ async function inflate(bytes) {
2156
+ const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate"));
2157
+ return new Uint8Array(await new Response(stream).arrayBuffer());
2158
+ }
2159
+ var utf8 = new TextDecoder("utf-8", { fatal: false });
2160
+ var utf8Encoder = new TextEncoder();
2161
+ function text(bytes) {
2162
+ return utf8.decode(bytes);
2163
+ }
2164
+ function encode(control, payload = new Uint8Array(0)) {
2165
+ const bytes = typeof payload === "string" ? utf8Encoder.encode(payload) : payload;
2166
+ const b64 = toBase64(bytes);
2167
+ const osc = (ctl, chunk) => `\x1B]${OSC};${ctl}${chunk === null ? "" : `;${chunk}`}\x1B\\`;
2168
+ if (b64.length <= CHUNK) return osc(control.encode(), b64 === "" ? null : b64);
2169
+ const quiet = control.get("q");
2170
+ let out = "";
2171
+ for (let i = 0; i < b64.length; i += CHUNK) {
2172
+ const last = i + CHUNK >= b64.length;
2173
+ let ctl;
2174
+ if (i === 0) {
2175
+ ctl = new Control([...control.pairs]).set("m", "1");
2176
+ } else {
2177
+ ctl = new Control([["m", last ? "0" : "1"]]);
2178
+ if (quiet !== void 0) ctl.set("q", quiet);
2179
+ }
2180
+ out += osc(ctl.encode(), b64.slice(i, i + CHUNK));
2181
+ }
2182
+ return out;
2183
+ }
2184
+ async function decodeAll(stream) {
2185
+ const a = new Assembler();
2186
+ const out = [];
2187
+ const re = /\x1b\]7279;([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
2188
+ for (const m of stream.matchAll(re)) {
2189
+ for (const d of await a.feed(m[1])) if (d.kind === "command") out.push(d.command);
2190
+ }
2191
+ return out;
2192
+ }
2193
+
2194
+ // src/addon.ts
2195
+ var mac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
2196
+ function browserKeys(e) {
2197
+ if (e.key === "F5" || e.key === "F11" || e.key === "F12") return true;
2198
+ if (mac && e.metaKey) return true;
2199
+ const mod = e.ctrlKey || e.metaKey;
2200
+ const k = e.key.toLowerCase();
2201
+ if (mod && !e.altKey && k === "r") return true;
2202
+ if (mod && ["=", "+", "-", "0"].includes(e.key)) return true;
2203
+ if (e.ctrlKey && e.shiftKey && ["i", "j", "c"].includes(k)) return true;
2204
+ return false;
2205
+ }
2206
+ var GESTURE_MS = 150;
2207
+ var EVENTS = ["click", "change", "input", "submit", "press", "drag", "focus", "blur", "resize", "fit"];
2208
+ var Failure = class extends Error {
2209
+ code;
2210
+ constructor(code, detail) {
2211
+ super(detail);
2212
+ this.code = code;
2213
+ }
2214
+ };
2215
+ var HottyAddon = class {
2216
+ term;
2217
+ opts;
2218
+ store;
2219
+ surfaces = /* @__PURE__ */ new Map();
2220
+ placements = /* @__PURE__ */ new Map();
2221
+ assembler = new Assembler();
2222
+ disposables = [];
2223
+ layer = null;
2224
+ /** Commands held while synchronized output is on (PROTOCOL §5). */
2225
+ held = [];
2226
+ heldTimer = null;
2227
+ css = "";
2228
+ policy;
2229
+ metrics = { cellW: 0, cellH: 0, key: "" };
2230
+ resizeTimer = null;
2231
+ /** A surface's key is being forwarded to the terminal (`key`), and the
2232
+ * terminal's own focus is held off meanwhile. A program in the page may
2233
+ * answer the key inside its dispatch, since xterm.js parses what follows
2234
+ * user input at once: a surface that gives the keyboard back then has
2235
+ * its focus carried out once the key is through. */
2236
+ forwarding = false;
2237
+ focusAfter = false;
2238
+ /** The wheel gesture under way: the surface its last wheel was over ("" for
2239
+ * the cells), where it goes, and when that wheel came. */
2240
+ gesture = null;
2241
+ constructor(options = {}) {
2242
+ this.opts = { maxSurfaces: 64, resourceQuota: 64 << 20, ...options };
2243
+ this.policy = clean(options.network);
2244
+ this.store = new Store(this.opts.resourceQuota);
2245
+ this.store.onChange = (names) => {
2246
+ for (const s of this.surfaces.values()) s.refresh(names);
2247
+ };
2248
+ }
2249
+ activate(term) {
2250
+ this.term = term;
2251
+ term.attachCustomKeyEventHandler((e) => !this.browserKey(e));
2252
+ const p = term.parser;
2253
+ this.disposables.push(
2254
+ p.registerOscHandler(OSC, (data) => this.onOsc(data)),
2255
+ // `CSI ? 2026 l` ends a synchronized batch: apply what was held, then
2256
+ // let xterm handle the mode as usual.
2257
+ p.registerCsiHandler({ prefix: "?", final: "l" }, (params) => {
2258
+ if (params.some((x) => x === 2026)) this.release();
2259
+ return false;
2260
+ }),
2261
+ // RIS: every surface goes (PROTOCOL §4).
2262
+ p.registerEscHandler({ final: "c" }, () => {
2263
+ this.reset();
2264
+ return false;
2265
+ }),
2266
+ term.buffer.onBufferChange((b) => this.onBufferChange(b.type)),
2267
+ term.onRender(() => {
2268
+ this.bindTouch();
2269
+ this.checkMetrics();
2270
+ }),
2271
+ term.onResize(() => this.checkMetrics()),
2272
+ term.onScroll(() => this.reposition()),
2273
+ term.onWriteParsed(() => this.reposition())
2274
+ );
2275
+ }
2276
+ dispose() {
2277
+ this.cellTouch?.dispose();
2278
+ this.cellTouch = null;
2279
+ this.reset();
2280
+ for (const d of this.disposables) d.dispose();
2281
+ this.disposables.length = 0;
2282
+ this.layer?.remove();
2283
+ this.layer = null;
2284
+ }
2285
+ /**
2286
+ * An element of a surface as the program wrote it (tag, attributes, text,
2287
+ * child elements): the shape the shared conformance vectors check
2288
+ * (conformance/README.md). `null` if there is no such surface or element.
2289
+ */
2290
+ inspect(surface, id) {
2291
+ const s = this.surfaces.get(surface);
2292
+ const el = s?.doc.getElementById(id);
2293
+ if (!s || !el) return null;
2294
+ return {
2295
+ tag: el.localName,
2296
+ attrs: Object.fromEntries(s.resolver.attributes(el)),
2297
+ text: el.textContent ?? "",
2298
+ children: Array.from(el.children, (c) => [c.localName, c.getAttribute("id"), c.textContent ?? ""])
2299
+ };
2300
+ }
2301
+ // --- Wire -----------------------------------------------------------------
2302
+ onOsc(data) {
2303
+ const decoded = this.assembler.feed(data);
2304
+ if (decoded instanceof Promise) return decoded.then((d) => this.run(d)).then(() => true);
2305
+ const r = this.run(decoded);
2306
+ return r instanceof Promise ? r.then(() => true) : true;
2307
+ }
2308
+ run(decoded) {
2309
+ let chain;
2310
+ for (const d of decoded) {
2311
+ if (d.kind === "invalid") this.opts.onInvalid?.(d.reason);
2312
+ if (d.kind !== "command") continue;
2313
+ const cmd = d.command;
2314
+ chain = sequence(chain, () => this.submit(cmd));
2315
+ }
2316
+ return chain;
2317
+ }
2318
+ submit(cmd) {
2319
+ if (this.term.modes.synchronizedOutputMode && cmd.control.get("a") !== "q") {
2320
+ this.held.push(cmd);
2321
+ this.heldTimer ??= setTimeout(() => this.release(), 1e3);
2322
+ return;
2323
+ }
2324
+ return this.apply([cmd]);
2325
+ }
2326
+ release() {
2327
+ if (this.heldTimer) clearTimeout(this.heldTimer);
2328
+ this.heldTimer = null;
2329
+ const batch = this.held;
2330
+ this.held = [];
2331
+ if (batch.length) void this.apply(batch);
2332
+ }
2333
+ apply(batch) {
2334
+ const t0 = performance.now();
2335
+ let chain;
2336
+ for (const cmd of batch) chain = sequence(chain, () => this.handle(cmd));
2337
+ const done = () => this.opts.onFrame?.({ commands: batch.length, applyMs: performance.now() - t0 });
2338
+ if (chain) return chain.then(done);
2339
+ done();
2340
+ }
2341
+ handle(cmd) {
2342
+ const action = cmd.control.get("a") ?? "";
2343
+ const quiet = Number(cmd.control.get("q") ?? "0");
2344
+ const ok = (r) => {
2345
+ if (quiet === 0) this.reply("ok", cmd, r ?? {});
2346
+ };
2347
+ const fail = (e) => {
2348
+ const code = e instanceof Failure || e instanceof DeltaError ? e.code : "EINVAL";
2349
+ const detail = e instanceof Error ? e.message : String(e);
2350
+ if (quiet < 2) this.reply("err", cmd, { body: JSON.stringify({ code, detail }) });
2351
+ };
2352
+ try {
2353
+ const r = this.dispatch(action, cmd);
2354
+ if (r instanceof Promise) return r.then(ok, fail);
2355
+ ok(r);
2356
+ } catch (e) {
2357
+ fail(e);
2358
+ }
2359
+ }
2360
+ reply(kind, cmd, r) {
2361
+ const c = new Control().set("a", kind);
2362
+ for (const k of ["n", "s"]) {
2363
+ const v = cmd.control.get(k);
2364
+ if (v !== void 0) c.set(k, v);
2365
+ }
2366
+ c.set("re", cmd.control.get("a") ?? "");
2367
+ for (const [k, v] of r.extra ?? []) c.set(k, v);
2368
+ this.send(encode(c, r.body ?? ""));
2369
+ }
2370
+ send(message) {
2371
+ this.term.input(message, false);
2372
+ }
2373
+ // --- Commands (PROTOCOL §3-§6) -------------------------------------------
2374
+ dispatch(action, cmd) {
2375
+ const c = cmd.control;
2376
+ switch (action) {
2377
+ case "q":
2378
+ return { body: JSON.stringify(this.capabilities()) };
2379
+ case "doc": {
2380
+ const name = this.surfaceName(c);
2381
+ let s = this.surfaces.get(name);
2382
+ if (!s) {
2383
+ if (this.surfaces.size >= this.opts.maxSurfaces) throw new Failure("EQUOTA", `at most ${this.opts.maxSurfaces} surfaces`);
2384
+ s = new Surface(name, this.host(), this.hostCss(), this.scheme());
2385
+ this.surfaces.set(name, s);
2386
+ }
2387
+ if (c.get("d") !== "1") s.cancelDrag();
2388
+ const scroll = c.get("scroll");
2389
+ s.setDocument(text(cmd.payload), c.get("d") === "1", scroll === "1" || scroll === "2" || scroll === "3" ? Number(scroll) : 0);
2390
+ return;
2391
+ }
2392
+ case "place":
2393
+ return this.place(this.existing(c), c);
2394
+ case "hide": {
2395
+ const s = this.existing(c);
2396
+ s.cancelDrag();
2397
+ if (s.hasKeyboard()) s.blur();
2398
+ this.unplace(s.name);
2399
+ return;
2400
+ }
2401
+ case "delta": {
2402
+ const s = this.existing(c);
2403
+ s.delta(c.get("op") ?? "morph", c.get("t"), c.get("k"), text(cmd.payload));
2404
+ return;
2405
+ }
2406
+ case "res": {
2407
+ const id = c.get("id");
2408
+ if (!id) throw new Failure("EINVAL", "res needs id=<name>");
2409
+ try {
2410
+ this.store.put(id, c.get("type") ?? "application/octet-stream", cmd.payload);
2411
+ } catch (e) {
2412
+ throw new Failure("EQUOTA", e.message);
2413
+ }
2414
+ return;
2415
+ }
2416
+ case "del": {
2417
+ const id = c.get("id");
2418
+ if (id !== void 0) {
2419
+ this.store.remove(id);
2420
+ return;
2421
+ }
2422
+ const name = c.get("s");
2423
+ if (name === void 0) {
2424
+ this.reset();
2425
+ return;
2426
+ }
2427
+ if (!this.surfaces.has(name)) throw new Failure("ENOENT", `no surface ${name}`);
2428
+ this.remove(name);
2429
+ return;
2430
+ }
2431
+ case "detach":
2432
+ this.existing(c).detach();
2433
+ return;
2434
+ case "focus": {
2435
+ const s = this.existing(c);
2436
+ if (s.detached) throw new Failure("EDETACHED", `surface ${s.name} is detached`);
2437
+ try {
2438
+ s.focus(c.get("t"));
2439
+ } catch (e) {
2440
+ throw new Failure("ENOTARGET", e.message);
2441
+ }
2442
+ return;
2443
+ }
2444
+ case "blur":
2445
+ this.existing(c).blur();
2446
+ return;
2447
+ case "":
2448
+ throw new Failure("EINVAL", "missing a=<action>");
2449
+ default:
2450
+ throw new Failure("EINVAL", `unknown action a=${action}`);
2451
+ }
2452
+ }
2453
+ capabilities() {
2454
+ const { cellW, cellH } = this.cell();
2455
+ const scale = window.devicePixelRatio || 1;
2456
+ return {
2457
+ v: "0.1",
2458
+ ops: OPS,
2459
+ events: EVENTS,
2460
+ cell: { w: Math.round(cellW * scale), h: Math.round(cellH * scale) },
2461
+ scale,
2462
+ scheme: this.scheme(),
2463
+ limits: { resources: this.store.quota, surfaces: this.opts.maxSurfaces },
2464
+ net: this.policy,
2465
+ scroll: true,
2466
+ host: HOST,
2467
+ version: VERSION
2468
+ };
2469
+ }
2470
+ surfaceName(c) {
2471
+ const s = c.get("s");
2472
+ if (s === void 0) throw new Failure("EINVAL", "missing s=<surface>");
2473
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(s)) throw new Failure("EINVAL", `bad surface name ${JSON.stringify(s)}`);
2474
+ return s;
2475
+ }
2476
+ existing(c) {
2477
+ const name = this.surfaceName(c);
2478
+ const s = this.surfaces.get(name);
2479
+ if (!s) throw new Failure("ENOENT", `no surface ${name}`);
2480
+ return s;
2481
+ }
2482
+ place(s, c) {
2483
+ const cols = Number(c.get("c"));
2484
+ if (c.get("c") === void 0 || !Number.isInteger(cols)) throw new Failure("EINVAL", "place needs c=<cols>");
2485
+ if (cols < 1 || cols > 1e3) throw new Failure("EINVAL", "c out of range");
2486
+ const { cellW, cellH } = this.cell();
2487
+ const r = c.get("r");
2488
+ const auto = r === void 0 || r === "auto";
2489
+ const rows = auto ? s.contentRows(cols, cellW, cellH) : Number(r);
2490
+ if (!Number.isInteger(rows) || rows < 1 || rows > 1e3) throw new Failure("EINVAL", "r out of range");
2491
+ const cell = (k, d) => {
2492
+ const v = c.get(k);
2493
+ if (v === void 0) return d;
2494
+ const n = Number(v);
2495
+ if (!Number.isInteger(n) || n < 0) throw new Failure("EINVAL", `bad ${k}`);
2496
+ return n;
2497
+ };
2498
+ const x = cell("x", 0);
2499
+ const y = cell("y", 0);
2500
+ const win = { x, y, w: cell("w", cols - x), h: cell("h", rows - y) };
2501
+ if (win.w < 1 || win.h < 1 || x + win.w > cols || y + win.h > rows) {
2502
+ throw new Failure("EINVAL", "the window is not inside the surface");
2503
+ }
2504
+ const z = c.get("z") === void 0 ? 0 : Number(c.get("z"));
2505
+ if (!Number.isInteger(z) || z < -1e3 || z > 1e3) throw new Failure("EINVAL", "z is an integer from -1000 to 1000");
2506
+ s.autoRows = auto;
2507
+ s.setSize(cols, rows, cellW, cellH, win);
2508
+ s.setZ(z);
2509
+ this.unplace(s.name);
2510
+ s.presses = c.get("p") === "1";
2511
+ s.setFit(c.get("f") === "1" ? rows : null);
2512
+ const buf = this.term.buffer.active;
2513
+ const col = buf.cursorX;
2514
+ const row = buf.cursorY;
2515
+ const p = { surface: s, kind: buf.type === "normal" ? "normal" : "alt", col, row, disposables: [] };
2516
+ if (p.kind === "normal") {
2517
+ const marker = this.term.registerMarker(0);
2518
+ if (marker) {
2519
+ p.marker = marker;
2520
+ p.disposables.push(marker.onDispose(() => this.unplace(s.name)));
2521
+ } else {
2522
+ p.kind = "alt";
2523
+ }
2524
+ }
2525
+ this.placements.set(s.name, p);
2526
+ this.position(p);
2527
+ if (c.get("C") !== "1") this.moveCursorBelow(win.h);
2528
+ return { extra: [["c", String(cols)], ["r", String(rows)]] };
2529
+ }
2530
+ /** As the native host: index `rows` times, then carriage return. */
2531
+ moveCursorBelow(rows) {
2532
+ const core = this.term._core;
2533
+ const ih = core?._inputHandler;
2534
+ if (ih && core?.buffer) {
2535
+ for (let i = 0; i < rows; i++) ih.lineFeed();
2536
+ core.buffer.x = 0;
2537
+ }
2538
+ }
2539
+ /** Puts every placed surface where its cells are now. */
2540
+ reposition() {
2541
+ for (const p of this.placements.values()) this.position(p);
2542
+ }
2543
+ position(p) {
2544
+ const { cellW, cellH } = this.cell();
2545
+ const s = p.surface;
2546
+ let row = p.row;
2547
+ if (p.kind === "normal") {
2548
+ const buf = this.term.buffer.active;
2549
+ if (buf.type !== "normal" || !p.marker || p.marker.line < 0) {
2550
+ s.hide();
2551
+ return;
2552
+ }
2553
+ row = p.marker.line - buf.viewportY;
2554
+ }
2555
+ if (row + s.win.h <= 0 || row >= this.term.rows) {
2556
+ s.hide();
2557
+ return;
2558
+ }
2559
+ s.setSize(s.cols, s.rows, cellW, cellH, s.win);
2560
+ s.show(Math.round(p.col * cellW), row * cellH);
2561
+ }
2562
+ unplace(name) {
2563
+ const p = this.placements.get(name);
2564
+ if (!p) return;
2565
+ this.placements.delete(name);
2566
+ for (const d of p.disposables) d.dispose();
2567
+ p.marker?.dispose();
2568
+ p.surface.presses = false;
2569
+ p.surface.setFit(null);
2570
+ p.surface.park();
2571
+ }
2572
+ remove(name) {
2573
+ this.unplace(name);
2574
+ this.surfaces.get(name)?.destroy();
2575
+ this.surfaces.delete(name);
2576
+ }
2577
+ reset() {
2578
+ for (const name of Array.from(this.surfaces.keys())) this.remove(name);
2579
+ this.held = [];
2580
+ }
2581
+ onBufferChange(type) {
2582
+ for (const p of Array.from(this.placements.values())) {
2583
+ if (p.kind === "alt" && type === "normal") this.remove(p.surface.name);
2584
+ }
2585
+ this.reposition();
2586
+ }
2587
+ // --- Geometry and look ---------------------------------------------------
2588
+ host() {
2589
+ return {
2590
+ layer: this.ensureLayer(),
2591
+ store: this.store,
2592
+ event: (surface, kind, target, detail) => {
2593
+ const c = new Control().set("a", "ev").set("s", surface).set("e", kind).set("t", target);
2594
+ this.send(encode(c, detail === void 0 ? "" : JSON.stringify(detail)));
2595
+ },
2596
+ browserKey: (e) => this.browserKey(e),
2597
+ key: (e) => {
2598
+ const ta = this.term.textarea;
2599
+ if (!ta) return;
2600
+ const init = {
2601
+ key: e.key,
2602
+ code: e.code,
2603
+ location: e.location,
2604
+ repeat: e.repeat,
2605
+ ctrlKey: e.ctrlKey,
2606
+ altKey: e.altKey,
2607
+ shiftKey: e.shiftKey,
2608
+ metaKey: e.metaKey,
2609
+ keyCode: e.keyCode,
2610
+ which: e.which,
2611
+ bubbles: true,
2612
+ cancelable: true
2613
+ };
2614
+ Object.defineProperty(ta, "focus", { value: () => {
2615
+ }, configurable: true });
2616
+ this.forwarding = true;
2617
+ try {
2618
+ const ev = new KeyboardEvent(e.type, init);
2619
+ ta.dispatchEvent(ev);
2620
+ if (e.type === "keydown" && !ev.defaultPrevented && [...e.key].length === 1) {
2621
+ const c = e.key.codePointAt(0);
2622
+ ta.dispatchEvent(new KeyboardEvent("keypress", { ...init, charCode: c, keyCode: c, which: c }));
2623
+ }
2624
+ } finally {
2625
+ delete ta.focus;
2626
+ this.forwarding = false;
2627
+ if (this.focusAfter) {
2628
+ this.focusAfter = false;
2629
+ this.term.focus();
2630
+ }
2631
+ }
2632
+ },
2633
+ focusTerminal: () => {
2634
+ if (this.forwarding) this.focusAfter = true;
2635
+ else this.term.focus();
2636
+ },
2637
+ policy: this.policy,
2638
+ pageScrolls: this.pageScrolls,
2639
+ scrollPage: (dx, dy) => this.scrollPage(dx, dy),
2640
+ wheelGesture: (surface, decide) => {
2641
+ const now = performance.now();
2642
+ const g = this.gesture;
2643
+ const same = g !== null && now - g.at < GESTURE_MS && (g.surface === surface || g.route === "terminal");
2644
+ const route = same ? g.route : decide();
2645
+ this.gesture = { surface, route, at: now };
2646
+ return route;
2647
+ },
2648
+ // A hyperlink (SPEC §9) goes where xterm.js sends an OSC 8 one: the
2649
+ // terminal's linkHandler, or its confirm-then-open default, and only
2650
+ // for http and https unless the handler allows other schemes.
2651
+ hyperlink: (kind, e, url, box) => {
2652
+ const handler = this.term.options.linkHandler;
2653
+ if (!handler?.allowNonHttpProtocols) {
2654
+ try {
2655
+ if (!["http:", "https:"].includes(new URL(url).protocol)) return;
2656
+ } catch {
2657
+ return;
2658
+ }
2659
+ }
2660
+ const range = this.cellRange(box);
2661
+ if (kind === "activate") {
2662
+ if (handler) handler.activate(e, url, range);
2663
+ else openAfterConfirm(url);
2664
+ } else if (kind === "hover") handler?.hover?.(e, url, range);
2665
+ else handler?.leave?.(e, url, range);
2666
+ },
2667
+ // Replayed on the terminal's screen, where xterm handles it as its own:
2668
+ // scrollback, or mouse reports and arrow keys on the alternate screen.
2669
+ wheel: (e, x, y) => this.forwardWheel(e, x, y),
2670
+ cells: (kind, e, x, y) => this.pressCells(kind, e, x, y)
2671
+ };
2672
+ }
2673
+ cellTouch = null;
2674
+ pageScrollBound = false;
2675
+ /** Whether the page scrolls, not the terminal (the `scroll` option). */
2676
+ get pageScrolls() {
2677
+ return this.opts.scroll === "page";
2678
+ }
2679
+ /**
2680
+ * Wheels and touches on the cells left to the browser, which scrolls the
2681
+ * page (`scroll: "page"`): they stop at the terminal's element, before
2682
+ * xterm.js, which would take them for its scrollback, or turn a drag into
2683
+ * arrow keys, and cancel the browser's scroll. Nothing here cancels it.
2684
+ */
2685
+ bindPageScroll(el) {
2686
+ if (this.pageScrollBound) return;
2687
+ this.pageScrollBound = true;
2688
+ const stop = (e) => e.stopPropagation();
2689
+ for (const type of ["wheel", "touchstart", "touchmove", "touchend", "touchcancel"]) {
2690
+ el.addEventListener(type, stop, { capture: true, passive: true });
2691
+ this.disposables.push({ dispose: () => el.removeEventListener(type, stop, { capture: true }) });
2692
+ }
2693
+ }
2694
+ /** Scrolls what scrolls the terminal's page: its nearest ancestor that
2695
+ * scrolls, or the document. */
2696
+ scrollPage(dx, dy) {
2697
+ let el = this.term.element?.parentElement ?? null;
2698
+ for (; el; el = el.parentElement) {
2699
+ const o = getComputedStyle(el).overflowY;
2700
+ if (/auto|scroll/.test(o) && el.scrollHeight > el.clientHeight) break;
2701
+ }
2702
+ (el ?? document.scrollingElement ?? document.documentElement).scrollBy(dx, dy);
2703
+ }
2704
+ cellWheelBound = false;
2705
+ /** A wheel over the cells begins or goes on with a gesture of the
2706
+ * terminal's: a document that scrolls under the pointer as the terminal
2707
+ * scrolls takes none of it (`wheelGesture`). */
2708
+ bindCellWheel(el) {
2709
+ if (this.cellWheelBound) return;
2710
+ this.cellWheelBound = true;
2711
+ const wheel = (e) => {
2712
+ if (e.isTrusted) this.gesture = { surface: "", route: "terminal", at: performance.now() };
2713
+ };
2714
+ el.addEventListener("wheel", wheel, { capture: true, passive: true });
2715
+ this.disposables.push({ dispose: () => el.removeEventListener("wheel", wheel, { capture: true }) });
2716
+ }
2717
+ /** Touch on the cells (the `touch` option), once the terminal is open. */
2718
+ bindTouch() {
2719
+ const el = this.term.element;
2720
+ if (el && this.pageScrolls) return this.bindPageScroll(el);
2721
+ if (el) this.bindCellWheel(el);
2722
+ if (this.cellTouch || !el || this.opts.touch === false) return;
2723
+ const screen = () => el.querySelector(".xterm-screen");
2724
+ this.cellTouch = new Touch(
2725
+ el,
2726
+ el.ownerDocument.defaultView,
2727
+ {
2728
+ scroll: (dx, dy, x, y) => this.forwardWheel(new WheelEvent("wheel", { deltaX: dx, deltaY: dy, deltaMode: 0 }), x, y),
2729
+ // A tap is the press and release a click would make: xterm.js
2730
+ // reports them to the program as it reports a mouse.
2731
+ tap: (x, y) => {
2732
+ const at = { clientX: x, clientY: y, button: 0, bubbles: true, cancelable: true, view: el.ownerDocument.defaultView };
2733
+ screen()?.dispatchEvent(new MouseEvent("mousedown", { ...at, buttons: 1 }));
2734
+ el.ownerDocument.dispatchEvent(new MouseEvent("mouseup", { ...at, buttons: 0 }));
2735
+ },
2736
+ toPage: (x, y) => [x, y]
2737
+ },
2738
+ true
2739
+ );
2740
+ }
2741
+ /** A wheel, at a point in the page, replayed on the terminal's screen,
2742
+ * where xterm.js handles it as its own: scrollback, or mouse reports and
2743
+ * arrow keys on the alternate screen. */
2744
+ forwardWheel(e, x, y) {
2745
+ const ev = new WheelEvent("wheel", {
2746
+ deltaX: e.deltaX,
2747
+ deltaY: e.deltaY,
2748
+ deltaMode: e.deltaMode,
2749
+ clientX: x,
2750
+ clientY: y,
2751
+ shiftKey: e.shiftKey,
2752
+ altKey: e.altKey,
2753
+ metaKey: e.metaKey,
2754
+ bubbles: true,
2755
+ cancelable: true
2756
+ });
2757
+ legacyWheelDelta(ev, e);
2758
+ this.term.element?.querySelector(".xterm-screen")?.dispatchEvent(ev);
2759
+ }
2760
+ /**
2761
+ * A press with Alt held on a surface, a move of its gesture, or its
2762
+ * release (SPEC §9.2), replayed on the cells beneath, where xterm.js
2763
+ * handles them as its own mouse: a report to the program, with Alt, or
2764
+ * its selection (rectangular, with Alt). The press goes to the screen,
2765
+ * where xterm.js listens for presses; the moves and the release to its
2766
+ * document, where it listens while a button is held. The keyboard goes
2767
+ * back to the terminal first, as on a click on the cells (§10.1): a
2768
+ * surface that had it sends `blur`.
2769
+ */
2770
+ pressCells(kind, e, x, y) {
2771
+ const el = this.term.element;
2772
+ if (!el) return;
2773
+ const init = {
2774
+ clientX: x,
2775
+ clientY: y,
2776
+ screenX: e.screenX,
2777
+ screenY: e.screenY,
2778
+ button: 0,
2779
+ buttons: e.buttons,
2780
+ // xterm.js selects on a press whose detail is the click count.
2781
+ detail: kind === "down" ? e.detail || 1 : e.detail,
2782
+ altKey: e.altKey,
2783
+ ctrlKey: e.ctrlKey,
2784
+ shiftKey: e.shiftKey,
2785
+ metaKey: e.metaKey,
2786
+ bubbles: true,
2787
+ cancelable: true,
2788
+ view: el.ownerDocument.defaultView
2789
+ };
2790
+ if (kind === "down") {
2791
+ for (const s of this.surfaces.values()) if (s.hasKeyboard()) s.blur();
2792
+ el.querySelector(".xterm-screen")?.dispatchEvent(new MouseEvent("mousedown", init));
2793
+ return;
2794
+ }
2795
+ el.ownerDocument.dispatchEvent(new MouseEvent(kind === "move" ? "mousemove" : "mouseup", init));
2796
+ }
2797
+ ensureLayer() {
2798
+ if (this.layer?.isConnected) return this.layer;
2799
+ const screen = this.term.element?.querySelector(".xterm-screen");
2800
+ if (!screen) throw new Failure("EINVAL", "terminal is not open");
2801
+ const layer = document.createElement("div");
2802
+ layer.className = "hotty-layer";
2803
+ Object.assign(layer.style, { position: "absolute", inset: "0", overflow: "hidden", zIndex: "6", pointerEvents: "none" });
2804
+ screen.append(layer);
2805
+ this.layer = layer;
2806
+ return layer;
2807
+ }
2808
+ browserKey(e) {
2809
+ return (this.opts.browserKeys ?? browserKeys)(e);
2810
+ }
2811
+ /** A box in the page as the buffer cells it covers (1-based, as xterm.js
2812
+ * gives an OSC 8 link's range). */
2813
+ cellRange(box) {
2814
+ const screen = this.term.element?.querySelector(".xterm-screen")?.getBoundingClientRect();
2815
+ const { cellW, cellH } = this.cell();
2816
+ const top = this.term.buffer.active.viewportY;
2817
+ const at = (x, y) => ({
2818
+ x: Math.floor((x - (screen?.left ?? 0)) / cellW) + 1,
2819
+ y: top + Math.floor((y - (screen?.top ?? 0)) / cellH) + 1
2820
+ });
2821
+ return { start: at(box.left, box.top), end: at(box.right - 1, box.bottom - 1) };
2822
+ }
2823
+ /** Cell size in CSS pixels (private in xterm.js), with a measured fallback. */
2824
+ cell() {
2825
+ const dims = this.term._core?._renderService?.dimensions?.css?.cell;
2826
+ if (dims && dims.width > 0 && dims.height > 0) return { cellW: dims.width, cellH: dims.height };
2827
+ const screen = this.term.element?.querySelector(".xterm-screen");
2828
+ if (screen && this.term.cols > 0) return { cellW: screen.clientWidth / this.term.cols, cellH: screen.clientHeight / this.term.rows };
2829
+ return { cellW: 9, cellH: 17 };
2830
+ }
2831
+ hostCss() {
2832
+ const { cellW, cellH } = this.cell();
2833
+ const o = this.term.options;
2834
+ this.css = hostCss(o.theme, { cellW, cellH, fontFamily: o.fontFamily ?? "monospace", fontSize: o.fontSize ?? 15 });
2835
+ return this.css;
2836
+ }
2837
+ scheme() {
2838
+ return palette(this.term.options.theme).dark ? "dark" : "light";
2839
+ }
2840
+ /** On every render: did the cell size, font or theme change? */
2841
+ checkMetrics() {
2842
+ const { cellW, cellH } = this.cell();
2843
+ const o = this.term.options;
2844
+ const key = `${cellW}x${cellH}|${o.fontFamily}|${o.fontSize}|${JSON.stringify(o.theme ?? {})}`;
2845
+ if (key === this.metrics.key) return;
2846
+ const sizeChanged = this.metrics.cellW !== 0 && (cellW !== this.metrics.cellW || cellH !== this.metrics.cellH);
2847
+ this.metrics = { cellW, cellH, key };
2848
+ if (this.surfaces.size === 0) return;
2849
+ const css = this.hostCss();
2850
+ const scheme = this.scheme();
2851
+ for (const s of this.surfaces.values()) s.setHostCss(css, scheme);
2852
+ this.reposition();
2853
+ if (sizeChanged) {
2854
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
2855
+ this.resizeTimer = setTimeout(() => {
2856
+ for (const p of this.placements.values()) {
2857
+ const s = p.surface;
2858
+ if (s.detached) continue;
2859
+ this.host().event(s.name, "resize", "", { w: Math.round(s.cols * cellW), h: s.rows * cellH });
2860
+ }
2861
+ }, 100);
2862
+ }
2863
+ }
2864
+ };
2865
+ function sequence(chain, f) {
2866
+ if (chain) return chain.then(f);
2867
+ const r = f();
2868
+ return r instanceof Promise ? r : void 0;
2869
+ }
2870
+ function openAfterConfirm(uri) {
2871
+ if (!confirm(`Do you want to navigate to ${uri}?
2872
+
2873
+ WARNING: This link could potentially be dangerous`)) return;
2874
+ const w = window.open();
2875
+ if (!w) {
2876
+ console.warn("Opening link blocked as opener could not be cleared");
2877
+ return;
2878
+ }
2879
+ try {
2880
+ w.opener = null;
2881
+ } catch {
2882
+ }
2883
+ w.location.href = uri;
2884
+ }
2885
+ function legacyWheelDelta(ev, from) {
2886
+ if (!("wheelDeltaY" in WheelEvent.prototype)) return;
2887
+ const legacy = from;
2888
+ const real = from.isTrusted;
2889
+ const pixels = from.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? 1 : from.deltaMode === WheelEvent.DOM_DELTA_LINE ? 20 : 400;
2890
+ const x = real && legacy.wheelDeltaX || -from.deltaX * pixels * (120 / 50);
2891
+ const y = real && legacy.wheelDeltaY || -from.deltaY * pixels * (120 / 50);
2892
+ Object.defineProperty(ev, "wheelDeltaX", { value: x });
2893
+ Object.defineProperty(ev, "wheelDeltaY", { value: y });
2894
+ }
2895
+ export {
2896
+ Control,
2897
+ EVENTS,
2898
+ HOST,
2899
+ HottyAddon,
2900
+ OSC,
2901
+ VERSION,
2902
+ browserKeys,
2903
+ decodeAll,
2904
+ encode
2905
+ };
2906
+ //# sourceMappingURL=hotty-xterm.js.map