@neuroplastio/xterm-addon-hotty 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,73 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
10
+
11
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
12
+
13
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
14
+
15
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
16
+
17
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
18
+
19
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
20
+
21
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
22
+
23
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
24
+
25
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
26
+
27
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
28
+
29
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
30
+
31
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
32
+
33
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
34
+
35
+ (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
36
+
37
+ (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
38
+
39
+ (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
40
+
41
+ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
42
+
43
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
44
+
45
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
46
+
47
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
48
+
49
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
50
+
51
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
52
+
53
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
54
+
55
+ END OF TERMS AND CONDITIONS
56
+
57
+ APPENDIX: How to apply the Apache License to your work.
58
+
59
+ To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
60
+
61
+ Copyright [yyyy] [name of copyright owner]
62
+
63
+ Licensed under the Apache License, Version 2.0 (the "License");
64
+ you may not use this file except in compliance with the License.
65
+ You may obtain a copy of the License at
66
+
67
+ http://www.apache.org/licenses/LICENSE-2.0
68
+
69
+ Unless required by applicable law or agreed to in writing, software
70
+ distributed under the License is distributed on an "AS IS" BASIS,
71
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
72
+ See the License for the specific language governing permissions and
73
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,358 @@
1
+ # xterm-addon-hotty: HOTTY for xterm.js
2
+
3
+ A [HOTTY](https://github.com/neuroplastio/hotty) host as an addon for
4
+ xterm.js, where the **browser is the engine**. Every surface is a real
5
+ document in a sandboxed iframe. It shares no code with hotty-blitz, the
6
+ other implementation, and passes the same conformance vectors. The HOTTY
7
+ repository's example programs run unchanged in a browser tab.
8
+
9
+ ```
10
+ npm install @neuroplastio/xterm-addon-hotty @xterm/xterm
11
+ ```
12
+
13
+ The package is an ES module with its TypeScript declarations, for xterm.js
14
+ 6, and has no dependencies. A commit not yet released installs from git
15
+ (`github:neuroplastio/xterm-addon-hotty#<commit>`): npm builds it as it
16
+ installs it (`prepare`).
17
+
18
+ ```ts
19
+ import { Terminal } from "@xterm/xterm";
20
+ import { HottyAddon } from "@neuroplastio/xterm-addon-hotty";
21
+
22
+ const term = new Terminal({
23
+ // Optional: key releases for programs that need them (games), SPEC §10.3.
24
+ vtExtensions: { kittyKeyboard: true },
25
+ });
26
+ term.loadAddon(new HottyAddon());
27
+ term.open(element);
28
+ // term.onData → your pty, as usual: replies and events travel that way too.
29
+ ```
30
+
31
+ ### Links and the network
32
+
33
+ A surface fetches nothing from the network unless the page grants it, and
34
+ the document asks for it (SPEC §7.2):
35
+
36
+ ```ts
37
+ new HottyAddon({
38
+ // The host's half of the network policy: directive → origins, or "https:".
39
+ network: { "img-src": ["https://example.com"] },
40
+ });
41
+ ```
42
+
43
+ - A document asks with `<meta name="hotty-network" content="img-src
44
+ https://example.com">` and gets what both allow; its `<base href>` sets
45
+ its base URL, so relative images and links resolve (§7.3). The capability
46
+ reply reports the grant as `net`.
47
+ - **Links** show, hover and copy as the links they are. A click is a
48
+ `click` event for the program, with `href` (the program's value) and
49
+ `url` (resolved), whether the link has an `id` or not. The addon never
50
+ opens one itself (§9). The context menu's own "open in new tab" and "copy
51
+ link" work as on any page.
52
+ - **Hyperlinks**, links with `target="_blank"` and a `url`, are the
53
+ terminal's, as OSC 8 links are (§9). One without a `url` (a relative
54
+ `href` with no base, say) is the program's, as any other link. They go where xterm.js sends an OSC 8 link: the
55
+ terminal's `linkHandler` (`activate`, `hover`, `leave`, with the link's
56
+ cells as the range), or xterm.js's confirm-then-open default. Only
57
+ `http` and `https` go through, unless the handler sets
58
+ `allowNonHttpProtocols`. The program hears nothing of them.
59
+ - The page's own CSP must allow the granted origins too (below).
60
+
61
+ ## Try it
62
+
63
+ Clone [neuroplastio/hotty](https://github.com/neuroplastio/hotty) next to
64
+ this repository (or set `HOTTY_DIR`), then:
65
+
66
+ ```
67
+ npm ci && npm run build
68
+ python3 serve.py --examples # /?run=card, /?run=dash, /?run=form, /?run=grid, /?run=bubbros
69
+ python3 serve.py -- python3 ../hotty/examples/dash.py # any program, at /
70
+ ```
71
+
72
+ `serve.py` is the test bridge: an HTTP server for `dist/` and a WebSocket to a
73
+ pty. It uses the standard library only, and listens on 127.0.0.1 because it
74
+ runs programs for whoever connects.
75
+
76
+ ## How it works
77
+
78
+ | piece | file | what it does |
79
+ | --- | --- | --- |
80
+ | wire | `src/wire.ts` | OSC 7279 control parsing, chunk reassembly, base64, zlib through `DecompressionStream`, and reply encoding |
81
+ | addon | `src/addon.ts` | OSC handler, replies through `term.input(…, false)`, placement, synchronized output, RIS, the alternate screen, zoom and theme, and keys a surface does not use, through xterm.js's own keyboard handling |
82
+ | surface | `src/surface.ts` | one sandboxed iframe per surface; events, focus and key routing |
83
+ | deltas | `src/delta.ts` | SPEC §6: the ops and morph |
84
+ | resources | `src/resources.ts`, `src/resolver.ts` | `cid:` as `blob:` URLs; sanitizing everything before it reaches a live document |
85
+ | host stylesheet | `src/hostcss.ts` | §8 from `term.options` (theme, font, cell size), in a cascade layer: the palette, `--hotty-accent`, and controls, focus, links and selection in the terminal's colours |
86
+
87
+ - **Placement.** On the normal buffer a surface hangs from a marker, which is
88
+ public API (decorations need `allowProposedApi`). It scrolls with the text,
89
+ stays visible while any of its rows is, and dies when its line leaves the
90
+ scrollback. On the alternate buffer, where markers do not work, it sits on
91
+ fixed cells and dies with the screen. The iframe never
92
+ moves in the DOM, since that would reload it; only its box moves.
93
+ - **Windows and hiding** (SPEC §5.2, §5.4). The iframe keeps the surface's
94
+ whole size inside a box the size of the window, offset by the window's
95
+ corner, so a new window moves the iframe and lays nothing out. A hidden
96
+ surface's box is `display: none`: its document stays, and the browser
97
+ skips its style, layout and paint until it is placed again.
98
+ - **A transparent document shows the cells beneath it.** The frame's
99
+ colour scheme is the document's (the theme's, as the host stylesheet
100
+ declares it): were they to differ, the browser would paint the frame an
101
+ opaque canvas.
102
+ - **Stacking** (SPEC §5.2). A placement's `z` is its box's `z-index`, and
103
+ the boxes stand in the layer in the order their surfaces were created,
104
+ so overlapping placements stack as the spec says, and the browser gives
105
+ the pointer to the topmost.
106
+ - **The keyboard** (SPEC §10.1). A click takes it only by focusing an
107
+ element that takes focus: an `input`, `select`, `textarea` or `button`, a
108
+ link with an `href` that is not a hyperlink, a details' first `summary`,
109
+ an editing host, or a `tabindex` of 0 or more (a `label` counts as its
110
+ control). A click on anything else, a hyperlink included, sends no
111
+ `focus`, and on a surface that had the keyboard it sends `blur`. The browser focuses the frame on any click; once the press
112
+ is done, the addon gives that focus back to the terminal, so text
113
+ selection works as usual. A right click leaves focus where the browser
114
+ put it, so the context menu's Copy copies the surface's selection.
115
+ - **Drags** (SPEC §9.1, a draft on hotty's `drag` branch). A mouse's or a
116
+ pen's primary press on an element with `drag` in its `data-on` and an id
117
+ reports `dragstart`, then `drag` each time the element under the pointer
118
+ changes (each cell while there is none), then `dragend`, with the
119
+ surface's cell and the keys held. The frame's root element captures the
120
+ pointer (`setPointerCapture`) until the release, so the moves keep coming
121
+ over the cells, other surfaces and outside the page, and none reach
122
+ xterm.js's mouse reporting; the root is never replaced, so deltas do not
123
+ lose the capture. The browser clicks the root on a captured release, so
124
+ the drag reports the click itself when it ends where it began. The host
125
+ stylesheet makes the elements that opt in unselectable, important in its
126
+ layer, whatever the document's CSS. A touch never drags.
127
+ - **Presses** (SPEC §5.2, §9: `p=1` on `a=place`). Every primary press of a
128
+ mouse or a pen in the window reports `press` on its pointerdown, before
129
+ the drag and the focus it causes; a tap reports it on the mousedown the
130
+ browser makes for it. A press on a hyperlink is the terminal's.
131
+ - **Fit** (SPEC §5.2, §9: `f=1` on `a=place`). The program hears `fit`
132
+ with the rows the document needs at the placement's width whenever they
133
+ differ from the rows it heard last, starting from the placement's own;
134
+ the placement keeps its size. The rows are measured as for `r=auto`: the
135
+ frame is laid out at that width and 1px high, then restored in the same
136
+ task. A check waits for the next frame, so a frame sends one `fit` at
137
+ most, with the rows it draws. Each of these asks for a check:
138
+ - a document or a delta;
139
+ - a resource arriving or changing;
140
+ - the host stylesheet changing (cell size, font);
141
+ - an image, a stylesheet or a font loading, whether a `cid:` resource or
142
+ one from the network;
143
+ - the root's or the body's box changing size (a `<details>` the user
144
+ opens, say).
145
+
146
+ A surface out of view checks once it is shown again. A check lays the
147
+ document out a second time, so only placements with `f=1` pay for it.
148
+ - **Presses with Alt** (SPEC §9.2) are the program's. The frame cancels the
149
+ press's mousedown (no focus, selection, drag or hyperlink), reports
150
+ nothing, and captures the pointer on its root until the release; the
151
+ addon replays the press on xterm.js's screen and the moves and release on
152
+ its document, where xterm.js listens, as it replays wheels: a mouse report
153
+ with Alt, or with reporting off, xterm.js's rectangular selection. A
154
+ surface that had the keyboard gives it back first (`blur`). The browser's
155
+ click on the release is not the surface's. Firefox drops the pressed
156
+ element's `:hover` only at the next move.
157
+ - **Detached surfaces** (SPEC §5.5: `a=detach`, or `d=1` on `a=doc`) send no
158
+ events and never take the keyboard. Their `input`, `select`, `textarea`
159
+ and `button` elements carry a `disabled` of the addon's own, which
160
+ inspection and morphs do not see, and which a delta cannot remove. Hover,
161
+ selection, `<details>` and hyperlinks work as before. Only a hyperlink
162
+ shows the hand, whatever the document's `cursor`; other links show the
163
+ text pointer.
164
+ - **The browser's keys stay the browser's** (the `browserKeys` option):
165
+ - reload (F5, and Ctrl or Cmd with R);
166
+ - zoom (Ctrl or Cmd with +, − or 0);
167
+ - full screen (F11);
168
+ - the developer tools (F12, and Ctrl+Shift with I, J or C);
169
+ - on a Mac, everything with Cmd.
170
+
171
+ Neither the terminal nor a surface holding the keyboard sends them to the
172
+ program, and the browser acts on them. A terminal cannot know which keys
173
+ a program binds, so these are the browser's own. The addon installs
174
+ xterm.js's custom key handler for this, so a page passes its own list
175
+ here, not to xterm.js. The default is exported, so a page can add keys
176
+ of its own:
177
+
178
+ ```ts
179
+ import { HottyAddon, browserKeys } from "@neuroplastio/xterm-addon-hotty";
180
+
181
+ new HottyAddon({ browserKeys: (e) => browserKeys(e) || (e.ctrlKey && e.key === "k") });
182
+ ```
183
+ - **Nothing in a surface scrolls unless its document asks** (SPEC §5.3, §9):
184
+ - It shows no scrollbars, and pans nothing on a touch
185
+ (`touch-action: none`). Any scroll offset the browser sets goes back to
186
+ zero, except a text field's own text.
187
+ - A wheel over a surface goes to the terminal, and so does a touch drag,
188
+ as wheel events at the finger. After the finger lifts, the drag keeps
189
+ going and slows down.
190
+ - Taps and long presses stay the surface's.
191
+ - On the cells, the addon handles touch too (the `touch` option, on by
192
+ default), in place of xterm.js's own. A drag scrolls as over a surface.
193
+ A tap is a click for the program: a press and a release at the finger.
194
+ xterm.js 6.1's own touch handling sends wheel reports with no position
195
+ (`NaN`, which a program reads as typing) and turns taps into nothing.
196
+ - Ctrl and the wheel stay the browser's zoom.
197
+ - xterm.js's scrollable reads the legacy `wheelDeltaY` where browsers have
198
+ it, so a forwarded wheel carries one, worked out from the drag: on a
199
+ constructed event the browser's own has the wrong sign in Chromium 15x.
200
+ - A drag is measured in the page, not in the surface, which moves as the
201
+ terminal scrolls.
202
+ - **A page that scrolls itself** (`scroll: "page"`): for a page that shows
203
+ a program's output whole, with the terminal as tall as what it shows,
204
+ such as a document printed by a program. Wheels and touch drags over
205
+ the surfaces and the cells are left to the browser, which scrolls the
206
+ page natively, with its own momentum, and the program hears no wheel.
207
+ xterm.js never sees them, since it would take a drag for its
208
+ scrollback, or turn it into arrow keys. Surfaces pan on a touch
209
+ (`touch-action: manipulation`), and over an element of the document
210
+ that the browser would scroll instead (`overflow: auto`), the addon
211
+ scrolls the page itself.
212
+
213
+ ```ts
214
+ new HottyAddon({ scroll: "page" });
215
+ ```
216
+ - **A document that scrolls** (`scroll=1`, `2` or `3` on `a=doc`, SPEC
217
+ §5.1, §5.3; the capabilities say `"scroll": true`):
218
+ - Along the axes it asked for, the browser scrolls it as it scrolls a
219
+ page: the root and every `overflow: auto` or `scroll` element, with
220
+ the browser's scrollbars, which take pixels inside the frame, never
221
+ cells. The root pans on a touch along those axes (`touch-action:
222
+ pan-y`, `pan-x`).
223
+ - Along an axis it did not ask for, the root and every element whose
224
+ `overflow` there is `auto` or `scroll` are `overflow: hidden`, important
225
+ in the host's layer: no scrollbar, and nothing the user does moves it.
226
+ CSS cannot select by a computed value, so the addon reads it after each
227
+ document, delta and host stylesheet, and marks those elements with an
228
+ attribute of its own, which inspection and morphs do not see.
229
+ - **Gestures.** A wheel gesture's first event decides where it goes, as
230
+ a browser latches scrolling to what it began on (a gesture is wheels
231
+ less than 150ms apart): to the document while the innermost box under
232
+ the pointer that can move that way can (the browser scrolls it), else
233
+ on to the terminal, as over the cells, unless `overscroll-behavior`
234
+ stops it. A gesture begun over the cells stays the terminal's when a
235
+ surface comes under the pointer. A touch drag decides the same way once
236
+ it has a direction: the browser pans the document, or the drag goes to
237
+ the terminal as before.
238
+ - **Keys.** While the surface has the keyboard, the keys a browser
239
+ scrolls with and the focused element does not use (SPEC §10.2) scroll
240
+ the innermost box, from the focused element outward, that can move
241
+ that way: the arrows by 40 pixels, Page Up and Page Down, Space and
242
+ Shift+Space by seven eighths of the box, Home and End to the ends. The
243
+ addon scrolls the box itself, since the browser's own action may be the
244
+ element's (a radio button's arrows). Where nothing can move that way,
245
+ the key goes on to the program, as any key the surface does not use.
246
+ - Focus scrolls an element into view, as the browser does. The program
247
+ hears nothing of scrolling. A delta keeps the offsets, and so do hiding
248
+ and placing again; a new document starts at the top left. `r=auto` and
249
+ `fit` measure the document with its root clipped, so the root's
250
+ scrollbar does not make its lines wrap.
251
+ - Where the page scrolls (`scroll: "page"`), the browser chains a
252
+ gesture from the document to the page natively.
253
+ - **Where an element is** (SPEC §9: `area` on `click` and `press`): the
254
+ cells of its bounding box in the frame, as the user sees it, scrolled
255
+ included, divided by the cell size. An edge within half a device pixel of
256
+ a cell's counts as on it, since layout rounds positions to fractions of a
257
+ pixel. A press with no id carries none.
258
+ - **Cursor.** After `a=place` the cursor moves below the surface, as in the
259
+ native host. xterm.js has no public API for that, so the addon uses the same
260
+ private calls as the official image addon.
261
+ - **Cell size** comes from xterm's render service (also private), with a
262
+ measured fallback.
263
+ - **xterm.js 6.1** is needed for the kitty keyboard protocol
264
+ (`vtExtensions.kittyKeyboard`); the addon itself works with 6.0.
265
+
266
+ ## Security
267
+
268
+ Program markup is untrusted: it may come from `cat`, or from a server on the
269
+ far side of SSH. Two independent layers stand between it and the page, and a
270
+ test checks each one (`tests/e2e/hostile.spec.ts`):
271
+
272
+ 1. **The sanitizer** (`resolver.ts`) works on inert parses. It removes
273
+ `script`, `iframe`, `object`, `embed`, `base`, `meta` and any `link` that
274
+ is not a stylesheet. It also removes `on*`, `srcdoc`, `autofocus` and
275
+ `ping`. URL attributes resolve to `blob:` for `cid:`, stay as they are for
276
+ `data:`, become absolute URLs where the network policy allows them (the
277
+ page's grant and the document's request, both), and fail closed as
278
+ `about:invalid` for everything else. A document's `<base>` and
279
+ `<meta name="hotty-network">` are read before they are removed.
280
+ 2. **The iframe:**
281
+ - `sandbox="allow-same-origin allow-forms"`: no scripts, popups or
282
+ navigation. `allow-forms` is there only so that `submit` fires; the
283
+ submission is cancelled.
284
+ - A CSP `<meta>` written by the parser before any program markup:
285
+ `default-src 'none'`, with `data:` and `blob:` allowed for images,
286
+ media and fonts, and the page's `network` grant; inline styles allowed,
287
+ and `form-action 'none'`. It holds the page's grant on its own: a
288
+ reference the sanitizer misses reaches nothing the page did not grant.
289
+ Network URLs inside `cid:` stylesheets are not fetched (a stylesheet
290
+ resource serves surfaces with different policies); a `<link>` to the
291
+ stylesheet on the network, or a `<style>`, is.
292
+
293
+ With the CSP removed, 9 requests get through, all from CSS (`@import`, `url()`
294
+ and `@font-face`). With the sanitizer passing URLs through, the CSP alone
295
+ stops everything. `position: fixed` stays inside the surface's own viewport.
296
+
297
+ **The embedding page's CSP applies inside the surfaces too** (about:blank
298
+ iframes inherit it, and it can only be narrowed). A page that embeds xterm.js
299
+ already has to allow inline styles, because xterm's DOM renderer sets them.
300
+ For HOTTY it must also allow `data:` and `blob:` images, fonts and media, and
301
+ whatever origins it grants in `network`. This is the recommended policy (with
302
+ no network grant), and `tests/e2e/embedder.spec.ts` checks it:
303
+
304
+ ```
305
+ default-src 'self'; style-src 'self' 'unsafe-inline';
306
+ img-src 'self' data: blob:; font-src 'self' data: blob:; media-src 'self' data: blob:
307
+ ```
308
+
309
+ ## Differences from hotty-blitz
310
+
311
+ - **Pixels differ.** The browser renders, so text, form controls and
312
+ antialiasing look like the browser's. The cell footprint is the same (§9).
313
+ - **More events.** The browser gives `resize` (after zoom or a font change) and
314
+ native `<select>`, IME and spellcheck.
315
+ - **Not here yet:**
316
+ - §9's text in xterm's buffer (search, serialize, and a selection across a
317
+ surface);
318
+ - following content that scrolls on the alternate screen;
319
+ - the embedder's web fonts inside surfaces (system fonts work);
320
+ - `hover` (§9.4, `v=1` on `a=place`): `EVENTS` does not list it, so a
321
+ program never asks, and the vectors that require it are skipped. The
322
+ frame would report the nearest id on `pointerover` with no button
323
+ down, and out on leaving the frame, an Alt press, or a release
324
+ outside it.
325
+
326
+ ## Costs
327
+
328
+ Measured 2026-09-29 in headless Chromium 153 (`bench/`):
329
+
330
+ - **A surface** takes 4.7 ms to create and place, 8.7 ms of main-thread time
331
+ with its first frame, and 1.2 MB of memory.
332
+ - **The dashboard** costs 4.9 ms of main-thread time per frame at 10 Hz.
333
+ - **Large documents** cost Chrome more per frame as they grow: its PrePaint
334
+ walks the tree. A one-cell delta costs 8.4 ms of main-thread time per frame
335
+ at 4,096 cells and 46 ms at 262,144. CSS containment brings the latter to
336
+ 30 ms; hotty-blitz does it in about 0.1 ms.
337
+ - **Bub-n-Bros** (`examples/bubbros.py`) costs about 4 ms per frame: deltas to
338
+ seven sprites out of about 380.
339
+
340
+ ## Tests
341
+
342
+ `npm run check` runs:
343
+
344
+ - the typecheck, the build and the declarations (what `npm pack` ships);
345
+ - the unit tests (`node --test`: the wire, keys, decoding the reference Python
346
+ client, and the conformance vectors' wire section);
347
+ - the Playwright tests, against Chromium (and Firefox once installed:
348
+ `npx playwright install firefox`):
349
+ - the protocol and the conformance vectors;
350
+ - interaction, with `form.py` through the bridge;
351
+ - the hostile page and the embedder's CSP;
352
+ - Bub-n-Bros with kitty keys (skipped unless the game is fetched).
353
+
354
+ `bench/measure.mjs` and `bench/trace.mjs` produce the cost numbers.
355
+
356
+ ## Licence
357
+
358
+ Apache-2.0 ([LICENSE](LICENSE)). xterm.js itself is MIT.