@daugasauron/dolly-host-display 0.1.4 → 0.1.5-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.
Files changed (3) hide show
  1. package/abi.mjs +25 -2
  2. package/display.mjs +91 -23
  3. package/package.json +2 -2
package/abi.mjs CHANGED
@@ -1,5 +1,5 @@
1
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;
2
+ export const DOLLY_DISPLAY_MAILBOX_SIZE = 176;
3
3
  export const DOLLY_DISPLAY_WORD_FLAGS = 0;
4
4
  export const DOLLY_DISPLAY_WORD_FRAME_SEQUENCE = 1;
5
5
  export const DOLLY_DISPLAY_WORD_FRAME_INDEX = 2;
@@ -24,10 +24,33 @@ export const DOLLY_DISPLAY_WORD_SURFACE_SEQUENCE = 20;
24
24
  export const DOLLY_DISPLAY_WORD_SURFACE_WIDTH = 21;
25
25
  export const DOLLY_DISPLAY_WORD_SURFACE_HEIGHT = 22;
26
26
  export const DOLLY_DISPLAY_WORD_SURFACE_SCALE_MILLI = 23;
27
+ export const DOLLY_DISPLAY_WORD_PALETTE_SEQUENCE = 24;
28
+ export const DOLLY_DISPLAY_WORD_PALETTE_BACKGROUND = 25;
29
+ export const DOLLY_DISPLAY_WORD_PALETTE_FOREGROUND = 26;
30
+ export const DOLLY_DISPLAY_WORD_PALETTE_CURSOR = 27;
31
+ export const DOLLY_DISPLAY_WORD_PALETTE_COLORS = 28;
32
+ export const DOLLY_DISPLAY_GRAPHICS_ACTIVE = 1;
33
+ export const DOLLY_DISPLAY_CURSOR_TEXT = 0;
34
+ export const DOLLY_DISPLAY_CURSOR_DEFAULT = 1;
35
+ export const DOLLY_DISPLAY_CURSOR_CROSSHAIR = 2;
36
+ export const DOLLY_DISPLAY_CURSOR_POINTER = 3;
37
+ export const DOLLY_DISPLAY_CURSOR_HIDDEN = 4;
38
+ export const DOLLY_DISPLAY_CURSOR_NS_RESIZE = 5;
39
+ export const DOLLY_DISPLAY_CURSOR_EW_RESIZE = 6;
40
+ export const DOLLY_DISPLAY_CURSOR_NWSE_RESIZE = 7;
41
+ export const DOLLY_DISPLAY_CURSOR_NESW_RESIZE = 8;
42
+ export const DOLLY_DISPLAY_CURSOR_MOVE = 9;
43
+ export const DOLLY_DISPLAY_CURSOR_WAIT = 10;
44
+ export const DOLLY_DISPLAY_CURSOR_PROGRESS = 11;
45
+ export const DOLLY_DISPLAY_CURSOR_NOT_ALLOWED = 12;
46
+ export const DOLLY_DISPLAY_CURSOR_HELP = 13;
47
+ export const DOLLY_DISPLAY_PIXEL_RGBA8 = 1;
27
48
  export const DOLLY_DISPLAY_MAX_WIDTH = 4096;
28
49
  export const DOLLY_DISPLAY_MAX_HEIGHT = 2304;
29
50
  export const DOLLY_DISPLAY_FRAME_COUNT = 2;
30
51
  export const DOLLY_DISPLAY_COPY_CAPACITY = 262144;
52
+ export const DOLLY_DISPLAY_COPY_AVAILABLE = 1;
53
+ export const DOLLY_DISPLAY_COPY_TRUNCATED = 2;
31
54
  export const DOLLY_DISPLAY_ACQUIRE = 96;
32
55
  export const DOLLY_DISPLAY_SET_SIZE = 97;
33
56
  export const DOLLY_DISPLAY_BEGIN_FRAME = 98;
@@ -37,4 +60,4 @@ export const DOLLY_DISPLAY_WAIT_FRAME = 101;
37
60
  export const DOLLY_DISPLAY_SET_CURSOR = 102;
38
61
  export const DOLLY_DISPLAY_RELEASE = 103;
39
62
  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";
63
+ export const DOLLY_DISPLAY_ABI_DIGEST = "89726f3e1b530de7fe61d30c407c68de7930bcb4ad30694b2cbf6789b95f2715";
package/display.mjs CHANGED
@@ -2,13 +2,39 @@ import * as A from "./abi.mjs";
2
2
  export { DOLLY_DISPLAY_ABI_DIGEST as digest } from "./abi.mjs";
3
3
 
