@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.
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,412 @@
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
+ | keys | `src/keys.ts` | SPEC §10.2, §10.4: key names, the keys in what the terminal sends, keymaps (`data-keys`), and the actions on a field's text |
84
+ | deltas | `src/delta.ts` | SPEC §6: the ops and morph |
85
+ | resources | `src/resources.ts`, `src/resolver.ts` | `cid:` as `blob:` URLs; sanitizing everything before it reaches a live document |
86
+ | 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 |
87
+
88
+ - **Placement.** On the normal buffer a surface hangs from a marker, which is
89
+ public API (decorations need `allowProposedApi`). It scrolls with the text,
90
+ stays visible while any of its rows is, and dies when its line leaves the
91
+ scrollback. On the alternate buffer, where markers do not work, it sits on
92
+ fixed cells and dies with the screen. The iframe never
93
+ moves in the DOM, since that would reload it; only its box moves.
94
+ - **Windows and hiding** (SPEC §5.2, §5.4). The iframe keeps the surface's
95
+ whole size inside a box the size of the window, offset by the window's
96
+ corner, so a new window moves the iframe and lays nothing out. A hidden
97
+ surface's box is `display: none`: its document stays, and the browser
98
+ skips its style, layout and paint until it is placed again.
99
+ - **A transparent document shows the cells beneath it.** The frame's
100
+ colour scheme is the document's (the theme's, as the host stylesheet
101
+ declares it): were they to differ, the browser would paint the frame an
102
+ opaque canvas.
103
+ - **Stacking** (SPEC §5.2). A placement's `z` is its box's `z-index`, and
104
+ the boxes stand in the layer in the order their surfaces were created,
105
+ so overlapping placements stack as the spec says, and the browser gives
106
+ the pointer to the topmost.
107
+ - **The keyboard** (SPEC §10.1). A click takes it only by focusing an
108
+ element that takes focus: an `input`, `select`, `textarea` or `button`, a
109
+ link with an `href` that is not a hyperlink, a details' first `summary`,
110
+ an editing host, or a `tabindex` of 0 or more (a `label` counts as its
111
+ control). A click on anything else, a hyperlink included, sends no
112
+ `focus`, and on a surface that had the keyboard it sends `blur`. The browser focuses the frame on any click; once the press
113
+ is done, the addon gives that focus back to the terminal, so text
114
+ selection works as usual. A right click leaves focus where the browser
115
+ put it, so the context menu's Copy copies the surface's selection.
116
+ - **A text field's keys** (SPEC §10.2, §10.4) are the program's keymap.
117
+ Each key goes through xterm.js's keyboard handling, with what it would
118
+ send the program kept back, and is named from that, so a field sees the
119
+ key the program would read, in the encoding the program enabled. The
120
+ field's keymap (the default, then each `data-keys` from the root to the
121
+ field) decides: an action, which the addon does itself on the field's
122
+ text with the editing commands, so the browser sends `input` and
123
+ `change` as for typing; a character, which the browser types; or the
124
+ program's, which gets what xterm.js encoded. Details:
125
+ - Enter that submits, in an `input`, is the browser's own implicit
126
+ submission; another key bound to `submit` does the same through the
127
+ form's default button, or the form.
128
+ - A textarea's rows are as it wraps them: a hidden copy of its text,
129
+ with the same width and font, measures where each position is.
130
+ - An email or number field, whose caret the browser keeps to itself, is
131
+ `text` while focused (inspection still reports its type, and
132
+ `inputmode` keeps the on-screen keyboard), and a number field types
133
+ only what a number holds. CSS that selects on `[type=email]` or
134
+ `[type=number]` does not match it while it is focused.
135
+ - An editing host's actions use the selection's moves, a character at a
136
+ time where the words and lines need to see the text.
137
+ - **Keys for the program** (SPEC §10.2). Any focused element has a keymap:
138
+ each `data-keys` from the root to it, over the default only in a text
139
+ field; with nothing focused there is none. A key it binds to `program`
140
+ (named as in a text field, a key with Shift looked up again without it)
141
+ goes to the program as xterm.js encoded it, and the browser does nothing
142
+ with it: a select's arrows pick nothing, and a document that scrolls does
143
+ not scroll. Outside a text field the keymap's other bindings do nothing
144
+ but the scroll actions (below), and a nearer one of a key still cancels a
145
+ farther `program`.
146
+ - **Scrolling keys** (SPEC §10.2). Outside a text field, a key the focused
147
+ element does not use and its keymap binds to a scroll action
148
+ (`scroll-down`, `scroll-page-up`, `scroll-half-page-down`, `scroll-end`,
149
+ …) scrolls, along an axis the document asked for: the nearest box from
150
+ the element outward that can still move that way, as a wheel goes, up to
151
+ the root and never the terminal; with nowhere to go, it does nothing and
152
+ the program hears nothing. The addon scrolls as Chromium's keys do: 40
153
+ CSS pixels for a line, 87.5% of the box for a page, half of it for a half
154
+ page, and to the end for `scroll-start` and `scroll-end`. Along an axis
155
+ the document did not ask for, the key goes on as if unbound. A text
156
+ field's keymap leaves scroll actions out, so a field inside a box that
157
+ scrolls with `j` still types `j`.
158
+ - **Selects** (SPEC §10.2) are the browser's own: its keys pick, each pick
159
+ is heard at once (`input` with `data-on~=input`, then `change`), and its
160
+ native list opens past the surface. While the list is open it has every
161
+ key: Chromium's keeps them from the document, and where one reaches it
162
+ (`:open`), the addon leaves it to the list.
163
+ - **Drags** (SPEC §9.1, a draft on hotty's `drag` branch). A mouse's or a
164
+ pen's primary press on an element with `drag` in its `data-on` and an id
165
+ reports `dragstart`, then `drag` each time the element under the pointer
166
+ changes (each cell while there is none), then `dragend`, with the
167
+ surface's cell and the keys held. The frame's root element captures the
168
+ pointer (`setPointerCapture`) until the release, so the moves keep coming
169
+ over the cells, other surfaces and outside the page, and none reach
170
+ xterm.js's mouse reporting; the root is never replaced, so deltas do not
171
+ lose the capture. The browser clicks the root on a captured release, so
172
+ the drag reports the click itself when it ends where it began. The host
173
+ stylesheet makes the elements that opt in unselectable, important in its
174
+ layer, whatever the document's CSS. A touch never drags.
175
+ - **Presses** (SPEC §5.2, §9: `p=1` on `a=place`). Every primary press of a
176
+ mouse or a pen in the window reports `press` on its pointerdown, before
177
+ the drag and the focus it causes; a tap reports it on the mousedown the
178
+ browser makes for it. A press on a hyperlink is the terminal's.
179
+ - **Fit** (SPEC §5.2, §9: `f=1` on `a=place`). The program hears `fit`
180
+ with the rows the document needs at the placement's width whenever they
181
+ differ from the rows it heard last, starting from the placement's own;
182
+ the placement keeps its size. The rows are measured as for `r=auto`: the
183
+ frame is laid out at that width and 1px high, then restored in the same
184
+ task. A check waits for the next frame, so a frame sends one `fit` at
185
+ most, with the rows it draws. Each of these asks for a check:
186
+ - a document or a delta;
187
+ - a resource arriving or changing;
188
+ - the host stylesheet changing (cell size, font);
189
+ - an image, a stylesheet or a font loading, whether a `cid:` resource or
190
+ one from the network;
191
+ - the root's or the body's box changing size (a `<details>` the user
192
+ opens, say).
193
+
194
+ A surface out of view checks once it is shown again. A check lays the
195
+ document out a second time, so only placements with `f=1` pay for it.
196
+ - **Presses with Alt** (SPEC §9.2) are the program's. The frame cancels the
197
+ press's mousedown (no focus, selection, drag or hyperlink), reports
198
+ nothing, and captures the pointer on its root until the release; the
199
+ addon replays the press on xterm.js's screen and the moves and release on
200
+ its document, where xterm.js listens, as it replays wheels: a mouse report
201
+ with Alt, or with reporting off, xterm.js's rectangular selection. A
202
+ surface that had the keyboard gives it back first (`blur`). The browser's
203
+ click on the release is not the surface's. Firefox drops the pressed
204
+ element's `:hover` only at the next move.
205
+ - **Detached surfaces** (SPEC §5.5: `a=detach`, or `d=1` on `a=doc`) send no
206
+ events and never take the keyboard. Their `input`, `select`, `textarea`
207
+ and `button` elements carry a `disabled` of the addon's own, which
208
+ inspection and morphs do not see, and which a delta cannot remove. Hover,
209
+ selection, `<details>` and hyperlinks work as before. Only a hyperlink
210
+ shows the hand, whatever the document's `cursor`; other links show the
211
+ text pointer.
212
+ - **The browser's keys stay the browser's** (the `browserKeys` option):
213
+ - reload (F5, and Ctrl or Cmd with R);
214
+ - zoom (Ctrl or Cmd with +, − or 0);
215
+ - full screen (F11);
216
+ - the developer tools (F12, and Ctrl+Shift with I, J or C);
217
+ - on a Mac, everything with Cmd.
218
+
219
+ Neither the terminal nor a surface holding the keyboard sends them to the
220
+ program, and the browser acts on them. A terminal cannot know which keys
221
+ a program binds, so these are the browser's own. The addon installs
222
+ xterm.js's custom key handler for this, so a page passes its own list
223
+ here, not to xterm.js. The default is exported, so a page can add keys
224
+ of its own:
225
+
226
+ ```ts
227
+ import { HottyAddon, browserKeys } from "@neuroplastio/xterm-addon-hotty";
228
+
229
+ new HottyAddon({ browserKeys: (e) => browserKeys(e) || (e.ctrlKey && e.key === "k") });
230
+ ```
231
+ - **Nothing in a surface scrolls unless its document asks** (SPEC §5.3, §9):
232
+ - It shows no scrollbars, and pans nothing on a touch
233
+ (`touch-action: none`). Any scroll offset the browser sets goes back to
234
+ zero, except a text field's own text.
235
+ - A wheel over a surface goes to the terminal, and so does a touch drag,
236
+ as wheel events at the finger. After the finger lifts, the drag keeps
237
+ going and slows down.
238
+ - Taps and long presses stay the surface's.
239
+ - On the cells, the addon handles touch too (the `touch` option, on by
240
+ default), in place of xterm.js's own. A drag scrolls as over a surface.
241
+ A tap is a click for the program: a press and a release at the finger.
242
+ xterm.js 6.1's own touch handling sends wheel reports with no position
243
+ (`NaN`, which a program reads as typing) and turns taps into nothing.
244
+ - Ctrl and the wheel stay the browser's zoom.
245
+ - xterm.js's scrollable reads the legacy `wheelDeltaY` where browsers have
246
+ it, so a forwarded wheel carries one, worked out from the drag: on a
247
+ constructed event the browser's own has the wrong sign in Chromium 15x.
248
+ - A drag is measured in the page, not in the surface, which moves as the
249
+ terminal scrolls.
250
+ - **A page that scrolls itself** (`scroll: "page"`): for a page that shows
251
+ a program's output whole, with the terminal as tall as what it shows,
252
+ such as a document printed by a program. Wheels and touch drags over
253
+ the surfaces and the cells are left to the browser, which scrolls the
254
+ page natively, with its own momentum, and the program hears no wheel.
255
+ xterm.js never sees them, since it would take a drag for its
256
+ scrollback, or turn it into arrow keys. Surfaces pan on a touch
257
+ (`touch-action: manipulation`), and over an element of the document
258
+ that the browser would scroll instead (`overflow: auto`), the addon
259
+ scrolls the page itself.
260
+
261
+ ```ts
262
+ new HottyAddon({ scroll: "page" });
263
+ ```
264
+ - **A document that scrolls** (`scroll=1`, `2` or `3` on `a=doc`, SPEC
265
+ §5.1, §5.3; the capabilities say `"scroll": true`):
266
+ - Along the axes it asked for, the browser scrolls it as it scrolls a
267
+ page: the root and every `overflow: auto` or `scroll` element, with
268
+ the browser's scrollbars, which take pixels inside the frame, never
269
+ cells. The root pans on a touch along those axes (`touch-action:
270
+ pan-y`, `pan-x`).
271
+ - Along an axis it did not ask for, the root and every element whose
272
+ `overflow` there is `auto` or `scroll` are `overflow: hidden`, important
273
+ in the host's layer: no scrollbar, and nothing the user does moves it.
274
+ CSS cannot select by a computed value, so the addon reads it after each
275
+ document, delta and host stylesheet, and marks those elements with an
276
+ attribute of its own, which inspection and morphs do not see.
277
+ - **Gestures.** A wheel gesture's first event decides where it goes, as
278
+ a browser latches scrolling to what it began on (a gesture is wheels
279
+ less than 150ms apart): to the document while the innermost box under
280
+ the pointer that can move that way can (the browser scrolls it), else
281
+ on to the terminal, as over the cells, unless `overscroll-behavior`
282
+ stops it. A gesture begun over the cells stays the terminal's when a
283
+ surface comes under the pointer. A touch drag decides the same way once
284
+ it has a direction: the browser pans the document, or the drag goes to
285
+ the terminal as before.
286
+ - **Keys.** While the surface has the keyboard, the keys a browser
287
+ scrolls with that the focused element neither uses nor gives the
288
+ program (SPEC §10.2) scroll the innermost box, from the focused
289
+ element outward, that can move that way: the arrows by 40 pixels, Page
290
+ Up and Page Down, Space and Shift+Space by seven eighths of the box,
291
+ Home and End to the ends. The addon scrolls the box itself, since the
292
+ browser's own action may be the element's (a radio button's arrows).
293
+ Where nothing can move that way, the key goes on to the program, as any
294
+ key the surface does not use.
295
+ - Focus scrolls an element into view, as the browser does. The program
296
+ hears nothing of scrolling. A delta keeps the offsets, and so do hiding
297
+ and placing again; a new document starts at the top left. `r=auto` and
298
+ `fit` measure the document with its root clipped, so the root's
299
+ scrollbar does not make its lines wrap.
300
+ - Where the page scrolls (`scroll: "page"`), the browser chains a
301
+ gesture from the document to the page natively.
302
+ - **Where an element is** (SPEC §9: `area` on `click` and `press`): the
303
+ cells of its bounding box in the frame, as the user sees it, scrolled
304
+ included, divided by the cell size. An edge within half a device pixel of
305
+ a cell's counts as on it, since layout rounds positions to fractions of a
306
+ pixel. A press with no id carries none.
307
+ - **Cursor.** After `a=place` the cursor moves below the surface, as in the
308
+ native host. xterm.js has no public API for that, so the addon uses the same
309
+ private calls as the official image addon.
310
+ - **What xterm.js sends for a key** a text field names (SPEC §10.4) is read
311
+ from its core service's `triggerDataEvent`, also private, which the addon
312
+ holds back while the key goes through xterm.js's keyboard handling.
313
+ - **Cell size** comes from xterm's render service (also private), with a
314
+ measured fallback.
315
+ - **xterm.js 6.1** is needed for the kitty keyboard protocol
316
+ (`vtExtensions.kittyKeyboard`); the addon itself works with 6.0.
317
+
318
+ ## Security
319
+
320
+ Program markup is untrusted: it may come from `cat`, or from a server on the
321
+ far side of SSH. Two independent layers stand between it and the page, and a
322
+ test checks each one (`tests/e2e/hostile.spec.ts`):
323
+
324
+ 1. **The sanitizer** (`resolver.ts`) works on inert parses. It removes
325
+ `script`, `iframe`, `object`, `embed`, `base`, `meta` and any `link` that
326
+ is not a stylesheet. It also removes `on*`, `srcdoc`, `autofocus` and
327
+ `ping`. URL attributes resolve to `blob:` for `cid:`, stay as they are for
328
+ `data:`, become absolute URLs where the network policy allows them (the
329
+ page's grant and the document's request, both), and fail closed as
330
+ `about:invalid` for everything else. A document's `<base>` and
331
+ `<meta name="hotty-network">` are read before they are removed.
332
+ 2. **The iframe:**
333
+ - `sandbox="allow-same-origin allow-forms"`: no scripts, popups or
334
+ navigation. `allow-forms` is there only so that `submit` fires; the
335
+ submission is cancelled.
336
+ - A CSP `<meta>` written by the parser before any program markup:
337
+ `default-src 'none'`, with `data:` and `blob:` allowed for images,
338
+ media and fonts, and the page's `network` grant; inline styles allowed,
339
+ and `form-action 'none'`. It holds the page's grant on its own: a
340
+ reference the sanitizer misses reaches nothing the page did not grant.
341
+ Network URLs inside `cid:` stylesheets are not fetched (a stylesheet
342
+ resource serves surfaces with different policies); a `<link>` to the
343
+ stylesheet on the network, or a `<style>`, is.
344
+
345
+ With the CSP removed, 9 requests get through, all from CSS (`@import`, `url()`
346
+ and `@font-face`). With the sanitizer passing URLs through, the CSP alone
347
+ stops everything. `position: fixed` stays inside the surface's own viewport.
348
+
349
+ **The embedding page's CSP applies inside the surfaces too** (about:blank
350
+ iframes inherit it, and it can only be narrowed). A page that embeds xterm.js
351
+ already has to allow inline styles, because xterm's DOM renderer sets them.
352
+ For HOTTY it must also allow `data:` and `blob:` images, fonts and media, and
353
+ whatever origins it grants in `network`. This is the recommended policy (with
354
+ no network grant), and `tests/e2e/embedder.spec.ts` checks it:
355
+
356
+ ```
357
+ default-src 'self'; style-src 'self' 'unsafe-inline';
358
+ img-src 'self' data: blob:; font-src 'self' data: blob:; media-src 'self' data: blob:
359
+ ```
360
+
361
+ ## Differences from hotty-blitz
362
+
363
+ - **Pixels differ.** The browser renders, so text, form controls and
364
+ antialiasing look like the browser's. The cell footprint is the same (§9).
365
+ - **More events.** The browser gives `resize` (after zoom or a font change) and
366
+ native `<select>`, IME and spellcheck.
367
+ - **Not here yet:**
368
+ - §9's text in xterm's buffer (search, serialize, and a selection across a
369
+ surface);
370
+ - following content that scrolls on the alternate screen;
371
+ - the embedder's web fonts inside surfaces (system fonts work);
372
+ - `hover` (§9.4, `v=1` on `a=place`): `EVENTS` does not list it, so a
373
+ program never asks, and the vectors that require it are skipped. The
374
+ frame would report the nearest id on `pointerover` with no button
375
+ down, and out on leaving the frame, an Alt press, or a release
376
+ outside it.
377
+
378
+ ## Costs
379
+
380
+ Measured 2026-09-29 in headless Chromium 153 (`bench/`):
381
+
382
+ - **A surface** takes 4.7 ms to create and place, 8.7 ms of main-thread time
383
+ with its first frame, and 1.2 MB of memory.
384
+ - **The dashboard** costs 4.9 ms of main-thread time per frame at 10 Hz.
385
+ - **Large documents** cost Chrome more per frame as they grow: its PrePaint
386
+ walks the tree. A one-cell delta costs 8.4 ms of main-thread time per frame
387
+ at 4,096 cells and 46 ms at 262,144. CSS containment brings the latter to
388
+ 30 ms; hotty-blitz does it in about 0.1 ms.
389
+ - **Bub-n-Bros** (`examples/bubbros.py`) costs about 4 ms per frame: deltas to
390
+ seven sprites out of about 380.
391
+
392
+ ## Tests
393
+
394
+ `npm run check` runs:
395
+
396
+ - the typecheck, the build and the declarations (what `npm pack` ships);
397
+ - the unit tests (`node --test`: the wire, keys, the keys any focused
398
+ element gives the program, decoding the reference Python client, and the
399
+ conformance vectors' wire, keys, keymap and edit sections);
400
+ - the Playwright tests, against Chromium (and Firefox once installed:
401
+ `npx playwright install firefox`):
402
+ - the protocol and the conformance vectors;
403
+ - interaction, with `form.py` through the bridge;
404
+ - text fields' keymaps beyond the vectors (`fields.spec.ts`);
405
+ - the hostile page and the embedder's CSP;
406
+ - Bub-n-Bros with kitty keys (skipped unless the game is fetched).
407
+
408
+ `bench/measure.mjs` and `bench/trace.mjs` produce the cost numbers.
409
+
410
+ ## Licence
411
+
412
+ Apache-2.0 ([LICENSE](LICENSE)). xterm.js itself is MIT.