wraithterm 0.0.0-stage → 0.2.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.
Files changed (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +313 -2
  3. package/css/xterm.css +285 -0
  4. package/dist/index.d.ts +88 -0
  5. package/dist/index.js +707 -0
  6. package/dist/keycodes.d.ts +1 -0
  7. package/dist/keycodes.js +179 -0
  8. package/dist/protocol.d.ts +117 -0
  9. package/dist/protocol.js +1 -0
  10. package/dist/types.d.ts +66 -0
  11. package/dist/types.js +19 -0
  12. package/dist/vite.d.ts +11 -0
  13. package/dist/vite.js +11 -0
  14. package/dist/worker.d.ts +1 -0
  15. package/dist/worker.js +302 -0
  16. package/dist/xterm-api.d.ts +1956 -0
  17. package/dist/xterm-atlas.d.ts +39 -0
  18. package/dist/xterm-atlas.js +176 -0
  19. package/dist/xterm-internals.d.ts +120 -0
  20. package/dist/xterm-internals.js +19 -0
  21. package/dist/xterm-renderer.d.ts +77 -0
  22. package/dist/xterm-renderer.js +540 -0
  23. package/dist/xterm.d.ts +17 -0
  24. package/dist/xterm.js +35 -0
  25. package/docs/ARCHITECTURE.md +179 -0
  26. package/docs/COMPATIBILITY.md +66 -0
  27. package/docs/PERFORMANCE.md +122 -0
  28. package/docs/TOP_TIER.md +124 -0
  29. package/docs/XTERM_API.md +163 -0
  30. package/docs/compatibility/xterm-chrome.json +34 -0
  31. package/licenses/Ghostty-MIT.txt +21 -0
  32. package/licenses/JetBrainsMono-OFL.txt +93 -0
  33. package/licenses/NOTICE.md +93 -0
  34. package/licenses/Zig-MIT.txt +21 -0
  35. package/licenses/rust/arrayvec-0.7.8/LICENSE-APACHE +201 -0
  36. package/licenses/rust/arrayvec-0.7.8/LICENSE-MIT +25 -0
  37. package/licenses/rust/autocfg-1.5.1/LICENSE-APACHE +201 -0
  38. package/licenses/rust/autocfg-1.5.1/LICENSE-MIT +25 -0
  39. package/licenses/rust/bit-set-0.10.0/LICENSE-APACHE +201 -0
  40. package/licenses/rust/bit-set-0.10.0/LICENSE-MIT +25 -0
  41. package/licenses/rust/bit-vec-0.9.1/LICENSE-APACHE +201 -0
  42. package/licenses/rust/bit-vec-0.9.1/LICENSE-MIT +25 -0
  43. package/licenses/rust/bitflags-2.13.2/LICENSE-APACHE +201 -0
  44. package/licenses/rust/bitflags-2.13.2/LICENSE-MIT +25 -0
  45. package/licenses/rust/bumpalo-3.20.3/LICENSE-APACHE +201 -0
  46. package/licenses/rust/bumpalo-3.20.3/LICENSE-MIT +25 -0
  47. package/licenses/rust/bytemuck-1.25.2/LICENSE-APACHE +61 -0
  48. package/licenses/rust/bytemuck-1.25.2/LICENSE-MIT +9 -0
  49. package/licenses/rust/bytemuck-1.25.2/LICENSE-ZLIB +11 -0
  50. package/licenses/rust/bytemuck_derive-1.12.1/LICENSE-APACHE +61 -0
  51. package/licenses/rust/bytemuck_derive-1.12.1/LICENSE-MIT +9 -0
  52. package/licenses/rust/bytemuck_derive-1.12.1/LICENSE-ZLIB +11 -0
  53. package/licenses/rust/cfg-if-1.0.5/LICENSE-APACHE +201 -0
  54. package/licenses/rust/cfg-if-1.0.5/LICENSE-MIT +25 -0
  55. package/licenses/rust/cfg_aliases-0.2.2/LICENSE +9 -0
  56. package/licenses/rust/codespan-reporting-0.13.1/LICENSE +201 -0
  57. package/licenses/rust/crunchy-0.2.4/LICENSE +21 -0
  58. package/licenses/rust/document-features-0.2.12/LICENSE-APACHE +73 -0
  59. package/licenses/rust/document-features-0.2.12/LICENSE-MIT +19 -0
  60. package/licenses/rust/equivalent-1.0.2/LICENSE-APACHE +201 -0
  61. package/licenses/rust/equivalent-1.0.2/LICENSE-MIT +25 -0
  62. package/licenses/rust/foldhash-0.2.0/LICENSE +19 -0
  63. package/licenses/rust/font-types-0.12.6/LICENSE-APACHE +67 -0
  64. package/licenses/rust/font-types-0.12.6/LICENSE-MIT +25 -0
  65. package/licenses/rust/futures-core-0.3.34/LICENSE-APACHE +202 -0
  66. package/licenses/rust/futures-core-0.3.34/LICENSE-MIT +26 -0
  67. package/licenses/rust/futures-task-0.3.34/LICENSE-APACHE +202 -0
  68. package/licenses/rust/futures-task-0.3.34/LICENSE-MIT +26 -0
  69. package/licenses/rust/futures-util-0.3.34/LICENSE-APACHE +202 -0
  70. package/licenses/rust/futures-util-0.3.34/LICENSE-MIT +26 -0
  71. package/licenses/rust/half-2.7.1/LICENSE-APACHE +176 -0
  72. package/licenses/rust/half-2.7.1/LICENSE-MIT +19 -0
  73. package/licenses/rust/harfrust-0.13.3/LICENSE +22 -0
  74. package/licenses/rust/hashbrown-0.17.1/LICENSE-APACHE +201 -0
  75. package/licenses/rust/hashbrown-0.17.1/LICENSE-MIT +25 -0
  76. package/licenses/rust/indexmap-2.14.2/LICENSE-APACHE +201 -0
  77. package/licenses/rust/indexmap-2.14.2/LICENSE-MIT +25 -0
  78. package/licenses/rust/js-sys-0.3.106/LICENSE-APACHE +201 -0
  79. package/licenses/rust/js-sys-0.3.106/LICENSE-MIT +25 -0
  80. package/licenses/rust/libc-0.2.190/LICENSE-APACHE +176 -0
  81. package/licenses/rust/libc-0.2.190/LICENSE-MIT +25 -0
  82. package/licenses/rust/libloading-0.8.9/LICENSE +12 -0
  83. package/licenses/rust/libm-0.2.16/LICENSE.txt +258 -0
  84. package/licenses/rust/litrs-1.0.0/LICENSE-APACHE +176 -0
  85. package/licenses/rust/litrs-1.0.0/LICENSE-MIT +25 -0
  86. package/licenses/rust/lock_api-0.4.14/LICENSE-APACHE +201 -0
  87. package/licenses/rust/lock_api-0.4.14/LICENSE-MIT +25 -0
  88. package/licenses/rust/log-0.4.34/LICENSE-APACHE +201 -0
  89. package/licenses/rust/log-0.4.34/LICENSE-MIT +25 -0
  90. package/licenses/rust/naga-30.0.1/LICENSE.APACHE +176 -0
  91. package/licenses/rust/naga-30.0.1/LICENSE.MIT +21 -0
  92. package/licenses/rust/naga-types-30.0.1/LICENSE.APACHE +176 -0
  93. package/licenses/rust/naga-types-30.0.1/LICENSE.MIT +21 -0
  94. package/licenses/rust/num-traits-0.2.19/LICENSE-APACHE +201 -0
  95. package/licenses/rust/num-traits-0.2.19/LICENSE-MIT +25 -0
  96. package/licenses/rust/once_cell-1.21.4/LICENSE-APACHE +201 -0
  97. package/licenses/rust/once_cell-1.21.4/LICENSE-MIT +23 -0
  98. package/licenses/rust/parking_lot-0.12.5/LICENSE-APACHE +201 -0
  99. package/licenses/rust/parking_lot-0.12.5/LICENSE-MIT +25 -0
  100. package/licenses/rust/parking_lot_core-0.9.12/LICENSE-APACHE +201 -0
  101. package/licenses/rust/parking_lot_core-0.9.12/LICENSE-MIT +25 -0
  102. package/licenses/rust/pin-project-lite-0.2.17/LICENSE-APACHE +177 -0
  103. package/licenses/rust/pin-project-lite-0.2.17/LICENSE-MIT +23 -0
  104. package/licenses/rust/portable-atomic-1.15.0/LICENSE-APACHE +177 -0
  105. package/licenses/rust/portable-atomic-1.15.0/LICENSE-MIT +23 -0
  106. package/licenses/rust/portable-atomic-util-0.2.8/LICENSE-APACHE +177 -0
  107. package/licenses/rust/portable-atomic-util-0.2.8/LICENSE-MIT +23 -0
  108. package/licenses/rust/proc-macro2-1.0.107/LICENSE-APACHE +176 -0
  109. package/licenses/rust/proc-macro2-1.0.107/LICENSE-MIT +23 -0
  110. package/licenses/rust/quote-1.0.47/LICENSE-APACHE +176 -0
  111. package/licenses/rust/quote-1.0.47/LICENSE-MIT +23 -0
  112. package/licenses/rust/raw-window-handle-0.6.2/LICENSE-APACHE.md +177 -0
  113. package/licenses/rust/raw-window-handle-0.6.2/LICENSE-MIT.md +21 -0
  114. package/licenses/rust/raw-window-handle-0.6.2/LICENSE-ZLIB.md +11 -0
  115. package/licenses/rust/read-fonts-0.41.0/LICENSE-APACHE +67 -0
  116. package/licenses/rust/read-fonts-0.41.0/LICENSE-MIT +25 -0
  117. package/licenses/rust/read-fonts-0.43.3/LICENSE-APACHE +67 -0
  118. package/licenses/rust/read-fonts-0.43.3/LICENSE-MIT +25 -0
  119. package/licenses/rust/redox_syscall-0.5.18/LICENSE +22 -0
  120. package/licenses/rust/renderdoc-sys-1.1.0/LICENSE-APACHE +201 -0
  121. package/licenses/rust/renderdoc-sys-1.1.0/LICENSE-MIT +25 -0
  122. package/licenses/rust/rustc-hash-1.1.0/LICENSE-APACHE +201 -0
  123. package/licenses/rust/rustc-hash-1.1.0/LICENSE-MIT +23 -0
  124. package/licenses/rust/rustversion-1.0.23/LICENSE-APACHE +176 -0
  125. package/licenses/rust/rustversion-1.0.23/LICENSE-MIT +23 -0
  126. package/licenses/rust/scopeguard-1.2.0/LICENSE-APACHE +201 -0
  127. package/licenses/rust/scopeguard-1.2.0/LICENSE-MIT +25 -0
  128. package/licenses/rust/serde-1.0.229/LICENSE-APACHE +176 -0
  129. package/licenses/rust/serde-1.0.229/LICENSE-MIT +23 -0
  130. package/licenses/rust/serde_core-1.0.229/LICENSE-APACHE +176 -0
  131. package/licenses/rust/serde_core-1.0.229/LICENSE-MIT +23 -0
  132. package/licenses/rust/serde_derive-1.0.229/LICENSE-APACHE +176 -0
  133. package/licenses/rust/serde_derive-1.0.229/LICENSE-MIT +23 -0
  134. package/licenses/rust/skrifa-0.44.0/LICENSE-APACHE +67 -0
  135. package/licenses/rust/skrifa-0.44.0/LICENSE-MIT +25 -0
  136. package/licenses/rust/slab-0.4.12/LICENSE +25 -0
  137. package/licenses/rust/smallvec-1.16.2/LICENSE-APACHE +201 -0
  138. package/licenses/rust/smallvec-1.16.2/LICENSE-MIT +25 -0
  139. package/licenses/rust/static_assertions-1.1.0/LICENSE-APACHE +202 -0
  140. package/licenses/rust/static_assertions-1.1.0/LICENSE-MIT +21 -0
  141. package/licenses/rust/swash-0.2.10/LICENSE-APACHE +201 -0
  142. package/licenses/rust/swash-0.2.10/LICENSE-MIT +25 -0
  143. package/licenses/rust/syn-2.0.119/LICENSE-APACHE +176 -0
  144. package/licenses/rust/syn-2.0.119/LICENSE-MIT +23 -0
  145. package/licenses/rust/syn-3.0.6/LICENSE-APACHE +176 -0
  146. package/licenses/rust/syn-3.0.6/LICENSE-MIT +23 -0
  147. package/licenses/rust/termcolor-1.4.1/COPYING +3 -0
  148. package/licenses/rust/termcolor-1.4.1/LICENSE-MIT +21 -0
  149. package/licenses/rust/termcolor-1.4.1/UNLICENSE +24 -0
  150. package/licenses/rust/thiserror-2.0.21/LICENSE-APACHE +176 -0
  151. package/licenses/rust/thiserror-2.0.21/LICENSE-MIT +23 -0
  152. package/licenses/rust/thiserror-impl-2.0.21/LICENSE-APACHE +176 -0
  153. package/licenses/rust/thiserror-impl-2.0.21/LICENSE-MIT +23 -0
  154. package/licenses/rust/tokio-1.53.2/LICENSE +21 -0
  155. package/licenses/rust/unicode-ident-1.0.26/LICENSE-APACHE +176 -0
  156. package/licenses/rust/unicode-ident-1.0.26/LICENSE-MIT +23 -0
  157. package/licenses/rust/unicode-ident-1.0.26/LICENSE-UNICODE +39 -0
  158. package/licenses/rust/unicode-width-0.2.2/LICENSE-APACHE +201 -0
  159. package/licenses/rust/unicode-width-0.2.2/LICENSE-MIT +25 -0
  160. package/licenses/rust/wasm-bindgen-0.2.129/LICENSE-APACHE +201 -0
  161. package/licenses/rust/wasm-bindgen-0.2.129/LICENSE-MIT +25 -0
  162. package/licenses/rust/wasm-bindgen-futures-0.4.79/LICENSE-APACHE +201 -0
  163. package/licenses/rust/wasm-bindgen-futures-0.4.79/LICENSE-MIT +25 -0
  164. package/licenses/rust/wasm-bindgen-macro-0.2.129/LICENSE-APACHE +201 -0
  165. package/licenses/rust/wasm-bindgen-macro-0.2.129/LICENSE-MIT +25 -0
  166. package/licenses/rust/wasm-bindgen-macro-support-0.2.129/LICENSE-APACHE +201 -0
  167. package/licenses/rust/wasm-bindgen-macro-support-0.2.129/LICENSE-MIT +25 -0
  168. package/licenses/rust/wasm-bindgen-shared-0.2.129/LICENSE-APACHE +201 -0
  169. package/licenses/rust/wasm-bindgen-shared-0.2.129/LICENSE-MIT +25 -0
  170. package/licenses/rust/web-sys-0.3.106/LICENSE-APACHE +201 -0
  171. package/licenses/rust/web-sys-0.3.106/LICENSE-MIT +25 -0
  172. package/licenses/rust/wgpu-30.0.1/LICENSE.APACHE +176 -0
  173. package/licenses/rust/wgpu-30.0.1/LICENSE.MIT +21 -0
  174. package/licenses/rust/wgpu-core-30.0.1/LICENSE.APACHE +176 -0
  175. package/licenses/rust/wgpu-core-30.0.1/LICENSE.MIT +21 -0
  176. package/licenses/rust/wgpu-core-deps-windows-linux-android-30.0.1/LICENSE.APACHE +176 -0
  177. package/licenses/rust/wgpu-core-deps-windows-linux-android-30.0.1/LICENSE.MIT +21 -0
  178. package/licenses/rust/wgpu-hal-30.0.1/LICENSE.APACHE +176 -0
  179. package/licenses/rust/wgpu-hal-30.0.1/LICENSE.MIT +21 -0
  180. package/licenses/rust/wgpu-naga-bridge-30.0.1/LICENSE.APACHE +176 -0
  181. package/licenses/rust/wgpu-naga-bridge-30.0.1/LICENSE.MIT +21 -0
  182. package/licenses/rust/wgpu-types-30.0.1/LICENSE.APACHE +176 -0
  183. package/licenses/rust/wgpu-types-30.0.1/LICENSE.MIT +21 -0
  184. package/licenses/rust/winapi-util-0.1.11/COPYING +3 -0
  185. package/licenses/rust/winapi-util-0.1.11/LICENSE-MIT +21 -0
  186. package/licenses/rust/winapi-util-0.1.11/UNLICENSE +24 -0
  187. package/licenses/rust/windows-link-0.2.1/license-apache-2.0 +201 -0
  188. package/licenses/rust/windows-link-0.2.1/license-mit +21 -0
  189. package/licenses/rust/windows-sys-0.61.2/license-apache-2.0 +201 -0
  190. package/licenses/rust/windows-sys-0.61.2/license-mit +21 -0
  191. package/licenses/rust/yazi-0.2.1/LICENSE-APACHE +201 -0
  192. package/licenses/rust/yazi-0.2.1/LICENSE-MIT +19 -0
  193. package/licenses/rust/zeno-0.3.3/LICENSE-APACHE +201 -0
  194. package/licenses/rust/zeno-0.3.3/LICENSE-MIT +25 -0
  195. package/licenses/rust/zerocopy-0.8.62/LICENSE-APACHE +202 -0
  196. package/licenses/rust/zerocopy-0.8.62/LICENSE-BSD +24 -0
  197. package/licenses/rust/zerocopy-0.8.62/LICENSE-MIT +26 -0
  198. package/licenses/rust/zerocopy-derive-0.8.62/LICENSE-APACHE +202 -0
  199. package/licenses/rust/zerocopy-derive-0.8.62/LICENSE-BSD +24 -0
  200. package/licenses/rust/zerocopy-derive-0.8.62/LICENSE-MIT +26 -0
  201. package/licenses/xterm-MIT.txt +21 -0
  202. package/package.json +69 -4
  203. package/wasm/Ghostty-MIT.txt +21 -0
  204. package/wasm/JetBrainsMono-Regular.ttf +0 -0
  205. package/wasm/OFL.txt +93 -0
  206. package/wasm/optimization.json +6 -0
  207. package/wasm/wraith_engine.d.ts +176 -0
  208. package/wasm/wraith_engine.js +2057 -0
  209. package/wasm/wraith_engine_bg.wasm +0 -0
  210. package/wasm/wraith_engine_bg.wasm.d.ts +71 -0
package/dist/index.js ADDED
@@ -0,0 +1,707 @@
1
+ import { KEY_CODES } from './keycodes.js';
2
+ import { BackpressureError, packColor, percentile, } from './types.js';
3
+ export * from './types.js';
4
+ class Signal {
5
+ listeners = new Set();
6
+ on(listener) {
7
+ this.listeners.add(listener);
8
+ return { dispose: () => this.listeners.delete(listener) };
9
+ }
10
+ emit(value) {
11
+ for (const listener of this.listeners) {
12
+ try {
13
+ listener(value);
14
+ }
15
+ catch (error) {
16
+ console.error('Wraith event listener', error);
17
+ }
18
+ }
19
+ }
20
+ clear() {
21
+ this.listeners.clear();
22
+ }
23
+ }
24
+ async function loadFont(value) {
25
+ if (value instanceof Uint8Array)
26
+ return value.slice();
27
+ const response = await fetch(value);
28
+ if (!response.ok)
29
+ throw new Error(`Font request failed: ${response.status}`);
30
+ return new Uint8Array(await response.arrayBuffer());
31
+ }
32
+ function mods(event) {
33
+ return (Number(event.shiftKey) |
34
+ (Number(event.ctrlKey) << 1) |
35
+ (Number(event.altKey) << 2) |
36
+ (Number(event.metaKey) << 3));
37
+ }
38
+ function validateGrid(cols, rows) {
39
+ if (!Number.isInteger(cols) ||
40
+ !Number.isInteger(rows) ||
41
+ cols < 1 ||
42
+ rows < 1 ||
43
+ cols > 512 ||
44
+ rows > 512 ||
45
+ cols * rows > 65536)
46
+ throw new RangeError('Grid must be 1..512 per dimension and at most 65,536 cells');
47
+ }
48
+ /** One independent worker, Wasm instance, and GPU device per terminal. */
49
+ export class Wraith {
50
+ element;
51
+ canvas;
52
+ input;
53
+ ready;
54
+ cols;
55
+ rows;
56
+ worker;
57
+ options;
58
+ dpr;
59
+ cellWidth = 9;
60
+ cellHeight = 18;
61
+ pending = new Map();
62
+ sequence = 1;
63
+ pendingBytes = 0;
64
+ /** Writes queued this task, encoded or copied once into one buffer that
65
+ * crosses to the worker as a single transfer with one acknowledgement. */
66
+ batch = new Uint8Array(0);
67
+ batchLength = 0;
68
+ batchIds = [];
69
+ flushScheduled = false;
70
+ observer;
71
+ abort = new AbortController();
72
+ disposed = false;
73
+ failed = false;
74
+ data = new Signal();
75
+ binary = new Signal();
76
+ title = new Signal();
77
+ bell = new Signal();
78
+ errors = new Signal();
79
+ statistics = new Signal();
80
+ resizes = new Signal();
81
+ decoder = new TextDecoder();
82
+ encoder = new TextEncoder();
83
+ readyResolve;
84
+ readyReject;
85
+ mouseTracking = false;
86
+ dragging = false;
87
+ selected = false;
88
+ anchor = [0, 0];
89
+ composing = false;
90
+ mirror;
91
+ mirrorTimer;
92
+ preedit;
93
+ theme;
94
+ constructor(host, options) {
95
+ this.options = options;
96
+ this.dpr = Math.min(3, window.devicePixelRatio || 1);
97
+ this.cols = options.cols ?? 80;
98
+ this.rows = options.rows ?? 24;
99
+ validateGrid(this.cols, this.rows);
100
+ this.theme = {
101
+ foreground: '#dee5e8',
102
+ background: '#0e1217',
103
+ cursor: '#91d6f0',
104
+ ...options.theme,
105
+ };
106
+ for (const color of Object.values(this.theme))
107
+ packColor(color);
108
+ this.ready = new Promise((resolve, reject) => {
109
+ this.readyResolve = resolve;
110
+ this.readyReject = reject;
111
+ });
112
+ // Prevent an unhandled rejection if font loading fails before mount awaits ready.
113
+ void this.ready.catch(() => { });
114
+ this.element = document.createElement('div');
115
+ this.element.className = 'wraith-terminal';
116
+ Object.assign(this.element.style, {
117
+ position: 'relative',
118
+ width: '100%',
119
+ height: '100%',
120
+ overflow: 'hidden',
121
+ background: this.theme.background,
122
+ });
123
+ this.element.setAttribute('role', 'group');
124
+ this.element.setAttribute('aria-label', 'Wraith terminal');
125
+ this.canvas = document.createElement('canvas');
126
+ this.canvas.setAttribute('aria-hidden', 'true');
127
+ Object.assign(this.canvas.style, { display: 'block', touchAction: 'none', userSelect: 'none' });
128
+ this.input = document.createElement('textarea');
129
+ this.input.setAttribute('aria-label', 'Terminal input');
130
+ this.input.setAttribute('autocapitalize', 'off');
131
+ this.input.setAttribute('autocomplete', 'off');
132
+ this.input.spellcheck = false;
133
+ Object.assign(this.input.style, {
134
+ position: 'absolute',
135
+ left: '0',
136
+ bottom: '0',
137
+ width: '1px',
138
+ height: '1em',
139
+ opacity: '0.01',
140
+ padding: '0',
141
+ border: '0',
142
+ resize: 'none',
143
+ });
144
+ this.preedit = document.createElement('span');
145
+ Object.assign(this.preedit.style, {
146
+ position: 'absolute',
147
+ left: '0',
148
+ bottom: '0',
149
+ background: this.theme.background,
150
+ color: this.theme.foreground,
151
+ pointerEvents: 'none',
152
+ fontFamily: 'monospace',
153
+ });
154
+ this.element.append(this.canvas, this.input, this.preedit);
155
+ host.append(this.element);
156
+ this.worker = new Worker(new URL('./worker.js', import.meta.url), {
157
+ type: 'module',
158
+ name: 'Wraith terminal',
159
+ });
160
+ this.worker.onmessage = (event) => this.receive(event.data);
161
+ this.worker.onerror = (event) => this.fail(new Error(event.message || 'Wraith worker failed'));
162
+ this.worker.onmessageerror = () => this.fail(new Error('Wraith worker message could not be decoded'));
163
+ this.bindInput();
164
+ }
165
+ static async mount(host, options = {}) {
166
+ if (!navigator.gpu)
167
+ throw new Error('Wraith requires WebGPU in a secure context (HTTPS or localhost).');
168
+ if (!HTMLCanvasElement.prototype.transferControlToOffscreen)
169
+ throw new Error('Wraith requires OffscreenCanvas support.');
170
+ const terminal = new Wraith(host, options);
171
+ try {
172
+ const [font, ...fonts] = await Promise.all([
173
+ options.font ?? new URL('../wasm/JetBrainsMono-Regular.ttf', import.meta.url),
174
+ ...(options.fallbackFonts ?? []),
175
+ ].map(loadFont));
176
+ const canvas = terminal.canvas.transferControlToOffscreen();
177
+ terminal.send({
178
+ type: 'init',
179
+ canvas,
180
+ font: font,
181
+ fonts,
182
+ cols: terminal.cols,
183
+ rows: terminal.rows,
184
+ size: (options.fontSize ?? 14) * terminal.dpr,
185
+ lineHeight: options.lineHeight ?? 1.35,
186
+ history: options.scrollback ?? 10000,
187
+ theme: Object.values(terminal.theme).map(packColor),
188
+ }, [canvas, ...[font, ...fonts].map((f) => f.buffer)]);
189
+ await terminal.ready;
190
+ terminal.dimensions();
191
+ if (options.fit !== false) {
192
+ terminal.observer = new ResizeObserver(() => terminal.fit());
193
+ terminal.observer.observe(host);
194
+ terminal.fit();
195
+ }
196
+ if (options.screenReaderMode)
197
+ terminal.enableMirror();
198
+ return terminal;
199
+ }
200
+ catch (error) {
201
+ terminal.fail(error instanceof Error ? error : new Error(String(error)));
202
+ await terminal.dispose();
203
+ throw error;
204
+ }
205
+ }
206
+ send(command, transfer = []) {
207
+ this.worker.postMessage(command, transfer);
208
+ }
209
+ assertLive() {
210
+ if (this.disposed || this.failed)
211
+ throw new Error('Wraith terminal has been disposed or failed');
212
+ }
213
+ request(command, bytes = 0) {
214
+ this.assertLive();
215
+ const id = this.sequence++;
216
+ return new Promise((resolve, reject) => {
217
+ this.pending.set(id, { resolve: (value) => resolve(value), reject, bytes });
218
+ this.send({ ...command, id });
219
+ });
220
+ }
221
+ receive(reply) {
222
+ switch (reply.type) {
223
+ case 'ready':
224
+ this.cellWidth = reply.width;
225
+ this.cellHeight = reply.height;
226
+ this.readyResolve();
227
+ break;
228
+ case 'written':
229
+ for (const id of reply.ids) {
230
+ const pending = this.pending.get(id);
231
+ if (!pending)
232
+ continue;
233
+ this.pending.delete(id);
234
+ this.pendingBytes -= pending.bytes;
235
+ if (reply.error)
236
+ pending.reject(new Error(reply.error));
237
+ else
238
+ pending.resolve(undefined);
239
+ }
240
+ break;
241
+ case 'result': {
242
+ const pending = this.pending.get(reply.id);
243
+ if (pending) {
244
+ this.pending.delete(reply.id);
245
+ this.pendingBytes -= pending.bytes;
246
+ if (reply.error)
247
+ pending.reject(new Error(reply.error));
248
+ else
249
+ pending.resolve(reply.value);
250
+ }
251
+ break;
252
+ }
253
+ case 'data':
254
+ this.binary.emit(reply.bytes);
255
+ this.data.emit(this.decoder.decode(reply.bytes, { stream: true }));
256
+ break;
257
+ case 'title':
258
+ this.title.emit(reply.title);
259
+ break;
260
+ case 'bell':
261
+ this.bell.emit();
262
+ break;
263
+ case 'modes':
264
+ this.mouseTracking = reply.mouse;
265
+ break;
266
+ case 'cursor': {
267
+ const left = `${(reply.x * this.cellWidth) / this.dpr}px`, top = `${(reply.y * this.cellHeight) / this.dpr}px`;
268
+ Object.assign(this.input.style, { left, top, bottom: 'auto' });
269
+ Object.assign(this.preedit.style, { left, top, bottom: 'auto' });
270
+ break;
271
+ }
272
+ case 'error':
273
+ this.fail(new Error(reply.error));
274
+ break;
275
+ case 'stats': {
276
+ const v = reply.values;
277
+ this.statistics.emit({
278
+ frames: v[0],
279
+ inputBytes: v[1],
280
+ parseMs: v[2],
281
+ renderMs: v[3],
282
+ uploadBytes: v[4],
283
+ atlasGlyphs: v[5],
284
+ atlasHits: v[6],
285
+ atlasMisses: v[7],
286
+ atlasResets: v[8],
287
+ renderP50Ms: percentile(reply.samples, 0.5),
288
+ renderP95Ms: percentile(reply.samples, 0.95),
289
+ renderP99Ms: percentile(reply.samples, 0.99),
290
+ activeFps: reply.fps,
291
+ adapter: reply.adapter,
292
+ });
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ fail(error) {
298
+ if (this.failed)
299
+ return;
300
+ this.failed = true;
301
+ this.readyReject(error);
302
+ this.errors.emit(error);
303
+ for (const pending of this.pending.values())
304
+ pending.reject(error);
305
+ this.pending.clear();
306
+ this.pendingBytes = 0;
307
+ this.batchIds = [];
308
+ this.batchLength = 0;
309
+ this.observer?.disconnect();
310
+ clearInterval(this.mirrorTimer);
311
+ this.worker.terminate();
312
+ }
313
+ /** Resolves after bytes have been parsed. Use flush() to wait for rendering. */
314
+ write(data) {
315
+ this.assertLive();
316
+ const limit = this.options.maxPendingBytes ?? 8 * 1024 * 1024;
317
+ // Strings encode straight into the batch; byte arrays are copied once,
318
+ // so callers retain ownership of their original buffer.
319
+ let length;
320
+ if (typeof data === 'string') {
321
+ if (this.pendingBytes + data.length > limit)
322
+ return Promise.reject(new BackpressureError());
323
+ this.reserve(data.length * 3);
324
+ length = this.encoder.encodeInto(data, this.batch.subarray(this.batchLength)).written;
325
+ }
326
+ else
327
+ length = data.byteLength;
328
+ if (this.pendingBytes + length > limit)
329
+ return Promise.reject(new BackpressureError());
330
+ if (typeof data !== 'string') {
331
+ this.reserve(length);
332
+ this.batch.set(data, this.batchLength);
333
+ }
334
+ this.batchLength += length;
335
+ const id = this.sequence++;
336
+ this.pendingBytes += length;
337
+ const done = new Promise((resolve, reject) => this.pending.set(id, { resolve: () => resolve(), reject, bytes: length }));
338
+ this.batchIds.push(id);
339
+ if (!this.flushScheduled) {
340
+ this.flushScheduled = true;
341
+ queueMicrotask(() => this.flushWrites());
342
+ }
343
+ return done;
344
+ }
345
+ reserve(bytes) {
346
+ if (this.batchLength + bytes <= this.batch.length)
347
+ return;
348
+ const next = new Uint8Array(Math.max(4096, (this.batchLength + bytes) * 2));
349
+ next.set(this.batch.subarray(0, this.batchLength));
350
+ this.batch = next;
351
+ }
352
+ flushWrites() {
353
+ this.flushScheduled = false;
354
+ if (this.disposed || this.failed || !this.batchIds.length)
355
+ return;
356
+ // Writes keep their order and each promise still resolves after its bytes
357
+ // are parsed; a burst crosses the worker boundary once.
358
+ const bytes = this.batch.subarray(0, this.batchLength);
359
+ this.send({ type: 'write', ids: this.batchIds, bytes }, [bytes.buffer]);
360
+ this.batch = new Uint8Array(0);
361
+ this.batchLength = 0;
362
+ this.batchIds = [];
363
+ }
364
+ async flush() {
365
+ this.flushWrites();
366
+ await this.request({ type: 'flush' });
367
+ }
368
+ async resize(cols, rows) {
369
+ validateGrid(cols, rows);
370
+ if (cols === this.cols && rows === this.rows)
371
+ return;
372
+ await this.request({ type: 'resize', cols, rows });
373
+ this.cols = cols;
374
+ this.rows = rows;
375
+ this.dimensions();
376
+ this.resizes.emit({ cols, rows });
377
+ }
378
+ fit() {
379
+ if (this.disposed || this.failed)
380
+ return;
381
+ const rect = this.element.getBoundingClientRect();
382
+ const cols = Math.min(512, Math.max(1, Math.floor((rect.width * this.dpr) / this.cellWidth)));
383
+ const rows = Math.min(512, Math.floor(65536 / cols), Math.max(1, Math.floor((rect.height * this.dpr) / this.cellHeight)));
384
+ if (rect.width > 0 && rect.height > 0)
385
+ void this.resize(cols, rows).catch((error) => this.errors.emit(error));
386
+ }
387
+ dimensions() {
388
+ this.canvas.style.width = `${(this.cols * this.cellWidth) / this.dpr}px`;
389
+ this.canvas.style.height = `${(this.rows * this.cellHeight) / this.dpr}px`;
390
+ }
391
+ focus() {
392
+ this.input.focus({ preventScroll: true });
393
+ }
394
+ onData(listener) {
395
+ return this.data.on(listener);
396
+ }
397
+ /** Byte-accurate output, including legacy binary mouse protocols. */
398
+ onBytes(listener) {
399
+ return this.binary.on(listener);
400
+ }
401
+ onTitleChange(listener) {
402
+ return this.title.on(listener);
403
+ }
404
+ onBell(listener) {
405
+ return this.bell.on(listener);
406
+ }
407
+ onError(listener) {
408
+ return this.errors.on(listener);
409
+ }
410
+ onStats(listener) {
411
+ return this.statistics.on(listener);
412
+ }
413
+ onResize(listener) {
414
+ return this.resizes.on(listener);
415
+ }
416
+ async paste(text) {
417
+ await this.request({ type: 'paste', text });
418
+ }
419
+ scroll(lines) {
420
+ this.assertLive();
421
+ this.send({ type: 'scroll', delta: Math.trunc(lines) });
422
+ }
423
+ async getText(selected = false) {
424
+ this.flushWrites();
425
+ return this.request({ type: 'text', selected });
426
+ }
427
+ async benchmark(options = {}) {
428
+ this.flushWrites();
429
+ return this.request({ type: 'benchmark', options });
430
+ }
431
+ setTheme(theme) {
432
+ this.assertLive();
433
+ const next = { ...this.theme, ...theme };
434
+ const colors = Object.values(next).map(packColor);
435
+ this.theme = next;
436
+ this.element.style.background = next.background;
437
+ this.send({ type: 'theme', colors });
438
+ }
439
+ connect(socket) {
440
+ let incoming = Promise.resolve();
441
+ const receive = (event) => {
442
+ incoming = incoming
443
+ .then(async () => {
444
+ const bytes = event.data instanceof Blob
445
+ ? new Uint8Array(await event.data.arrayBuffer())
446
+ : event.data instanceof ArrayBuffer
447
+ ? new Uint8Array(event.data)
448
+ : String(event.data);
449
+ await this.write(bytes);
450
+ })
451
+ .catch((error) => this.errors.emit(error));
452
+ };
453
+ const output = this.onBytes((bytes) => {
454
+ if (socket.readyState === WebSocket.OPEN)
455
+ socket.send(bytes);
456
+ });
457
+ socket.addEventListener('message', receive);
458
+ return {
459
+ dispose: () => {
460
+ output.dispose();
461
+ socket.removeEventListener('message', receive);
462
+ },
463
+ };
464
+ }
465
+ bindInput() {
466
+ const options = { signal: this.abort.signal };
467
+ this.input.addEventListener('keydown', (event) => {
468
+ if (this.composing || event.isComposing || event.key === 'Process' || event.key === 'Dead')
469
+ return;
470
+ if ((event.metaKey || (event.ctrlKey && event.shiftKey)) &&
471
+ event.key.toLowerCase() === 'c') {
472
+ event.preventDefault();
473
+ void this.getText(true)
474
+ .then((text) => navigator.clipboard.writeText(text))
475
+ .catch((error) => this.errors.emit(error));
476
+ return;
477
+ }
478
+ if ((event.metaKey || (event.ctrlKey && event.shiftKey)) && event.key.toLowerCase() === 'v')
479
+ return;
480
+ if (event.metaKey)
481
+ return; // Keep browser and OS shortcuts available.
482
+ event.preventDefault();
483
+ const character = [...event.key].length === 1 ? event.key : '';
484
+ const unshifted = event.code.startsWith('Key')
485
+ ? (event.code.slice(3).toLowerCase().codePointAt(0) ?? 0)
486
+ : event.code.startsWith('Digit')
487
+ ? (event.code.slice(5).codePointAt(0) ?? 0)
488
+ : (character.toLowerCase().codePointAt(0) ?? 0);
489
+ this.send({
490
+ type: 'key',
491
+ key: KEY_CODES[event.code] ?? 0,
492
+ mods: mods(event),
493
+ action: event.repeat ? 2 : 1,
494
+ text: character,
495
+ unshifted,
496
+ });
497
+ this.selected = false;
498
+ this.send({ type: 'select', x1: -1, y1: 0, x2: 0, y2: 0 });
499
+ }, options);
500
+ this.input.addEventListener('keyup', (event) => {
501
+ if (event.isComposing || event.metaKey)
502
+ return;
503
+ this.send({
504
+ type: 'key',
505
+ key: KEY_CODES[event.code] ?? 0,
506
+ mods: mods(event),
507
+ action: 0,
508
+ text: '',
509
+ unshifted: 0,
510
+ });
511
+ }, options);
512
+ this.input.addEventListener('compositionstart', () => {
513
+ this.composing = true;
514
+ }, options);
515
+ this.input.addEventListener('compositionupdate', (event) => {
516
+ this.preedit.textContent = event.data;
517
+ }, options);
518
+ this.input.addEventListener('compositionend', (event) => {
519
+ this.composing = false;
520
+ this.preedit.textContent = '';
521
+ if (event.data)
522
+ this.send({ type: 'key', key: 0, mods: 0, action: 1, text: event.data, unshifted: 0 });
523
+ this.input.value = '';
524
+ }, options);
525
+ this.input.addEventListener('input', (event) => {
526
+ const input = event;
527
+ if (this.composing ||
528
+ input.inputType === 'insertFromComposition' ||
529
+ input.inputType === 'insertCompositionText')
530
+ return;
531
+ if (input.data)
532
+ this.send({ type: 'key', key: 0, mods: 0, action: 1, text: input.data, unshifted: 0 });
533
+ this.input.value = '';
534
+ }, options);
535
+ this.input.addEventListener('paste', (event) => {
536
+ event.preventDefault();
537
+ const text = event.clipboardData?.getData('text/plain');
538
+ if (text)
539
+ void this.paste(text).catch((error) => this.errors.emit(error));
540
+ }, options);
541
+ this.input.addEventListener('focus', () => this.send({ type: 'focus', focused: true }), options);
542
+ this.input.addEventListener('blur', () => this.send({ type: 'focus', focused: false }), options);
543
+ const point = (event) => {
544
+ const rect = this.canvas.getBoundingClientRect();
545
+ return [
546
+ Math.max(0, Math.min(this.cols - 1, Math.floor(((event.clientX - rect.left) * this.dpr) / this.cellWidth))),
547
+ Math.max(0, Math.min(this.rows - 1, Math.floor(((event.clientY - rect.top) * this.dpr) / this.cellHeight))),
548
+ ];
549
+ };
550
+ const mouse = (event, action) => {
551
+ const rect = this.canvas.getBoundingClientRect();
552
+ this.send({
553
+ type: 'mouse',
554
+ action,
555
+ button: event.buttons === 0 && action === 2 ? 0 : ([1, 3, 2][event.button] ?? 0),
556
+ mods: mods(event),
557
+ x: Math.max(0, Math.min(this.cols * this.cellWidth - 1, (event.clientX - rect.left) * this.dpr)),
558
+ y: Math.max(0, Math.min(this.rows * this.cellHeight - 1, (event.clientY - rect.top) * this.dpr)),
559
+ pressed: event.buttons,
560
+ });
561
+ };
562
+ this.canvas.addEventListener('pointerdown', (event) => {
563
+ this.focus();
564
+ this.canvas.setPointerCapture(event.pointerId);
565
+ if (this.mouseTracking && !event.shiftKey) {
566
+ event.preventDefault();
567
+ mouse(event, 0);
568
+ return;
569
+ }
570
+ if (event.button !== 0)
571
+ return;
572
+ this.anchor = point(event);
573
+ this.dragging = true;
574
+ this.selected = true;
575
+ this.send({
576
+ type: 'select',
577
+ x1: this.anchor[0],
578
+ y1: this.anchor[1],
579
+ x2: this.anchor[0],
580
+ y2: this.anchor[1],
581
+ });
582
+ }, options);
583
+ this.canvas.addEventListener('pointermove', (event) => {
584
+ if (this.mouseTracking && !event.shiftKey) {
585
+ mouse(event, 2);
586
+ return;
587
+ }
588
+ if (this.dragging) {
589
+ const end = point(event);
590
+ this.send({
591
+ type: 'select',
592
+ x1: this.anchor[0],
593
+ y1: this.anchor[1],
594
+ x2: end[0],
595
+ y2: end[1],
596
+ });
597
+ }
598
+ }, options);
599
+ this.canvas.addEventListener('pointerup', (event) => {
600
+ this.dragging = false;
601
+ if (this.mouseTracking && !event.shiftKey)
602
+ mouse(event, 1);
603
+ }, options);
604
+ this.canvas.addEventListener('pointercancel', () => {
605
+ this.dragging = false;
606
+ }, options);
607
+ this.canvas.addEventListener('click', (event) => {
608
+ if (!event.ctrlKey && !event.metaKey)
609
+ return;
610
+ const [x, y] = point(event);
611
+ void this.request({ type: 'link', x, y })
612
+ .then((link) => {
613
+ if (!link)
614
+ return;
615
+ const url = new URL(link);
616
+ if (['https:', 'http:', 'mailto:'].includes(url.protocol))
617
+ window.open(url.href, '_blank', 'noopener,noreferrer');
618
+ })
619
+ .catch((error) => this.errors.emit(error));
620
+ }, options);
621
+ this.canvas.addEventListener('wheel', (event) => {
622
+ event.preventDefault();
623
+ if (this.mouseTracking && !event.shiftKey) {
624
+ const rect = this.canvas.getBoundingClientRect();
625
+ this.send({
626
+ type: 'mouse',
627
+ action: 0,
628
+ button: event.deltaY < 0 ? 4 : 5,
629
+ mods: mods(event),
630
+ x: Math.max(0, Math.min(this.cols * this.cellWidth - 1, (event.clientX - rect.left) * this.dpr)),
631
+ y: Math.max(0, Math.min(this.rows * this.cellHeight - 1, (event.clientY - rect.top) * this.dpr)),
632
+ pressed: 0,
633
+ });
634
+ }
635
+ else
636
+ this.scroll(Math.sign(event.deltaY) *
637
+ Math.max(1, Math.round(Math.abs(event.deltaY) / (event.deltaMode === 0 ? this.cellHeight / this.dpr : 1))));
638
+ }, { ...options, passive: false });
639
+ this.canvas.addEventListener('contextmenu', (event) => {
640
+ if (this.mouseTracking)
641
+ event.preventDefault();
642
+ }, options);
643
+ document.addEventListener('visibilitychange', () => this.send({ type: 'visibility', visible: !document.hidden }), options);
644
+ }
645
+ enableMirror() {
646
+ this.mirror = document.createElement('pre');
647
+ this.mirror.setAttribute('role', 'log');
648
+ this.mirror.setAttribute('aria-live', 'polite');
649
+ Object.assign(this.mirror.style, {
650
+ position: 'absolute',
651
+ width: '1px',
652
+ height: '1px',
653
+ overflow: 'hidden',
654
+ clipPath: 'inset(50%)',
655
+ });
656
+ this.element.append(this.mirror);
657
+ this.mirrorTimer = setInterval(() => {
658
+ void this.getText()
659
+ .then((text) => {
660
+ if (this.mirror && this.mirror.textContent !== text)
661
+ this.mirror.textContent = text;
662
+ })
663
+ .catch(() => { });
664
+ }, 250);
665
+ }
666
+ async dispose() {
667
+ if (this.disposed)
668
+ return;
669
+ this.disposed = true;
670
+ this.abort.abort();
671
+ this.observer?.disconnect();
672
+ clearInterval(this.mirrorTimer);
673
+ for (const pending of this.pending.values())
674
+ pending.reject(new Error('Wraith disposed'));
675
+ this.pending.clear();
676
+ this.batchIds = [];
677
+ this.batchLength = 0;
678
+ // Allow Ghostty and wgpu destructors to run; terminate a stalled worker after a bounded wait.
679
+ if (!this.failed)
680
+ await new Promise((resolve) => {
681
+ const id = this.sequence++;
682
+ const finish = () => {
683
+ this.worker.terminate();
684
+ resolve();
685
+ };
686
+ const timer = setTimeout(finish, 1000);
687
+ this.pending.set(id, {
688
+ bytes: 0,
689
+ reject: finish,
690
+ resolve: () => {
691
+ clearTimeout(timer);
692
+ finish();
693
+ },
694
+ });
695
+ this.send({ type: 'dispose', id });
696
+ });
697
+ this.element.remove();
698
+ this.data.clear();
699
+ this.binary.clear();
700
+ this.title.clear();
701
+ this.bell.clear();
702
+ this.errors.clear();
703
+ this.statistics.clear();
704
+ this.resizes.clear();
705
+ }
706
+ }
707
+ export { Terminal } from './xterm.js';