@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.
- package/abi.mjs +25 -2
- package/display.mjs +91 -23
- 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 =
|
|
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 = "
|
|
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
|
-
//
|
|
6
|
-
const cursorStyles = [
|
|
7
|
-
|
|
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 &
|
|
76
|
-
if ((flags &
|
|
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) &
|
|
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
|
|
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
|
|
255
|
-
// grid, geometry, cursor, the lease) the Worker
|
|
256
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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": {
|