4
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"];
5
+ // The cursor styles of dolly-display-0.wat: the page's own cursors, nothing a guest draws.
6
+ const cursorStyles = new Map([[A.DOLLY_DISPLAY_CURSOR_TEXT, "text"], [A.DOLLY_DISPLAY_CURSOR_DEFAULT, "default"],
7
+ [A.DOLLY_DISPLAY_CURSOR_CROSSHAIR, "crosshair"], [A.DOLLY_DISPLAY_CURSOR_POINTER, "pointer"],
8
+ [A.DOLLY_DISPLAY_CURSOR_HIDDEN, "none"], [A.DOLLY_DISPLAY_CURSOR_NS_RESIZE, "ns-resize"],
9
+ [A.DOLLY_DISPLAY_CURSOR_EW_RESIZE, "ew-resize"], [A.DOLLY_DISPLAY_CURSOR_NWSE_RESIZE, "nwse-resize"],
10
+ [A.DOLLY_DISPLAY_CURSOR_NESW_RESIZE, "nesw-resize"], [A.DOLLY_DISPLAY_CURSOR_MOVE, "move"],
11
+ [A.DOLLY_DISPLAY_CURSOR_WAIT, "wait"], [A.DOLLY_DISPLAY_CURSOR_PROGRESS, "progress"],
12
+ [A.DOLLY_DISPLAY_CURSOR_NOT_ALLOWED, "not-allowed"], [A.DOLLY_DISPLAY_CURSOR_HELP, "help"]]);
8
13
  // The presenter keeps requesting animation frames this long after the last
9
14
  // frame, so the next one is painted on the frame after it is published.
10
15
  const lingerMilliseconds = 250;
11
16
 
17
+ // The terminal's colours are the page's: an embedding sets these on boot()'s
18
+ // element or above it (docs/embedding.md), and the canvas inherits them. The
19
+ // first three are the popups' too. Unset, they are Dolly's, and of the 16
20
+ // colours programs name, black is the background, yellow and bright yellow the
21
+ // accent, bright black #77736c and the rest the text's colour.
22
+ const shared = { background: "--dolly-background", color: "--dolly-color", accent: "--dolly-accent" };
23
+ const paletteProperties = Array.from({ length: 16 }, (_, index) => `--dolly-display-palette-${index}`);
24
+ const themeProperties = [...Object.values(shared), ...paletteProperties];
25
+ function readPalette(style, rgb) {
26
+ const read = (name, fallback) => {
27
+ const value = style.getPropertyValue(name).trim();
28
+ return CSS.supports("color", value) ? value : fallback;
29
+ };
30
+ const background = read(shared.background, "#262626"), color = read(shared.color, "#e8e3d7"),
31
+ accent = read(shared.accent, "#f2d45c");
32
+ const unset = [background, color, color, accent, color, color, color, color,
33
+ "#77736c", color, color, accent, color, color, color, color];
34
+ return { background: rgb(background), foreground: rgb(color), cursor: rgb(accent),
35
+ colors: paletteProperties.map((name, index) => rgb(read(name, unset[index]))) };
36
+ }
37
+
12
38
  // The Worker's end of the display mailbox: it reads frames, the terminal's
13
39
  // geometry and its selection, and writes the surface and the animation frame.
