@daugasauron/dolly-host-display 0.0.0-stage → 0.1.4-rc.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/README.md CHANGED
@@ -1,3 +1,4 @@
1
- # Temporary Holding Version
1
+ # @daugasauron/dolly-host-display
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Dolly's `display@0` host module: the terminal and programs' frames; the selection on Ctrl+Shift+C. Give the URL of its
4
+ `module.json` to `boot()`'s `modules`; @daugasauron/dolly's README says how.
package/abi.mjs ADDED
@@ -0,0 +1,40 @@
1
+ // Generated from host/display/dolly-display-0.wat, host/display/display.h, include/dolly/process.h.
2
+ export const DOLLY_DISPLAY_MAILBOX_SIZE = 96;
3
+ export const DOLLY_DISPLAY_WORD_FLAGS = 0;
4
+ export const DOLLY_DISPLAY_WORD_FRAME_SEQUENCE = 1;
5
+ export const DOLLY_DISPLAY_WORD_FRAME_INDEX = 2;
6
+ export const DOLLY_DISPLAY_WORD_FRAME_WIDTH = 3;
7
+ export const DOLLY_DISPLAY_WORD_FRAME_HEIGHT = 4;
8
+ export const DOLLY_DISPLAY_WORD_FRAME_STRIDE = 5;
9
+ export const DOLLY_DISPLAY_WORD_TERMINAL_COLS = 6;
10
+ export const DOLLY_DISPLAY_WORD_TERMINAL_ROWS = 7;
11
+ export const DOLLY_DISPLAY_WORD_FONT_SIZE_MILLI = 8;
12
+ export const DOLLY_DISPLAY_WORD_COPY_SEQUENCE = 9;
13
+ export const DOLLY_DISPLAY_WORD_COPY_LENGTH = 10;
14
+ export const DOLLY_DISPLAY_WORD_COPY_FLAGS = 11;
15
+ export const DOLLY_DISPLAY_WORD_CURSOR_COL = 12;
16
+ export const DOLLY_DISPLAY_WORD_CURSOR_ROW = 13;
17
+ export const DOLLY_DISPLAY_WORD_CELL_WIDTH = 14;
18
+ export const DOLLY_DISPLAY_WORD_CELL_HEIGHT = 15;
19
+ export const DOLLY_DISPLAY_WORD_PADDING_X = 16;
20
+ export const DOLLY_DISPLAY_WORD_PADDING_Y = 17;
21
+ export const DOLLY_DISPLAY_WORD_CURSOR_STYLE = 18;
22
+ export const DOLLY_DISPLAY_WORD_ANIMATION_FRAME_SEQUENCE = 19;
23
+ export const DOLLY_DISPLAY_WORD_SURFACE_SEQUENCE = 20;
24
+ export const DOLLY_DISPLAY_WORD_SURFACE_WIDTH = 21;
25
+ export const DOLLY_DISPLAY_WORD_SURFACE_HEIGHT = 22;
26
+ export const DOLLY_DISPLAY_WORD_SURFACE_SCALE_MILLI = 23;
27
+ export const DOLLY_DISPLAY_MAX_WIDTH = 4096;
28
+ export const DOLLY_DISPLAY_MAX_HEIGHT = 2304;
29
+ export const DOLLY_DISPLAY_FRAME_COUNT = 2;
30
+ export const DOLLY_DISPLAY_COPY_CAPACITY = 262144;
31
+ export const DOLLY_DISPLAY_ACQUIRE = 96;
32
+ export const DOLLY_DISPLAY_SET_SIZE = 97;
33
+ export const DOLLY_DISPLAY_BEGIN_FRAME = 98;
34
+ export const DOLLY_DISPLAY_WRITE_FRAME = 99;
35
+ export const DOLLY_DISPLAY_PRESENT = 100;
36
+ export const DOLLY_DISPLAY_WAIT_FRAME = 101;
37
+ export const DOLLY_DISPLAY_SET_CURSOR = 102;
38
+ export const DOLLY_DISPLAY_RELEASE = 103;
39
+ export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
40
+ export const DOLLY_DISPLAY_ABI_DIGEST = "e24b5ca332894d3630171a9e56c54c7f4936116ab304df030f093c74788f4900";
package/display.mjs ADDED
@@ -0,0 +1,414 @@
1
+ import * as A from "./abi.mjs";
2
+ export { DOLLY_DISPLAY_ABI_DIGEST as digest } from "./abi.mjs";
3
+
4
+ const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
5
+ // dolly_display_cursor (display.h), by value: the page's own cursors, nothing a guest draws.
6
+ const cursorStyles = ["text", "default", "crosshair", "pointer", "none", "ns-resize", "ew-resize", "nwse-resize",
7
+ "nesw-resize", "move", "wait", "progress", "not-allowed", "help"];
8
+ // The presenter keeps requesting animation frames this long after the last
9
+ // frame, so the next one is painted on the frame after it is published.
10
+ const lingerMilliseconds = 250;
11
+
12
+ // The Worker's end of the display mailbox: it reads frames, the terminal's
13
+ // geometry and its selection, and writes the surface and the animation frame.
14
+ export class DisplayTransport {
15
+ static flags = A.DOLLY_DISPLAY_WORD_FLAGS;
16
+ static frameSequence = A.DOLLY_DISPLAY_WORD_FRAME_SEQUENCE;
17
+ static frameIndex = A.DOLLY_DISPLAY_WORD_FRAME_INDEX;
18
+ static frameWidth = A.DOLLY_DISPLAY_WORD_FRAME_WIDTH;
19
+ static frameHeight = A.DOLLY_DISPLAY_WORD_FRAME_HEIGHT;
20
+ static frameStride = A.DOLLY_DISPLAY_WORD_FRAME_STRIDE;
21
+ static terminalCols = A.DOLLY_DISPLAY_WORD_TERMINAL_COLS;
22
+ static terminalRows = A.DOLLY_DISPLAY_WORD_TERMINAL_ROWS;
23
+ static fontSizeMilli = A.DOLLY_DISPLAY_WORD_FONT_SIZE_MILLI;
24
+ static copySequence = A.DOLLY_DISPLAY_WORD_COPY_SEQUENCE;
25
+ static copyLength = A.DOLLY_DISPLAY_WORD_COPY_LENGTH;
26
+ static copyFlags = A.DOLLY_DISPLAY_WORD_COPY_FLAGS;
27
+ static cursorCol = A.DOLLY_DISPLAY_WORD_CURSOR_COL;
28
+ static cursorRow = A.DOLLY_DISPLAY_WORD_CURSOR_ROW;
29
+ static cellWidth = A.DOLLY_DISPLAY_WORD_CELL_WIDTH;
30
+ static cellHeight = A.DOLLY_DISPLAY_WORD_CELL_HEIGHT;
31
+ static paddingX = A.DOLLY_DISPLAY_WORD_PADDING_X;
32
+ static paddingY = A.DOLLY_DISPLAY_WORD_PADDING_Y;
33
+ static cursorStyle = A.DOLLY_DISPLAY_WORD_CURSOR_STYLE;
34
+ static animationFrameSequence = A.DOLLY_DISPLAY_WORD_ANIMATION_FRAME_SEQUENCE;
35
+ static surfaceSequence = A.DOLLY_DISPLAY_WORD_SURFACE_SEQUENCE;
36
+ static surfaceWidth = A.DOLLY_DISPLAY_WORD_SURFACE_WIDTH;
37
+ static surfaceHeight = A.DOLLY_DISPLAY_WORD_SURFACE_HEIGHT;
38
+ static surfaceScaleMilli = A.DOLLY_DISPLAY_WORD_SURFACE_SCALE_MILLI;
39
+
40
+ static copyAvailable = 1;
41
+ static copyTruncated = 2;
42
+
43
+ // The copy buffer's size is the contract's; a test may shrink it.
44
+ constructor(buffer, address, copyAddress, { copyCapacity = A.DOLLY_DISPLAY_COPY_CAPACITY } = {}) {
45
+ if (!(buffer instanceof SharedArrayBuffer)) {
46
+ throw new Error("Dolly display transport requires shared Wasm memory");
47
+ }
48
+ const within = (start, length) => Number.isSafeInteger(start) && Number.isSafeInteger(length) &&
49
+ start > 0 && length > 0 && start <= buffer.byteLength - length;
50
+ if (address % 4 !== 0 || !within(address, A.DOLLY_DISPLAY_MAILBOX_SIZE) || !within(copyAddress, copyCapacity)) {
51
+ throw new Error("Dolly supplied an invalid display mailbox");
52
+ }
53
+ this.bytes = new Uint8Array(buffer);
54
+ this.words = new Int32Array(buffer);
55
+ this.address = address;
56
+ this.word = address / 4;
57
+ this.copyAddress = copyAddress;
58
+ this.copyCapacity = copyCapacity;
59
+ }
60
+
61
+ copySelection() {
62
+ for (let attempt = 0; attempt < 3; attempt++) {
63
+ const before = Atomics.load(
64
+ this.words,
65
+ this.word + DisplayTransport.copySequence,
66
+ ) >>> 0;
67
+ const flags = Atomics.load(
68
+ this.words,
69
+ this.word + DisplayTransport.copyFlags,
70
+ ) >>> 0;
71
+ const length = Atomics.load(
72
+ this.words,
73
+ this.word + DisplayTransport.copyLength,
74
+ ) >>> 0;
75
+ if ((flags & DisplayTransport.copyAvailable) === 0) return null;
76
+ if ((flags & DisplayTransport.copyTruncated) !== 0 ||
77
+ length > this.copyCapacity) {
78
+ throw new Error("Dolly selection exceeds the clipboard bridge capacity");
79
+ }
80
+ const bytes = new Uint8Array(
81
+ new Uint8Array(this.bytes.buffer, this.copyAddress, length),
82
+ );
83
+ const after = Atomics.load(
84
+ this.words,
85
+ this.word + DisplayTransport.copySequence,
86
+ ) >>> 0;
87
+ if (before === after) return textDecoder.decode(bytes);
88
+ }
89
+ throw new Error("Dolly selection changed while copying");
90
+ }
91
+
92
+ // The surface frames are shown on: the terminal lays its grid out for it.
93
+ publishSurface(width, height, devicePixelRatio) {
94
+ Atomics.add(this.words, this.word + DisplayTransport.surfaceSequence, 1);
95
+ Atomics.store(this.words, this.word + DisplayTransport.surfaceWidth, Math.max(1, Math.round(width)));
96
+ Atomics.store(this.words, this.word + DisplayTransport.surfaceHeight, Math.max(1, Math.round(height)));
97
+ Atomics.store(this.words, this.word + DisplayTransport.surfaceScaleMilli,
98
+ Math.max(500, Math.min(4000, Math.round(devicePixelRatio * 1000))));
99
+ Atomics.add(this.words, this.word + DisplayTransport.surfaceSequence, 1);
100
+ }
101
+
102
+ graphicsActive() {
103
+ return (Atomics.load(this.words, this.word + DisplayTransport.flags) & 1) !== 0;
104
+ }
105
+
106
+ publishAnimationFrame() {
107
+ if (!this.graphicsActive()) return;
108
+ Atomics.add(this.words, this.word + DisplayTransport.animationFrameSequence, 1);
109
+ Atomics.notify(this.words, this.word + DisplayTransport.animationFrameSequence);
110
+ }
111
+
112
+ cursorStyle() {
113
+ return Atomics.load(
114
+ this.words,
115
+ this.word + DisplayTransport.cursorStyle,
116
+ ) >>> 0;
117
+ }
118
+
119
+ fontSize() {
120
+ return Atomics.load(this.words, this.word + DisplayTransport.fontSizeMilli) / 1000;
121
+ }
122
+
123
+ dimensions() {
124
+ return {
125
+ cols: Atomics.load(this.words, this.word + DisplayTransport.terminalCols),
126
+ rows: Atomics.load(this.words, this.word + DisplayTransport.terminalRows),
127
+ };
128
+ }
129
+
130
+ geometry() {
131
+ return {
132
+ cursorCol: Atomics.load(this.words, this.word + DisplayTransport.cursorCol) >>> 0,
133
+ cursorRow: Atomics.load(this.words, this.word + DisplayTransport.cursorRow) >>> 0,
134
+ cellWidth: Atomics.load(this.words, this.word + DisplayTransport.cellWidth) >>> 0,
135
+ cellHeight: Atomics.load(this.words, this.word + DisplayTransport.cellHeight) >>> 0,
136
+ paddingX: Atomics.load(this.words, this.word + DisplayTransport.paddingX) >>> 0,
137
+ paddingY: Atomics.load(this.words, this.word + DisplayTransport.paddingY) >>> 0,
138
+ };
139
+ }
140
+ }
141
+
142
+ // Paints the published frames on the page's canvas, from the Worker (an
143
+ // OffscreenCanvas). painted(sequence) follows each frame painted, cursor(style)
144
+ // each change of the pointer's cursor.
145
+ export class FramebufferPresenter {
146
+ constructor(surface, buffer, frameAddresses, capacity, displayTransport, { fatal, painted, cursor }) {
147
+ this.context = surface.getContext("2d", { alpha: false });
148
+ if (!this.context) throw new Error("Dolly requires a 2D canvas context");
149
+ this.surface = surface;
150
+ this.buffer = buffer;
151
+ this.frameAddresses = frameAddresses;
152
+ this.capacity = capacity;
153
+ this.transport = displayTransport;
154
+ this.fatal = fatal;
155
+ this.painted = painted;
156
+ this.cursor = cursor;
157
+ this.cursorStyle = undefined;
158
+ this.sequence = -1;
159
+ this.running = true;
160
+ }
161
+
162
+ // Paints once per animation frame while frames arrive or a program owns the
163
+ // display. After lingerMilliseconds without either it requests no frames
164
+ // and waits on the frame sequence: worker().service notifies it of a new
165
+ // frame, lease or cursor.
166
+ start() {
167
+ const { words, word } = this.transport;
168
+ let requested = false, waiting = false, activeUntil = 0;
169
+ const frame = now => {
170
+ requested = false;
171
+ if (!this.running) return;
172
+ try {
173
+ this.transport.publishAnimationFrame();
174
+ this.updateCursor();
175
+ if (this.paint() || this.transport.graphicsActive()) activeUntil = now + lingerMilliseconds;
176
+ } catch (error) {
177
+ this.stop();
178
+ this.fatal(error.message);
179
+ return;
180
+ }
181
+ if (now < activeUntil) return request();
182
+ // One wait at a time: any notify after it began ends it.
183
+ if (!waiting) {
184
+ const parked = Atomics.waitAsync(words, word + DisplayTransport.frameSequence, this.sequence | 0);
185
+ waiting = parked.async;
186
+ if (waiting) parked.value.then(() => { waiting = false; this.wake(); });
187
+ else this.wake();
188
+ }
189
+ // A lease or cursor set before the wait began was notified to nobody.
190
+ if (this.transport.graphicsActive() || this.transport.cursorStyle() !== this.cursorStyle) this.wake();
191
+ };
192
+ const request = () => {
193
+ if (requested || !this.running) return;
194
+ requested = true;
195
+ requestAnimationFrame(frame);
196
+ };
197
+ this.wake = () => {
198
+ activeUntil = performance.now() + lingerMilliseconds;
199
+ request();
200
+ };
201
+ this.wake();
202
+ }
203
+
204
+ stop() {
205
+ this.running = false;
206
+ }
207
+
208
+ updateCursor() {
209
+ const cursor = this.transport.cursorStyle();
210
+ if (cursor === this.cursorStyle) return;
211
+ this.cursor(this.cursorStyle = cursor);
212
+ }
213
+
214
+ // Copies a newly published frame to the canvas; returns whether it painted one.
215
+ paint() {
216
+ const { words, word } = this.transport;
217
+ const sequence = Atomics.load(words, word + DisplayTransport.frameSequence) >>> 0;
218
+ if (sequence === this.sequence) return false;
219
+ for (let attempt = 0; attempt < 3; attempt++) {
220
+ const before = Atomics.load(words, word + DisplayTransport.frameSequence) >>> 0;
221
+ const index = Atomics.load(words, word + DisplayTransport.frameIndex) >>> 0;
222
+ const width = Atomics.load(words, word + DisplayTransport.frameWidth) >>> 0;
223
+ const height = Atomics.load(words, word + DisplayTransport.frameHeight) >>> 0;
224
+ const stride = Atomics.load(words, word + DisplayTransport.frameStride) >>> 0;
225
+ const length = stride * height;
226
+ const address = this.frameAddresses[index];
227
+ if (index > 1 || width === 0 || height === 0 || stride !== width * 4 ||
228
+ length > this.capacity || address + length > this.buffer.byteLength) {
229
+ throw new Error("Dolly published an invalid framebuffer");
230
+ }
231
+ // One image, and one view of each frame buffer, per frame size: a frame
232
+ // costs one copy out of shared memory and allocates nothing.
233
+ if (this.image?.width !== width || this.image.height !== height) {
234
+ this.image = new ImageData(width, height);
235
+ this.frames = [];
236
+ }
237
+ this.image.data.set(this.frames[index] ??= new Uint8ClampedArray(this.buffer, address, length));
238
+ const after = Atomics.load(words, word + DisplayTransport.frameSequence) >>> 0;
239
+ if (before !== after) continue;
240
+ if (this.surface.width !== width || this.surface.height !== height) {
241
+ this.surface.width = width;
242
+ this.surface.height = height;
243
+ }
244
+ this.context.putImageData(this.image, 0, 0);
245
+ this.sequence = after;
246
+ this.painted(after);
247
+ return true;
248
+ }
249
+ return false;
250
+ }
251
+ }
252
+
253
+ // The page's display: the canvas, handed to the Worker that paints it, the
254
+ // surface it measures, and the copy chord. What the terminal publishes (its
255
+ // grid, geometry, cursor, the lease) the Worker reports by message.
256
+ export function browser({ mount, canvas, keyboard, fatal, showStatus, send, ask }) {
257
+ const surface = canvas.transferControlToOffscreen();
258
+ let state, resizeObserver;
259
+ const publishSurface = () => send({ type: "display-surface", width: mount.clientWidth, height: mount.clientHeight,
260
+ devicePixelRatio });
261
+ const copySelection = () => ask({ type: "display-copy" });
262
+
263
+ // Ctrl+Shift+C writes the terminal's selection to the clipboard.
264
+ async function copyToClipboard() {
265
+ try {
266
+ const text = await copySelection();
267
+ if (text === null) {
268
+ document.documentElement.dataset.clipboard = "empty";
269
+ return;
270
+ }
271
+ await navigator.clipboard.writeText(text).then(
272
+ () => { document.documentElement.dataset.clipboard = "copied"; },
273
+ () => { document.documentElement.dataset.clipboard = "denied"; },
274
+ );
275
+ } catch (error) {
276
+ document.documentElement.dataset.clipboard = "failed";
277
+ showStatus(`Copy failed: ${error.message}`);
278
+ }
279
+ }
280
+
281
+ const transport = {
282
+ graphicsActive: () => state.graphicsActive,
283
+ cursorStyle: () => state.cursor,
284
+ fontSize: () => state.fontSize,
285
+ dimensions: () => ({ cols: state.cols, rows: state.rows }),
286
+ geometry: () => state.geometry,
287
+ copySelection,
288
+ };
289
+ return {
290
+ configuration: { canvas: surface }, transfers: [surface],
291
+ page: {
292
+ get transport() { return state && transport; },
293
+ get graphicsActive() { return state.graphicsActive; },
294
+ get fontSize() { return state.fontSize; },
295
+ copySelection,
296
+ // The last frame painted, as ImageData, or null before the first.
297
+ frame: () => ask({ type: "display-frame" }),
298
+ },
299
+ // The copy chord is the page's: the guest reads neither its press nor its
300
+ // release. A key typed into a module's own UI is that module's to claim.
301
+ claimsKey(event) {
302
+ if (!state || !event.ctrlKey || !event.shiftKey || event.altKey || event.metaKey ||
303
+ event.code !== "KeyC" || ![keyboard, document.body].includes(event.target)) return false;
304
+ event.preventDefault();
305
+ event.stopImmediatePropagation();
306
+ if (event.type === "keydown") void copyToClipboard();
307
+ return "key";
308
+ },
309
+ messages: {
310
+ "display-state"(message) {
311
+ const dataset = document.documentElement.dataset;
312
+ if (message.cursor !== state?.cursor) canvas.style.cursor = cursorStyles[message.cursor] ?? "default";
313
+ if (message.frame !== state?.frame) dataset.frameSequence = message.frame;
314
+ if (message.cols !== state?.cols) dataset.terminalCols = message.cols;
315
+ if (message.rows !== state?.rows) dataset.terminalRows = message.rows;
316
+ state = message;
317
+ },
318
+ "display-failed"({ message }) { fatal(message); },
319
+ // The last frame stays on screen once the image has ended, behind the
320
+ // canvas whose Worker is gone.
321
+ "display-ended"({ image }) {
322
+ const kept = Object.assign(document.createElement("canvas"), { width: image.width, height: image.height });
323
+ kept.getContext("2d").putImageData(image, 0, 0);
324
+ kept.toBlob(blob => {
325
+ if (blob) canvas.style.background = `url(${URL.createObjectURL(blob)}) left top / contain no-repeat`;
326
+ });
327
+ },
328
+ },
329
+ // The initial surface, then every change of the terminal area.
330
+ entryStarted() {
331
+ publishSurface();
332
+ resizeObserver = new ResizeObserver(publishSurface);
333
+ resizeObserver.observe(mount);
334
+ canvas.hidden = false;
335
+ document.documentElement.dataset.terminal = "ghostty-rgba-wasm";
336
+ },
337
+ dispose() { resizeObserver?.disconnect(); },
338
+ };
339
+ }
340
+
341
+ export function worker({ get, send, configuration }) {
342
+ let kernel, transport, presenter, words, word, seen = {}, reported = "";
343
+ // What the page reads of the terminal, sent when it changes.
344
+ function report() {
345
+ if (!transport) return;
346
+ const { cols, rows } = transport.dimensions();
347
+ const state = { graphicsActive: transport.graphicsActive(), cursor: transport.cursorStyle(),
348
+ fontSize: transport.fontSize(), cols, rows, geometry: transport.geometry(),
349
+ frame: Atomics.load(words, word + DisplayTransport.frameSequence) >>> 0 };
350
+ const text = JSON.stringify(state);
351
+ if (text === reported) return;
352
+ reported = text;
353
+ send({ type: "display-state", ...state });
354
+ }
355
+ return {
356
+ service() {
357
+ if (!kernel) return;
358
+ const status = kernel.dolly_terminal_present_pending();
359
+ if (status !== 0) throw new Error(`Dolly terminal presentation failed with status ${status}`);
360
+ report();
361
+ // Wakes the presenter, which waits on the frame sequence while idle.
362
+ const frame = Atomics.load(words, word + A.DOLLY_DISPLAY_WORD_FRAME_SEQUENCE);
363
+ const flags = Atomics.load(words, word + A.DOLLY_DISPLAY_WORD_FLAGS);
364
+ const cursor = Atomics.load(words, word + A.DOLLY_DISPLAY_WORD_CURSOR_STYLE);
365
+ if (frame === seen.frame && flags === seen.flags && cursor === seen.cursor) return;
366
+ seen = { frame, flags, cursor };
367
+ Atomics.notify(words, word + A.DOLLY_DISPLAY_WORD_FRAME_SEQUENCE);
368
+ },
369
+ messages: {
370
+ "display-surface"({ width, height, devicePixelRatio }) { transport?.publishSurface(width, height, devicePixelRatio); },
371
+ "display-copy"() { return transport.copySelection(); },
372
+ "display-frame"() { return presenter?.image ?? null; },
373
+ },
374
+ start({ kernel: exports, memory }) {
375
+ kernel = exports;
376
+ if (kernel.dolly_display_prepare() !== 0) throw new Error("Dolly display preparation failed");
377
+ const address = Number(kernel.dolly_display_module_address()), size = Number(kernel.dolly_display_module_size());
378
+ if (!Number.isSafeInteger(address) || !Number.isSafeInteger(size) || address <= 0 ||
379
+ size <= 0 || size > 64 * 1024 * 1024 || address > memory.buffer.byteLength - size) {
380
+ throw new Error("invalid resident display plugin range");
381
+ }
382
+ const display = get("runtime").kernelPlugin(new Uint8Array(memory.buffer, address, size).slice(), kernel);
383
+ const getDriver = display.exports.dolly_display_driver_get_v5;
384
+ if (typeof getDriver !== "function" || kernel.dolly_display_install(getDriver()) !== 0) {
385
+ throw new Error("Dolly display installation failed");
386
+ }
387
+ const mailbox = Number(kernel.dolly_display_mailbox_address());
388
+ transport = new DisplayTransport(memory.buffer, mailbox, Number(kernel.dolly_display_copy_buffer_address()));
389
+ words = transport.words;
390
+ word = transport.word;
391
+ const capacity = A.DOLLY_DISPLAY_MAX_WIDTH * A.DOLLY_DISPLAY_MAX_HEIGHT * 4, limit = memory.buffer.byteLength;
392
+ const frameAddresses = [0, 1].map(index => Number(kernel.dolly_display_framebuffer_address(index)));
393
+ if (frameAddresses.some(address => !Number.isSafeInteger(address) || address <= 0 || address > limit - capacity)) {
394
+ throw new Error("invalid display framebuffer range");
395
+ }
396
+ presenter = new FramebufferPresenter(configuration.canvas, memory.buffer, frameAddresses, capacity, transport, {
397
+ fatal: message => send({ type: "display-failed", message }),
398
+ painted: report,
399
+ cursor: report,
400
+ });
401
+ presenter.start();
402
+ // The page notifies the animation frame: a program waiting for it
403
+ // resumes then, not at the next service tick.
404
+ get("runtime").serviceOnNotify(words, word + A.DOLLY_DISPLAY_WORD_ANIMATION_FRAME_SEQUENCE);
405
+ report();
406
+ },
407
+ // The image has ended: the page keeps the last frame it published.
408
+ dispose() {
409
+ try { presenter?.paint(); } catch {}
410
+ presenter?.stop();
411
+ if (presenter?.image) send({ type: "display-ended", image: presenter.image });
412
+ },
413
+ };
414
+ }
package/module.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "display",
3
+ "version": 0,
4
+ "gives": "the terminal and programs' frames; the selection on Ctrl+Shift+C",
5
+ "fixed": "its driver is installed once, before ENTRY",
6
+ "dependencies": ["runtime@0"],
7
+ "phase": "image",
8
+ "imports": [],
9
+ "host": "display.mjs",
10
+ "contracts": ["dolly-display-0.wat"],
11
+ "process": [],
12
+ "headers": ["display.h", "display-abi.h"],
13
+ "kernel": ["kernel.c"],
14
+ "client": ["client.c"]
15
+ }
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
+ "version": "0.1.4-rc.0",
3
+ "type": "module",
2
4
  "name": "@daugasauron/dolly-host-display",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
5
+ "license": "MIT",
6
+ "dependencies": {
7
+ "@daugasauron/dolly": "0.1.4-rc.0"
8
+ },
9
+ "description": "Dolly's display@0 host module: the terminal and programs' frames; the selection on Ctrl+Shift+C.",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/daugasauron/dolly.git"
13
+ }
14
+ }