@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 +73 -0
- package/README.md +358 -0
- package/dist/hotty-xterm.js +2906 -0
- package/dist/hotty-xterm.js.map +7 -0
- package/dist/types/addon.d.ts +170 -0
- package/dist/types/delta.d.ts +19 -0
- package/dist/types/hostcss.d.ts +48 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/network.d.ts +18 -0
- package/dist/types/resolver.d.ts +41 -0
- package/dist/types/resources.d.ts +32 -0
- package/dist/types/surface.d.ts +405 -0
- package/dist/types/touch.d.ts +37 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types/wire.d.ts +59 -0
- package/package.json +54 -0
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.
|