14
40
  export class DisplayTransport {
@@ -36,9 +62,7 @@ export class DisplayTransport {
36
62
  static surfaceWidth = A.DOLLY_DISPLAY_WORD_SURFACE_WIDTH;
37
63
  static surfaceHeight = A.DOLLY_DISPLAY_WORD_SURFACE_HEIGHT;
38
64
  static surfaceScaleMilli = A.DOLLY_DISPLAY_WORD_SURFACE_SCALE_MILLI;
39
-
40
- static copyAvailable = 1;
41
- static copyTruncated = 2;
65
+ static paletteSequence = A.DOLLY_DISPLAY_WORD_PALETTE_SEQUENCE;
42
66
 
43
67
  // The copy buffer's size is the contract's; a test may shrink it.
44
68
  constructor(buffer, address, copyAddress, { copyCapacity = A.DOLLY_DISPLAY_COPY_CAPACITY } = {}) {
@@ -72,8 +96,8 @@ export class DisplayTransport {
72
96
  this.words,
73
97
  this.word + DisplayTransport.copyLength,
74
98
  ) >>> 0;
75
- if ((flags & DisplayTransport.copyAvailable) === 0) return null;
76
- if ((flags & DisplayTransport.copyTruncated) !== 0 ||
99
+ if ((flags & A.DOLLY_DISPLAY_COPY_AVAILABLE) === 0) return null;
100
+ if ((flags & A.DOLLY_DISPLAY_COPY_TRUNCATED) !== 0 ||
77
101
  length > this.copyCapacity) {
78
102
  throw new Error("Dolly selection exceeds the clipboard bridge capacity");
79
103
  }
@@ -99,8 +123,21 @@ export class DisplayTransport {
99
123
  Atomics.add(this.words, this.word + DisplayTransport.surfaceSequence, 1);
100
124
  }
101
125
 
126
+ // The terminal's colours, 0xRRGGBB each: { background, foreground, cursor, colors: [16] }.
127
+ publishPalette({ background, foreground, cursor, colors }) {
128
+ const words = [background, foreground, cursor, ...colors];
129
+ if (colors.length !== 16 || !words.every(word => Number.isInteger(word) && word >= 0 && word <= 0xffffff)) {
130
+ throw new Error("Dolly display palette must be 19 colours of 0xRRGGBB");
131
+ }
132
+ Atomics.add(this.words, this.word + DisplayTransport.paletteSequence, 1);
133
+ for (const [index, word] of words.entries()) {
134
+ Atomics.store(this.words, this.word + A.DOLLY_DISPLAY_WORD_PALETTE_BACKGROUND + index, word);
135
+ }
136
+ Atomics.add(this.words, this.word + DisplayTransport.paletteSequence, 1);
137
+ }
138
+
102
139
  graphicsActive() {
103
- return (Atomics.load(this.words, this.word + DisplayTransport.flags) & 1) !== 0;
140
+ return (Atomics.load(this.words, this.word + DisplayTransport.flags) & A.DOLLY_DISPLAY_GRAPHICS_ACTIVE) !== 0;
104
141
  }
105
142
 
106
143
  publishAnimationFrame() {
@@ -224,7 +261,7 @@ export class FramebufferPresenter {
224
261
  const stride = Atomics.load(words, word + DisplayTransport.frameStride) >>> 0;
225
262
  const length = stride * height;
226
263
  const address = this.frameAddresses[index];
227
- if (index > 1 || width === 0 || height === 0 || stride !== width * 4 ||
264
+ if (index >= A.DOLLY_DISPLAY_FRAME_COUNT || width === 0 || height === 0 || stride !== width * 4 ||
228
265
  length > this.capacity || address + length > this.buffer.byteLength) {
229
266
  throw new Error("Dolly published an invalid framebuffer");
230
267
  }
@@ -237,6 +274,9 @@ export class FramebufferPresenter {
237
274
  this.image.data.set(this.frames[index] ??= new Uint8ClampedArray(this.buffer, address, length));
238
275
  const after = Atomics.load(words, word + DisplayTransport.frameSequence) >>> 0;
239
276
  if (before !== after) continue;
277
+ // A frame is opaque, as a window is: the fourth byte of a pixel is not shown.
278
+ const pixels = this.image.data;
279
+ for (let alpha = 3; alpha < pixels.length; alpha += 4) pixels[alpha] = 255;
240
280
  if (this.surface.width !== width || this.surface.height !== height) {
241
281
  this.surface.width = width;
242
282
  this.surface.height = height;
@@ -251,12 +291,34 @@ export class FramebufferPresenter {
251
291
  }
252
292
 
253
293
  // 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 }) {
294
+ // surface it measures, the colours it reads, and the copy chord. What the
295
+ // terminal publishes (its grid, geometry, cursor, the lease) the Worker
296
+ // reports by message.
297
+ export function browser({ canvas, keyboard, fatal, showStatus, send, ask }) {
257
298
  const surface = canvas.transferControlToOffscreen();
258
299
  let state, resizeObserver;
259
- const publishSurface = () => send({ type: "display-surface", width: mount.clientWidth, height: mount.clientHeight,
300
+ // A CSS colour as 0xRRGGBB, as the page paints it.
301
+ const probe = new OffscreenCanvas(1, 1).getContext("2d", { willReadFrequently: true });
302
+ const rgb = color => {
303
+ probe.clearRect(0, 0, 1, 1);
304
+ probe.fillStyle = color;
305
+ probe.fillRect(0, 0, 1, 1);
306
+ const [red, green, blue] = probe.getImageData(0, 0, 1, 1).data;
307
+ return red << 16 | green << 8 | blue;
308
+ };
309
+ let palette = readPalette(getComputedStyle(canvas), rgb);
310
+ const publishPalette = () => {
311
+ const read = readPalette(getComputedStyle(canvas), rgb);
312
+ if (JSON.stringify(read) === JSON.stringify(palette)) return;
313
+ palette = read;
314
+ send({ type: "display-palette", palette });
315
+ };
316
+ // A change of the properties, by whatever rule or media query, ends a
317
+ // transition of them on the canvas, which shows nothing.
318
+ canvas.style.transition = themeProperties.map(name => `${name} 1ms allow-discrete`).join(", ");
319
+ canvas.addEventListener("transitionend", publishPalette);
320
+ // The terminal is the canvas's box: what the element holds beside it (input@0's bar) is not.
321
+ const publishSurface = () => send({ type: "display-surface", width: canvas.clientWidth, height: canvas.clientHeight,
260
322
  devicePixelRatio });
261
323
  const copySelection = () => ask({ type: "display-copy" });
262
324
 
@@ -287,7 +349,7 @@ export function browser({ mount, canvas, keyboard, fatal, showStatus, send, ask
287
349
  copySelection,
288
350
  };
289
351
  return {
290
- configuration: { canvas: surface }, transfers: [surface],
352
+ configuration: { canvas: surface, palette }, transfers: [surface],
291
353
  page: {
292
354
  get transport() { return state && transport; },
293
355
  get graphicsActive() { return state.graphicsActive; },
@@ -309,7 +371,7 @@ export function browser({ mount, canvas, keyboard, fatal, showStatus, send, ask
309
371
  messages: {
310
372
  "display-state"(message) {
311
373
  const dataset = document.documentElement.dataset;
312
- if (message.cursor !== state?.cursor) canvas.style.cursor = cursorStyles[message.cursor] ?? "default";
374
+ if (message.cursor !== state?.cursor) canvas.style.cursor = cursorStyles.get(message.cursor) ?? "default";
313
375
  if (message.frame !== state?.frame) dataset.frameSequence = message.frame;
314
376
  if (message.cols !== state?.cols) dataset.terminalCols = message.cols;
315
377
  if (message.rows !== state?.rows) dataset.terminalRows = message.rows;
@@ -326,12 +388,14 @@ export function browser({ mount, canvas, keyboard, fatal, showStatus, send, ask
326
388
  });
327
389
  },
328
390
  },
329
- // The initial surface, then every change of the terminal area.
391
+ // The initial surface, then every change of the terminal area; the colours
392
+ // as they are now, for a hidden canvas transitions nothing.
330
393
  entryStarted() {
394
+ canvas.hidden = false;
331
395
  publishSurface();
396
+ publishPalette();
332
397
  resizeObserver = new ResizeObserver(publishSurface);
333
- resizeObserver.observe(mount);
334
- canvas.hidden = false;
398
+ resizeObserver.observe(canvas);
335
399
  document.documentElement.dataset.terminal = "ghostty-rgba-wasm";
336
400
  },
337
401
  dispose() { resizeObserver?.disconnect(); },
@@ -368,6 +432,7 @@ export function worker({ get, send, configuration }) {
368
432
  },
369
433
  messages: {
370
434
  "display-surface"({ width, height, devicePixelRatio }) { transport?.publishSurface(width, height, devicePixelRatio); },
435
+ "display-palette"({ palette }) { transport?.publishPalette(palette); },
371
436
  "display-copy"() { return transport.copySelection(); },
372
437
  "display-frame"() { return presenter?.image ?? null; },
373
438
  },
@@ -380,16 +445,19 @@ export function worker({ get, send, configuration }) {
380
445
  throw new Error("invalid resident display plugin range");
381
446
  }
382
447
  const display = get("runtime").kernelPlugin(new Uint8Array(memory.buffer, address, size).slice(), kernel);
383
- const getDriver = display.exports.dolly_display_driver_get_v5;
448
+ const getDriver = display.exports.dolly_display_driver_get_v6;
449
+ const mailbox = Number(kernel.dolly_display_mailbox_address());
450
+ transport = new DisplayTransport(memory.buffer, mailbox, Number(kernel.dolly_display_copy_buffer_address()));
451
+ // The driver draws its first frame in the page's colours.
452
+ transport.publishPalette(configuration.palette);
384
453
  if (typeof getDriver !== "function" || kernel.dolly_display_install(getDriver()) !== 0) {
385
454
  throw new Error("Dolly display installation failed");
386
455
  }
387
- const mailbox = Number(kernel.dolly_display_mailbox_address());
388
- transport = new DisplayTransport(memory.buffer, mailbox, Number(kernel.dolly_display_copy_buffer_address()));
389
456
  words = transport.words;
390
457
  word = transport.word;
391
458
  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)));
459
+ const frameAddresses = Array.from({ length: A.DOLLY_DISPLAY_FRAME_COUNT },
460
+ (_, index) => Number(kernel.dolly_display_framebuffer_address(index)));
393
461
  if (frameAddresses.some(address => !Number.isSafeInteger(address) || address <= 0 || address > limit - capacity)) {
394
462
  throw new Error("invalid display framebuffer range");
395
463
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
- "version": "0.1.4",
2
+ "version": "0.1.5-rc.0",
3
3
  "type": "module",
4
4
  "name": "@daugasauron/dolly-host-display",
5
5
  "license": "MIT",
6
6
  "dependencies": {
7
- "@daugasauron/dolly": "0.1.4"
7
+ "@daugasauron/dolly": "0.1.5-rc.0"
8
8
  },
9
9
  "description": "Dolly's display@0 host module: the terminal and programs' frames; the selection on Ctrl+Shift+C.",
10
10
  "repository": {