@vui-rs/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/char-width.d.ts +7 -0
- package/dist/char-width.js +20 -0
- package/dist/color-names.js +46 -0
- package/dist/color.d.ts +5 -0
- package/dist/color.js +57 -0
- package/dist/image-decode.d.ts +21 -0
- package/dist/image-decode.js +42 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.js +27 -0
- package/dist/keys.d.ts +76 -0
- package/dist/keys.js +373 -0
- package/dist/named-colors.d.ts +7 -0
- package/dist/named-colors.js +20 -0
- package/dist/native/darwin-arm64/libvui_core.dylib +0 -0
- package/dist/native/darwin-x64/libvui_core.dylib +0 -0
- package/dist/native/ffi-symbols.d.ts +453 -0
- package/dist/native/ffi-symbols.js +680 -0
- package/dist/native/linux-arm64/libvui_core.so +0 -0
- package/dist/native/linux-x64/libvui_core.so +0 -0
- package/dist/native/load-native-lib.d.ts +384 -0
- package/dist/native/load-native-lib.js +63 -0
- package/dist/native/win32-x64/vui_core.dll +0 -0
- package/dist/node.d.ts +61 -0
- package/dist/node.js +157 -0
- package/dist/offscreen-buffer.d.ts +28 -0
- package/dist/offscreen-buffer.js +73 -0
- package/dist/renderer.d.ts +106 -0
- package/dist/renderer.js +186 -0
- package/dist/style.d.ts +48 -0
- package/dist/style.js +134 -0
- package/dist/terminal-session.d.ts +43 -0
- package/dist/terminal-session.js +82 -0
- package/dist/text/edit-buffer.d.ts +31 -0
- package/dist/text/edit-buffer.js +96 -0
- package/dist/text/editor-view.d.ts +22 -0
- package/dist/text/editor-view.js +48 -0
- package/dist/text/index.d.ts +5 -0
- package/dist/text/index.js +5 -0
- package/dist/text/text-buffer-view.d.ts +22 -0
- package/dist/text/text-buffer-view.js +49 -0
- package/dist/text/text-buffer.d.ts +16 -0
- package/dist/text/text-buffer.js +43 -0
- package/package.json +46 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { TextStyle } from "./renderer.js";
|
|
2
|
+
import { Pointer } from "bun:ffi";
|
|
3
|
+
|
|
4
|
+
//#region src/offscreen-buffer.d.ts
|
|
5
|
+
declare class OffscreenBuffer {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(width: number, height: number);
|
|
8
|
+
get width(): number;
|
|
9
|
+
get height(): number;
|
|
10
|
+
/** The native `*mut CellBuffer`. Used by `Renderer.blit`; do not retain across `free()`. */
|
|
11
|
+
get nativePtr(): Pointer;
|
|
12
|
+
drawText(x: number, y: number, text: string, style?: TextStyle): void;
|
|
13
|
+
fillRect(x: number, y: number, w: number, h: number, bg: number): void;
|
|
14
|
+
setCell(x: number, y: number, ch: number, style?: TextStyle): void;
|
|
15
|
+
clear(bg?: number): void;
|
|
16
|
+
/** Reallocate to a new size (clears it). Any previously fetched view dangles after. */
|
|
17
|
+
resize(width: number, height: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Zero-copy `Uint8Array` view over the native cells (stride `CELL_BYTES`). The
|
|
20
|
+
* view aliases native memory and dangles after `resize()`/`free()` — fetch a
|
|
21
|
+
* fresh one each time. Same wide-glyph pairing caveat as `Renderer.backBufferView`.
|
|
22
|
+
*/
|
|
23
|
+
view(): Uint8Array;
|
|
24
|
+
/** Free the native buffer. Idempotent; the instance is unusable after. */
|
|
25
|
+
free(): void;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { OffscreenBuffer };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Status } from "./native/ffi-symbols.js";
|
|
2
|
+
import { loadNativeLib } from "./native/load-native-lib.js";
|
|
3
|
+
import "./renderer.js";
|
|
4
|
+
import { toArrayBuffer } from "bun:ffi";
|
|
5
|
+
//#region src/offscreen-buffer.ts
|
|
6
|
+
const DEFAULT_FG = 3857049087;
|
|
7
|
+
const DEFAULT_BG = 255;
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
function check(status, op) {
|
|
10
|
+
if (status !== Status.OK) throw new Error(`vui-core offscreen ${op} failed with status ${status}`);
|
|
11
|
+
}
|
|
12
|
+
var OffscreenBuffer = class {
|
|
13
|
+
#lib = loadNativeLib();
|
|
14
|
+
#ptr;
|
|
15
|
+
#width;
|
|
16
|
+
#height;
|
|
17
|
+
constructor(width, height) {
|
|
18
|
+
const ptr = this.#lib.symbols.vui_cbuf_new(width, height);
|
|
19
|
+
if (ptr === null) throw new Error("vui-core: failed to allocate offscreen buffer");
|
|
20
|
+
this.#ptr = ptr;
|
|
21
|
+
this.#width = width;
|
|
22
|
+
this.#height = height;
|
|
23
|
+
}
|
|
24
|
+
get width() {
|
|
25
|
+
return this.#width;
|
|
26
|
+
}
|
|
27
|
+
get height() {
|
|
28
|
+
return this.#height;
|
|
29
|
+
}
|
|
30
|
+
/** The native `*mut CellBuffer`. Used by `Renderer.blit`; do not retain across `free()`. */
|
|
31
|
+
get nativePtr() {
|
|
32
|
+
return this.#ptr;
|
|
33
|
+
}
|
|
34
|
+
drawText(x, y, text, style = {}) {
|
|
35
|
+
const bytes = encoder.encode(text);
|
|
36
|
+
check(this.#lib.symbols.vui_cbuf_draw_text(this.#ptr, x, y, bytes, bytes.byteLength, style.fg ?? DEFAULT_FG, style.bg ?? DEFAULT_BG, style.attrs ?? 0), "draw_text");
|
|
37
|
+
}
|
|
38
|
+
fillRect(x, y, w, h, bg) {
|
|
39
|
+
check(this.#lib.symbols.vui_cbuf_fill_rect(this.#ptr, x, y, w, h, bg), "fill_rect");
|
|
40
|
+
}
|
|
41
|
+
setCell(x, y, ch, style = {}) {
|
|
42
|
+
check(this.#lib.symbols.vui_cbuf_set_cell(this.#ptr, x, y, ch, style.fg ?? DEFAULT_FG, style.bg ?? DEFAULT_BG, style.attrs ?? 0), "set_cell");
|
|
43
|
+
}
|
|
44
|
+
clear(bg = DEFAULT_BG) {
|
|
45
|
+
check(this.#lib.symbols.vui_cbuf_clear(this.#ptr, bg), "clear");
|
|
46
|
+
}
|
|
47
|
+
/** Reallocate to a new size (clears it). Any previously fetched view dangles after. */
|
|
48
|
+
resize(width, height) {
|
|
49
|
+
check(this.#lib.symbols.vui_cbuf_resize(this.#ptr, width, height), "resize");
|
|
50
|
+
this.#width = width;
|
|
51
|
+
this.#height = height;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Zero-copy `Uint8Array` view over the native cells (stride `CELL_BYTES`). The
|
|
55
|
+
* view aliases native memory and dangles after `resize()`/`free()` — fetch a
|
|
56
|
+
* fresh one each time. Same wide-glyph pairing caveat as `Renderer.backBufferView`.
|
|
57
|
+
*/
|
|
58
|
+
view() {
|
|
59
|
+
const ptr = this.#lib.symbols.vui_cbuf_ptr(this.#ptr);
|
|
60
|
+
if (ptr === null) throw new Error("vui-core: offscreen buffer pointer is null");
|
|
61
|
+
const cells = Number(this.#lib.symbols.vui_cbuf_len(this.#ptr));
|
|
62
|
+
return new Uint8Array(toArrayBuffer(ptr, 0, cells * 16));
|
|
63
|
+
}
|
|
64
|
+
/** Free the native buffer. Idempotent; the instance is unusable after. */
|
|
65
|
+
free() {
|
|
66
|
+
if (this.#ptr !== null) {
|
|
67
|
+
this.#lib.symbols.vui_cbuf_free(this.#ptr);
|
|
68
|
+
this.#ptr = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
export { OffscreenBuffer };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { VuiNode } from "./node.js";
|
|
2
|
+
import { OffscreenBuffer } from "./offscreen-buffer.js";
|
|
3
|
+
import { TextBufferView } from "./text/text-buffer-view.js";
|
|
4
|
+
import { EditorView } from "./text/editor-view.js";
|
|
5
|
+
|
|
6
|
+
//#region src/renderer.d.ts
|
|
7
|
+
/** Pack 8-bit channels into the `0xRRGGBBAA` u32 the FFI expects. */
|
|
8
|
+
declare function rgba(r: number, g: number, b: number, a?: number): number;
|
|
9
|
+
interface TextStyle {
|
|
10
|
+
fg?: number;
|
|
11
|
+
bg?: number;
|
|
12
|
+
attrs?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Half-open clip rect `[x0,x1) × [y0,y1)`; the JS twin of the native `ClipRect`. */
|
|
15
|
+
interface ClipRect {
|
|
16
|
+
x0: number;
|
|
17
|
+
y0: number;
|
|
18
|
+
x1: number;
|
|
19
|
+
y1: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Thin, safe wrapper over the native renderer. Owns a `*mut Renderer` and
|
|
23
|
+
* mirrors the draw primitives. Drawing mutates the back buffer; `render()`
|
|
24
|
+
* diffs it against the screen and writes the minimal frame to stdout.
|
|
25
|
+
*
|
|
26
|
+
* Call `free()` exactly once when done — the instance must not be used after.
|
|
27
|
+
*/
|
|
28
|
+
declare class Renderer {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(width: number, height: number);
|
|
31
|
+
get width(): number;
|
|
32
|
+
get height(): number;
|
|
33
|
+
drawText(x: number, y: number, text: string, style?: TextStyle): void;
|
|
34
|
+
fillRect(x: number, y: number, w: number, h: number, bg: number): void;
|
|
35
|
+
setCell(x: number, y: number, ch: number, style?: TextStyle): void;
|
|
36
|
+
clear(bg?: number): void;
|
|
37
|
+
drawTextClipped(x: number, y: number, text: string, style: TextStyle, clip: ClipRect): void;
|
|
38
|
+
fillRectClipped(x: number, y: number, w: number, h: number, bg: number, clip: ClipRect): void;
|
|
39
|
+
setCellClipped(x: number, y: number, ch: number, style: TextStyle, clip: ClipRect): void;
|
|
40
|
+
/** Composite an offscreen buffer into the back buffer at `(dstX, dstY)`, clipped. */
|
|
41
|
+
blit(src: OffscreenBuffer, dstX: number, dstY: number, clip: ClipRect): void;
|
|
42
|
+
/** Draw a native text-buffer view into the back buffer, clipped. */
|
|
43
|
+
drawTextBuffer(view: TextBufferView, x: number, y: number, style: TextStyle, clip: ClipRect): void;
|
|
44
|
+
/** Draw a native editor view, including its cursor when focused. */
|
|
45
|
+
drawEditor(view: EditorView, x: number, y: number, style: TextStyle & {
|
|
46
|
+
cursorBg?: number;
|
|
47
|
+
}, clip: ClipRect): void;
|
|
48
|
+
/** Diff the back buffer and write the frame to stdout. */
|
|
49
|
+
render(): void;
|
|
50
|
+
/**
|
|
51
|
+
* JS-host emit: diff + write the back buffer exactly as it was drawn, WITHOUT
|
|
52
|
+
* composing the native node tree. The JS paint walk clears + stamps the back
|
|
53
|
+
* buffer (via the clip-aware prims), then calls this to flush the frame.
|
|
54
|
+
*/
|
|
55
|
+
flush(): void;
|
|
56
|
+
/** Drop all staged OSC 8 links; call before re-staging a frame's link table. */
|
|
57
|
+
clearLinks(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Stage raw escape bytes to emit out-of-band on the next frame (image transmit,
|
|
60
|
+
* OSC 52 clipboard). Host-built sequences ONLY — never user text. A non-empty
|
|
61
|
+
* channel forces a frame; it clears after emit. Multiple calls concatenate.
|
|
62
|
+
*/
|
|
63
|
+
stagePassthrough(bytes: Uint8Array): void;
|
|
64
|
+
/** Drop all Kitty image placements; call before re-staging a frame's placements. */
|
|
65
|
+
clearImagePlacements(): void;
|
|
66
|
+
/** Register image `id`'s on-screen top-left cell for placeholder placement. */
|
|
67
|
+
stageImagePlacement(id: number, x0: number, y0: number): void;
|
|
68
|
+
/**
|
|
69
|
+
* Stage one OSC 8 link table entry (`id` → URI). The emitter wraps cells whose
|
|
70
|
+
* `attrs` high byte equals `id` in the hyperlink. `id` 0 is "no link" (ignored).
|
|
71
|
+
*/
|
|
72
|
+
stageLink(id: number, uri: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* The implicit root node, created with the renderer and sized to the terminal.
|
|
75
|
+
* Build the UI as its descendants; `render()` lays out and paints the tree.
|
|
76
|
+
*/
|
|
77
|
+
rootNode(): VuiNode;
|
|
78
|
+
/** Create a detached node (`"box"`, `"text"`, or `"edit"`); attach under a parent. */
|
|
79
|
+
createNode(kind: 'box' | 'text' | 'edit'): VuiNode;
|
|
80
|
+
/** Native structural hash of the tree; compare to `hostTreeHash` for desync. */
|
|
81
|
+
treeHash(): bigint;
|
|
82
|
+
/**
|
|
83
|
+
* Run taffy layout over the node tree (JS-host path) WITHOUT painting, sizing
|
|
84
|
+
* to the terminal by default. Read each node's box with `VuiNode.layoutRect`.
|
|
85
|
+
* Dirty-gate on the caller side (skip when no style/text changed).
|
|
86
|
+
*/
|
|
87
|
+
computeLayout(width?: number, height?: number): void;
|
|
88
|
+
/** Reallocate to a new size; forces a full repaint on the next `render()`. */
|
|
89
|
+
resize(width: number, height: number): void;
|
|
90
|
+
/**
|
|
91
|
+
* Zero-copy `Uint8Array` view over the native back buffer (stride
|
|
92
|
+
* `CELL_BYTES`). The view aliases native memory and is NOT lifetime-tracked:
|
|
93
|
+
* it dangles after `resize()` (reallocates) or `free()` (deallocates). Do not
|
|
94
|
+
* retain a view across either call — fetch a fresh one each time. Intended for
|
|
95
|
+
* bulk writes and tests; prefer the draw methods for normal use. Raw writes
|
|
96
|
+
* must uphold the wide-glyph pairing invariant the draw methods maintain: a
|
|
97
|
+
* WIDE_CONTINUATION cell sits immediately right of a width-2 leader and only
|
|
98
|
+
* there — so when clearing a continuation cell, also blank its leader (and
|
|
99
|
+
* vice versa), or a half-glyph can linger on screen.
|
|
100
|
+
*/
|
|
101
|
+
backBufferView(): Uint8Array;
|
|
102
|
+
/** Free the native renderer. Idempotent; the instance is unusable after. */
|
|
103
|
+
free(): void;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { ClipRect, Renderer, TextStyle, rgba };
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { NodeKindCode, Status } from "./native/ffi-symbols.js";
|
|
2
|
+
import { loadNativeLib } from "./native/load-native-lib.js";
|
|
3
|
+
import { VuiNode } from "./node.js";
|
|
4
|
+
import "./offscreen-buffer.js";
|
|
5
|
+
import { toArrayBuffer } from "bun:ffi";
|
|
6
|
+
//#region src/renderer.ts
|
|
7
|
+
/** Pack 8-bit channels into the `0xRRGGBBAA` u32 the FFI expects. */
|
|
8
|
+
function rgba(r, g, b, a = 255) {
|
|
9
|
+
return ((r & 255) << 24 | (g & 255) << 16 | (b & 255) << 8 | a & 255) >>> 0;
|
|
10
|
+
}
|
|
11
|
+
const DEFAULT_FG = rgba(229, 229, 229);
|
|
12
|
+
const DEFAULT_BG = rgba(0, 0, 0);
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
function check(status, op) {
|
|
15
|
+
if (status !== Status.OK) throw new Error(`vui-core ${op} failed with status ${status}`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thin, safe wrapper over the native renderer. Owns a `*mut Renderer` and
|
|
19
|
+
* mirrors the draw primitives. Drawing mutates the back buffer; `render()`
|
|
20
|
+
* diffs it against the screen and writes the minimal frame to stdout.
|
|
21
|
+
*
|
|
22
|
+
* Call `free()` exactly once when done — the instance must not be used after.
|
|
23
|
+
*/
|
|
24
|
+
var Renderer = class {
|
|
25
|
+
#lib = loadNativeLib();
|
|
26
|
+
#ptr;
|
|
27
|
+
#width;
|
|
28
|
+
#height;
|
|
29
|
+
/** Reused scratch for clip rects passed to the native clipped prims (no per-op alloc). */
|
|
30
|
+
#clip = new Int32Array(4);
|
|
31
|
+
constructor(width, height) {
|
|
32
|
+
const ptr = this.#lib.symbols.vui_renderer_new(width, height);
|
|
33
|
+
if (ptr === null) throw new Error("vui-core: failed to allocate renderer");
|
|
34
|
+
this.#ptr = ptr;
|
|
35
|
+
this.#width = width;
|
|
36
|
+
this.#height = height;
|
|
37
|
+
}
|
|
38
|
+
get width() {
|
|
39
|
+
return this.#width;
|
|
40
|
+
}
|
|
41
|
+
get height() {
|
|
42
|
+
return this.#height;
|
|
43
|
+
}
|
|
44
|
+
drawText(x, y, text, style = {}) {
|
|
45
|
+
const bytes = encoder.encode(text);
|
|
46
|
+
check(this.#lib.symbols.vui_buffer_draw_text(this.#ptr, x, y, bytes, bytes.byteLength, style.fg ?? DEFAULT_FG, style.bg ?? DEFAULT_BG, style.attrs ?? 0), "draw_text");
|
|
47
|
+
}
|
|
48
|
+
fillRect(x, y, w, h, bg) {
|
|
49
|
+
check(this.#lib.symbols.vui_buffer_fill_rect(this.#ptr, x, y, w, h, bg), "fill_rect");
|
|
50
|
+
}
|
|
51
|
+
setCell(x, y, ch, style = {}) {
|
|
52
|
+
check(this.#lib.symbols.vui_buffer_set_cell(this.#ptr, x, y, ch, style.fg ?? DEFAULT_FG, style.bg ?? DEFAULT_BG, style.attrs ?? 0), "set_cell");
|
|
53
|
+
}
|
|
54
|
+
clear(bg = DEFAULT_BG) {
|
|
55
|
+
check(this.#lib.symbols.vui_buffer_clear(this.#ptr, bg), "clear");
|
|
56
|
+
}
|
|
57
|
+
#packClip(clip) {
|
|
58
|
+
this.#clip[0] = clip.x0;
|
|
59
|
+
this.#clip[1] = clip.y0;
|
|
60
|
+
this.#clip[2] = clip.x1;
|
|
61
|
+
this.#clip[3] = clip.y1;
|
|
62
|
+
return this.#clip;
|
|
63
|
+
}
|
|
64
|
+
drawTextClipped(x, y, text, style, clip) {
|
|
65
|
+
const bytes = encoder.encode(text);
|
|
66
|
+
check(this.#lib.symbols.vui_buffer_draw_text_clipped(this.#ptr, x, y, bytes, bytes.byteLength, style.fg ?? DEFAULT_FG, style.bg ?? DEFAULT_BG, style.attrs ?? 0, this.#packClip(clip)), "draw_text_clipped");
|
|
67
|
+
}
|
|
68
|
+
fillRectClipped(x, y, w, h, bg, clip) {
|
|
69
|
+
check(this.#lib.symbols.vui_buffer_fill_rect_clipped(this.#ptr, x, y, w, h, bg, this.#packClip(clip)), "fill_rect_clipped");
|
|
70
|
+
}
|
|
71
|
+
setCellClipped(x, y, ch, style, clip) {
|
|
72
|
+
check(this.#lib.symbols.vui_buffer_set_cell_clipped(this.#ptr, x, y, ch, style.fg ?? DEFAULT_FG, style.bg ?? DEFAULT_BG, style.attrs ?? 0, this.#packClip(clip)), "set_cell_clipped");
|
|
73
|
+
}
|
|
74
|
+
/** Composite an offscreen buffer into the back buffer at `(dstX, dstY)`, clipped. */
|
|
75
|
+
blit(src, dstX, dstY, clip) {
|
|
76
|
+
check(this.#lib.symbols.vui_buffer_blit(this.#ptr, src.nativePtr, dstX, dstY, this.#packClip(clip)), "blit");
|
|
77
|
+
}
|
|
78
|
+
/** Draw a native text-buffer view into the back buffer, clipped. */
|
|
79
|
+
drawTextBuffer(view, x, y, style, clip) {
|
|
80
|
+
check(this.#lib.symbols.vui_buffer_draw_textbuffer(this.#ptr, view.nativePtr, x, y, style.fg ?? DEFAULT_FG, style.bg ?? 0, style.bg === void 0 ? 0 : 1, style.attrs ?? 0, this.#packClip(clip)), "draw_textbuffer");
|
|
81
|
+
}
|
|
82
|
+
/** Draw a native editor view, including its cursor when focused. */
|
|
83
|
+
drawEditor(view, x, y, style, clip) {
|
|
84
|
+
check(this.#lib.symbols.vui_buffer_draw_editor(this.#ptr, view.nativePtr, x, y, style.fg ?? DEFAULT_FG, style.bg ?? DEFAULT_BG, style.cursorBg ?? style.fg ?? DEFAULT_FG, style.attrs ?? 0, this.#packClip(clip)), "draw_editor");
|
|
85
|
+
}
|
|
86
|
+
/** Diff the back buffer and write the frame to stdout. */
|
|
87
|
+
render() {
|
|
88
|
+
check(this.#lib.symbols.vui_renderer_render(this.#ptr), "render");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* JS-host emit: diff + write the back buffer exactly as it was drawn, WITHOUT
|
|
92
|
+
* composing the native node tree. The JS paint walk clears + stamps the back
|
|
93
|
+
* buffer (via the clip-aware prims), then calls this to flush the frame.
|
|
94
|
+
*/
|
|
95
|
+
flush() {
|
|
96
|
+
check(this.#lib.symbols.vui_renderer_flush(this.#ptr), "flush");
|
|
97
|
+
}
|
|
98
|
+
/** Drop all staged OSC 8 links; call before re-staging a frame's link table. */
|
|
99
|
+
clearLinks() {
|
|
100
|
+
check(this.#lib.symbols.vui_renderer_clear_links(this.#ptr), "clear_links");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Stage raw escape bytes to emit out-of-band on the next frame (image transmit,
|
|
104
|
+
* OSC 52 clipboard). Host-built sequences ONLY — never user text. A non-empty
|
|
105
|
+
* channel forces a frame; it clears after emit. Multiple calls concatenate.
|
|
106
|
+
*/
|
|
107
|
+
stagePassthrough(bytes) {
|
|
108
|
+
if (bytes.byteLength === 0) return;
|
|
109
|
+
check(this.#lib.symbols.vui_renderer_stage_passthrough(this.#ptr, bytes, bytes.byteLength), "stage_passthrough");
|
|
110
|
+
}
|
|
111
|
+
/** Drop all Kitty image placements; call before re-staging a frame's placements. */
|
|
112
|
+
clearImagePlacements() {
|
|
113
|
+
check(this.#lib.symbols.vui_renderer_clear_image_placements(this.#ptr), "clear_image_placements");
|
|
114
|
+
}
|
|
115
|
+
/** Register image `id`'s on-screen top-left cell for placeholder placement. */
|
|
116
|
+
stageImagePlacement(id, x0, y0) {
|
|
117
|
+
check(this.#lib.symbols.vui_renderer_stage_image_placement(this.#ptr, id, x0, y0), "stage_image_placement");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Stage one OSC 8 link table entry (`id` → URI). The emitter wraps cells whose
|
|
121
|
+
* `attrs` high byte equals `id` in the hyperlink. `id` 0 is "no link" (ignored).
|
|
122
|
+
*/
|
|
123
|
+
stageLink(id, uri) {
|
|
124
|
+
const bytes = encoder.encode(uri);
|
|
125
|
+
check(this.#lib.symbols.vui_renderer_stage_link(this.#ptr, id, bytes, bytes.byteLength), "stage_link");
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The implicit root node, created with the renderer and sized to the terminal.
|
|
129
|
+
* Build the UI as its descendants; `render()` lays out and paints the tree.
|
|
130
|
+
*/
|
|
131
|
+
rootNode() {
|
|
132
|
+
const id = this.#lib.symbols.vui_renderer_set_root(this.#ptr);
|
|
133
|
+
return new VuiNode(this.#lib, this.#ptr, id, 0);
|
|
134
|
+
}
|
|
135
|
+
/** Create a detached node (`"box"`, `"text"`, or `"edit"`); attach under a parent. */
|
|
136
|
+
createNode(kind) {
|
|
137
|
+
const code = kind === "text" ? NodeKindCode.Text : kind === "edit" ? NodeKindCode.Edit : NodeKindCode.Box;
|
|
138
|
+
const id = this.#lib.symbols.vui_node_new(this.#ptr, code);
|
|
139
|
+
if (id === 0) throw new Error("vui-core: failed to create node");
|
|
140
|
+
return new VuiNode(this.#lib, this.#ptr, id, code);
|
|
141
|
+
}
|
|
142
|
+
/** Native structural hash of the tree; compare to `hostTreeHash` for desync. */
|
|
143
|
+
treeHash() {
|
|
144
|
+
return this.#lib.symbols.vui_debug_tree_hash(this.#ptr);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Run taffy layout over the node tree (JS-host path) WITHOUT painting, sizing
|
|
148
|
+
* to the terminal by default. Read each node's box with `VuiNode.layoutRect`.
|
|
149
|
+
* Dirty-gate on the caller side (skip when no style/text changed).
|
|
150
|
+
*/
|
|
151
|
+
computeLayout(width = this.#width, height = this.#height) {
|
|
152
|
+
check(this.#lib.symbols.vui_layout_compute(this.#ptr, width, height), "layout_compute");
|
|
153
|
+
}
|
|
154
|
+
/** Reallocate to a new size; forces a full repaint on the next `render()`. */
|
|
155
|
+
resize(width, height) {
|
|
156
|
+
check(this.#lib.symbols.vui_renderer_resize(this.#ptr, width, height), "resize");
|
|
157
|
+
this.#width = width;
|
|
158
|
+
this.#height = height;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Zero-copy `Uint8Array` view over the native back buffer (stride
|
|
162
|
+
* `CELL_BYTES`). The view aliases native memory and is NOT lifetime-tracked:
|
|
163
|
+
* it dangles after `resize()` (reallocates) or `free()` (deallocates). Do not
|
|
164
|
+
* retain a view across either call — fetch a fresh one each time. Intended for
|
|
165
|
+
* bulk writes and tests; prefer the draw methods for normal use. Raw writes
|
|
166
|
+
* must uphold the wide-glyph pairing invariant the draw methods maintain: a
|
|
167
|
+
* WIDE_CONTINUATION cell sits immediately right of a width-2 leader and only
|
|
168
|
+
* there — so when clearing a continuation cell, also blank its leader (and
|
|
169
|
+
* vice versa), or a half-glyph can linger on screen.
|
|
170
|
+
*/
|
|
171
|
+
backBufferView() {
|
|
172
|
+
const ptr = this.#lib.symbols.vui_renderer_back_buffer_ptr(this.#ptr);
|
|
173
|
+
if (ptr === null) throw new Error("vui-core: back buffer pointer is null");
|
|
174
|
+
const cells = Number(this.#lib.symbols.vui_renderer_buffer_len(this.#ptr));
|
|
175
|
+
return new Uint8Array(toArrayBuffer(ptr, 0, cells * 16));
|
|
176
|
+
}
|
|
177
|
+
/** Free the native renderer. Idempotent; the instance is unusable after. */
|
|
178
|
+
free() {
|
|
179
|
+
if (this.#ptr !== null) {
|
|
180
|
+
this.#lib.symbols.vui_renderer_free(this.#ptr);
|
|
181
|
+
this.#ptr = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
//#endregion
|
|
186
|
+
export { Renderer, rgba };
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/style.d.ts
|
|
2
|
+
/** A length in cells (number), a percentage, or `auto`. */
|
|
3
|
+
type Dim = number | 'auto' | {
|
|
4
|
+
pct: number;
|
|
5
|
+
};
|
|
6
|
+
/** One side value, or per-side values; a scalar applies to all four sides. */
|
|
7
|
+
type Sides = Dim | {
|
|
8
|
+
left?: Dim;
|
|
9
|
+
right?: Dim;
|
|
10
|
+
top?: Dim;
|
|
11
|
+
bottom?: Dim;
|
|
12
|
+
};
|
|
13
|
+
type AlignValue = 'start' | 'end' | 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch';
|
|
14
|
+
type JustifyValue = 'start' | 'end' | 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-evenly' | 'space-around';
|
|
15
|
+
interface VuiStyle {
|
|
16
|
+
display?: 'flex' | 'none';
|
|
17
|
+
position?: 'relative' | 'absolute';
|
|
18
|
+
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
|
|
19
|
+
flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
|
|
20
|
+
alignItems?: AlignValue;
|
|
21
|
+
alignSelf?: AlignValue;
|
|
22
|
+
justifyContent?: JustifyValue;
|
|
23
|
+
flexGrow?: number;
|
|
24
|
+
flexShrink?: number;
|
|
25
|
+
flexBasis?: Dim;
|
|
26
|
+
width?: Dim;
|
|
27
|
+
height?: Dim;
|
|
28
|
+
minWidth?: Dim;
|
|
29
|
+
minHeight?: Dim;
|
|
30
|
+
maxWidth?: Dim;
|
|
31
|
+
maxHeight?: Dim;
|
|
32
|
+
padding?: Sides;
|
|
33
|
+
margin?: Sides;
|
|
34
|
+
border?: Sides;
|
|
35
|
+
inset?: Sides;
|
|
36
|
+
gap?: number | {
|
|
37
|
+
width?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pack a `VuiStyle` into the native `StyleFfi` byte layout. Writes every field
|
|
43
|
+
* (seeding CSS defaults for omitted ones) so the buffer never carries stale
|
|
44
|
+
* bytes. Returns a `Uint8Array` to pass as the `*const StyleFfi` pointer arg.
|
|
45
|
+
*/
|
|
46
|
+
declare function packStyle(style: VuiStyle): Uint8Array;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { AlignValue, Dim, JustifyValue, Sides, VuiStyle, packStyle };
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import "./native/ffi-symbols.js";
|
|
2
|
+
//#region src/style.ts
|
|
3
|
+
const DIM_AUTO = 0;
|
|
4
|
+
const DIM_LENGTH = 1;
|
|
5
|
+
const DIM_PERCENT = 2;
|
|
6
|
+
const DISPLAY = {
|
|
7
|
+
flex: 0,
|
|
8
|
+
none: 1
|
|
9
|
+
};
|
|
10
|
+
const POSITION = {
|
|
11
|
+
relative: 0,
|
|
12
|
+
absolute: 1
|
|
13
|
+
};
|
|
14
|
+
const FLEX_DIR = {
|
|
15
|
+
row: 0,
|
|
16
|
+
column: 1,
|
|
17
|
+
"row-reverse": 2,
|
|
18
|
+
"column-reverse": 3
|
|
19
|
+
};
|
|
20
|
+
const FLEX_WRAP = {
|
|
21
|
+
nowrap: 0,
|
|
22
|
+
wrap: 1,
|
|
23
|
+
"wrap-reverse": 2
|
|
24
|
+
};
|
|
25
|
+
const ALIGN = {
|
|
26
|
+
start: 1,
|
|
27
|
+
end: 2,
|
|
28
|
+
"flex-start": 3,
|
|
29
|
+
"flex-end": 4,
|
|
30
|
+
center: 5,
|
|
31
|
+
baseline: 6,
|
|
32
|
+
stretch: 7,
|
|
33
|
+
"space-between": 8,
|
|
34
|
+
"space-evenly": 9,
|
|
35
|
+
"space-around": 10
|
|
36
|
+
};
|
|
37
|
+
/** Resolve a `Dim` to its `(kind, value)` pair. */
|
|
38
|
+
function dimParts(d) {
|
|
39
|
+
if (d === "auto") return [DIM_AUTO, 0];
|
|
40
|
+
if (typeof d === "number") return [DIM_LENGTH, d];
|
|
41
|
+
return [DIM_PERCENT, d.pct];
|
|
42
|
+
}
|
|
43
|
+
/** Expand a `Sides` shorthand into explicit per-side dims. */
|
|
44
|
+
function sides(s, fallback) {
|
|
45
|
+
if (s === void 0) return {
|
|
46
|
+
left: fallback,
|
|
47
|
+
right: fallback,
|
|
48
|
+
top: fallback,
|
|
49
|
+
bottom: fallback
|
|
50
|
+
};
|
|
51
|
+
if (typeof s === "number" || s === "auto" || "pct" in s) {
|
|
52
|
+
const d = s;
|
|
53
|
+
return {
|
|
54
|
+
left: d,
|
|
55
|
+
right: d,
|
|
56
|
+
top: d,
|
|
57
|
+
bottom: d
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const o = s;
|
|
61
|
+
return {
|
|
62
|
+
left: o.left ?? fallback,
|
|
63
|
+
right: o.right ?? fallback,
|
|
64
|
+
top: o.top ?? fallback,
|
|
65
|
+
bottom: o.bottom ?? fallback
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Pack a `VuiStyle` into the native `StyleFfi` byte layout. Writes every field
|
|
70
|
+
* (seeding CSS defaults for omitted ones) so the buffer never carries stale
|
|
71
|
+
* bytes. Returns a `Uint8Array` to pass as the `*const StyleFfi` pointer arg.
|
|
72
|
+
*/
|
|
73
|
+
function packStyle(style) {
|
|
74
|
+
const buf = /* @__PURE__ */ new ArrayBuffer(236);
|
|
75
|
+
const view = new DataView(buf);
|
|
76
|
+
let off = 0;
|
|
77
|
+
const u32 = (v) => {
|
|
78
|
+
view.setUint32(off, v >>> 0, true);
|
|
79
|
+
off += 4;
|
|
80
|
+
};
|
|
81
|
+
const f32 = (v) => {
|
|
82
|
+
view.setFloat32(off, v, true);
|
|
83
|
+
off += 4;
|
|
84
|
+
};
|
|
85
|
+
const dim = (d) => {
|
|
86
|
+
const [kind, value] = dimParts(d);
|
|
87
|
+
u32(kind);
|
|
88
|
+
f32(value);
|
|
89
|
+
};
|
|
90
|
+
u32(DISPLAY[style.display ?? "flex"]);
|
|
91
|
+
u32(POSITION[style.position ?? "relative"]);
|
|
92
|
+
u32(FLEX_DIR[style.flexDirection ?? "row"]);
|
|
93
|
+
u32(FLEX_WRAP[style.flexWrap ?? "nowrap"]);
|
|
94
|
+
u32(style.alignItems ? ALIGN[style.alignItems] : 0);
|
|
95
|
+
u32(style.alignSelf ? ALIGN[style.alignSelf] : 0);
|
|
96
|
+
u32(style.justifyContent ? ALIGN[style.justifyContent] : 0);
|
|
97
|
+
f32(style.flexGrow ?? 0);
|
|
98
|
+
f32(style.flexShrink ?? 1);
|
|
99
|
+
dim(style.flexBasis ?? "auto");
|
|
100
|
+
dim(style.width ?? "auto");
|
|
101
|
+
dim(style.height ?? "auto");
|
|
102
|
+
dim(style.minWidth ?? "auto");
|
|
103
|
+
dim(style.minHeight ?? "auto");
|
|
104
|
+
dim(style.maxWidth ?? "auto");
|
|
105
|
+
dim(style.maxHeight ?? "auto");
|
|
106
|
+
const pad = sides(style.padding, 0);
|
|
107
|
+
dim(pad.left);
|
|
108
|
+
dim(pad.right);
|
|
109
|
+
dim(pad.top);
|
|
110
|
+
dim(pad.bottom);
|
|
111
|
+
const mar = sides(style.margin, 0);
|
|
112
|
+
dim(mar.left);
|
|
113
|
+
dim(mar.right);
|
|
114
|
+
dim(mar.top);
|
|
115
|
+
dim(mar.bottom);
|
|
116
|
+
const bor = sides(style.border, 0);
|
|
117
|
+
dim(bor.left);
|
|
118
|
+
dim(bor.right);
|
|
119
|
+
dim(bor.top);
|
|
120
|
+
dim(bor.bottom);
|
|
121
|
+
const ins = sides(style.inset, "auto");
|
|
122
|
+
dim(ins.left);
|
|
123
|
+
dim(ins.right);
|
|
124
|
+
dim(ins.top);
|
|
125
|
+
dim(ins.bottom);
|
|
126
|
+
const gapW = typeof style.gap === "number" ? style.gap : style.gap?.width ?? 0;
|
|
127
|
+
const gapH = typeof style.gap === "number" ? style.gap : style.gap?.height ?? 0;
|
|
128
|
+
dim(gapW);
|
|
129
|
+
dim(gapH);
|
|
130
|
+
if (off !== 236) throw new Error(`packStyle wrote ${off} bytes, expected 236`);
|
|
131
|
+
return new Uint8Array(buf);
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
export { packStyle };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/terminal-session.d.ts
|
|
2
|
+
/** The slice of `process.stdin` this module needs (injectable for tests). */
|
|
3
|
+
interface InputStream {
|
|
4
|
+
isTTY?: boolean;
|
|
5
|
+
setRawMode?(mode: boolean): void;
|
|
6
|
+
resume?(): void;
|
|
7
|
+
pause?(): void;
|
|
8
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
9
|
+
off(event: string, listener: (...args: unknown[]) => void): void;
|
|
10
|
+
}
|
|
11
|
+
/** The slice of `process.stdout` this module needs (injectable for tests). */
|
|
12
|
+
interface OutputStream {
|
|
13
|
+
columns?: number;
|
|
14
|
+
rows?: number;
|
|
15
|
+
write(data: string): void;
|
|
16
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
17
|
+
off(event: string, listener: (...args: unknown[]) => void): void;
|
|
18
|
+
}
|
|
19
|
+
interface TerminalSessionOptions {
|
|
20
|
+
input?: InputStream;
|
|
21
|
+
output?: OutputStream;
|
|
22
|
+
/** Enter the alt screen on start / leave it on stop. Default true. */
|
|
23
|
+
altScreen?: boolean;
|
|
24
|
+
/** Wire exit/signal/uncaught handlers for guaranteed restore. Default true. */
|
|
25
|
+
installSignalHandlers?: boolean;
|
|
26
|
+
/** Push the Kitty keyboard protocol (disambiguate flag) on start. Default true. */
|
|
27
|
+
kittyKeyboard?: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface TerminalSession {
|
|
30
|
+
/** Enter raw mode + alt screen, begin emitting data/resize callbacks. */
|
|
31
|
+
start(): void;
|
|
32
|
+
onData(cb: (data: string) => void): void;
|
|
33
|
+
onResize(cb: (cols: number, rows: number) => void): void;
|
|
34
|
+
/** Idempotent restore: leave raw/alt-screen state, detach all listeners. */
|
|
35
|
+
stop(): void;
|
|
36
|
+
readonly size: {
|
|
37
|
+
cols: number;
|
|
38
|
+
rows: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
declare function createTerminalSession(options?: TerminalSessionOptions): TerminalSession;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { TerminalSession, TerminalSessionOptions, createTerminalSession };
|