@neuroplastio/xterm-addon-hotty 0.1.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3804 @@
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 plan2 = [];
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
+ plan2.push([match, nk]);
303
+ }
304
+ for (const ok of oldKids) if (!used.has(ok)) ok.remove();
305
+ plan2.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 one2 = (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 "${one2(u, ["style-src"])}"`).replace(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi, (_m, _q, u) => `url("${one2(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
+ /** Gives an attribute the program wrote a live value of the host's,
714
+ * while the program's stays the one reported, until `unmask`. */
715
+ mask(el, name, live) {
716
+ const value = this.get(el, name);
717
+ if (value === null) return;
718
+ let o = this.orig.get(el);
719
+ if (!o) this.orig.set(el, o = /* @__PURE__ */ new Map());
720
+ o.set(name, value);
721
+ el.setAttribute(name, live);
722
+ }
723
+ /** The program's value again, after `mask`. */
724
+ unmask(el, name) {
725
+ const o = this.orig.get(el);
726
+ const value = o?.get(name);
727
+ if (value === void 0) return;
728
+ o.delete(name);
729
+ el.setAttribute(name, value);
730
+ }
731
+ /** Removes an attribute, if it is the host's own. */
732
+ removeOwn(el, name) {
733
+ if (!this.own.get(el)?.delete(name)) return;
734
+ el.removeAttribute(name);
735
+ }
736
+ /** Sets an attribute to the program's value, resolving URLs and CSS. */
737
+ set(el, name, value) {
738
+ const n = name.toLowerCase();
739
+ if (n.startsWith("on") || DROP_ATTRS.has(n)) return;
740
+ const names = /* @__PURE__ */ new Set();
741
+ let live = value;
742
+ if (n === "style") live = this.store.rewriteCss(value, names, this.ctx);
743
+ else if (isUrlAttr(n)) {
744
+ const d = directiveFor(el.localName, n, el.getAttribute("rel") ?? "");
745
+ live = n === "srcset" ? this.srcset(value, names) : this.store.resolveUrl(value, names, this.ctx, d);
746
+ }
747
+ if ((el.localName === "a" || el.localName === "area") && n === "href") live = value;
748
+ try {
749
+ el.setAttribute(name, live);
750
+ } catch {
751
+ return;
752
+ }
753
+ this.own.get(el)?.delete(n);
754
+ this.remember(el, name, value, live, names);
755
+ }
756
+ remove(el, name) {
757
+ el.removeAttribute(name);
758
+ this.orig.get(el)?.delete(name);
759
+ this.own.get(el)?.delete(name.toLowerCase());
760
+ }
761
+ /** A `<style>`'s CSS as the program wrote it. */
762
+ cssOf(el) {
763
+ return this.css.get(el) ?? el.textContent ?? "";
764
+ }
765
+ setCss(el, text2) {
766
+ const names = /* @__PURE__ */ new Set();
767
+ const live = this.store.rewriteCss(text2, names, this.ctx);
768
+ if (el.textContent !== live) el.textContent = live;
769
+ if (live !== text2) this.css.set(el, text2);
770
+ else this.css.delete(el);
771
+ if (names.size > 0) this.track(el, names);
772
+ }
773
+ /** Re-resolves everything that names one of `names` (a resource changed);
774
+ * whether anything did. */
775
+ refresh(names) {
776
+ let any = false;
777
+ for (const [el, uses] of Array.from(this.refs)) {
778
+ if (!el.isConnected) {
779
+ this.refs.delete(el);
780
+ continue;
781
+ }
782
+ if (![...uses].some((u) => names.has(u))) continue;
783
+ any = true;
784
+ this.refs.delete(el);
785
+ const o = this.orig.get(el);
786
+ if (o) for (const [attr, value] of Array.from(o)) this.set(el, attr, value);
787
+ const css = this.css.get(el);
788
+ if (css !== void 0) this.setCss(el, css);
789
+ }
790
+ return any;
791
+ }
792
+ srcset(value, names) {
793
+ return value.split(",").map((part) => {
794
+ const [url, ...rest] = part.trim().split(/\s+/);
795
+ return [this.store.resolveUrl(url ?? "", names, this.ctx, "img-src"), ...rest].join(" ");
796
+ }).join(", ");
797
+ }
798
+ remember(el, name, value, live, names) {
799
+ let o = this.orig.get(el);
800
+ if (value !== live) {
801
+ if (!o) this.orig.set(el, o = /* @__PURE__ */ new Map());
802
+ o.set(name, value);
803
+ } else {
804
+ o?.delete(name);
805
+ }
806
+ if (names.size > 0) this.track(el, names);
807
+ }
808
+ track(el, names) {
809
+ const uses = this.refs.get(el) ?? /* @__PURE__ */ new Set();
810
+ for (const n of names) uses.add(n);
811
+ this.refs.set(el, uses);
812
+ }
813
+ };
814
+
815
+ // src/keys.ts
816
+ var MODIFIERS = ["Control", "Alt", "Meta", "Shift"];
817
+ var ACTIONS = /* @__PURE__ */ new Set([
818
+ "char-backward",
819
+ "char-forward",
820
+ "word-backward",
821
+ "word-forward",
822
+ "line-start",
823
+ "line-end",
824
+ "delete-char-backward",
825
+ "delete-char-forward",
826
+ "delete-word-backward",
827
+ "delete-word-forward",
828
+ "delete-to-line-start",
829
+ "delete-to-line-end",
830
+ "line-previous",
831
+ "line-next",
832
+ "page-up",
833
+ "page-down",
834
+ "input-start",
835
+ "input-end",
836
+ "newline",
837
+ "submit",
838
+ "program",
839
+ "scroll-up",
840
+ "scroll-down",
841
+ "scroll-left",
842
+ "scroll-right",
843
+ "scroll-page-up",
844
+ "scroll-page-down",
845
+ "scroll-half-page-up",
846
+ "scroll-half-page-down",
847
+ "scroll-start",
848
+ "scroll-end"
849
+ ]);
850
+ var SCROLL_ACTIONS = new Set([...ACTIONS].filter((a) => a.startsWith("scroll-")));
851
+ var MULTILINE_ACTIONS = /* @__PURE__ */ new Set(["line-previous", "line-next", "page-up", "page-down", "input-start", "input-end", "newline"]);
852
+ var ROW_ACTIONS = /* @__PURE__ */ new Set(["line-previous", "line-next", "page-up", "page-down"]);
853
+ var BACKWARD = /* @__PURE__ */ new Set(["char-backward", "word-backward", "line-start", "line-previous", "page-up", "input-start"]);
854
+ var INSERT = "insert";
855
+ var TERMINAL_KEYS = [
856
+ "ArrowLeft=char-backward Control+b=char-backward ArrowRight=char-forward Control+f=char-forward",
857
+ "Alt+ArrowLeft=word-backward Control+ArrowLeft=word-backward Alt+b=word-backward",
858
+ "Alt+ArrowRight=word-forward Control+ArrowRight=word-forward Alt+f=word-forward",
859
+ "Home=line-start Control+a=line-start End=line-end Control+e=line-end",
860
+ "Backspace=delete-char-backward Control+h=delete-char-backward",
861
+ "Delete=delete-char-forward Control+d=delete-char-forward",
862
+ "Alt+Backspace=delete-word-backward Control+w=delete-word-backward Control+Backspace=delete-word-backward",
863
+ "Alt+Delete=delete-word-forward Alt+d=delete-word-forward Control+Delete=delete-word-forward",
864
+ "Control+u=delete-to-line-start Control+k=delete-to-line-end",
865
+ "ArrowUp=line-previous Control+p=line-previous ArrowDown=line-next Control+n=line-next",
866
+ "PageUp=page-up PageDown=page-down",
867
+ "Alt+<=input-start Control+Home=input-start Alt+>=input-end Control+End=input-end",
868
+ "Control+m=newline"
869
+ ].join(" ");
870
+ var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
871
+ function chars(s) {
872
+ return Array.from(segmenter.segment(s), (g) => g.segment);
873
+ }
874
+ var WHITE_SPACE = /* @__PURE__ */ new Set([9, 10, 11, 12, 13, 32, 133, 160, 5760, 8232, 8233, 8239, 8287, 12288]);
875
+ function isSpace(c) {
876
+ const cp = c.codePointAt(0) ?? 0;
877
+ return WHITE_SPACE.has(cp) || cp >= 8192 && cp <= 8202;
878
+ }
879
+ function isBreak(c) {
880
+ return c === "\n" || c === "\r" || c === "\r\n";
881
+ }
882
+ var NAMED = /^[A-Z][A-Za-z0-9]+$/;
883
+ function isChar(v) {
884
+ if (v === "") return false;
885
+ const cp = v.codePointAt(0);
886
+ return chars(v).length === 1 && cp >= 32 && cp !== 127 && !(cp >= 128 && cp < 160);
887
+ }
888
+ function splitKey(name) {
889
+ let head;
890
+ let value;
891
+ if (name.endsWith("++") && name.length > 2) [head, value] = [name.slice(0, -2), "+"];
892
+ else if (name === "+") [head, value] = ["", "+"];
893
+ else {
894
+ const i = name.lastIndexOf("+");
895
+ [head, value] = i >= 0 ? [name.slice(0, i), name.slice(i + 1)] : ["", name];
896
+ }
897
+ const mods = head ? head.split("+") : [];
898
+ if (mods.some((m) => !MODIFIERS.includes(m)) || new Set(mods).size !== mods.length) return null;
899
+ if (value === "Space") value = " ";
900
+ if (!isChar(value) && !NAMED.test(value)) return null;
901
+ return [mods, value];
902
+ }
903
+ function keyName(mods, value) {
904
+ const m = new Set(mods);
905
+ if (m.has("Shift") && isChar(value) && [...value].length === 1) {
906
+ const up = value.toUpperCase();
907
+ const low = value.toLowerCase();
908
+ if ([...up].length === 1 && up !== value) {
909
+ value = up;
910
+ m.delete("Shift");
911
+ } else if ([...low].length === 1 && low !== value) m.delete("Shift");
912
+ }
913
+ return [...MODIFIERS.filter((x) => m.has(x)), value === " " ? "Space" : value].join("+");
914
+ }
915
+ function parseKey(name) {
916
+ const k = splitKey(name);
917
+ return k ? keyName(...k) : null;
918
+ }
919
+ var C0 = {
920
+ 0: "Control+Space",
921
+ 8: "Control+h",
922
+ 9: "Tab",
923
+ 13: "Enter",
924
+ 27: "Escape",
925
+ 127: "Backspace",
926
+ 28: "Control+\\",
927
+ 29: "Control+]",
928
+ 30: "Control+^",
929
+ 31: "Control+_"
930
+ };
931
+ var FINAL = { A: "ArrowUp", B: "ArrowDown", C: "ArrowRight", D: "ArrowLeft", H: "Home", F: "End" };
932
+ var TILDE = { 1: "Home", 7: "Home", 4: "End", 8: "End", 2: "Insert", 3: "Delete", 5: "PageUp", 6: "PageDown" };
933
+ var CODES = { 9: "Tab", 13: "Enter", 27: "Escape", 8: "Backspace", 127: "Backspace" };
934
+ var KITTY = {
935
+ 57409: ".",
936
+ 57410: "/",
937
+ 57411: "*",
938
+ 57412: "-",
939
+ 57413: "+",
940
+ 57414: "Enter",
941
+ 57415: "=",
942
+ 57417: "ArrowLeft",
943
+ 57418: "ArrowRight",
944
+ 57419: "ArrowUp",
945
+ 57420: "ArrowDown",
946
+ 57421: "PageUp",
947
+ 57422: "PageDown",
948
+ 57423: "Home",
949
+ 57424: "End",
950
+ 57425: "Insert",
951
+ 57426: "Delete",
952
+ 57441: "Shift",
953
+ 57442: "Control",
954
+ 57443: "Alt",
955
+ 57444: "Meta",
956
+ 57447: "Shift",
957
+ 57448: "Control",
958
+ 57449: "Alt",
959
+ 57450: "Meta"
960
+ };
961
+ for (let i = 0; i < 10; i++) KITTY[57399 + i] = String(i);
962
+ function withMods(name, add) {
963
+ if (name === null) return null;
964
+ const [mods, value] = splitKey(name);
965
+ return keyName([...mods, ...add.filter((m) => !mods.includes(m))], value);
966
+ }
967
+ function modsOf(field) {
968
+ const [ms = "", es = ""] = (field ?? "").split(":");
969
+ const m = ms === "" ? 1 : Number(ms);
970
+ const e = es === "" ? 1 : Number(es);
971
+ if (!Number.isInteger(m) || !Number.isInteger(e) || e === 3) return null;
972
+ const bits = m < 2 ? 0 : m - 1;
973
+ const out = [];
974
+ if (bits & 1) out.push("Shift");
975
+ if (bits & 2) out.push("Alt");
976
+ if (bits & 4) out.push("Control");
977
+ if (bits & (8 | 32)) out.push("Meta");
978
+ return out;
979
+ }
980
+ function codeKey(code, mods, shifted, text2) {
981
+ if (CODES[code]) return keyName(mods, CODES[code]);
982
+ if (code >= 57344) return KITTY[code] ? keyName(mods, KITTY[code]) : null;
983
+ if (code < 32 || code === 127 || code > 1114111) return null;
984
+ const base = String.fromCodePoint(code);
985
+ let value = base;
986
+ if (mods.includes("Shift")) {
987
+ if (shifted) value = String.fromCodePoint(shifted);
988
+ else if (text2) value = text2;
989
+ else if ([...base.toUpperCase()].length === 1) value = base.toUpperCase();
990
+ if (value !== base) mods = mods.filter((m) => m !== "Shift");
991
+ }
992
+ return [...MODIFIERS.filter((x) => mods.includes(x)), value === " " ? "Space" : value].join("+");
993
+ }
994
+ function csi(params, final) {
995
+ if (/^[<=>?]/.test(params)) return null;
996
+ const fields = params.split(";");
997
+ const num = (s) => s === void 0 || s === "" ? 0 : Number(s);
998
+ if (final === "u") {
999
+ const codes = fields[0].split(":").map(num);
1000
+ const mods = modsOf(fields[1]);
1001
+ if (mods === null || codes.some((c) => !Number.isInteger(c))) return null;
1002
+ const text2 = fields[2]?.split(":").filter(Boolean).map((x) => String.fromCodePoint(Number(x))).join("");
1003
+ return codeKey(codes[0], mods, codes[1] || void 0, text2 || void 0);
1004
+ }
1005
+ if (final === "~") {
1006
+ const n = num(fields[0]);
1007
+ if (n === 27 && fields.length >= 3) {
1008
+ const mods2 = modsOf(fields[1]);
1009
+ const c = num(fields[2]);
1010
+ return mods2 === null || !Number.isInteger(c) ? null : codeKey(c, mods2);
1011
+ }
1012
+ const mods = modsOf(fields[1]);
1013
+ return TILDE[n] && mods !== null ? keyName(mods, TILDE[n]) : null;
1014
+ }
1015
+ if (FINAL[final]) {
1016
+ const mods = modsOf(fields[1]);
1017
+ return mods === null ? null : keyName(mods, FINAL[final]);
1018
+ }
1019
+ if (final === "Z") return "Shift+Tab";
1020
+ return null;
1021
+ }
1022
+ function one(s, i) {
1023
+ const c = s.charCodeAt(i);
1024
+ if (c < 32 || c === 127) return [C0[c] ?? `Control+${String.fromCharCode(c + 96)}`, 1];
1025
+ const cl = segmenter.segment(s.slice(i)).containing(0).segment;
1026
+ return [isChar(cl) ? cl === " " ? "Space" : cl : null, cl.length];
1027
+ }
1028
+ function inputKeys(s) {
1029
+ const out = [];
1030
+ let i = 0;
1031
+ while (i < s.length) {
1032
+ const start = i;
1033
+ let key;
1034
+ if (s.charCodeAt(i) !== 27) {
1035
+ let n;
1036
+ [key, n] = one(s, i);
1037
+ i += n;
1038
+ } else if (i + 1 === s.length) {
1039
+ key = "Escape";
1040
+ i += 1;
1041
+ } else if ((s[i + 1] === "[" || s[i + 1] === "O") && i + 2 < s.length) {
1042
+ if (s[i + 1] === "O") {
1043
+ key = "ABCDHF".includes(s[i + 2]) ? FINAL[s[i + 2]] : null;
1044
+ i += 3;
1045
+ } else {
1046
+ let j = i + 2;
1047
+ while (j < s.length && s.charCodeAt(j) >= 48 && s.charCodeAt(j) <= 63) j++;
1048
+ const params = s.slice(i + 2, j);
1049
+ while (j < s.length && s.charCodeAt(j) >= 32 && s.charCodeAt(j) <= 47) j++;
1050
+ if (j === s.length) {
1051
+ key = null;
1052
+ i = j;
1053
+ } else {
1054
+ key = s.charCodeAt(j) >= 64 && s.charCodeAt(j) <= 126 ? csi(params, s[j]) : null;
1055
+ i = j + 1;
1056
+ }
1057
+ }
1058
+ } else {
1059
+ const [k, n] = one(s, i + 1);
1060
+ key = withMods(k, ["Alt"]);
1061
+ i += 1 + n;
1062
+ }
1063
+ out.push({ key, data: s.slice(start, i) });
1064
+ }
1065
+ return out;
1066
+ }
1067
+ var UNBOUND = /* @__PURE__ */ new Set(["Tab", "Shift+Tab", "Escape"]);
1068
+ var Keymap = class {
1069
+ bindings = /* @__PURE__ */ new Map();
1070
+ multiline;
1071
+ constructor(multiline = false) {
1072
+ this.multiline = multiline;
1073
+ }
1074
+ bind(key, action) {
1075
+ this.bindings.set(key, action);
1076
+ }
1077
+ format() {
1078
+ return Array.from(this.bindings, ([k, a]) => `${k}=${a}`).join(" ");
1079
+ }
1080
+ /** The action a key is bound to: its own binding, or for a key with
1081
+ * Shift that has none, the binding of the key without it (SPEC §10.2). */
1082
+ action(name) {
1083
+ const k = parseKey(name);
1084
+ if (k === null || UNBOUND.has(k)) return void 0;
1085
+ const [mods, value] = splitKey(k);
1086
+ const a = this.bindings.get(k);
1087
+ if (a !== void 0 || !mods.includes("Shift")) return a;
1088
+ return this.bindings.get(
1089
+ keyName(
1090
+ mods.filter((m) => m !== "Shift"),
1091
+ value
1092
+ )
1093
+ );
1094
+ }
1095
+ /** Whether the key is the program's (SPEC §10.2, *Keys for the
1096
+ * program*): bound to `program`. It reaches the program before the
1097
+ * focused element uses it and before the surface scrolls with it. */
1098
+ program(name) {
1099
+ return this.action(name) === "program";
1100
+ }
1101
+ /** The scroll action the key is bound to, or null (SPEC §10.2,
1102
+ * *Scrolling keys*): outside a text field, a key the element does not
1103
+ * use scrolls with it. */
1104
+ scroll(name) {
1105
+ const a = this.action(name);
1106
+ return a !== void 0 && SCROLL_ACTIONS.has(a) ? a : null;
1107
+ }
1108
+ /** What the field does with a key: an action, `INSERT` for a character it
1109
+ * types, or null when the key is not the field's. */
1110
+ lookup(name) {
1111
+ const k = parseKey(name);
1112
+ if (k === null || UNBOUND.has(k)) return null;
1113
+ const a = this.action(k);
1114
+ if (a !== void 0) return a === "program" || MULTILINE_ACTIONS.has(a) && !this.multiline ? null : a;
1115
+ const [mods, value] = splitKey(k);
1116
+ if (isChar(value) && !mods.some((m) => m !== "Shift")) return INSERT;
1117
+ return null;
1118
+ }
1119
+ };
1120
+ function* bindingsOf(value) {
1121
+ for (const b of value.split(/[ \t\n\f\r]+/)) {
1122
+ const i = b.lastIndexOf("=");
1123
+ if (i < 0) continue;
1124
+ const k = parseKey(b.slice(0, i));
1125
+ const a = b.slice(i + 1);
1126
+ if (k === null || !ACTIONS.has(a) || UNBOUND.has(k)) continue;
1127
+ yield [k, a];
1128
+ }
1129
+ }
1130
+ function parseKeymap(value, multiline = false) {
1131
+ const m = new Keymap(multiline);
1132
+ for (const [k, a] of bindingsOf(value)) m.bind(k, a);
1133
+ return m;
1134
+ }
1135
+ function elementKeymap(...values) {
1136
+ const m = new Keymap();
1137
+ for (const v of values) for (const [k, a] of parseKeymap(v).bindings) m.bind(k, a);
1138
+ return m;
1139
+ }
1140
+ function resolve(multiline, ...values) {
1141
+ const m = new Keymap(multiline);
1142
+ for (const [k, a] of [
1143
+ ["ArrowLeft", "char-backward"],
1144
+ ["ArrowRight", "char-forward"],
1145
+ ["Home", "line-start"],
1146
+ ["End", "line-end"],
1147
+ ["Backspace", "delete-char-backward"],
1148
+ ["Delete", "delete-char-forward"],
1149
+ ["ArrowUp", "line-previous"],
1150
+ ["ArrowDown", "line-next"],
1151
+ ["PageUp", "page-up"],
1152
+ ["PageDown", "page-down"],
1153
+ ["Enter", multiline ? "newline" : "submit"]
1154
+ ])
1155
+ m.bind(k, a);
1156
+ for (const v of values) for (const [k, a] of bindingsOf(v)) if (!SCROLL_ACTIONS.has(a)) m.bind(k, a);
1157
+ return m;
1158
+ }
1159
+ function lineOf(t, p) {
1160
+ if (!t.multiline) return [0, t.chars.length];
1161
+ let s = p;
1162
+ while (s > 0 && !isBreak(t.chars[s - 1])) s--;
1163
+ let e = p;
1164
+ while (e < t.chars.length && !isBreak(t.chars[e])) e++;
1165
+ return [s, e];
1166
+ }
1167
+ function wordBack(t, p) {
1168
+ if (t.password) return 0;
1169
+ while (p > 0 && isSpace(t.chars[p - 1])) p--;
1170
+ while (p > 0 && !isSpace(t.chars[p - 1])) p--;
1171
+ return p;
1172
+ }
1173
+ function wordForward(t, p) {
1174
+ if (t.password) return t.chars.length;
1175
+ while (p < t.chars.length && isSpace(t.chars[p])) p++;
1176
+ while (p < t.chars.length && !isSpace(t.chars[p])) p++;
1177
+ return p;
1178
+ }
1179
+ function plan(t, action, rows, goal = null) {
1180
+ if (MULTILINE_ACTIONS.has(action) && !t.multiline) return { kind: "none" };
1181
+ const n = t.chars.length;
1182
+ const selected = t.start < t.end;
1183
+ if (action.startsWith("delete-")) {
1184
+ if (selected) return { kind: "replace", from: t.start, to: t.end, text: "" };
1185
+ const p2 = t.start;
1186
+ const [s2, e2] = lineOf(t, p2);
1187
+ const [from, to] = {
1188
+ "delete-char-backward": [Math.max(p2 - 1, 0), p2],
1189
+ "delete-char-forward": [p2, Math.min(p2 + 1, n)],
1190
+ "delete-word-backward": [wordBack(t, p2), p2],
1191
+ "delete-word-forward": [p2, wordForward(t, p2)],
1192
+ "delete-to-line-start": [s2, p2],
1193
+ "delete-to-line-end": [p2, e2]
1194
+ }[action] ?? [p2, p2];
1195
+ return from < to ? { kind: "replace", from, to, text: "" } : { kind: "none" };
1196
+ }
1197
+ if (action === "newline") return { kind: "replace", from: t.start, to: t.end, text: "\n" };
1198
+ const p = BACKWARD.has(action) ? t.start : t.end;
1199
+ if (selected && (action === "char-backward" || action === "char-forward")) return { kind: "move", to: p };
1200
+ const [s, e] = lineOf(t, p);
1201
+ switch (action) {
1202
+ case "char-backward":
1203
+ return { kind: "move", to: Math.max(p - 1, 0) };
1204
+ case "char-forward":
1205
+ return { kind: "move", to: Math.min(p + 1, n) };
1206
+ case "word-backward":
1207
+ return { kind: "move", to: wordBack(t, p) };
1208
+ case "word-forward":
1209
+ return { kind: "move", to: wordForward(t, p) };
1210
+ case "line-start":
1211
+ return { kind: "move", to: s };
1212
+ case "line-end":
1213
+ return { kind: "move", to: e };
1214
+ case "input-start":
1215
+ return { kind: "move", to: 0 };
1216
+ case "input-end":
1217
+ return { kind: "move", to: n };
1218
+ case "line-previous":
1219
+ case "line-next":
1220
+ case "page-up":
1221
+ case "page-down": {
1222
+ if (!rows) return { kind: "none" };
1223
+ const by = { "line-previous": -1, "line-next": 1, "page-up": -rows.page, "page-down": rows.page }[action];
1224
+ const r = rows.move(p, by, goal);
1225
+ return { kind: "move", to: r.to, goal: r.goal };
1226
+ }
1227
+ }
1228
+ return { kind: "none" };
1229
+ }
1230
+
1231
+ // src/surface.ts
1232
+ function csp(host) {
1233
+ const with_ = (fixed, d) => [fixed, cspSources(host, d)].filter(Boolean).join(" ");
1234
+ return [
1235
+ "default-src 'none'",
1236
+ with_("img-src data: blob:", "img-src"),
1237
+ with_("media-src data: blob:", "media-src"),
1238
+ with_("font-src data: blob:", "font-src"),
1239
+ with_("style-src 'unsafe-inline' blob:", "style-src"),
1240
+ "form-action 'none'",
1241
+ "base-uri http: https:"
1242
+ ].join("; ");
1243
+ }
1244
+ function baseOf(doc) {
1245
+ const href = doc.querySelector("base[href]")?.getAttribute("href") ?? "";
1246
+ try {
1247
+ const u = new URL(href);
1248
+ if (u.protocol === "http:" || u.protocol === "https:") return u.href;
1249
+ } catch {
1250
+ }
1251
+ return NO_BASE;
1252
+ }
1253
+ var SCROLL_ACTIONS2 = {
1254
+ "scroll-up": { axis: "y", sign: -1, by: "line" },
1255
+ "scroll-down": { axis: "y", sign: 1, by: "line" },
1256
+ "scroll-left": { axis: "x", sign: -1, by: "line" },
1257
+ "scroll-right": { axis: "x", sign: 1, by: "line" },
1258
+ "scroll-page-up": { axis: "y", sign: -1, by: "page" },
1259
+ "scroll-page-down": { axis: "y", sign: 1, by: "page" },
1260
+ "scroll-half-page-up": { axis: "y", sign: -1, by: "half" },
1261
+ "scroll-half-page-down": { axis: "y", sign: 1, by: "half" },
1262
+ "scroll-start": { axis: "y", sign: -1, by: "end" },
1263
+ "scroll-end": { axis: "y", sign: 1, by: "end" }
1264
+ };
1265
+ var LINE_PX = 40;
1266
+ var PAGE_FRACTION = 0.875;
1267
+ var FIELD_TYPES = /* @__PURE__ */ new Set(["text", "email", "password", "search", "tel", "url", "number"]);
1268
+ var DATE_TYPES = /* @__PURE__ */ new Set(["date", "datetime-local", "month", "time", "week"]);
1269
+ var NO_CARET = /* @__PURE__ */ new Set(["email", "number"]);
1270
+ var NUMBER_CHARS = /^[0-9.,+\-eE]$/;
1271
+ var LAYOUT_STYLES = [
1272
+ "font-family",
1273
+ "font-size",
1274
+ "font-weight",
1275
+ "font-style",
1276
+ "font-variant",
1277
+ "font-stretch",
1278
+ "font-feature-settings",
1279
+ "font-kerning",
1280
+ "letter-spacing",
1281
+ "word-spacing",
1282
+ "line-height",
1283
+ "text-transform",
1284
+ "text-indent",
1285
+ "tab-size",
1286
+ "white-space",
1287
+ "overflow-wrap",
1288
+ "word-break",
1289
+ "direction",
1290
+ "text-align",
1291
+ "padding-left",
1292
+ "padding-right",
1293
+ "padding-top",
1294
+ "padding-bottom"
1295
+ ];
1296
+ var CONTROLS = "input, select, textarea, button";
1297
+ var HYPERLINK = "data-xterm-hotty-hyperlink";
1298
+ var DETACHED_CSS = `@layer hotty-host {
1299
+ * { cursor: auto !important; }
1300
+ a[href], a[href] * { cursor: text !important; }
1301
+ a[href][${HYPERLINK}], a[href][${HYPERLINK}] * { cursor: pointer !important; }
1302
+ }`;
1303
+ var Surface = class {
1304
+ name;
1305
+ box;
1306
+ frame;
1307
+ doc;
1308
+ resolver;
1309
+ deltas;
1310
+ cols = 80;
1311
+ rows = 24;
1312
+ /** The part of the surface on screen, in cells (SPEC §5.2). */
1313
+ win = { x: 0, y: 0, w: 80, h: 24 };
1314
+ autoRows = false;
1315
+ host;
1316
+ hostStyle;
1317
+ /** Set while the program moves focus, so no `focus` event echoes back. */
1318
+ programFocus = false;
1319
+ /** The document's base URL (SPEC §7.3). */
1320
+ base = NO_BASE;
1321
+ keyboard = false;
1322
+ /** Detached (SPEC §5.5): nothing in the surface reaches the program. */
1323
+ detachedState = false;
1324
+ /** A press is on something that takes no focus (SPEC §10.1): the focus
1325
+ * the browser gives the frame for it is not the surface's. */
1326
+ pressNothing = false;
1327
+ /** The element a press focuses, until the browser is done with it. */
1328
+ pressed = null;
1329
+ /** A press of another button than the primary one, until the browser is
1330
+ * done with it: it neither takes nor gives back the keyboard. */
1331
+ auxPress = false;
1332
+ settling = null;
1333
+ /** A drag under way (SPEC §9.1). */
1334
+ drag = null;
1335
+ /** A drag just ended at a release: it reported its own click, so the
1336
+ * browser's, which follows in the same task, is not reported. */
1337
+ dragReleased = false;
1338
+ /** The placement asked for presses (`p=1`, SPEC §5.2). */
1339
+ presses = false;
1340
+ /** A mouse's or a pen's pointerdown came first: the mousedown that
1341
+ * follows is the same press, not a tap's. */
1342
+ pointerPress = false;
1343
+ /** The pointer of a press with Alt held (SPEC §9.2), until its release:
1344
+ * the gesture is the program's. */
1345
+ altPointer = null;
1346
+ /** Such a gesture just ended at a release: the browser's click that
1347
+ * follows in the same task is not the surface's. */
1348
+ altReleased = false;
1349
+ /** One cell, in CSS pixels: a drag's detail counts cells (SPEC §9.1). */
1350
+ cellW = 9;
1351
+ cellH = 17;
1352
+ css = "";
1353
+ /** Keys whose keydown went to the program, so their keyup follows. */
1354
+ forwarded = /* @__PURE__ */ new Set();
1355
+ /** A run of row moves in a field (SPEC §10.2): the caret it left, and the
1356
+ * place along the row it keeps. Anything else that moves the caret ends
1357
+ * it. */
1358
+ goal = null;
1359
+ /** The field shown as `text` while focused (`NO_CARET`). */
1360
+ masked = null;
1361
+ /** Tab is moving focus: the field it reaches selects its text. */
1362
+ tabbing = false;
1363
+ /** The rows the program heard last, while the placement asked for `fit`
1364
+ * (`f=1`, SPEC §5.2); null when it did not. */
1365
+ fitRows = null;
1366
+ /** The height may have changed since the last check. */
1367
+ fitDirty = false;
1368
+ /** The frame a check waits for. */
1369
+ fitFrame = 0;
1370
+ fitObserver = null;
1371
+ /** The axes the document asked to scroll along (`scroll`, SPEC §5.1): 1
1372
+ * vertically, 2 horizontally, 3 both; 0, nothing scrolls. */
1373
+ axes = 0;
1374
+ /** Where the touch drag under way goes, once it has a direction. */
1375
+ touchRoute = "terminal";
1376
+ /** The box the wheel gesture under way scrolls, if it went to the
1377
+ * document: the gesture moves no other (`scrollWheel`). */
1378
+ wheelBox = null;
1379
+ /** Some element may carry `CLIP`. */
1380
+ clipping = false;
1381
+ constructor(name, host, hostCss2, scheme) {
1382
+ this.name = name;
1383
+ this.host = host;
1384
+ this.box = document.createElement("div");
1385
+ this.box.className = "hotty-surface";
1386
+ this.box.dataset.surface = name;
1387
+ Object.assign(this.box.style, {
1388
+ position: "absolute",
1389
+ left: "0px",
1390
+ top: "0px",
1391
+ overflow: "hidden",
1392
+ pointerEvents: "auto",
1393
+ visibility: "hidden"
1394
+ });
1395
+ this.frame = document.createElement("iframe");
1396
+ this.frame.setAttribute("sandbox", "allow-same-origin allow-forms");
1397
+ this.frame.setAttribute("title", `HOTTY surface ${name}`);
1398
+ Object.assign(this.frame.style, {
1399
+ border: "0",
1400
+ position: "absolute",
1401
+ left: "0px",
1402
+ top: "0px",
1403
+ width: "100%",
1404
+ height: "100%",
1405
+ display: "block",
1406
+ colorScheme: scheme
1407
+ });
1408
+ this.box.append(this.frame);
1409
+ host.layer.append(this.box);
1410
+ const doc = this.frame.contentDocument;
1411
+ if (!doc) throw new Error("surface iframe has no document");
1412
+ doc.open();
1413
+ doc.write(
1414
+ `<!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>`
1415
+ );
1416
+ doc.close();
1417
+ this.doc = doc;
1418
+ this.hostStyle = doc.getElementById("hotty-host");
1419
+ this.css = hostCss2;
1420
+ this.restyle();
1421
+ this.resolver = new Resolver(host.store);
1422
+ this.deltas = new Deltas(doc, this.resolver);
1423
+ this.listen();
1424
+ }
1425
+ setHostCss(css, scheme) {
1426
+ this.css = css;
1427
+ this.restyle();
1428
+ if (this.frame.style.colorScheme !== scheme) this.frame.style.colorScheme = scheme;
1429
+ this.refit();
1430
+ }
1431
+ restyle() {
1432
+ const css = [this.css, scrollCss(this.axes, this.host.pageScrolls), ...this.detachedState ? [DETACHED_CSS] : []].join("\n");
1433
+ if (this.hostStyle.textContent !== css) this.hostStyle.textContent = css;
1434
+ this.clip();
1435
+ }
1436
+ /** Replaces the whole document (`a=doc`): its head's styles and its body.
1437
+ * `detached` (`d=1`) detaches the surface first; without it, the surface
1438
+ * is the program's again (SPEC §5.1, §5.5). `axes` (`scroll`) are the
1439
+ * axes it scrolls along, 0 for none (§5.3). */
1440
+ setDocument(html, detached = false, axes = 0) {
1441
+ if (detached) this.detach();
1442
+ const scrolled = this.axes !== 0;
1443
+ this.axes = axes;
1444
+ const parsed = new DOMParser().parseFromString(html, "text/html");
1445
+ this.base = baseOf(parsed);
1446
+ const request = parse(parsed.querySelector('meta[name="hotty-network" i]')?.getAttribute("content") ?? "");
1447
+ this.resolver.ctx = { base: this.base, policy: intersect(this.host.policy, request) };
1448
+ const ours = this.doc.head.querySelector("base");
1449
+ if (ours && ours.getAttribute("href") !== this.base) ours.setAttribute("href", this.base);
1450
+ this.resolver.adopt(parsed.documentElement);
1451
+ const head = this.doc.head;
1452
+ for (const n of Array.from(head.childNodes)) {
1453
+ const el = n;
1454
+ const ours2 = el.nodeType === 1 && (el.id === "hotty-host" || el.localName === "meta" || el.localName === "base");
1455
+ if (!ours2) n.remove();
1456
+ }
1457
+ head.append(...Array.from(parsed.head.childNodes));
1458
+ this.copyAttributes(parsed.documentElement, this.doc.documentElement);
1459
+ this.copyAttributes(parsed.body, this.doc.body);
1460
+ this.doc.body.replaceChildren(...Array.from(parsed.body.childNodes));
1461
+ this.detachedState = detached;
1462
+ this.sync();
1463
+ if (scrolled) {
1464
+ this.doc.documentElement.scrollTo(0, 0);
1465
+ this.doc.body.scrollTo(0, 0);
1466
+ }
1467
+ this.refit();
1468
+ }
1469
+ /** A delta (SPEC §6). On a detached surface, the controls it adds are
1470
+ * disabled too, and so is one whose `disabled` it removes. */
1471
+ delta(op, target, key, payload) {
1472
+ try {
1473
+ this.deltas.apply(op, target, key, payload);
1474
+ } finally {
1475
+ if (this.detachedState) this.sync();
1476
+ else this.clip();
1477
+ this.refit();
1478
+ }
1479
+ }
1480
+ // --- Ownership (SPEC §5.5) -----------------------------------------------
1481
+ get detached() {
1482
+ return this.detachedState;
1483
+ }
1484
+ /** `a=detach`: nothing in the surface reaches the program any more. A
1485
+ * surface that has the keyboard gives it back silently, with no `change`
1486
+ * and no `blur`, and its form controls act disabled. */
1487
+ detach() {
1488
+ if (!this.detachedState) {
1489
+ this.detachedState = true;
1490
+ this.dropDrag();
1491
+ const had = this.keyboard || this.frameFocused();
1492
+ this.keyboard = false;
1493
+ this.focusedControl()?.blur();
1494
+ if (had) this.giveBack();
1495
+ }
1496
+ this.sync();
1497
+ }
1498
+ /** Disables the form controls of a detached surface, as if each had the
1499
+ * `disabled` attribute, or enables those it disabled, and marks its
1500
+ * hyperlinks for the host's stylesheet. The attributes are the host's
1501
+ * own: the document reports the program's (SPEC §16). */
1502
+ sync() {
1503
+ const detached = this.detachedState;
1504
+ for (const el of Array.from(this.doc.querySelectorAll(CONTROLS))) {
1505
+ if (!detached) this.resolver.removeOwn(el, "disabled");
1506
+ else if (!el.hasAttribute("disabled")) this.resolver.setOwn(el, "disabled", "");
1507
+ }
1508
+ for (const a of Array.from(this.doc.querySelectorAll("a"))) {
1509
+ if (detached && this.isHyperlink(a)) {
1510
+ if (!a.hasAttribute(HYPERLINK)) this.resolver.setOwn(a, HYPERLINK, "");
1511
+ } else this.resolver.removeOwn(a, HYPERLINK);
1512
+ }
1513
+ this.restyle();
1514
+ }
1515
+ copyAttributes(from, to) {
1516
+ for (const [name] of this.resolver.attributes(to)) {
1517
+ if (!from.hasAttribute(name)) this.resolver.remove(to, name);
1518
+ }
1519
+ for (const [name, value] of this.resolver.attributes(from)) this.resolver.set(to, name, value);
1520
+ }
1521
+ /** Rows the content needs at `cols` columns (`r=auto`). */
1522
+ contentRows(cols, cellW, cellH) {
1523
+ Object.assign(this.box.style, { display: "block", visibility: "hidden" });
1524
+ return this.neededRows(cols, cellW, cellH);
1525
+ }
1526
+ /**
1527
+ * Rows the document needs at `cols` columns: `r=auto`'s, and `fit`'s
1528
+ * (SPEC §5.2). The frame is laid out at the width a placement gives it
1529
+ * and a height of 1px, so the viewport adds nothing, then restored in the
1530
+ * same task: nothing is drawn in between.
1531
+ */
1532
+ neededRows(cols, cellW, cellH) {
1533
+ const f = this.frame.style;
1534
+ const [width, height] = [f.width, f.height];
1535
+ const root = this.doc.documentElement;
1536
+ if (this.axes) this.resolver.setOwn(root, MEASURE, "");
1537
+ Object.assign(f, { width: `${Math.round(cols * cellW)}px`, height: "1px" });
1538
+ const h = root.scrollHeight;
1539
+ Object.assign(f, { width, height });
1540
+ if (this.axes) this.resolver.removeOwn(root, MEASURE);
1541
+ return Math.max(1, Math.min(1e3, Math.ceil(h / cellH)));
1542
+ }
1543
+ /**
1544
+ * In a document that scrolls along one axis only, the elements whose
1545
+ * `overflow` along the other is `auto` or `scroll` are marked (`CLIP`), so
1546
+ * that the host stylesheet clips that axis as `overflow: hidden` does
1547
+ * (SPEC §5.3): no scrollbar, and nothing the user does scrolls it. CSS
1548
+ * cannot select by a computed value, so the addon reads it, with its own
1549
+ * rule off (`UNCLIPPED`), each time the document or its style may have
1550
+ * changed: a document, a delta, the host stylesheet. Style only, no
1551
+ * layout, and only for a document with `scroll` of 1 or 2.
1552
+ */
1553
+ clip() {
1554
+ const prop = this.axes === 1 ? "overflowX" : this.axes === 2 ? "overflowY" : null;
1555
+ if (!prop && !this.clipping) return;
1556
+ const marked = new Set(this.doc.querySelectorAll(`[${CLIP}]`));
1557
+ const want = /* @__PURE__ */ new Set();
1558
+ if (prop) {
1559
+ const root = this.doc.documentElement;
1560
+ const win = this.frame.contentWindow;
1561
+ this.resolver.setOwn(root, UNCLIPPED, "");
1562
+ for (const el of [this.doc.body, ...Array.from(this.doc.body.querySelectorAll("*"))]) {
1563
+ if (/auto|scroll/.test(win.getComputedStyle(el)[prop])) want.add(el);
1564
+ }
1565
+ this.resolver.removeOwn(root, UNCLIPPED);
1566
+ }
1567
+ for (const el of marked) if (!want.has(el)) this.resolver.removeOwn(el, CLIP);
1568
+ for (const el of want) if (!marked.has(el)) this.resolver.setOwn(el, CLIP, "");
1569
+ this.clipping = want.size > 0;
1570
+ }
1571
+ // --- Fit (SPEC §5.2: f=1 on a=place) --------------------------------------
1572
+ /**
1573
+ * `f=1` on the placement: from `rows`, the placement's own, the program
1574
+ * hears `fit` whenever the rows the document needs differ from those it
1575
+ * heard last. `null`: the placement did not ask (or is gone).
1576
+ *
1577
+ * What can change the height asks for a check in the next frame drawn:
1578
+ * a document, a delta, a resource (`refresh`), the host stylesheet (cell
1579
+ * size, font), an image, stylesheet or font loading, and, for whatever
1580
+ * else does (the user opening a `<details>`, say), the root's and the
1581
+ * body's boxes changing size. One check per frame, so one `fit` at most.
1582
+ */
1583
+ setFit(rows) {
1584
+ this.fitRows = rows;
1585
+ if (rows === null) {
1586
+ this.fitObserver?.disconnect();
1587
+ this.fitObserver = null;
1588
+ if (this.fitFrame) this.frame.ownerDocument.defaultView?.cancelAnimationFrame(this.fitFrame);
1589
+ this.fitFrame = 0;
1590
+ this.fitDirty = false;
1591
+ return;
1592
+ }
1593
+ if (!this.fitObserver) {
1594
+ this.fitObserver = new ResizeObserver(() => this.refit());
1595
+ this.fitObserver.observe(this.doc.documentElement);
1596
+ this.fitObserver.observe(this.doc.body);
1597
+ }
1598
+ this.refit();
1599
+ }
1600
+ /** The document's height may have changed: a check in the next frame
1601
+ * drawn. A surface out of view draws none; it checks once shown. */
1602
+ refit() {
1603
+ if (this.fitRows === null) return;
1604
+ this.fitDirty = true;
1605
+ if (this.fitFrame || this.box.style.display === "none") return;
1606
+ this.fitFrame = this.frame.ownerDocument.defaultView.requestAnimationFrame(() => {
1607
+ this.fitFrame = 0;
1608
+ this.checkFit();
1609
+ });
1610
+ }
1611
+ checkFit() {
1612
+ if (this.fitRows === null || !this.fitDirty || this.box.style.display === "none") return;
1613
+ this.fitDirty = false;
1614
+ if (this.detachedState) return;
1615
+ const rows = this.neededRows(this.cols, this.cellW, this.cellH);
1616
+ if (rows === this.fitRows) return;
1617
+ this.fitRows = rows;
1618
+ this.emit("fit", "", { r: rows });
1619
+ }
1620
+ /** A resource changed (`a=res`, `a=del`): what names it resolves again. */
1621
+ refresh(names) {
1622
+ if (this.resolver.refresh(names)) this.refit();
1623
+ }
1624
+ /** The surface is `cols`×`rows` cells, and the box shows `win` of it:
1625
+ * the document keeps the whole size, offset by the window's corner. */
1626
+ setSize(cols, rows, cellW, cellH, win = { x: 0, y: 0, w: cols, h: rows }) {
1627
+ this.cols = cols;
1628
+ this.rows = rows;
1629
+ this.win = win;
1630
+ this.cellW = cellW;
1631
+ this.cellH = cellH;
1632
+ Object.assign(this.frame.style, {
1633
+ left: `${-Math.round(win.x * cellW)}px`,
1634
+ top: `${-win.y * cellH}px`,
1635
+ width: `${Math.round(cols * cellW)}px`,
1636
+ height: `${rows * cellH}px`
1637
+ });
1638
+ this.box.style.width = `${Math.round(win.w * cellW)}px`;
1639
+ this.box.style.height = `${win.h * cellH}px`;
1640
+ }
1641
+ /** The placement's z (SPEC §5.2): CSS z-index on the box, so overlapping
1642
+ * boxes stack by it, and by their order in the layer at the same z. */
1643
+ setZ(z) {
1644
+ this.box.style.zIndex = String(z);
1645
+ }
1646
+ /** Shows the surface with its top-left corner at (x, y) in the screen's pixels. */
1647
+ show(x, y) {
1648
+ this.box.style.left = `${x}px`;
1649
+ this.box.style.top = `${y}px`;
1650
+ this.box.style.visibility = "visible";
1651
+ this.box.style.display = "block";
1652
+ if (this.fitDirty) this.refit();
1653
+ }
1654
+ /** Out of view (its line left the screen): a drag under way ends
1655
+ * (SPEC §9.1). */
1656
+ hide() {
1657
+ this.cancelDrag();
1658
+ this.dropAlt();
1659
+ this.box.style.display = "none";
1660
+ }
1661
+ /** No placement (hidden, or not placed yet): the document stays, and the
1662
+ * browser skips its style, layout and paint until it is placed again. */
1663
+ park() {
1664
+ this.box.style.display = "none";
1665
+ }
1666
+ destroy() {
1667
+ this.setFit(null);
1668
+ this.dropDrag();
1669
+ this.dropAlt();
1670
+ if (this.settling) clearTimeout(this.settling);
1671
+ if (this.keyboard || this.frameFocused()) this.host.focusTerminal();
1672
+ this.box.remove();
1673
+ }
1674
+ // --- Keyboard (PROTOCOL §8, Q-0005) -------------------------------------
1675
+ hasKeyboard() {
1676
+ return this.keyboard;
1677
+ }
1678
+ /** `a=focus`: the surface takes the keyboard, at `target` if given. */
1679
+ focus(target) {
1680
+ const el = target ? this.doc.getElementById(target) : null;
1681
+ if (target && !el) throw new Error(target);
1682
+ this.programFocus = true;
1683
+ try {
1684
+ this.frame.focus();
1685
+ this.frame.contentWindow?.focus();
1686
+ if (el) el.focus();
1687
+ else if (!this.focusedControl()) this.tabbable()[0]?.focus();
1688
+ this.keyboard = true;
1689
+ } finally {
1690
+ setTimeout(() => this.programFocus = false, 0);
1691
+ }
1692
+ }
1693
+ /** `a=blur`, or the keyboard leaving: the edited control commits first. */
1694
+ blur() {
1695
+ this.commit();
1696
+ if (this.keyboard) this.giveBack();
1697
+ }
1698
+ /** The browser's focus goes from the frame to the terminal. The frame lets
1699
+ * go of it first: Firefox can leave the frame focused otherwise. */
1700
+ giveBack() {
1701
+ this.frame.blur();
1702
+ this.host.focusTerminal();
1703
+ }
1704
+ commit() {
1705
+ const active = this.focusedControl();
1706
+ if (active) active.blur();
1707
+ }
1708
+ focusedControl() {
1709
+ const a = this.doc.activeElement;
1710
+ return a && a !== this.doc.body && a !== this.doc.documentElement ? a : null;
1711
+ }
1712
+ /** Whether the browser's focus is in this surface's frame. */
1713
+ frameFocused() {
1714
+ return this.frame.ownerDocument.activeElement === this.frame;
1715
+ }
1716
+ /**
1717
+ * A press (SPEC §10.1). On an element that takes focus, the focus that
1718
+ * follows gives the surface the keyboard (`onFocusIn`). On anything else,
1719
+ * or anywhere in a detached surface, it takes nothing: a surface that had
1720
+ * the keyboard gives it back (its control commits, then `blur`), and the
1721
+ * focus the browser gives the frame anyway goes back to the terminal.
1722
+ */
1723
+ onPress(e) {
1724
+ if (e.type === "pointerdown" && e.pointerType === "touch") return;
1725
+ if (e.button !== 0) {
1726
+ this.auxPress = true;
1727
+ setTimeout(() => this.auxPress = false, 0);
1728
+ return;
1729
+ }
1730
+ const el = this.detachedState ? null : focusTargetAt(e, this.hyper);
1731
+ if (el) {
1732
+ this.pressed = el;
1733
+ this.settle();
1734
+ return;
1735
+ }
1736
+ this.pressNothing = true;
1737
+ if (this.keyboard) {
1738
+ this.commit();
1739
+ this.keyboard = false;
1740
+ this.emit("blur", "");
1741
+ }
1742
+ this.settle();
1743
+ }
1744
+ /**
1745
+ * Once the browser is done with a press (its default action focuses the
1746
+ * frame): an element that takes focus has it, even where the browser does
1747
+ * not focus one on a click (a button on a Mac, say); and focus the
1748
+ * surface does not hold goes back to the terminal. The frame keeps it
1749
+ * until then, so text selection starts as usual.
1750
+ */
1751
+ settle() {
1752
+ if (this.settling) return;
1753
+ this.settling = setTimeout(() => {
1754
+ this.settling = null;
1755
+ const el = this.pressed;
1756
+ this.pressed = null;
1757
+ this.pressNothing = false;
1758
+ if (el && !this.keyboard && !this.detachedState && el.isConnected && this.doc.activeElement !== el) {
1759
+ this.frame.focus();
1760
+ el.focus({ preventScroll: true });
1761
+ }
1762
+ if (!this.keyboard && this.frameFocused()) this.giveBack();
1763
+ }, 0);
1764
+ }
1765
+ /** An element got focus: the surface takes the keyboard if the element is
1766
+ * one that takes focus, and the focus is neither the program's (no echo)
1767
+ * nor a detached surface's. */
1768
+ onFocusIn(e) {
1769
+ if (this.programFocus || this.auxPress) return;
1770
+ if (this.detachedState || this.pressNothing || !isElement(e.target) || !takesFocus(e.target, this.hyper)) {
1771
+ this.settle();
1772
+ return;
1773
+ }
1774
+ if (this.keyboard) return;
1775
+ this.keyboard = true;
1776
+ this.emit("focus", "");
1777
+ }
1778
+ tabbable() {
1779
+ const all = this.doc.querySelectorAll(
1780
+ "a[href], button, input:not([type=hidden]), select, textarea, summary, [tabindex], [contenteditable=''], [contenteditable=true]"
1781
+ );
1782
+ return Array.from(all).filter((el) => el.tabIndex >= 0 && !el.disabled && el.getClientRects().length > 0);
1783
+ }
1784
+ controlKind(el) {
1785
+ if (!el) return "none";
1786
+ const tag = el.localName;
1787
+ if (tag === "textarea" || el.isContentEditable) return "textarea";
1788
+ if (tag === "select") return "select";
1789
+ if (tag === "input") {
1790
+ const type = this.inputType(el);
1791
+ if (FIELD_TYPES.has(type)) return "text";
1792
+ if (DATE_TYPES.has(type)) return "date";
1793
+ return "activatable";
1794
+ }
1795
+ if (tag === "button" || tag === "a" || tag === "summary") return "activatable";
1796
+ return "none";
1797
+ }
1798
+ /** Whether the focused element uses the key; if not, the program gets it. */
1799
+ consumes(e, kind) {
1800
+ const plain = !e.ctrlKey && !e.altKey && !e.metaKey;
1801
+ const k = e.key;
1802
+ const printable = [...k].length === 1;
1803
+ if (k === "Escape") return false;
1804
+ switch (kind) {
1805
+ case "date":
1806
+ return (plain || e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) && (printable || ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown", "Enter"].includes(k));
1807
+ case "select":
1808
+ return plain && (printable || ["ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown", "Enter", " "].includes(k));
1809
+ case "activatable":
1810
+ return plain && (k === " " || k === "Enter");
1811
+ default:
1812
+ return false;
1813
+ }
1814
+ }
1815
+ onKey(e) {
1816
+ if (e.isComposing || e.defaultPrevented) return;
1817
+ if (!this.keyboard) {
1818
+ this.settle();
1819
+ if (this.host.browserKey(e)) return;
1820
+ this.forward(e);
1821
+ return;
1822
+ }
1823
+ if (listOpen(this.focusedControl())) return;
1824
+ if (e.key === "Tab" && !e.ctrlKey && !e.altKey && !e.metaKey) {
1825
+ const order = this.tabbable();
1826
+ const at = order.indexOf(this.doc.activeElement);
1827
+ const leaving = order.length === 0 || (e.shiftKey ? at <= 0 : at === order.length - 1 || at < 0);
1828
+ if (leaving) {
1829
+ e.preventDefault();
1830
+ this.blur();
1831
+ } else {
1832
+ this.tabbing = true;
1833
+ setTimeout(() => this.tabbing = false, 0);
1834
+ }
1835
+ return;
1836
+ }
1837
+ const active = this.focusedControl();
1838
+ const kind = this.controlKind(active);
1839
+ if (kind === "text" || kind === "textarea") {
1840
+ this.fieldKey(e, active, kind === "textarea");
1841
+ return;
1842
+ }
1843
+ if (this.host.browserKey(e)) return;
1844
+ if (active && this.programKey(e, active)) return;
1845
+ if (this.consumes(e, kind)) return;
1846
+ if (active && this.scrollActionKey(e, active)) return;
1847
+ if (this.axes && this.scrollKey(e)) return;
1848
+ this.forward(e);
1849
+ }
1850
+ /**
1851
+ * A key the focused element's keymap gives the program (SPEC §10.2, *Keys
1852
+ * for the program*), outside a text field: it reaches the program before
1853
+ * the element uses it (a select's arrows) and before the document
1854
+ * scrolls with it. The keymap is each `data-keys` from the root down to
1855
+ * the element, with no default, and only its `program` bindings count.
1856
+ * The key is named as in a text field (`fieldKey`).
1857
+ */
1858
+ programKey(e, el) {
1859
+ const values = keysValues(el);
1860
+ if (!values.some((v) => v.includes("program"))) return false;
1861
+ const keymap = elementKeymap(...values);
1862
+ const data = this.host.encodeKey(e);
1863
+ const keys = data === null ? [{ key: domKeyName(e), data: "" }] : inputKeys(data);
1864
+ if (!keys.some((k) => k.key !== null && keymap.program(k.key))) return false;
1865
+ this.toProgram(e, data);
1866
+ return true;
1867
+ }
1868
+ /**
1869
+ * A key the focused element's keymap binds to a scroll action (SPEC
1870
+ * §10.2, *Scrolling keys*), outside a text field, and that the element
1871
+ * does not use. Along an axis the document scrolls, it scrolls the
1872
+ * nearest box from the element outward that can still move that way, as
1873
+ * a gesture goes (§5.3), and never the terminal: where nothing can move,
1874
+ * the key is used and does nothing. Along an axis the document does not
1875
+ * scroll, it goes on as if its keymap did not bind it. The key is named
1876
+ * as in a text field (`fieldKey`).
1877
+ */
1878
+ scrollActionKey(e, el) {
1879
+ const values = keysValues(el);
1880
+ if (!values.some((v) => v.includes("scroll-"))) return false;
1881
+ const keymap = elementKeymap(...values);
1882
+ const data = this.host.encodeKey(e);
1883
+ const keys = data === null ? [{ key: domKeyName(e), data: "" }] : inputKeys(data);
1884
+ const action = keys.map((k2) => k2.key === null ? null : keymap.scroll(k2.key)).find((a) => a !== null);
1885
+ const k = action ? SCROLL_ACTIONS2[action] : void 0;
1886
+ if (!k || !(this.axes & (k.axis === "y" ? 1 : 2))) return false;
1887
+ e.preventDefault();
1888
+ e.stopPropagation();
1889
+ const to = this.scroller(el, k.axis, k.sign);
1890
+ if (typeof to !== "string") this.scrollBox(to, k);
1891
+ return true;
1892
+ }
1893
+ /** Scrolls `to` as a key does (`KeyScroll`), by Chromium's amounts. */
1894
+ scrollBox(to, k) {
1895
+ const y = k.axis === "y";
1896
+ const port = y ? to.clientHeight : to.clientWidth;
1897
+ const by = k.by === "line" ? LINE_PX : k.by === "page" ? Math.max(1, port * PAGE_FRACTION) : k.by === "half" ? Math.max(1, port / 2) : y ? to.scrollHeight : to.scrollWidth;
1898
+ to.scrollBy(y ? { top: k.sign * by } : { left: k.sign * by });
1899
+ }
1900
+ /** A key the program has, as xterm.js encoded it (`data`; null when it
1901
+ * cannot say, and the key goes through it). Its release follows. */
1902
+ toProgram(e, data) {
1903
+ e.preventDefault();
1904
+ e.stopPropagation();
1905
+ this.forwarded.add(e.code);
1906
+ if (data === null) this.host.key(e);
1907
+ else if (data) this.host.sendKey(data);
1908
+ }
1909
+ // --- Text fields (SPEC §10.2, §10.4) ------------------------------------
1910
+ /** An input's type, as the browser has it; the program's for a field
1911
+ * that is `text` while focused (`NO_CARET`). */
1912
+ inputType(el) {
1913
+ return el === this.masked ? (this.resolver.get(el, "type") ?? "").toLowerCase() : el.type;
1914
+ }
1915
+ /**
1916
+ * A key in a text field. It is named as the program would read it: from
1917
+ * what xterm.js would send the program for it, in the encoding the
1918
+ * program enabled, read as SPEC §10.4 says. The field's keymap (the
1919
+ * default, then each `data-keys` from the root to the field) says what
1920
+ * the field does with each key; what it does not use is the program's,
1921
+ * sent as xterm.js encoded it.
1922
+ */
1923
+ fieldKey(e, el, multiline) {
1924
+ if (this.host.browserKey(e)) return;
1925
+ const data = this.host.encodeKey(e);
1926
+ const keys = data === null ? [{ key: domKeyName(e), data: "" }] : inputKeys(data);
1927
+ const keymap = this.keymapOf(el, multiline);
1928
+ const uses = keys.map((k) => k.key === null ? null : keymap.lookup(k.key));
1929
+ if (uses.every((u) => u === null)) {
1930
+ const program = keys.some((k) => k.key !== null && keymap.program(k.key));
1931
+ if (!program && this.axes && this.scrollKey(e)) return;
1932
+ this.toProgram(e, data);
1933
+ return;
1934
+ }
1935
+ const number = el.localName === "input" && this.inputType(el) === "number";
1936
+ if (keys.length === 1) {
1937
+ const [use, key] = [uses[0], keys[0].key];
1938
+ const typed = use === INSERT && !(number && !NUMBER_CHARS.test(keyText(key)));
1939
+ const enter = use === "submit" && e.key === "Enter" && el.localName === "input";
1940
+ if (typed || enter) {
1941
+ this.goal = null;
1942
+ return;
1943
+ }
1944
+ }
1945
+ e.preventDefault();
1946
+ e.stopPropagation();
1947
+ keys.forEach((k, i) => {
1948
+ const use = uses[i];
1949
+ if (use === null) {
1950
+ if (k.data) this.host.sendKey(k.data);
1951
+ } else if (use === INSERT) {
1952
+ const text2 = keyText(k.key);
1953
+ if (!number || NUMBER_CHARS.test(text2)) this.edit(el, multiline, { kind: "type", text: text2 });
1954
+ } else if (use === "submit") {
1955
+ this.goal = null;
1956
+ submitFrom(el);
1957
+ } else this.edit(el, multiline, { kind: "action", action: use });
1958
+ });
1959
+ }
1960
+ /** A field's keymap (SPEC §10.2): the default, then the `data-keys` of
1961
+ * each element from the root down to the field. */
1962
+ keymapOf(el, multiline) {
1963
+ return resolve(multiline, ...keysValues(el));
1964
+ }
1965
+ /** Does an action in a field, or types text in it. */
1966
+ edit(el, multiline, what) {
1967
+ if (what.kind === "action" && MULTILINE_ACTIONS.has(what.action) && !multiline) return;
1968
+ if (el.localName === "input" || el.localName === "textarea") this.editValue(el, multiline, what);
1969
+ else this.editHost(el, what);
1970
+ }
1971
+ /** An input's or a textarea's value: the action's plan, applied with
1972
+ * the editing commands, so that the browser sends `input` (and `change`
1973
+ * when focus leaves) as for typing. */
1974
+ editValue(el, multiline, what) {
1975
+ const cl = chars(el.value);
1976
+ const at = [0];
1977
+ for (const c of cl) at.push(at[at.length - 1] + c.length);
1978
+ const index = (cu) => {
1979
+ let i = 0;
1980
+ while (i < cl.length && at[i + 1] <= cu) i++;
1981
+ return i;
1982
+ };
1983
+ const start = index(el.selectionStart ?? el.value.length);
1984
+ const end = index(el.selectionEnd ?? el.value.length);
1985
+ if (what.kind === "type") {
1986
+ this.goal = null;
1987
+ this.replace(el, at[start], at[end], what.text);
1988
+ return;
1989
+ }
1990
+ const action = what.action;
1991
+ const password = el.localName === "input" && this.inputType(el) === "password";
1992
+ const text2 = { chars: cl, start, end, multiline, password };
1993
+ const g = this.goal;
1994
+ const goal = ROW_ACTIONS.has(action) && g && g.el === el && start === end && g.caret === start ? g.x : null;
1995
+ const rows = multiline && ROW_ACTIONS.has(action) ? this.textareaRows(el, cl) : null;
1996
+ const p = plan(text2, action, rows, goal);
1997
+ this.goal = p.kind === "move" && p.goal !== void 0 ? { el, caret: p.to, x: p.goal } : null;
1998
+ if (p.kind === "move") el.setSelectionRange(at[p.to], at[p.to]);
1999
+ else if (p.kind === "replace") this.replace(el, at[p.from], at[p.to], p.text);
2000
+ }
2001
+ /** Replaces code units `from` to `to` of a field's value with `text`, the
2002
+ * caret after it, as typing does. */
2003
+ replace(el, from, to, text2) {
2004
+ el.setSelectionRange(from, to);
2005
+ if (from === to && !text2) return;
2006
+ const done = text2 ? this.doc.execCommand("insertText", false, text2) : this.doc.execCommand("delete", false);
2007
+ if (done) return;
2008
+ el.setRangeText(text2, from, to, "end");
2009
+ const win = this.frame.contentWindow;
2010
+ el.dispatchEvent(new win.InputEvent("input", { bubbles: true, inputType: text2 ? "insertText" : "deleteContent", data: text2 || null }));
2011
+ }
2012
+ /**
2013
+ * A textarea's rows as it lays them out (SPEC §10.2: a line that wraps is
2014
+ * several rows): a hidden copy of its text, wrapped as it wraps, with
2015
+ * every character in an element of its own, measured where each
2016
+ * position is. The place along a row is a position's x.
2017
+ */
2018
+ textareaRows(el, cl) {
2019
+ const win = this.frame.contentWindow;
2020
+ const cs = win.getComputedStyle(el);
2021
+ const copy = this.doc.createElement("hotty-rows");
2022
+ for (const p of LAYOUT_STYLES) copy.style.setProperty(p, cs.getPropertyValue(p));
2023
+ const width = el.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
2024
+ Object.assign(copy.style, {
2025
+ display: "block",
2026
+ position: "absolute",
2027
+ left: "0px",
2028
+ top: "0px",
2029
+ visibility: "hidden",
2030
+ boxSizing: "content-box",
2031
+ width: `${Math.max(width, 1)}px`,
2032
+ height: "auto",
2033
+ border: "0",
2034
+ margin: "0"
2035
+ });
2036
+ const cells = [...cl, "\u200B"].map((c) => {
2037
+ const s = this.doc.createElement("hotty-c");
2038
+ s.style.cssText = "display:inline;margin:0;padding:0;border:0;font:inherit;letter-spacing:inherit";
2039
+ s.textContent = c;
2040
+ return s;
2041
+ });
2042
+ copy.append(...cells);
2043
+ this.doc.body.append(copy);
2044
+ const pos = cells.map((s) => {
2045
+ const r = s.getClientRects()[0] ?? s.getBoundingClientRect();
2046
+ return { x: r.left, y: Math.round(r.top) };
2047
+ });
2048
+ copy.remove();
2049
+ const tops = [...new Set(pos.map((p) => p.y))].sort((a, b) => a - b);
2050
+ const line = tops.length > 1 ? tops[1] - tops[0] : parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.2;
2051
+ const shown = el.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom);
2052
+ const n = cl.length;
2053
+ return {
2054
+ page: Math.max(1, Math.round(shown / line)),
2055
+ move(from, by, goal) {
2056
+ const g = goal ?? pos[from].x;
2057
+ const r = tops.indexOf(pos[from].y) + by;
2058
+ if (r < 0) return { to: 0, goal: g };
2059
+ if (r >= tops.length) return { to: n, goal: g };
2060
+ let best = -1;
2061
+ for (let i = 0; i <= n; i++) {
2062
+ if (pos[i].y !== tops[r]) continue;
2063
+ if (best < 0 || Math.abs(pos[i].x - g) < Math.abs(pos[best].x - g)) best = i;
2064
+ }
2065
+ return { to: best, goal: g };
2066
+ }
2067
+ };
2068
+ }
2069
+ /**
2070
+ * An editing host (`contenteditable`): the actions with the selection's
2071
+ * own moves, a character at a time where they need to see the text, so
2072
+ * that words and lines are SPEC §10.2's, then the editing commands.
2073
+ */
2074
+ editHost(host, what) {
2075
+ this.goal = null;
2076
+ const sel = this.doc.getSelection();
2077
+ if (!sel || !sel.rangeCount) return;
2078
+ const d = this.doc;
2079
+ if (what.kind === "type") {
2080
+ d.execCommand("insertText", false, what.text);
2081
+ return;
2082
+ }
2083
+ const action = what.action;
2084
+ if (action === "newline") {
2085
+ d.execCommand("insertLineBreak", false);
2086
+ return;
2087
+ }
2088
+ const within = () => sel.focusNode !== null && host.contains(sel.focusNode);
2089
+ const next = (dir) => {
2090
+ const [an, ao, fn, fo] = [sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset];
2091
+ sel.collapse(fn, fo);
2092
+ sel.modify("extend", dir, "character");
2093
+ const c = within() ? sel.toString() : "";
2094
+ sel.setBaseAndExtent(an, ao, fn, fo);
2095
+ return c;
2096
+ };
2097
+ const walk = (alter, dir, go) => {
2098
+ for (let i = 0; i < 1e5; i++) {
2099
+ const c = next(dir);
2100
+ if (!c || !go(c)) return;
2101
+ sel.modify(alter, dir, "character");
2102
+ }
2103
+ };
2104
+ const back = (alter, unit) => {
2105
+ if (unit === "word") {
2106
+ walk(alter, "backward", isSpace);
2107
+ walk(alter, "backward", (c) => !isSpace(c));
2108
+ } else if (unit === "line") walk(alter, "backward", (c) => !isBreak(c));
2109
+ else sel.modify(alter, "backward", "character");
2110
+ };
2111
+ const forward = (alter, unit) => {
2112
+ if (unit === "word") {
2113
+ walk(alter, "forward", isSpace);
2114
+ walk(alter, "forward", (c) => !isSpace(c));
2115
+ } else if (unit === "line") walk(alter, "forward", (c) => !isBreak(c));
2116
+ else sel.modify(alter, "forward", "character");
2117
+ };
2118
+ if (action.startsWith("delete-")) {
2119
+ if (sel.isCollapsed) {
2120
+ const unit = action.includes("word") ? "word" : action.includes("line") ? "line" : "char";
2121
+ if (action.endsWith("backward") || action.endsWith("start")) back("extend", unit);
2122
+ else forward("extend", unit);
2123
+ }
2124
+ if (!sel.isCollapsed) d.execCommand("delete", false);
2125
+ return;
2126
+ }
2127
+ const backward = ["char-backward", "word-backward", "line-start", "line-previous", "page-up", "input-start"].includes(action);
2128
+ if (!sel.isCollapsed) {
2129
+ if (backward) sel.collapseToStart();
2130
+ else sel.collapseToEnd();
2131
+ if (action === "char-backward" || action === "char-forward") return;
2132
+ }
2133
+ const by = { "char-backward": "char", "char-forward": "char", "word-backward": "word", "word-forward": "word", "line-start": "line", "line-end": "line" }[action];
2134
+ if (by) {
2135
+ if (backward) back("move", by);
2136
+ else forward("move", by);
2137
+ return;
2138
+ }
2139
+ if (action === "input-start" || action === "input-end") {
2140
+ sel.modify("move", backward ? "backward" : "forward", "documentboundary");
2141
+ return;
2142
+ }
2143
+ if (ROW_ACTIONS.has(action)) {
2144
+ const rows = action.startsWith("page") ? Math.max(1, Math.round(host.clientHeight / (parseFloat(this.frame.contentWindow.getComputedStyle(host).lineHeight) || 16))) : 1;
2145
+ for (let i = 0; i < rows; i++) {
2146
+ const [fn, fo] = [sel.focusNode, sel.focusOffset];
2147
+ sel.modify("move", backward ? "backward" : "forward", "line");
2148
+ if (sel.focusNode === fn && sel.focusOffset === fo) {
2149
+ sel.modify("move", backward ? "backward" : "forward", "documentboundary");
2150
+ return;
2151
+ }
2152
+ }
2153
+ }
2154
+ }
2155
+ /** A field that hides its caret (`NO_CARET`) is `text` while focused:
2156
+ * from the press that focuses it, so that the caret lands where the
2157
+ * press does, or from the focus. */
2158
+ maskField(target, focusing) {
2159
+ const el = target;
2160
+ if (!el || el.nodeType !== 1 || el.localName !== "input" || el === this.masked || this.detachedState) return;
2161
+ const type = (this.resolver.get(el, "type") ?? "").toLowerCase();
2162
+ if (!NO_CARET.has(type) || el.disabled) return;
2163
+ this.unmaskField();
2164
+ this.masked = el;
2165
+ this.resolver.mask(el, "type", "text");
2166
+ if (!el.hasAttribute("inputmode")) this.resolver.setOwn(el, "inputmode", type === "number" ? "decimal" : "email");
2167
+ if (focusing) el.setSelectionRange(this.tabbing ? 0 : el.value.length, el.value.length);
2168
+ }
2169
+ unmaskField() {
2170
+ const el = this.masked;
2171
+ if (!el) return;
2172
+ this.masked = null;
2173
+ this.resolver.removeOwn(el, "inputmode");
2174
+ this.resolver.unmask(el, "type");
2175
+ }
2176
+ /**
2177
+ * A key a browser scrolls with, in a document that scrolls (SPEC §5.3):
2178
+ * it scrolls the innermost box, from the focused element outward (from
2179
+ * the root when none is), that can still move that way. Where none can,
2180
+ * the key goes on to the program, as every key the surface does not use
2181
+ * does (§10.2), unless `overscroll-behavior` stops it. The addon scrolls
2182
+ * the box itself: the browser's own action could be the focused
2183
+ * element's instead (a radio button's arrows, a number field's).
2184
+ */
2185
+ scrollKey(e) {
2186
+ const k = keyScroll(e);
2187
+ if (!k) return false;
2188
+ const to = this.scroller(this.focusedControl(), k.axis, k.sign);
2189
+ if (to === "terminal") return false;
2190
+ e.preventDefault();
2191
+ if (to === "stop") return true;
2192
+ this.scrollBox(to, k);
2193
+ return true;
2194
+ }
2195
+ forward(e) {
2196
+ e.preventDefault();
2197
+ e.stopPropagation();
2198
+ this.forwarded.add(e.code);
2199
+ this.host.key(e);
2200
+ }
2201
+ /** The release of a key the program had: the program hears it too, if it
2202
+ * asked the terminal for releases (SPEC §10.3). */
2203
+ onKeyUp(e) {
2204
+ if (!this.forwarded.delete(e.code)) return;
2205
+ e.preventDefault();
2206
+ e.stopPropagation();
2207
+ this.host.key(e);
2208
+ }
2209
+ // --- Drags (SPEC §9.1) ----------------------------------------------------
2210
+ /**
2211
+ * A press of a mouse's or a pen's primary button on an element with
2212
+ * `drag` in its `data-on` (the nearest, from the pressed one outward, and
2213
+ * only if it has an id) starts a drag. The surface then holds the pointer
2214
+ * until the release: captured by its root element, which no delta
2215
+ * replaces, so every move comes here wherever it is (over the cells,
2216
+ * another surface, or outside the page), and nothing else hears it.
2217
+ */
2218
+ onDragDown(e) {
2219
+ this.cancelDrag();
2220
+ if (this.detachedState || e.pointerType === "touch" || e.button !== 0 || !e.isPrimary) return;
2221
+ let start = null;
2222
+ for (const n of e.composedPath()) {
2223
+ if (isElement(n) && listens(n, "drag")) {
2224
+ start = n;
2225
+ break;
2226
+ }
2227
+ }
2228
+ const id = start?.getAttribute("id");
2229
+ if (!id) return;
2230
+ const [c, r] = this.cellOf(e);
2231
+ const keys = keysOf(e);
2232
+ this.drag = { pointer: e.pointerId, start: id, target: id, c, r, keys };
2233
+ this.emit("dragstart", id, { c, r, keys });
2234
+ try {
2235
+ this.doc.documentElement.setPointerCapture(e.pointerId);
2236
+ } catch {
2237
+ }
2238
+ }
2239
+ /** A move during a drag: an event each time its target changes, and
2240
+ * while it has none, each time its cell does. */
2241
+ onDragMove(e) {
2242
+ const d = this.drag;
2243
+ if (!d || e.pointerId !== d.pointer) return;
2244
+ const [c, r] = this.cellOf(e);
2245
+ const target = this.dragTarget(this.elementAt(e, c, r));
2246
+ const keys = keysOf(e);
2247
+ if (target !== d.target || target === "" && (c !== d.c || r !== d.r)) {
2248
+ this.emit("drag", target, { c, r, keys });
2249
+ }
2250
+ Object.assign(d, { target, c, r, keys });
2251
+ }
2252
+ /** The release ends the drag, wherever it is. Where it began, it is the
2253
+ * click it would have been without the drag; anywhere else, no click. */
2254
+ onDragUp(e) {
2255
+ const d = this.drag;
2256
+ if (!d || e.pointerId !== d.pointer || e.button !== 0) return;
2257
+ this.drag = null;
2258
+ const [c, r] = this.cellOf(e);
2259
+ const at = this.elementAt(e, c, r);
2260
+ const target = this.dragTarget(at);
2261
+ this.emit("dragend", target, { c, r, keys: keysOf(e) });
2262
+ this.dragReleased = true;
2263
+ setTimeout(() => this.dragReleased = false, 0);
2264
+ if (target === d.start && at) this.reportClick(ancestry(at));
2265
+ }
2266
+ /** The browser took the pointer away before the release. */
2267
+ lostPointer(e) {
2268
+ if (this.drag && e.pointerId === this.drag.pointer) this.cancelDrag();
2269
+ if (e.pointerId === this.altPointer) this.altPointer = null;
2270
+ }
2271
+ /**
2272
+ * Ends a drag under way without a release (SPEC §9.1: its placement went
2273
+ * away, a new document came, or the pointer was lost): `dragend` with no
2274
+ * target, at the last cell the program heard of.
2275
+ */
2276
+ cancelDrag() {
2277
+ const d = this.drag;
2278
+ if (!d) return;
2279
+ this.dropDrag();
2280
+ this.emit("dragend", "", { c: d.c, r: d.r, keys: d.keys });
2281
+ }
2282
+ /** Ends a drag under way, silently (a detached or deleted surface). */
2283
+ dropDrag() {
2284
+ const d = this.drag;
2285
+ if (!d) return;
2286
+ this.drag = null;
2287
+ try {
2288
+ if (this.doc.documentElement.hasPointerCapture(d.pointer)) this.doc.documentElement.releasePointerCapture(d.pointer);
2289
+ } catch {
2290
+ }
2291
+ }
2292
+ /** The surface's cell under the pointer, from its top left (the frame's
2293
+ * origin), counting on past its edges. */
2294
+ cellOf(e) {
2295
+ return [Math.floor(e.clientX / this.cellW), Math.floor(e.clientY / this.cellH)];
2296
+ }
2297
+ /** The element under the pointer, if it is in the window (SPEC §5.2). */
2298
+ elementAt(e, c, r) {
2299
+ const w = this.win;
2300
+ if (c < w.x || c >= w.x + w.w || r < w.y || r >= w.y + w.h) return null;
2301
+ return this.doc.elementFromPoint(e.clientX, e.clientY);
2302
+ }
2303
+ /** A drag's target: the nearest element with an id and `drag` in its
2304
+ * `data-on`, from `el` outward; "" where there is none. */
2305
+ dragTarget(el) {
2306
+ for (let n = el; n; n = n.parentElement) {
2307
+ const id = n.getAttribute("id");
2308
+ if (id && listens(n, "drag")) return id;
2309
+ }
2310
+ return "";
2311
+ }
2312
+ // --- Presses with Alt (SPEC §9.2) ---------------------------------------
2313
+ /**
2314
+ * A press of a mouse's or a pen's primary button with Alt held is the
2315
+ * program's: the addon replays it on the cells beneath, and the surface
2316
+ * hears nothing of it (no press, drag, click, focus or selection; its
2317
+ * mousedown is cancelled). It holds the pointer until the release, as a
2318
+ * drag does, so every move and the release go to the cells, wherever the
2319
+ * pointer is. Whether it is the program's is decided here, at the press:
2320
+ * Alt let go later changes nothing.
2321
+ */
2322
+ onAltDown(e) {
2323
+ if (e.pointerType === "touch" || e.button !== 0 || !e.altKey || !e.isPrimary) return false;
2324
+ this.cancelDrag();
2325
+ this.dropAlt();
2326
+ this.pointerPress = true;
2327
+ this.altPointer = e.pointerId;
2328
+ this.onLeave(e);
2329
+ try {
2330
+ this.doc.documentElement.setPointerCapture(e.pointerId);
2331
+ } catch {
2332
+ }
2333
+ const [x, y] = this.toPage(e);
2334
+ this.host.cells("down", e, x, y);
2335
+ return true;
2336
+ }
2337
+ onAltMove(e) {
2338
+ if (e.pointerId !== this.altPointer) return false;
2339
+ const [x, y] = this.toPage(e);
2340
+ this.host.cells("move", e, x, y);
2341
+ return true;
2342
+ }
2343
+ onAltUp(e) {
2344
+ if (e.pointerId !== this.altPointer || e.button !== 0) return false;
2345
+ this.dropAlt();
2346
+ this.altReleased = true;
2347
+ setTimeout(() => this.altReleased = false, 0);
2348
+ const [x, y] = this.toPage(e);
2349
+ this.host.cells("up", e, x, y);
2350
+ return true;
2351
+ }
2352
+ /** Ends such a gesture without a release: the frame goes away. */
2353
+ dropAlt() {
2354
+ const p = this.altPointer;
2355
+ if (p === null) return;
2356
+ this.altPointer = null;
2357
+ try {
2358
+ if (this.doc.documentElement.hasPointerCapture(p)) this.doc.documentElement.releasePointerCapture(p);
2359
+ } catch {
2360
+ }
2361
+ }
2362
+ /** A point of the frame's document in the page. */
2363
+ toPage(e) {
2364
+ const r = this.frame.getBoundingClientRect();
2365
+ return [r.left + e.clientX, r.top + e.clientY];
2366
+ }
2367
+ /** `press` (SPEC §9), if the placement asked for it: `t` is the nearest
2368
+ * element with an id. A press on a hyperlink is the terminal's. */
2369
+ reportPress(e) {
2370
+ if (!this.presses) return;
2371
+ const link = this.linkIn(e);
2372
+ if (link && this.isHyperlink(link)) return;
2373
+ for (const n of e.composedPath()) {
2374
+ const id = isElement(n) ? n.getAttribute("id") : null;
2375
+ if (id) {
2376
+ this.emit("press", id, { area: this.areaOf(n) });
2377
+ return;
2378
+ }
2379
+ }
2380
+ this.emit("press", "");
2381
+ }
2382
+ /**
2383
+ * The cells an element's border box covers as the user sees it, scrolled
2384
+ * included (SPEC §9: `area`), counted from the surface's top left cell:
2385
+ * whole, where it is clipped or scrolled away. An edge within half a
2386
+ * device pixel of a cell's is on it: layout rounds positions to fractions
2387
+ * of a pixel, and the user sees no less than a device pixel.
2388
+ */
2389
+ areaOf(el) {
2390
+ const b = el.getBoundingClientRect();
2391
+ const tol = 0.5 / (this.frame.ownerDocument.defaultView?.devicePixelRatio || 1);
2392
+ const c = Math.floor((b.left + tol) / this.cellW);
2393
+ const r = Math.floor((b.top + tol) / this.cellH);
2394
+ const right = Math.ceil((b.right - tol) / this.cellW);
2395
+ const bottom = Math.ceil((b.bottom - tol) / this.cellH);
2396
+ return { c, r, w: Math.max(0, right - c), h: Math.max(0, bottom - r) };
2397
+ }
2398
+ // --- Events for the program (PROTOCOL §8) --------------------------------
2399
+ listen() {
2400
+ const d = this.doc;
2401
+ const win = this.frame.contentWindow;
2402
+ d.addEventListener(
2403
+ "pointerdown",
2404
+ (e) => {
2405
+ if (this.onAltDown(e)) return;
2406
+ if (e.button === 0) this.maskField(e.target, false);
2407
+ this.pointerPress = e.pointerType !== "touch";
2408
+ if (this.pointerPress && e.button === 0 && e.isPrimary) this.reportPress(e);
2409
+ this.onDragDown(e);
2410
+ this.onPress(e);
2411
+ },
2412
+ true
2413
+ );
2414
+ d.addEventListener("pointermove", (e) => this.onAltMove(e) || this.onDragMove(e), true);
2415
+ d.addEventListener("pointerup", (e) => this.onAltUp(e) || this.onDragUp(e), true);
2416
+ d.addEventListener("pointercancel", (e) => this.lostPointer(e), true);
2417
+ d.addEventListener("lostpointercapture", (e) => this.lostPointer(e), true);
2418
+ d.addEventListener(
2419
+ "mousedown",
2420
+ (e) => {
2421
+ if (this.altPointer !== null) {
2422
+ e.preventDefault();
2423
+ e.stopImmediatePropagation();
2424
+ return;
2425
+ }
2426
+ if (!this.pointerPress && e.button === 0) this.reportPress(e);
2427
+ this.pointerPress = false;
2428
+ this.onPress(e);
2429
+ },
2430
+ true
2431
+ );
2432
+ d.addEventListener("focusin", (e) => {
2433
+ this.maskField(e.target, true);
2434
+ this.onFocusIn(e);
2435
+ }, true);
2436
+ d.addEventListener("focusout", (e) => {
2437
+ if (e.target === this.masked) this.unmaskField();
2438
+ }, true);
2439
+ d.addEventListener("click", (e) => this.onClick(e), true);
2440
+ d.addEventListener("auxclick", (e) => this.onAuxClick(e), true);
2441
+ d.addEventListener("mouseover", (e) => this.onOver(e), true);
2442
+ d.addEventListener("mouseout", (e) => this.onOut(e), true);
2443
+ d.addEventListener("submit", (e) => this.onSubmit(e), true);
2444
+ d.addEventListener("change", (e) => this.onChange(e), true);
2445
+ d.addEventListener("input", (e) => this.onInput(e), true);
2446
+ d.addEventListener("keydown", (e) => this.onKey(e), true);
2447
+ d.addEventListener("keyup", (e) => this.onKeyUp(e), true);
2448
+ d.addEventListener("dragstart", (e) => e.preventDefault(), true);
2449
+ d.addEventListener("wheel", (e) => this.onWheel(e), { capture: true, passive: false });
2450
+ d.addEventListener("scroll", (e) => this.onScroll(e), true);
2451
+ d.addEventListener("load", () => this.refit(), true);
2452
+ d.addEventListener("error", () => this.refit(), true);
2453
+ d.fonts.addEventListener("loadingdone", () => this.refit());
2454
+ const toPage = (x, y) => {
2455
+ const r = this.frame.getBoundingClientRect();
2456
+ return [r.left + x, r.top + y];
2457
+ };
2458
+ if (this.host.pageScrolls) {
2459
+ new Touch(
2460
+ d,
2461
+ this.frame.ownerDocument.defaultView,
2462
+ { scroll: (dx, dy) => this.host.scrollPage(dx, dy), toPage },
2463
+ false,
2464
+ (e) => !this.axes && this.inScroller(e.target)
2465
+ );
2466
+ } else {
2467
+ new Touch(d, this.frame.ownerDocument.defaultView, {
2468
+ scroll: (dx, dy, x, y) => {
2469
+ if (this.touchRoute === "terminal") this.host.wheel(new WheelEvent("wheel", { deltaX: dx, deltaY: dy, deltaMode: 0, clientX: x, clientY: y }), x, y);
2470
+ },
2471
+ toPage,
2472
+ claim: (dx, dy, e) => {
2473
+ this.touchRoute = this.axes ? this.gestureRoute(e.target, dx, dy) : "terminal";
2474
+ return this.touchRoute !== "doc";
2475
+ }
2476
+ });
2477
+ }
2478
+ win.addEventListener("focus", () => {
2479
+ if (!this.programFocus && !this.auxPress && !this.keyboard) this.settle();
2480
+ });
2481
+ win.addEventListener("blur", () => {
2482
+ this.commit();
2483
+ if (this.keyboard) {
2484
+ this.keyboard = false;
2485
+ this.emit("blur", "");
2486
+ }
2487
+ });
2488
+ }
2489
+ /**
2490
+ * Nothing in a surface scrolls, and a wheel over it is the terminal's
2491
+ * (SPEC §5.3, §9). Wheel events never leave an iframe, so without this
2492
+ * the terminal's scrollback (or, on the alternate screen, the program's
2493
+ * wheel input) would stall under the pointer. Ctrl and the wheel stay the
2494
+ * browser's: its zoom. Where the page scrolls (the `scroll` option), the
2495
+ * wheel is the browser's too: nothing in the frame scrolls, so it scrolls
2496
+ * the page, as over the cells; but over an element it would scroll
2497
+ * instead, the surface scrolls the page itself.
2498
+ */
2499
+ onWheel(e) {
2500
+ if (e.ctrlKey) return;
2501
+ if (this.axes) {
2502
+ this.scrollWheel(e);
2503
+ return;
2504
+ }
2505
+ if (this.host.pageScrolls) {
2506
+ if (!this.inScroller(e.target)) return;
2507
+ e.preventDefault();
2508
+ const px = e.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? 1 : e.deltaMode === WheelEvent.DOM_DELTA_LINE ? 20 : 400;
2509
+ this.host.scrollPage(e.deltaX * px, e.deltaY * px);
2510
+ return;
2511
+ }
2512
+ e.preventDefault();
2513
+ const r = this.frame.getBoundingClientRect();
2514
+ this.host.wheel(e, r.left + e.clientX, r.top + e.clientY);
2515
+ }
2516
+ /** Whether a wheel or a touch at target would scroll an element of the
2517
+ * document, overflowing with `overflow: auto` or `scroll`: the browser
2518
+ * gives the gesture to it before the page, and nothing in a surface
2519
+ * scrolls (SPEC §5.3), so the gesture would go nowhere. */
2520
+ inScroller(target) {
2521
+ for (let n = target && isElement(target) ? target : null; n; n = n.parentElement) {
2522
+ const s = n.ownerDocument.defaultView.getComputedStyle(n);
2523
+ const y = /auto|scroll/.test(s.overflowY) && n.scrollHeight > n.clientHeight;
2524
+ const x = /auto|scroll/.test(s.overflowX) && n.scrollWidth > n.clientWidth;
2525
+ if (x || y) return true;
2526
+ }
2527
+ return false;
2528
+ }
2529
+ /**
2530
+ * A wheel over a document that scrolls (SPEC §5.3). A gesture's first
2531
+ * wheel decides where all of it goes, as a browser latches scrolling to
2532
+ * what it began on: while an element under the pointer can still move
2533
+ * that way, the browser scrolls it; where none can, the gesture goes on
2534
+ * to the terminal, as over the cells beneath (§9), unless
2535
+ * `overscroll-behavior` stops it. A gesture the document took goes no
2536
+ * further when it reaches the end there, as in a browser. Where the page
2537
+ * scrolls (the `scroll` option), the page is the terminal, and the
2538
+ * browser chains to it from the document natively. A wheel the browser
2539
+ * does not let the page cancel is the browser's already.
2540
+ */
2541
+ scrollWheel(e) {
2542
+ if (this.host.pageScrolls || !e.cancelable) return;
2543
+ const route = this.host.wheelGesture(this.name, () => {
2544
+ const to = this.gestureTarget(e.target, e.deltaX, e.deltaY);
2545
+ this.wheelBox = typeof to === "string" ? null : to;
2546
+ return typeof to === "string" ? to : "doc";
2547
+ });
2548
+ const [axis, d] = mainAxis(e.deltaX, e.deltaY);
2549
+ if (route === "doc" && this.wheelBox && this.canMove(this.wheelBox, axis, d)) return;
2550
+ e.preventDefault();
2551
+ if (route !== "terminal") return;
2552
+ const r = this.frame.getBoundingClientRect();
2553
+ this.host.wheel(e, r.left + e.clientX, r.top + e.clientY);
2554
+ }
2555
+ /** Where a gesture that scrolls by dx, dy pixels at `target` goes, by its
2556
+ * main axis (SPEC §5.3). */
2557
+ gestureRoute(target, dx, dy) {
2558
+ const to = this.gestureTarget(target, dx, dy);
2559
+ return typeof to === "string" ? to : "doc";
2560
+ }
2561
+ gestureTarget(target, dx, dy) {
2562
+ const [axis, d] = mainAxis(dx, dy);
2563
+ return this.scroller(target && isElement(target) ? target : null, axis, d);
2564
+ }
2565
+ /** Whether a box that scrolls can still move along `axis`, `d`'s way. */
2566
+ canMove(el, axis, d) {
2567
+ if (!d || !(this.axes & (axis === "y" ? 1 : 2))) return false;
2568
+ const [at, span] = axis === "y" ? [el.scrollTop, el.scrollHeight - el.clientHeight] : [el.scrollLeft, el.scrollWidth - el.clientWidth];
2569
+ const rtl = axis === "x" && this.frame.contentWindow.getComputedStyle(el).direction === "rtl";
2570
+ const [min, max] = rtl ? [-span, 0] : [0, span];
2571
+ return d > 0 ? at < max - 0.5 : at > min + 0.5;
2572
+ }
2573
+ /**
2574
+ * What a scroll along `axis`, `d`'s way, from `from` (an element, or the
2575
+ * root for none) moves in a document that scrolls (SPEC §5.3): the
2576
+ * innermost box, from `from` outward, that the user scrolls along it
2577
+ * (`overflow` `auto` or `scroll`; the root's, unless `hidden` or `clip`),
2578
+ * that overflows there, and that can still move that way. At a box that
2579
+ * cannot, the scroll stops if its `overscroll-behavior` is `contain` or
2580
+ * `none`, and goes on outward otherwise. Past the root, and along an axis
2581
+ * the document did not ask for, it is the terminal's.
2582
+ */
2583
+ scroller(from, axis, d) {
2584
+ if (!d || !(this.axes & (axis === "y" ? 1 : 2))) return "terminal";
2585
+ const win = this.frame.contentWindow;
2586
+ const root = this.doc.documentElement;
2587
+ const body = this.doc.body;
2588
+ const rootStyle = win.getComputedStyle(root);
2589
+ const fromBody = rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
2590
+ const prop = axis === "y" ? "overflowY" : "overflowX";
2591
+ for (let el = from ?? root; el; el = el.parentElement) {
2592
+ if (el === body && fromBody) continue;
2593
+ const s = el === root ? rootStyle : win.getComputedStyle(el);
2594
+ const scrolls = el === root ? !/hidden|clip/.test((fromBody ? win.getComputedStyle(body) : s)[prop]) : /auto|scroll/.test(s[prop]);
2595
+ const span = axis === "y" ? el.scrollHeight - el.clientHeight : el.scrollWidth - el.clientWidth;
2596
+ if (!scrolls || span < 1) continue;
2597
+ if (this.canMove(el, axis, d)) return el;
2598
+ const stops = axis === "y" ? s.overscrollBehaviorY : s.overscrollBehaviorX;
2599
+ if (stops === "contain" || stops === "none") return "stop";
2600
+ }
2601
+ return "terminal";
2602
+ }
2603
+ /** Whatever the browser scrolled along an axis the document did not ask
2604
+ * for (a focused element into view, say) goes back to zero. A text
2605
+ * field's own text follows its caret (SPEC §5.3). */
2606
+ onScroll(e) {
2607
+ if (this.axes === 3) return;
2608
+ const t = e.target;
2609
+ const el = t === this.doc ? this.doc.scrollingElement : t && isElement(t) ? t : null;
2610
+ if (!el || t !== this.doc && (el.localName === "input" || el.localName === "textarea")) return;
2611
+ const x = !(this.axes & 2) && el.scrollLeft !== 0;
2612
+ const y = !(this.axes & 1) && el.scrollTop !== 0;
2613
+ if (x || y) el.scrollTo(x ? 0 : el.scrollLeft, y ? 0 : el.scrollTop);
2614
+ }
2615
+ /** An event for the program (SPEC §9); a detached surface sends none (§5.5). */
2616
+ emit(kind, target, detail) {
2617
+ if (this.detachedState) return;
2618
+ this.host.event(this.name, kind, target, detail);
2619
+ }
2620
+ linkIn(e) {
2621
+ for (const n of e.composedPath()) {
2622
+ if (isElement(n) && n.localName === "a" && n.hasAttribute("href")) return n;
2623
+ }
2624
+ return null;
2625
+ }
2626
+ /** A link's `url` (SPEC §9): its href resolved against the document's
2627
+ * base, or "" under hotty.invalid or when it is no URL. */
2628
+ urlOf(link) {
2629
+ const href = this.resolver.get(link, "href") ?? "";
2630
+ let url = "";
2631
+ try {
2632
+ url = new URL(href, this.base).href;
2633
+ } catch {
2634
+ }
2635
+ return url.startsWith(NO_BASE) ? "" : url;
2636
+ }
2637
+ /** The link's box in the page (the iframe may be offset for a window). */
2638
+ pageBox(el) {
2639
+ const f = this.frame.getBoundingClientRect();
2640
+ const r = el.getBoundingClientRect();
2641
+ return new DOMRect(f.left + r.left, f.top + r.top, r.width, r.height);
2642
+ }
2643
+ /** A hyperlink (SPEC §9): a link with target="_blank" and a `url`. One
2644
+ * without a url is the program's, as any other link. */
2645
+ isHyperlink(link) {
2646
+ return link.getAttribute("target") === "_blank" && this.urlOf(link) !== "";
2647
+ }
2648
+ hyper = (link) => this.isHyperlink(link);
2649
+ /** A link's click (SPEC §9). A hyperlink is the terminal's, as an OSC 8
2650
+ * one is, and not reported; any other link is the program's: an event,
2651
+ * with or without an id. The host opens nothing itself. */
2652
+ onLink(link, e) {
2653
+ e.preventDefault();
2654
+ const url = this.urlOf(link);
2655
+ if (this.isHyperlink(link)) {
2656
+ if (e.button === 0) this.host.hyperlink("activate", e, url, this.pageBox(link));
2657
+ return;
2658
+ }
2659
+ const detail = { href: this.resolver.get(link, "href") ?? "" };
2660
+ if (url) detail.url = url;
2661
+ detail.area = this.areaOf(link);
2662
+ this.emit("click", link.getAttribute("id") ?? "", detail);
2663
+ }
2664
+ /** Other buttons on a link do nothing: no navigation, no new tab. */
2665
+ onAuxClick(e) {
2666
+ if (this.linkIn(e)) e.preventDefault();
2667
+ }
2668
+ hovered = null;
2669
+ /** The pointer onto a hyperlink: the terminal hears it, as it does over
2670
+ * an OSC 8 one. */
2671
+ onOver(e) {
2672
+ if (this.altPointer !== null) return;
2673
+ const link = this.linkIn(e);
2674
+ const hyper = link && this.isHyperlink(link) ? link : null;
2675
+ if (hyper === this.hovered?.link) return;
2676
+ this.onLeave(e);
2677
+ const url = hyper ? this.urlOf(hyper) : "";
2678
+ if (hyper) {
2679
+ this.hovered = { link: hyper, url };
2680
+ this.host.hyperlink("hover", e, url, this.pageBox(hyper));
2681
+ }
2682
+ }
2683
+ onOut(e) {
2684
+ if (this.altPointer !== null) return;
2685
+ const to = e.relatedTarget;
2686
+ if (this.hovered && to && this.hovered.link.contains(to)) return;
2687
+ this.onLeave(e);
2688
+ }
2689
+ onLeave(e) {
2690
+ const h = this.hovered;
2691
+ if (!h) return;
2692
+ this.hovered = null;
2693
+ this.host.hyperlink("leave", e, h.url, this.pageBox(h.link));
2694
+ }
2695
+ onClick(e) {
2696
+ const link = this.linkIn(e);
2697
+ if (this.altReleased) {
2698
+ this.altReleased = false;
2699
+ e.preventDefault();
2700
+ e.stopImmediatePropagation();
2701
+ return;
2702
+ }
2703
+ if (this.dragReleased) {
2704
+ this.dragReleased = false;
2705
+ if (link) e.preventDefault();
2706
+ return;
2707
+ }
2708
+ if (link) {
2709
+ this.onLink(link, e);
2710
+ return;
2711
+ }
2712
+ this.reportClick(e.composedPath());
2713
+ }
2714
+ /** The click of the nearest element on `path` that reports one (SPEC §9). */
2715
+ reportClick(path) {
2716
+ for (const n of path) {
2717
+ if (!isElement(n)) continue;
2718
+ if (n.localName === "a" && n.hasAttribute("href")) {
2719
+ if (this.isHyperlink(n)) return;
2720
+ const url = this.urlOf(n);
2721
+ const detail = { href: this.resolver.get(n, "href") ?? "" };
2722
+ if (url) detail.url = url;
2723
+ detail.area = this.areaOf(n);
2724
+ this.emit("click", n.getAttribute("id") ?? "", detail);
2725
+ return;
2726
+ }
2727
+ const el = n;
2728
+ const tag = el.localName;
2729
+ const type = (el.getAttribute("type") ?? "").toLowerCase();
2730
+ const wants = (el.getAttribute("data-on") ?? "").split(/\s+/).includes("click");
2731
+ const reportable = tag === "button" || tag === "a" || tag === "summary" || tag === "input" && ["button", "submit", "reset"].includes(type) || wants;
2732
+ if (!reportable) continue;
2733
+ const id = el.getAttribute("id");
2734
+ if (id) {
2735
+ const detail = {};
2736
+ const href = this.resolver.get(el, "href");
2737
+ if (href !== null) detail.href = href;
2738
+ const value = this.resolver.get(el, "value");
2739
+ if (value !== null) detail.value = value;
2740
+ detail.area = this.areaOf(el);
2741
+ this.emit("click", id, detail);
2742
+ }
2743
+ break;
2744
+ }
2745
+ }
2746
+ onSubmit(e) {
2747
+ e.preventDefault();
2748
+ const form = e.target;
2749
+ const id = form.getAttribute("id");
2750
+ if (!id) return;
2751
+ const fields = {};
2752
+ const data = new this.frame.contentWindow.FormData(form, e.submitter ?? void 0);
2753
+ for (const [k, v] of data) if (typeof v === "string") fields[k] = v;
2754
+ this.emit("submit", id, fields);
2755
+ }
2756
+ onChange(e) {
2757
+ const el = e.target;
2758
+ const id = el.getAttribute?.("id");
2759
+ if (!id) return;
2760
+ const type = (el.getAttribute("type") ?? "").toLowerCase();
2761
+ if (el.localName === "input" && (type === "checkbox" || type === "radio")) {
2762
+ this.emit("change", id, { checked: el.checked, value: this.resolver.get(el, "value") });
2763
+ } else if ("value" in el) {
2764
+ this.emit("change", id, { value: el.value });
2765
+ }
2766
+ }
2767
+ onInput(e) {
2768
+ const el = e.target;
2769
+ const id = el.getAttribute?.("id");
2770
+ if (!id) return;
2771
+ const type = (el.getAttribute("type") ?? "").toLowerCase();
2772
+ if (type === "checkbox" || type === "radio") return;
2773
+ if ((el.getAttribute("data-on") ?? "").split(/\s+/).includes("input") && "value" in el) {
2774
+ this.emit("input", id, { value: el.value });
2775
+ }
2776
+ }
2777
+ };
2778
+ function isElement(n) {
2779
+ return n.nodeType === 1;
2780
+ }
2781
+ function listens(el, what) {
2782
+ return (el.getAttribute("data-on") ?? "").split(/\s+/).includes(what);
2783
+ }
2784
+ function ancestry(el) {
2785
+ const out = [];
2786
+ for (let n = el; n; n = n.parentElement) out.push(n);
2787
+ return out;
2788
+ }
2789
+ function keysOf(e) {
2790
+ const keys = [];
2791
+ if (e.shiftKey) keys.push("shift");
2792
+ if (e.ctrlKey) keys.push("ctrl");
2793
+ if (e.altKey) keys.push("alt");
2794
+ if (e.metaKey) keys.push("meta");
2795
+ return keys;
2796
+ }
2797
+ function takesFocus(el, hyper) {
2798
+ if (el.matches(":disabled")) return false;
2799
+ switch (el.localName) {
2800
+ case "button":
2801
+ case "select":
2802
+ case "textarea":
2803
+ return true;
2804
+ case "input":
2805
+ return el.type !== "hidden";
2806
+ case "a":
2807
+ case "area":
2808
+ if (el.hasAttribute("href")) return !hyper(el);
2809
+ break;
2810
+ case "summary": {
2811
+ const d = el.parentElement;
2812
+ if (d?.localName === "details" && d.querySelector(":scope > summary") === el) return true;
2813
+ break;
2814
+ }
2815
+ }
2816
+ const ce = el.getAttribute("contenteditable");
2817
+ if (ce !== null && ce.toLowerCase() !== "false" && el.isContentEditable) return true;
2818
+ return el.hasAttribute("tabindex") && el.tabIndex >= 0;
2819
+ }
2820
+ function focusTargetAt(e, hyper) {
2821
+ for (const n of e.composedPath()) {
2822
+ if (!isElement(n)) continue;
2823
+ if (n.localName === "a" && n.hasAttribute("href") && hyper(n)) return null;
2824
+ if (takesFocus(n, hyper)) return n;
2825
+ const control = n.localName === "label" ? n.control : null;
2826
+ if (control) return takesFocus(control, hyper) ? control : null;
2827
+ }
2828
+ return null;
2829
+ }
2830
+ function mainAxis(dx, dy) {
2831
+ return Math.abs(dy) >= Math.abs(dx) ? ["y", dy] : ["x", dx];
2832
+ }
2833
+ function keyScroll(e) {
2834
+ if (e.ctrlKey || e.altKey || e.metaKey) return null;
2835
+ if (e.shiftKey) return e.key === " " ? { axis: "y", sign: -1, by: "page" } : null;
2836
+ switch (e.key) {
2837
+ case "ArrowDown":
2838
+ return { axis: "y", sign: 1, by: "line" };
2839
+ case "ArrowUp":
2840
+ return { axis: "y", sign: -1, by: "line" };
2841
+ case "ArrowRight":
2842
+ return { axis: "x", sign: 1, by: "line" };
2843
+ case "ArrowLeft":
2844
+ return { axis: "x", sign: -1, by: "line" };
2845
+ case "PageDown":
2846
+ case " ":
2847
+ return { axis: "y", sign: 1, by: "page" };
2848
+ case "PageUp":
2849
+ return { axis: "y", sign: -1, by: "page" };
2850
+ case "End":
2851
+ return { axis: "y", sign: 1, by: "end" };
2852
+ case "Home":
2853
+ return { axis: "y", sign: -1, by: "end" };
2854
+ }
2855
+ return null;
2856
+ }
2857
+ function listOpen(el) {
2858
+ if (el?.localName !== "select") return false;
2859
+ try {
2860
+ return el.matches(":open");
2861
+ } catch {
2862
+ return false;
2863
+ }
2864
+ }
2865
+ function keysValues(el) {
2866
+ const values = [];
2867
+ for (let n = el; n; n = n.parentElement) {
2868
+ const v = n.getAttribute("data-keys");
2869
+ if (v !== null) values.unshift(v);
2870
+ }
2871
+ return values;
2872
+ }
2873
+ function keyText(key) {
2874
+ const v = key.endsWith("++") ? "+" : key.slice(key.lastIndexOf("+") + 1);
2875
+ return v === "Space" ? " " : v;
2876
+ }
2877
+ function domKeyName(e) {
2878
+ const char = [...e.key].length === 1;
2879
+ const mods = [e.ctrlKey && "Control", e.altKey && "Alt", e.metaKey && "Meta", e.shiftKey && !char && "Shift"].filter(Boolean);
2880
+ return [...mods, e.key === " " ? "Space" : e.key].join("+");
2881
+ }
2882
+ function submitFrom(el) {
2883
+ const form = el.form ?? el.closest("form");
2884
+ if (!form) return;
2885
+ const controls = Array.from(form.elements);
2886
+ const button = controls.find((c) => c.localName === "button" && (c.type || "submit") === "submit" || c.localName === "input" && (c.type === "submit" || c.type === "image"));
2887
+ if (button) {
2888
+ if (!button.disabled) button.click();
2889
+ return;
2890
+ }
2891
+ const blocking = controls.filter((c) => c.localName === "input" && (FIELD_TYPES.has(c.type) || DATE_TYPES.has(c.type)));
2892
+ if (blocking.length <= 1) form.requestSubmit();
2893
+ }
2894
+
2895
+ // src/version.ts
2896
+ var VERSION = "0.1.0";
2897
+ var HOST = "@neuroplastio/xterm-addon-hotty";
2898
+
2899
+ // src/wire.ts
2900
+ var OSC = 7279;
2901
+ var CHUNK = 4096;
2902
+ var MAX_COMMAND = 16 << 20;
2903
+ var Control = class _Control {
2904
+ pairs;
2905
+ constructor(pairs = []) {
2906
+ this.pairs = pairs;
2907
+ }
2908
+ static parse(text2) {
2909
+ const pairs = [];
2910
+ if (text2 === "") return new _Control(pairs);
2911
+ for (const part of text2.split(":")) {
2912
+ const eq = part.indexOf("=");
2913
+ if (eq <= 0) throw new Error(`bad control pair ${JSON.stringify(part)}`);
2914
+ pairs.push([part.slice(0, eq), part.slice(eq + 1)]);
2915
+ }
2916
+ return new _Control(pairs);
2917
+ }
2918
+ get(key) {
2919
+ for (const [k, v] of this.pairs) if (k === key) return v;
2920
+ return void 0;
2921
+ }
2922
+ set(key, value) {
2923
+ const i = this.pairs.findIndex(([k]) => k === key);
2924
+ if (i >= 0) this.pairs[i] = [key, value];
2925
+ else this.pairs.push([key, value]);
2926
+ return this;
2927
+ }
2928
+ delete(key) {
2929
+ const i = this.pairs.findIndex(([k]) => k === key);
2930
+ if (i >= 0) this.pairs.splice(i, 1);
2931
+ return this;
2932
+ }
2933
+ /** A continuation chunk carries nothing but `m` and `q`. */
2934
+ onlyChunkKeys() {
2935
+ return this.pairs.every(([k]) => k === "m" || k === "q");
2936
+ }
2937
+ encode() {
2938
+ return this.pairs.map(([k, v]) => `${k}=${clean2(v)}`).join(":");
2939
+ }
2940
+ };
2941
+ function clean2(value) {
2942
+ return value.replace(/[^\x20-\x7e]|[:;=]/gu, "_");
2943
+ }
2944
+ var Assembler = class {
2945
+ partial = null;
2946
+ /** Feeds the data of one OSC 7279 sequence. */
2947
+ feed(data) {
2948
+ const semi = data.indexOf(";");
2949
+ const ctlText = semi < 0 ? data : data.slice(0, semi);
2950
+ const b64 = semi < 0 ? "" : data.slice(semi + 1);
2951
+ let control;
2952
+ try {
2953
+ control = Control.parse(ctlText);
2954
+ } catch (e) {
2955
+ this.partial = null;
2956
+ return [{ kind: "invalid", reason: String(e.message) }];
2957
+ }
2958
+ const more = control.get("m") === "1";
2959
+ const partial = this.partial;
2960
+ this.partial = null;
2961
+ if (partial && control.onlyChunkKeys()) {
2962
+ partial.b64.push(b64);
2963
+ partial.size += b64.length;
2964
+ if (partial.size > MAX_COMMAND) return [{ kind: "invalid", reason: "command too large" }];
2965
+ if (more) {
2966
+ this.partial = partial;
2967
+ return [{ kind: "partial" }];
2968
+ }
2969
+ return settle([], emit(partial.control, partial.b64.join("")));
2970
+ }
2971
+ const out = [];
2972
+ if (partial) out.push({ kind: "invalid", reason: "chunked command interrupted" });
2973
+ if (more) {
2974
+ this.partial = { control, b64: [b64], size: b64.length };
2975
+ out.push({ kind: "partial" });
2976
+ return out;
2977
+ }
2978
+ return settle(out, emit(control, b64));
2979
+ }
2980
+ };
2981
+ function settle(out, d) {
2982
+ if (d instanceof Promise) return d.then((x) => [...out, x]);
2983
+ out.push(d);
2984
+ return out;
2985
+ }
2986
+ function emit(control, b64) {
2987
+ const fail = (e) => ({ kind: "invalid", reason: `bad payload: ${e.message}` });
2988
+ let bytes;
2989
+ try {
2990
+ bytes = fromBase64(b64);
2991
+ } catch (e) {
2992
+ return fail(e);
2993
+ }
2994
+ const done = (payload) => {
2995
+ control.delete("m").delete("o");
2996
+ return { kind: "command", command: { control, payload } };
2997
+ };
2998
+ if (control.get("o") === "z") return inflate(bytes).then(done, fail);
2999
+ return done(bytes);
3000
+ }
3001
+ function fromBase64(b64) {
3002
+ const s = b64.replace(/\s+/g, "");
3003
+ if (s === "") return new Uint8Array(0);
3004
+ const bin = atob(s);
3005
+ const out = new Uint8Array(bin.length);
3006
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
3007
+ return out;
3008
+ }
3009
+ function toBase64(bytes) {
3010
+ let bin = "";
3011
+ for (let i = 0; i < bytes.length; i += 32768) {
3012
+ bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
3013
+ }
3014
+ return btoa(bin);
3015
+ }
3016
+ async function inflate(bytes) {
3017
+ const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate"));
3018
+ return new Uint8Array(await new Response(stream).arrayBuffer());
3019
+ }
3020
+ var utf8 = new TextDecoder("utf-8", { fatal: false });
3021
+ var utf8Encoder = new TextEncoder();
3022
+ function text(bytes) {
3023
+ return utf8.decode(bytes);
3024
+ }
3025
+ function encode(control, payload = new Uint8Array(0)) {
3026
+ const bytes = typeof payload === "string" ? utf8Encoder.encode(payload) : payload;
3027
+ const b64 = toBase64(bytes);
3028
+ const osc = (ctl, chunk) => `\x1B]${OSC};${ctl}${chunk === null ? "" : `;${chunk}`}\x1B\\`;
3029
+ if (b64.length <= CHUNK) return osc(control.encode(), b64 === "" ? null : b64);
3030
+ const quiet = control.get("q");
3031
+ let out = "";
3032
+ for (let i = 0; i < b64.length; i += CHUNK) {
3033
+ const last = i + CHUNK >= b64.length;
3034
+ let ctl;
3035
+ if (i === 0) {
3036
+ ctl = new Control([...control.pairs]).set("m", "1");
3037
+ } else {
3038
+ ctl = new Control([["m", last ? "0" : "1"]]);
3039
+ if (quiet !== void 0) ctl.set("q", quiet);
3040
+ }
3041
+ out += osc(ctl.encode(), b64.slice(i, i + CHUNK));
3042
+ }
3043
+ return out;
3044
+ }
3045
+ async function decodeAll(stream) {
3046
+ const a = new Assembler();
3047
+ const out = [];
3048
+ const re = /\x1b\]7279;([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
3049
+ for (const m of stream.matchAll(re)) {
3050
+ for (const d of await a.feed(m[1])) if (d.kind === "command") out.push(d.command);
3051
+ }
3052
+ return out;
3053
+ }
3054
+
3055
+ // src/addon.ts
3056
+ var mac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
3057
+ function browserKeys(e) {
3058
+ if (e.key === "F5" || e.key === "F11" || e.key === "F12") return true;
3059
+ if (mac && e.metaKey) return true;
3060
+ const mod = e.ctrlKey || e.metaKey;
3061
+ const k = e.key.toLowerCase();
3062
+ if (mod && !e.altKey && k === "r") return true;
3063
+ if (mod && ["=", "+", "-", "0"].includes(e.key)) return true;
3064
+ if (e.ctrlKey && e.shiftKey && ["i", "j", "c"].includes(k)) return true;
3065
+ return false;
3066
+ }
3067
+ var GESTURE_MS = 150;
3068
+ var EVENTS = ["click", "change", "input", "submit", "press", "drag", "focus", "blur", "resize", "fit"];
3069
+ var Failure = class extends Error {
3070
+ code;
3071
+ constructor(code, detail) {
3072
+ super(detail);
3073
+ this.code = code;
3074
+ }
3075
+ };
3076
+ var HottyAddon = class {
3077
+ term;
3078
+ opts;
3079
+ store;
3080
+ surfaces = /* @__PURE__ */ new Map();
3081
+ placements = /* @__PURE__ */ new Map();
3082
+ assembler = new Assembler();
3083
+ disposables = [];
3084
+ layer = null;
3085
+ /** Commands held while synchronized output is on (PROTOCOL §5). */
3086
+ held = [];
3087
+ heldTimer = null;
3088
+ css = "";
3089
+ policy;
3090
+ metrics = { cellW: 0, cellH: 0, key: "" };
3091
+ resizeTimer = null;
3092
+ /** A surface's key is being forwarded to the terminal (`key`), and the
3093
+ * terminal's own focus is held off meanwhile. A program in the page may
3094
+ * answer the key inside its dispatch, since xterm.js parses what follows
3095
+ * user input at once: a surface that gives the keyboard back then has
3096
+ * its focus carried out once the key is through. */
3097
+ forwarding = false;
3098
+ focusAfter = false;
3099
+ /** The wheel gesture under way: the surface its last wheel was over ("" for
3100
+ * the cells), where it goes, and when that wheel came. */
3101
+ gesture = null;
3102
+ constructor(options = {}) {
3103
+ this.opts = { maxSurfaces: 64, resourceQuota: 64 << 20, ...options };
3104
+ this.policy = clean(options.network);
3105
+ this.store = new Store(this.opts.resourceQuota);
3106
+ this.store.onChange = (names) => {
3107
+ for (const s of this.surfaces.values()) s.refresh(names);
3108
+ };
3109
+ }
3110
+ activate(term) {
3111
+ this.term = term;
3112
+ term.attachCustomKeyEventHandler((e) => !this.browserKey(e));
3113
+ const p = term.parser;
3114
+ this.disposables.push(
3115
+ p.registerOscHandler(OSC, (data) => this.onOsc(data)),
3116
+ // `CSI ? 2026 l` ends a synchronized batch: apply what was held, then
3117
+ // let xterm handle the mode as usual.
3118
+ p.registerCsiHandler({ prefix: "?", final: "l" }, (params) => {
3119
+ if (params.some((x) => x === 2026)) this.release();
3120
+ return false;
3121
+ }),
3122
+ // RIS: every surface goes (PROTOCOL §4).
3123
+ p.registerEscHandler({ final: "c" }, () => {
3124
+ this.reset();
3125
+ return false;
3126
+ }),
3127
+ term.buffer.onBufferChange((b) => this.onBufferChange(b.type)),
3128
+ term.onRender(() => {
3129
+ this.bindTouch();
3130
+ this.checkMetrics();
3131
+ }),
3132
+ term.onResize(() => this.checkMetrics()),
3133
+ term.onScroll(() => this.reposition()),
3134
+ term.onWriteParsed(() => this.reposition())
3135
+ );
3136
+ }
3137
+ dispose() {
3138
+ this.cellTouch?.dispose();
3139
+ this.cellTouch = null;
3140
+ this.reset();
3141
+ for (const d of this.disposables) d.dispose();
3142
+ this.disposables.length = 0;
3143
+ this.layer?.remove();
3144
+ this.layer = null;
3145
+ }
3146
+ /**
3147
+ * An element of a surface as the program wrote it (tag, attributes, text,
3148
+ * child elements): the shape the shared conformance vectors check
3149
+ * (conformance/README.md). `null` if there is no such surface or element.
3150
+ */
3151
+ inspect(surface, id) {
3152
+ const s = this.surfaces.get(surface);
3153
+ const el = s?.doc.getElementById(id);
3154
+ if (!s || !el) return null;
3155
+ return {
3156
+ tag: el.localName,
3157
+ attrs: Object.fromEntries(s.resolver.attributes(el)),
3158
+ text: el.textContent ?? "",
3159
+ children: Array.from(el.children, (c) => [c.localName, c.getAttribute("id"), c.textContent ?? ""])
3160
+ };
3161
+ }
3162
+ // --- Wire -----------------------------------------------------------------
3163
+ onOsc(data) {
3164
+ const decoded = this.assembler.feed(data);
3165
+ if (decoded instanceof Promise) return decoded.then((d) => this.run(d)).then(() => true);
3166
+ const r = this.run(decoded);
3167
+ return r instanceof Promise ? r.then(() => true) : true;
3168
+ }
3169
+ run(decoded) {
3170
+ let chain;
3171
+ for (const d of decoded) {
3172
+ if (d.kind === "invalid") this.opts.onInvalid?.(d.reason);
3173
+ if (d.kind !== "command") continue;
3174
+ const cmd = d.command;
3175
+ chain = sequence(chain, () => this.submit(cmd));
3176
+ }
3177
+ return chain;
3178
+ }
3179
+ submit(cmd) {
3180
+ if (this.term.modes.synchronizedOutputMode && cmd.control.get("a") !== "q") {
3181
+ this.held.push(cmd);
3182
+ this.heldTimer ??= setTimeout(() => this.release(), 1e3);
3183
+ return;
3184
+ }
3185
+ return this.apply([cmd]);
3186
+ }
3187
+ release() {
3188
+ if (this.heldTimer) clearTimeout(this.heldTimer);
3189
+ this.heldTimer = null;
3190
+ const batch = this.held;
3191
+ this.held = [];
3192
+ if (batch.length) void this.apply(batch);
3193
+ }
3194
+ apply(batch) {
3195
+ const t0 = performance.now();
3196
+ let chain;
3197
+ for (const cmd of batch) chain = sequence(chain, () => this.handle(cmd));
3198
+ const done = () => this.opts.onFrame?.({ commands: batch.length, applyMs: performance.now() - t0 });
3199
+ if (chain) return chain.then(done);
3200
+ done();
3201
+ }
3202
+ handle(cmd) {
3203
+ const action = cmd.control.get("a") ?? "";
3204
+ const quiet = Number(cmd.control.get("q") ?? "0");
3205
+ const ok = (r) => {
3206
+ if (quiet === 0) this.reply("ok", cmd, r ?? {});
3207
+ };
3208
+ const fail = (e) => {
3209
+ const code = e instanceof Failure || e instanceof DeltaError ? e.code : "EINVAL";
3210
+ const detail = e instanceof Error ? e.message : String(e);
3211
+ if (quiet < 2) this.reply("err", cmd, { body: JSON.stringify({ code, detail }) });
3212
+ };
3213
+ try {
3214
+ const r = this.dispatch(action, cmd);
3215
+ if (r instanceof Promise) return r.then(ok, fail);
3216
+ ok(r);
3217
+ } catch (e) {
3218
+ fail(e);
3219
+ }
3220
+ }
3221
+ reply(kind, cmd, r) {
3222
+ const c = new Control().set("a", kind);
3223
+ for (const k of ["n", "s"]) {
3224
+ const v = cmd.control.get(k);
3225
+ if (v !== void 0) c.set(k, v);
3226
+ }
3227
+ c.set("re", cmd.control.get("a") ?? "");
3228
+ for (const [k, v] of r.extra ?? []) c.set(k, v);
3229
+ this.send(encode(c, r.body ?? ""));
3230
+ }
3231
+ send(message) {
3232
+ this.term.input(message, false);
3233
+ }
3234
+ // --- Commands (PROTOCOL §3-§6) -------------------------------------------
3235
+ dispatch(action, cmd) {
3236
+ const c = cmd.control;
3237
+ switch (action) {
3238
+ case "q":
3239
+ return { body: JSON.stringify(this.capabilities()) };
3240
+ case "doc": {
3241
+ const name = this.surfaceName(c);
3242
+ let s = this.surfaces.get(name);
3243
+ if (!s) {
3244
+ if (this.surfaces.size >= this.opts.maxSurfaces) throw new Failure("EQUOTA", `at most ${this.opts.maxSurfaces} surfaces`);
3245
+ s = new Surface(name, this.host(), this.hostCss(), this.scheme());
3246
+ this.surfaces.set(name, s);
3247
+ }
3248
+ if (c.get("d") !== "1") s.cancelDrag();
3249
+ const scroll = c.get("scroll");
3250
+ s.setDocument(text(cmd.payload), c.get("d") === "1", scroll === "1" || scroll === "2" || scroll === "3" ? Number(scroll) : 0);
3251
+ return;
3252
+ }
3253
+ case "place":
3254
+ return this.place(this.existing(c), c);
3255
+ case "hide": {
3256
+ const s = this.existing(c);
3257
+ s.cancelDrag();
3258
+ if (s.hasKeyboard()) s.blur();
3259
+ this.unplace(s.name);
3260
+ return;
3261
+ }
3262
+ case "delta": {
3263
+ const s = this.existing(c);
3264
+ s.delta(c.get("op") ?? "morph", c.get("t"), c.get("k"), text(cmd.payload));
3265
+ return;
3266
+ }
3267
+ case "res": {
3268
+ const id = c.get("id");
3269
+ if (!id) throw new Failure("EINVAL", "res needs id=<name>");
3270
+ try {
3271
+ this.store.put(id, c.get("type") ?? "application/octet-stream", cmd.payload);
3272
+ } catch (e) {
3273
+ throw new Failure("EQUOTA", e.message);
3274
+ }
3275
+ return;
3276
+ }
3277
+ case "del": {
3278
+ const id = c.get("id");
3279
+ if (id !== void 0) {
3280
+ this.store.remove(id);
3281
+ return;
3282
+ }
3283
+ const name = c.get("s");
3284
+ if (name === void 0) {
3285
+ this.reset();
3286
+ return;
3287
+ }
3288
+ if (!this.surfaces.has(name)) throw new Failure("ENOENT", `no surface ${name}`);
3289
+ this.remove(name);
3290
+ return;
3291
+ }
3292
+ case "detach":
3293
+ this.existing(c).detach();
3294
+ return;
3295
+ case "focus": {
3296
+ const s = this.existing(c);
3297
+ if (s.detached) throw new Failure("EDETACHED", `surface ${s.name} is detached`);
3298
+ try {
3299
+ s.focus(c.get("t"));
3300
+ } catch (e) {
3301
+ throw new Failure("ENOTARGET", e.message);
3302
+ }
3303
+ return;
3304
+ }
3305
+ case "blur":
3306
+ this.existing(c).blur();
3307
+ return;
3308
+ case "":
3309
+ throw new Failure("EINVAL", "missing a=<action>");
3310
+ default:
3311
+ throw new Failure("EINVAL", `unknown action a=${action}`);
3312
+ }
3313
+ }
3314
+ capabilities() {
3315
+ const { cellW, cellH } = this.cell();
3316
+ const scale = window.devicePixelRatio || 1;
3317
+ return {
3318
+ v: "0.1",
3319
+ ops: OPS,
3320
+ events: EVENTS,
3321
+ cell: { w: Math.round(cellW * scale), h: Math.round(cellH * scale) },
3322
+ scale,
3323
+ scheme: this.scheme(),
3324
+ limits: { resources: this.store.quota, surfaces: this.opts.maxSurfaces },
3325
+ net: this.policy,
3326
+ scroll: true,
3327
+ host: HOST,
3328
+ version: VERSION
3329
+ };
3330
+ }
3331
+ surfaceName(c) {
3332
+ const s = c.get("s");
3333
+ if (s === void 0) throw new Failure("EINVAL", "missing s=<surface>");
3334
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(s)) throw new Failure("EINVAL", `bad surface name ${JSON.stringify(s)}`);
3335
+ return s;
3336
+ }
3337
+ existing(c) {
3338
+ const name = this.surfaceName(c);
3339
+ const s = this.surfaces.get(name);
3340
+ if (!s) throw new Failure("ENOENT", `no surface ${name}`);
3341
+ return s;
3342
+ }
3343
+ place(s, c) {
3344
+ const cols = Number(c.get("c"));
3345
+ if (c.get("c") === void 0 || !Number.isInteger(cols)) throw new Failure("EINVAL", "place needs c=<cols>");
3346
+ if (cols < 1 || cols > 1e3) throw new Failure("EINVAL", "c out of range");
3347
+ const { cellW, cellH } = this.cell();
3348
+ const r = c.get("r");
3349
+ const auto = r === void 0 || r === "auto";
3350
+ const rows = auto ? s.contentRows(cols, cellW, cellH) : Number(r);
3351
+ if (!Number.isInteger(rows) || rows < 1 || rows > 1e3) throw new Failure("EINVAL", "r out of range");
3352
+ const cell = (k, d) => {
3353
+ const v = c.get(k);
3354
+ if (v === void 0) return d;
3355
+ const n = Number(v);
3356
+ if (!Number.isInteger(n) || n < 0) throw new Failure("EINVAL", `bad ${k}`);
3357
+ return n;
3358
+ };
3359
+ const x = cell("x", 0);
3360
+ const y = cell("y", 0);
3361
+ const win = { x, y, w: cell("w", cols - x), h: cell("h", rows - y) };
3362
+ if (win.w < 1 || win.h < 1 || x + win.w > cols || y + win.h > rows) {
3363
+ throw new Failure("EINVAL", "the window is not inside the surface");
3364
+ }
3365
+ const z = c.get("z") === void 0 ? 0 : Number(c.get("z"));
3366
+ if (!Number.isInteger(z) || z < -1e3 || z > 1e3) throw new Failure("EINVAL", "z is an integer from -1000 to 1000");
3367
+ s.autoRows = auto;
3368
+ s.setSize(cols, rows, cellW, cellH, win);
3369
+ s.setZ(z);
3370
+ this.unplace(s.name);
3371
+ s.presses = c.get("p") === "1";
3372
+ s.setFit(c.get("f") === "1" ? rows : null);
3373
+ const buf = this.term.buffer.active;
3374
+ const col = buf.cursorX;
3375
+ const row = buf.cursorY;
3376
+ const p = { surface: s, kind: buf.type === "normal" ? "normal" : "alt", col, row, disposables: [] };
3377
+ if (p.kind === "normal") {
3378
+ const marker = this.term.registerMarker(0);
3379
+ if (marker) {
3380
+ p.marker = marker;
3381
+ p.disposables.push(marker.onDispose(() => this.unplace(s.name)));
3382
+ } else {
3383
+ p.kind = "alt";
3384
+ }
3385
+ }
3386
+ this.placements.set(s.name, p);
3387
+ this.position(p);
3388
+ if (c.get("C") !== "1") this.moveCursorBelow(win.h);
3389
+ return { extra: [["c", String(cols)], ["r", String(rows)]] };
3390
+ }
3391
+ /** As the native host: index `rows` times, then carriage return. */
3392
+ moveCursorBelow(rows) {
3393
+ const core = this.term._core;
3394
+ const ih = core?._inputHandler;
3395
+ if (ih && core?.buffer) {
3396
+ for (let i = 0; i < rows; i++) ih.lineFeed();
3397
+ core.buffer.x = 0;
3398
+ }
3399
+ }
3400
+ /** Puts every placed surface where its cells are now. */
3401
+ reposition() {
3402
+ for (const p of this.placements.values()) this.position(p);
3403
+ }
3404
+ position(p) {
3405
+ const { cellW, cellH } = this.cell();
3406
+ const s = p.surface;
3407
+ let row = p.row;
3408
+ if (p.kind === "normal") {
3409
+ const buf = this.term.buffer.active;
3410
+ if (buf.type !== "normal" || !p.marker || p.marker.line < 0) {
3411
+ s.hide();
3412
+ return;
3413
+ }
3414
+ row = p.marker.line - buf.viewportY;
3415
+ }
3416
+ if (row + s.win.h <= 0 || row >= this.term.rows) {
3417
+ s.hide();
3418
+ return;
3419
+ }
3420
+ s.setSize(s.cols, s.rows, cellW, cellH, s.win);
3421
+ s.show(Math.round(p.col * cellW), row * cellH);
3422
+ }
3423
+ unplace(name) {
3424
+ const p = this.placements.get(name);
3425
+ if (!p) return;
3426
+ this.placements.delete(name);
3427
+ for (const d of p.disposables) d.dispose();
3428
+ p.marker?.dispose();
3429
+ p.surface.presses = false;
3430
+ p.surface.setFit(null);
3431
+ p.surface.park();
3432
+ }
3433
+ remove(name) {
3434
+ this.unplace(name);
3435
+ this.surfaces.get(name)?.destroy();
3436
+ this.surfaces.delete(name);
3437
+ }
3438
+ reset() {
3439
+ for (const name of Array.from(this.surfaces.keys())) this.remove(name);
3440
+ this.held = [];
3441
+ }
3442
+ onBufferChange(type) {
3443
+ for (const p of Array.from(this.placements.values())) {
3444
+ if (p.kind === "alt" && type === "normal") this.remove(p.surface.name);
3445
+ }
3446
+ this.reposition();
3447
+ }
3448
+ // --- Geometry and look ---------------------------------------------------
3449
+ host() {
3450
+ return {
3451
+ layer: this.ensureLayer(),
3452
+ store: this.store,
3453
+ event: (surface, kind, target, detail) => {
3454
+ const c = new Control().set("a", "ev").set("s", surface).set("e", kind).set("t", target);
3455
+ this.send(encode(c, detail === void 0 ? "" : JSON.stringify(detail)));
3456
+ },
3457
+ browserKey: (e) => this.browserKey(e),
3458
+ key: (e) => this.terminalKey(e),
3459
+ encodeKey: (e) => {
3460
+ const cs = this.term._core?.coreService;
3461
+ if (typeof cs?.triggerDataEvent !== "function") return null;
3462
+ let data = "";
3463
+ const own = Object.prototype.hasOwnProperty.call(cs, "triggerDataEvent");
3464
+ const prev = cs.triggerDataEvent;
3465
+ cs.triggerDataEvent = (d) => void (data += d);
3466
+ try {
3467
+ this.terminalKey(e);
3468
+ } finally {
3469
+ if (own) cs.triggerDataEvent = prev;
3470
+ else delete cs.triggerDataEvent;
3471
+ }
3472
+ return data;
3473
+ },
3474
+ sendKey: (data) => {
3475
+ const cs = this.term._core.coreService;
3476
+ this.throughTerminal(() => cs.triggerDataEvent(data, true));
3477
+ },
3478
+ focusTerminal: () => {
3479
+ if (this.forwarding) this.focusAfter = true;
3480
+ else this.term.focus();
3481
+ },
3482
+ policy: this.policy,
3483
+ pageScrolls: this.pageScrolls,
3484
+ scrollPage: (dx, dy) => this.scrollPage(dx, dy),
3485
+ wheelGesture: (surface, decide) => {
3486
+ const now = performance.now();
3487
+ const g = this.gesture;
3488
+ const same = g !== null && now - g.at < GESTURE_MS && (g.surface === surface || g.route === "terminal");
3489
+ const route = same ? g.route : decide();
3490
+ this.gesture = { surface, route, at: now };
3491
+ return route;
3492
+ },
3493
+ // A hyperlink (SPEC §9) goes where xterm.js sends an OSC 8 one: the
3494
+ // terminal's linkHandler, or its confirm-then-open default, and only
3495
+ // for http and https unless the handler allows other schemes.
3496
+ hyperlink: (kind, e, url, box) => {
3497
+ const handler = this.term.options.linkHandler;
3498
+ if (!handler?.allowNonHttpProtocols) {
3499
+ try {
3500
+ if (!["http:", "https:"].includes(new URL(url).protocol)) return;
3501
+ } catch {
3502
+ return;
3503
+ }
3504
+ }
3505
+ const range = this.cellRange(box);
3506
+ if (kind === "activate") {
3507
+ if (handler) handler.activate(e, url, range);
3508
+ else openAfterConfirm(url);
3509
+ } else if (kind === "hover") handler?.hover?.(e, url, range);
3510
+ else handler?.leave?.(e, url, range);
3511
+ },
3512
+ // Replayed on the terminal's screen, where xterm handles it as its own:
3513
+ // scrollback, or mouse reports and arrow keys on the alternate screen.
3514
+ wheel: (e, x, y) => this.forwardWheel(e, x, y),
3515
+ cells: (kind, e, x, y) => this.pressCells(kind, e, x, y)
3516
+ };
3517
+ }
3518
+ /**
3519
+ * A key through xterm.js's own keyboard handling, so it is encoded the
3520
+ * way the program asked: DECCKM, modifyOtherKeys, the kitty keyboard
3521
+ * protocol. A printable key xterm.js leaves to keypress (legacy input)
3522
+ * gets one.
3523
+ */
3524
+ terminalKey(e) {
3525
+ const ta = this.term.textarea;
3526
+ if (!ta) return;
3527
+ const init = {
3528
+ key: e.key,
3529
+ code: e.code,
3530
+ location: e.location,
3531
+ repeat: e.repeat,
3532
+ ctrlKey: e.ctrlKey,
3533
+ altKey: e.altKey,
3534
+ shiftKey: e.shiftKey,
3535
+ metaKey: e.metaKey,
3536
+ keyCode: e.keyCode,
3537
+ which: e.which,
3538
+ bubbles: true,
3539
+ cancelable: true
3540
+ };
3541
+ this.throughTerminal(() => {
3542
+ const ev = new KeyboardEvent(e.type, init);
3543
+ ta.dispatchEvent(ev);
3544
+ if (e.type === "keydown" && !ev.defaultPrevented && [...e.key].length === 1) {
3545
+ const c = e.key.codePointAt(0);
3546
+ ta.dispatchEvent(new KeyboardEvent("keypress", { ...init, charCode: c, keyCode: c, which: c }));
3547
+ }
3548
+ });
3549
+ }
3550
+ /** Something a surface sends through the terminal (a key, or what one
3551
+ * stands for). xterm.js focuses the terminal on every keyup; the surface
3552
+ * keeps the keyboard, so its focus does nothing meanwhile, and a surface
3553
+ * the program blurs meanwhile gives the keyboard back once it is
3554
+ * through. */
3555
+ throughTerminal(fn) {
3556
+ const ta = this.term.textarea;
3557
+ if (ta) Object.defineProperty(ta, "focus", { value: () => {
3558
+ }, configurable: true });
3559
+ this.forwarding = true;
3560
+ try {
3561
+ fn();
3562
+ } finally {
3563
+ if (ta) delete ta.focus;
3564
+ this.forwarding = false;
3565
+ if (this.focusAfter) {
3566
+ this.focusAfter = false;
3567
+ this.term.focus();
3568
+ }
3569
+ }
3570
+ }
3571
+ cellTouch = null;
3572
+ pageScrollBound = false;
3573
+ /** Whether the page scrolls, not the terminal (the `scroll` option). */
3574
+ get pageScrolls() {
3575
+ return this.opts.scroll === "page";
3576
+ }
3577
+ /**
3578
+ * Wheels and touches on the cells left to the browser, which scrolls the
3579
+ * page (`scroll: "page"`): they stop at the terminal's element, before
3580
+ * xterm.js, which would take them for its scrollback, or turn a drag into
3581
+ * arrow keys, and cancel the browser's scroll. Nothing here cancels it.
3582
+ */
3583
+ bindPageScroll(el) {
3584
+ if (this.pageScrollBound) return;
3585
+ this.pageScrollBound = true;
3586
+ const stop = (e) => e.stopPropagation();
3587
+ for (const type of ["wheel", "touchstart", "touchmove", "touchend", "touchcancel"]) {
3588
+ el.addEventListener(type, stop, { capture: true, passive: true });
3589
+ this.disposables.push({ dispose: () => el.removeEventListener(type, stop, { capture: true }) });
3590
+ }
3591
+ }
3592
+ /** Scrolls what scrolls the terminal's page: its nearest ancestor that
3593
+ * scrolls, or the document. */
3594
+ scrollPage(dx, dy) {
3595
+ let el = this.term.element?.parentElement ?? null;
3596
+ for (; el; el = el.parentElement) {
3597
+ const o = getComputedStyle(el).overflowY;
3598
+ if (/auto|scroll/.test(o) && el.scrollHeight > el.clientHeight) break;
3599
+ }
3600
+ (el ?? document.scrollingElement ?? document.documentElement).scrollBy(dx, dy);
3601
+ }
3602
+ cellWheelBound = false;
3603
+ /** A wheel over the cells begins or goes on with a gesture of the
3604
+ * terminal's: a document that scrolls under the pointer as the terminal
3605
+ * scrolls takes none of it (`wheelGesture`). */
3606
+ bindCellWheel(el) {
3607
+ if (this.cellWheelBound) return;
3608
+ this.cellWheelBound = true;
3609
+ const wheel = (e) => {
3610
+ if (e.isTrusted) this.gesture = { surface: "", route: "terminal", at: performance.now() };
3611
+ };
3612
+ el.addEventListener("wheel", wheel, { capture: true, passive: true });
3613
+ this.disposables.push({ dispose: () => el.removeEventListener("wheel", wheel, { capture: true }) });
3614
+ }
3615
+ /** Touch on the cells (the `touch` option), once the terminal is open. */
3616
+ bindTouch() {
3617
+ const el = this.term.element;
3618
+ if (el && this.pageScrolls) return this.bindPageScroll(el);
3619
+ if (el) this.bindCellWheel(el);
3620
+ if (this.cellTouch || !el || this.opts.touch === false) return;
3621
+ const screen = () => el.querySelector(".xterm-screen");
3622
+ this.cellTouch = new Touch(
3623
+ el,
3624
+ el.ownerDocument.defaultView,
3625
+ {
3626
+ scroll: (dx, dy, x, y) => this.forwardWheel(new WheelEvent("wheel", { deltaX: dx, deltaY: dy, deltaMode: 0 }), x, y),
3627
+ // A tap is the press and release a click would make: xterm.js
3628
+ // reports them to the program as it reports a mouse.
3629
+ tap: (x, y) => {
3630
+ const at = { clientX: x, clientY: y, button: 0, bubbles: true, cancelable: true, view: el.ownerDocument.defaultView };
3631
+ screen()?.dispatchEvent(new MouseEvent("mousedown", { ...at, buttons: 1 }));
3632
+ el.ownerDocument.dispatchEvent(new MouseEvent("mouseup", { ...at, buttons: 0 }));
3633
+ },
3634
+ toPage: (x, y) => [x, y]
3635
+ },
3636
+ true
3637
+ );
3638
+ }
3639
+ /** A wheel, at a point in the page, replayed on the terminal's screen,
3640
+ * where xterm.js handles it as its own: scrollback, or mouse reports and
3641
+ * arrow keys on the alternate screen. */
3642
+ forwardWheel(e, x, y) {
3643
+ const ev = new WheelEvent("wheel", {
3644
+ deltaX: e.deltaX,
3645
+ deltaY: e.deltaY,
3646
+ deltaMode: e.deltaMode,
3647
+ clientX: x,
3648
+ clientY: y,
3649
+ shiftKey: e.shiftKey,
3650
+ altKey: e.altKey,
3651
+ metaKey: e.metaKey,
3652
+ bubbles: true,
3653
+ cancelable: true
3654
+ });
3655
+ legacyWheelDelta(ev, e);
3656
+ this.term.element?.querySelector(".xterm-screen")?.dispatchEvent(ev);
3657
+ }
3658
+ /**
3659
+ * A press with Alt held on a surface, a move of its gesture, or its
3660
+ * release (SPEC §9.2), replayed on the cells beneath, where xterm.js
3661
+ * handles them as its own mouse: a report to the program, with Alt, or
3662
+ * its selection (rectangular, with Alt). The press goes to the screen,
3663
+ * where xterm.js listens for presses; the moves and the release to its
3664
+ * document, where it listens while a button is held. The keyboard goes
3665
+ * back to the terminal first, as on a click on the cells (§10.1): a
3666
+ * surface that had it sends `blur`.
3667
+ */
3668
+ pressCells(kind, e, x, y) {
3669
+ const el = this.term.element;
3670
+ if (!el) return;
3671
+ const init = {
3672
+ clientX: x,
3673
+ clientY: y,
3674
+ screenX: e.screenX,
3675
+ screenY: e.screenY,
3676
+ button: 0,
3677
+ buttons: e.buttons,
3678
+ // xterm.js selects on a press whose detail is the click count.
3679
+ detail: kind === "down" ? e.detail || 1 : e.detail,
3680
+ altKey: e.altKey,
3681
+ ctrlKey: e.ctrlKey,
3682
+ shiftKey: e.shiftKey,
3683
+ metaKey: e.metaKey,
3684
+ bubbles: true,
3685
+ cancelable: true,
3686
+ view: el.ownerDocument.defaultView
3687
+ };
3688
+ if (kind === "down") {
3689
+ for (const s of this.surfaces.values()) if (s.hasKeyboard()) s.blur();
3690
+ el.querySelector(".xterm-screen")?.dispatchEvent(new MouseEvent("mousedown", init));
3691
+ return;
3692
+ }
3693
+ el.ownerDocument.dispatchEvent(new MouseEvent(kind === "move" ? "mousemove" : "mouseup", init));
3694
+ }
3695
+ ensureLayer() {
3696
+ if (this.layer?.isConnected) return this.layer;
3697
+ const screen = this.term.element?.querySelector(".xterm-screen");
3698
+ if (!screen) throw new Failure("EINVAL", "terminal is not open");
3699
+ const layer = document.createElement("div");
3700
+ layer.className = "hotty-layer";
3701
+ Object.assign(layer.style, { position: "absolute", inset: "0", overflow: "hidden", zIndex: "6", pointerEvents: "none" });
3702
+ screen.append(layer);
3703
+ this.layer = layer;
3704
+ return layer;
3705
+ }
3706
+ browserKey(e) {
3707
+ return (this.opts.browserKeys ?? browserKeys)(e);
3708
+ }
3709
+ /** A box in the page as the buffer cells it covers (1-based, as xterm.js
3710
+ * gives an OSC 8 link's range). */
3711
+ cellRange(box) {
3712
+ const screen = this.term.element?.querySelector(".xterm-screen")?.getBoundingClientRect();
3713
+ const { cellW, cellH } = this.cell();
3714
+ const top = this.term.buffer.active.viewportY;
3715
+ const at = (x, y) => ({
3716
+ x: Math.floor((x - (screen?.left ?? 0)) / cellW) + 1,
3717
+ y: top + Math.floor((y - (screen?.top ?? 0)) / cellH) + 1
3718
+ });
3719
+ return { start: at(box.left, box.top), end: at(box.right - 1, box.bottom - 1) };
3720
+ }
3721
+ /** Cell size in CSS pixels (private in xterm.js), with a measured fallback. */
3722
+ cell() {
3723
+ const dims = this.term._core?._renderService?.dimensions?.css?.cell;
3724
+ if (dims && dims.width > 0 && dims.height > 0) return { cellW: dims.width, cellH: dims.height };
3725
+ const screen = this.term.element?.querySelector(".xterm-screen");
3726
+ if (screen && this.term.cols > 0) return { cellW: screen.clientWidth / this.term.cols, cellH: screen.clientHeight / this.term.rows };
3727
+ return { cellW: 9, cellH: 17 };
3728
+ }
3729
+ hostCss() {
3730
+ const { cellW, cellH } = this.cell();
3731
+ const o = this.term.options;
3732
+ this.css = hostCss(o.theme, { cellW, cellH, fontFamily: o.fontFamily ?? "monospace", fontSize: o.fontSize ?? 15 });
3733
+ return this.css;
3734
+ }
3735
+ scheme() {
3736
+ return palette(this.term.options.theme).dark ? "dark" : "light";
3737
+ }
3738
+ /** On every render: did the cell size, font or theme change? */
3739
+ checkMetrics() {
3740
+ const { cellW, cellH } = this.cell();
3741
+ const o = this.term.options;
3742
+ const key = `${cellW}x${cellH}|${o.fontFamily}|${o.fontSize}|${JSON.stringify(o.theme ?? {})}`;
3743
+ if (key === this.metrics.key) return;
3744
+ const sizeChanged = this.metrics.cellW !== 0 && (cellW !== this.metrics.cellW || cellH !== this.metrics.cellH);
3745
+ this.metrics = { cellW, cellH, key };
3746
+ if (this.surfaces.size === 0) return;
3747
+ const css = this.hostCss();
3748
+ const scheme = this.scheme();
3749
+ for (const s of this.surfaces.values()) s.setHostCss(css, scheme);
3750
+ this.reposition();
3751
+ if (sizeChanged) {
3752
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
3753
+ this.resizeTimer = setTimeout(() => {
3754
+ for (const p of this.placements.values()) {
3755
+ const s = p.surface;
3756
+ if (s.detached) continue;
3757
+ this.host().event(s.name, "resize", "", { w: Math.round(s.cols * cellW), h: s.rows * cellH });
3758
+ }
3759
+ }, 100);
3760
+ }
3761
+ }
3762
+ };
3763
+ function sequence(chain, f) {
3764
+ if (chain) return chain.then(f);
3765
+ const r = f();
3766
+ return r instanceof Promise ? r : void 0;
3767
+ }
3768
+ function openAfterConfirm(uri) {
3769
+ if (!confirm(`Do you want to navigate to ${uri}?
3770
+
3771
+ WARNING: This link could potentially be dangerous`)) return;
3772
+ const w = window.open();
3773
+ if (!w) {
3774
+ console.warn("Opening link blocked as opener could not be cleared");
3775
+ return;
3776
+ }
3777
+ try {
3778
+ w.opener = null;
3779
+ } catch {
3780
+ }
3781
+ w.location.href = uri;
3782
+ }
3783
+ function legacyWheelDelta(ev, from) {
3784
+ if (!("wheelDeltaY" in WheelEvent.prototype)) return;
3785
+ const legacy = from;
3786
+ const real = from.isTrusted;
3787
+ const pixels = from.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? 1 : from.deltaMode === WheelEvent.DOM_DELTA_LINE ? 20 : 400;
3788
+ const x = real && legacy.wheelDeltaX || -from.deltaX * pixels * (120 / 50);
3789
+ const y = real && legacy.wheelDeltaY || -from.deltaY * pixels * (120 / 50);
3790
+ Object.defineProperty(ev, "wheelDeltaX", { value: x });
3791
+ Object.defineProperty(ev, "wheelDeltaY", { value: y });
3792
+ }
3793
+ export {
3794
+ Control,
3795
+ EVENTS,
3796
+ HOST,
3797
+ HottyAddon,
3798
+ OSC,
3799
+ VERSION,
3800
+ browserKeys,
3801
+ decodeAll,
3802
+ encode
3803
+ };
3804
+ //# sourceMappingURL=hotty-xterm.js.map