@cueloop/client 0.1.0-alpha.41 → 0.1.0-alpha.42

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/native/build.sh CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
  # Rebuild libcueloopvt.dylib: libghostty-vt (Ghostty's VT core) statically linked
3
- # behind the flat FFI shim in src/shim.c. Pinned to a Ghostty commit + Zig 0.16.0.
4
- # Output lands in native/<os>-<arch>/libcueloopvt.dylib. Requires curl, git, clang.
3
+ # behind the flat FFI shim. Pinned to a Ghostty commit + Zig 0.16.0.
4
+ # Output lands in native/<os>-<arch>/libcueloopvt.dylib. Requires curl and git.
5
5
  set -eu
6
6
 
7
7
  GHOSTTY_COMMIT="5834a0e3df621802e9578e4562d88b0c2ad4ada8"
@@ -10,47 +10,73 @@ ZIG_VERSION="0.16.0"
10
10
  here="$(cd "$(dirname "$0")" && pwd)"
11
11
  work="${TMPDIR:-/tmp}/cueloop-libghostty-build"
12
12
  mkdir -p "$work"
13
+ export ZIG_GLOBAL_CACHE_DIR="$work/zig-gcache"
14
+ export ZIG_LOCAL_CACHE_DIR="$work/zig-lcache"
13
15
  cd "$work"
14
16
 
15
- # host triple for the prebuilt dir + zig download
17
+ # Host triple for the Bun prebuilt dir and Zig download. The two tools use
18
+ # different architecture names.
16
19
  arch="$(uname -m)"; os="$(uname -s | tr '[:upper:]' '[:lower:]')"
17
20
  case "$os" in darwin) zig_os="macos"; dylib="libcueloopvt.dylib";; linux) zig_os="linux"; dylib="libcueloopvt.so";; *) echo "unsupported os $os" >&2; exit 1;; esac
18
- outdir="$here/${os}-${arch}"
21
+ case "$arch-$os" in
22
+ arm64-darwin|aarch64-linux) zig_arch="aarch64"; bun_arch="arm64";;
23
+ x86_64-darwin|x86_64-linux) zig_arch="x86_64"; bun_arch="x64";;
24
+ *) echo "unsupported architecture $arch on $os" >&2; exit 1;;
25
+ esac
26
+ case "$zig_arch-$zig_os" in
27
+ aarch64-macos) zig_sha="b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489";;
28
+ x86_64-macos) zig_sha="0387557ed1877bc6a2e1802c8391953baddba76081876301c522f52977b52ba7";;
29
+ aarch64-linux) zig_sha="ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17";;
30
+ x86_64-linux) zig_sha="70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00";;
31
+ esac
32
+ outdir="$here/${os}-${bun_arch}"
33
+ zig_archive="zig-$zig_arch-$zig_os-$ZIG_VERSION.tar.xz"
19
34
 
20
- if [ ! -x "zig-$arch-$zig_os-$ZIG_VERSION/zig" ]; then
35
+ if [ ! -f "$zig_archive" ]; then
21
36
  echo "== fetching zig $ZIG_VERSION =="
22
- curl -sSL -o zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-$arch-$zig_os-$ZIG_VERSION.tar.xz"
23
- tar -xf zig.tar.xz
37
+ curl -fsSL -o "$zig_archive" "https://ziglang.org/download/$ZIG_VERSION/$zig_archive"
24
38
  fi
25
- zig="$work/zig-$arch-$zig_os-$ZIG_VERSION/zig"
39
+ printf '%s %s\n' "$zig_sha" "$zig_archive" | shasum -a 256 -c -
26
40
 
27
41
  if [ ! -d ghostty ]; then
28
42
  echo "== cloning ghostty @ $GHOSTTY_COMMIT =="
29
43
  mkdir ghostty && cd ghostty && git init -q
30
44
  git remote add origin https://github.com/ghostty-org/ghostty.git
31
- git fetch -q --depth 1 origin "$GHOSTTY_COMMIT"
32
- git checkout -q FETCH_HEAD
33
45
  cd ..
34
46
  fi
47
+ if ! git -C ghostty cat-file -e "$GHOSTTY_COMMIT^{commit}" 2>/dev/null; then
48
+ git -C ghostty fetch -q --depth 1 origin "$GHOSTTY_COMMIT"
49
+ fi
50
+
51
+ # Build only from fresh trees materialized from the verified archive and exact
52
+ # Git object. Cached working-tree edits and stale install outputs cannot enter.
53
+ build_root="$(mktemp -d "$work/native-build.XXXXXX")"
54
+ trap 'rm -rf "$build_root"' EXIT HUP INT TERM
55
+ tar -xf "$zig_archive" -C "$build_root"
56
+ zig="$build_root/zig-$zig_arch-$zig_os-$ZIG_VERSION/zig"
57
+ ghostty_source="$build_root/ghostty"
58
+ mkdir "$ghostty_source"
59
+ git -C ghostty archive "$GHOSTTY_COMMIT" | tar -x -C "$ghostty_source"
35
60
 
36
61
  echo "== building libghostty-vt (ReleaseFast) =="
37
- cd ghostty
62
+ cd "$ghostty_source"
38
63
  # The final xcframework packaging step can fail while the .a we need is produced,
39
64
  # so tolerate a non-zero exit here but assert the static lib exists below.
40
- ZIG_GLOBAL_CACHE_DIR="$work/zig-gcache" ZIG_LOCAL_CACHE_DIR="$work/zig-lcache" \
41
- "$zig" build -Demit-lib-vt -Doptimize=ReleaseFast --prefix "$work/vt-install" || true
65
+ vt_install="$build_root/vt-install"
66
+ "$zig" build -Demit-lib-vt -Doptimize=ReleaseFast --prefix "$vt_install" || true
42
67
  cd ..
43
- staticlib="$work/vt-install/lib/libghostty-vt.a"
68
+ staticlib="$vt_install/lib/libghostty-vt.a"
44
69
  [ -f "$staticlib" ] || { echo "build failed: $staticlib not produced" >&2; exit 1; }
45
70
 
46
71
  echo "== linking shim -> $outdir/$dylib =="
47
72
  mkdir -p "$outdir"
48
73
  if [ "$os" = "darwin" ]; then
49
- clang -O2 -dynamiclib -o "$outdir/$dylib" "$here/src/shim.c" \
50
- -I"$work/vt-install/include" "$staticlib" \
74
+ "$zig" build-lib -dynamic -OReleaseFast -lc -femit-bin="$outdir/$dylib" \
75
+ -install_name "@rpath/$dylib" \
76
+ -I"$vt_install/include" "$here/src/shim.zig" "$staticlib" \
51
77
  -framework CoreFoundation -framework CoreText -framework CoreGraphics
52
78
  else
53
- clang -O2 -shared -fPIC -o "$outdir/$dylib" "$here/src/shim.c" \
54
- -I"$work/vt-install/include" "$staticlib" -lm -lpthread
79
+ "$zig" build-lib -dynamic -OReleaseFast -lc -femit-bin="$outdir/$dylib" \
80
+ -I"$vt_install/include" "$here/src/shim.zig" "$staticlib" -lm -lpthread
55
81
  fi
56
82
  echo "== done: $outdir/$dylib =="
@@ -0,0 +1,114 @@
1
+ // Flat FFI wrapper over libghostty-vt. It hides the allocator vtable, the
2
+ // by-value GhosttyPoint that bun:ffi cannot pass, and packed-cell decoding.
3
+ const std = @import("std");
4
+ const ghostty = @cImport({
5
+ @cInclude("ghostty/vt/types.h");
6
+ @cInclude("ghostty/vt/allocator.h");
7
+ @cInclude("ghostty/vt/terminal.h");
8
+ @cInclude("ghostty/vt/point.h");
9
+ @cInclude("ghostty/vt/grid_ref.h");
10
+ @cInclude("ghostty/vt/screen.h");
11
+ @cInclude("ghostty/vt/style.h");
12
+ @cInclude("ghostty/vt/color.h");
13
+ });
14
+
15
+ const CvtCell = extern struct {
16
+ codepoint: u32,
17
+ fg_r: u8,
18
+ fg_g: u8,
19
+ fg_b: u8,
20
+ fg_kind: u8,
21
+ bg_r: u8,
22
+ bg_g: u8,
23
+ bg_b: u8,
24
+ bg_kind: u8,
25
+ width: u8,
26
+ flags: u8,
27
+ _pad: u16,
28
+ };
29
+
30
+ comptime {
31
+ std.debug.assert(@sizeOf(CvtCell) == 16);
32
+ }
33
+
34
+ export fn cvt_new(cols: u16, rows: u16) ?*anyopaque {
35
+ var terminal: ghostty.GhosttyTerminal = null;
36
+ if (ghostty.ghostty_terminal_new(null, &terminal, cols, rows) != ghostty.GHOSTTY_SUCCESS) {
37
+ return null;
38
+ }
39
+ return terminal;
40
+ }
41
+
42
+ export fn cvt_free(terminal: ?*anyopaque) void {
43
+ if (terminal) |handle| ghostty.ghostty_terminal_free(@ptrCast(handle));
44
+ }
45
+
46
+ export fn cvt_write(terminal: *anyopaque, data: [*]const u8, len: usize) void {
47
+ ghostty.ghostty_terminal_vt_write(@ptrCast(terminal), data, len);
48
+ }
49
+
50
+ export fn cvt_resize(terminal: *anyopaque, cols: u16, rows: u16) void {
51
+ // Cell pixel size does not affect the character grid.
52
+ _ = ghostty.ghostty_terminal_resize(@ptrCast(terminal), cols, rows, 1, 1);
53
+ }
54
+
55
+ // Writes [col u16 LE, row u16 LE, visible u8] into the caller's eight bytes.
56
+ export fn cvt_cursor(terminal: *anyopaque, out: [*]u8) void {
57
+ var x: u16 = 0;
58
+ var y: u16 = 0;
59
+ var visible = false;
60
+ _ = ghostty.ghostty_terminal_get(@ptrCast(terminal), ghostty.GHOSTTY_TERMINAL_DATA_CURSOR_X, &x);
61
+ _ = ghostty.ghostty_terminal_get(@ptrCast(terminal), ghostty.GHOSTTY_TERMINAL_DATA_CURSOR_Y, &y);
62
+ _ = ghostty.ghostty_terminal_get(@ptrCast(terminal), ghostty.GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE, &visible);
63
+ out[0] = @truncate(x);
64
+ out[1] = @truncate(x >> 8);
65
+ out[2] = @truncate(y);
66
+ out[3] = @truncate(y >> 8);
67
+ out[4] = @intFromBool(visible);
68
+ }
69
+
70
+ fn decodeColor(color: *const ghostty.GhosttyStyleColor, red: *u8, green: *u8, blue: *u8, kind: *u8) void {
71
+ if (color.tag == ghostty.GHOSTTY_STYLE_COLOR_RGB) {
72
+ kind.* = 2;
73
+ red.* = color.value.rgb.r;
74
+ green.* = color.value.rgb.g;
75
+ blue.* = color.value.rgb.b;
76
+ } else if (color.tag == ghostty.GHOSTTY_STYLE_COLOR_PALETTE) {
77
+ kind.* = 1;
78
+ red.* = @intCast(color.value.palette);
79
+ }
80
+ }
81
+
82
+ export fn cvt_cell(terminal: *anyopaque, x: u16, y: u32, out: *CvtCell) c_int {
83
+ out.* = std.mem.zeroes(CvtCell);
84
+
85
+ var point: ghostty.GhosttyPoint = undefined;
86
+ point.tag = ghostty.GHOSTTY_POINT_TAG_VIEWPORT;
87
+ point.value.coordinate.x = x;
88
+ point.value.coordinate.y = y;
89
+
90
+ var reference: ghostty.GhosttyGridRef = undefined;
91
+ if (ghostty.ghostty_terminal_grid_ref(@ptrCast(terminal), point, &reference) != ghostty.GHOSTTY_SUCCESS) return -1;
92
+
93
+ var cell: ghostty.GhosttyCell = undefined;
94
+ if (ghostty.ghostty_grid_ref_cell(&reference, &cell) != ghostty.GHOSTTY_SUCCESS) return -1;
95
+
96
+ _ = ghostty.ghostty_cell_get(cell, ghostty.GHOSTTY_CELL_DATA_CODEPOINT, &out.codepoint);
97
+ var width: u32 = 0;
98
+ _ = ghostty.ghostty_cell_get(cell, ghostty.GHOSTTY_CELL_DATA_WIDE, &width);
99
+ out.width = @intCast(width);
100
+
101
+ var style: ghostty.GhosttyStyle = undefined;
102
+ ghostty.ghostty_style_default(&style);
103
+ if (ghostty.ghostty_grid_ref_style(&reference, &style) == ghostty.GHOSTTY_SUCCESS) {
104
+ decodeColor(&style.fg_color, &out.fg_r, &out.fg_g, &out.fg_b, &out.fg_kind);
105
+ decodeColor(&style.bg_color, &out.bg_r, &out.bg_g, &out.bg_b, &out.bg_kind);
106
+ if (style.bold) out.flags |= 1;
107
+ if (style.italic) out.flags |= 2;
108
+ if (style.underline != 0) out.flags |= 4;
109
+ if (style.inverse) out.flags |= 8;
110
+ if (style.faint) out.flags |= 16;
111
+ if (style.strikethrough) out.flags |= 32;
112
+ }
113
+ return 0;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.41",
3
+ "version": "0.1.0-alpha.42",
4
4
  "description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -26,10 +26,10 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@cueloop/daemon": "0.1.0-alpha.41",
30
- "@cueloop/extension-api": "0.1.0-alpha.41",
31
- "@cueloop/integration-obsidian": "0.1.0-alpha.41",
32
- "@cueloop/schema": "0.1.0-alpha.41",
29
+ "@cueloop/daemon": "0.1.0-alpha.42",
30
+ "@cueloop/extension-api": "0.1.0-alpha.42",
31
+ "@cueloop/integration-obsidian": "0.1.0-alpha.42",
32
+ "@cueloop/schema": "0.1.0-alpha.42",
33
33
  "@opentui/core": "^0.5.2",
34
34
  "@opentui/keymap": "0.5.2",
35
35
  "@opentui/react": "^0.5.2",
@@ -120,7 +120,7 @@ export class TerminalPaneRenderable extends Renderable {
120
120
  for (let y = 0; y < this.height; y++) {
121
121
  for (let x = 0; x < this.width; x++) {
122
122
  const cell = this.vt.readCell(x, y);
123
- // width 0 = the trailing half of a wide glyph already painted at x-1: skip it.
123
+ // Skip an empty normal-width cell; the buffer already starts blank.
124
124
  if (cell && cell.width === 0 && cell.codepoint === 0) continue;
125
125
  const char = cell && cell.codepoint ? String.fromCodePoint(cell.codepoint) : " ";
126
126
  let fg = cell ? resolveColor(cell.fg, DEFAULT_FG) : DEFAULT_FG;
@@ -42,4 +42,41 @@ describe("ghostty-terminal FFI", () => {
42
42
  expect(cursor.x).toBe(3);
43
43
  term.free();
44
44
  });
45
+
46
+ test.skipIf(!factory)(
47
+ "preserves the native screen contract across resize and styled text",
48
+ () => {
49
+ // Arrange
50
+ const term = factory!.create(8, 2)!;
51
+
52
+ // Act - resize first, then write RGB colors, every supported style, and a wide glyph
53
+ term.resize(6, 3);
54
+ term.write(
55
+ new TextEncoder().encode(
56
+ "\x1b[38;2;12;34;56;48;2;78;90;123;1;3;4;7;9mA\x1b[0m\x1b[2mF\x1b[0m\u754c",
57
+ ),
58
+ );
59
+
60
+ // Assert - the flat native ABI retains cell data and rejects coordinates outside the viewport
61
+ expect(term.readCell(0, 0)).toEqual({
62
+ codepoint: 0x41,
63
+ fg: { kind: "rgb", r: 12, g: 34, b: 56 },
64
+ bg: { kind: "rgb", r: 78, g: 90, b: 123 },
65
+ width: 0,
66
+ bold: true,
67
+ italic: true,
68
+ underline: true,
69
+ inverse: true,
70
+ faint: false,
71
+ strikethrough: true,
72
+ });
73
+ expect(term.readCell(1, 0)?.faint).toBe(true);
74
+ expect(term.readCell(2, 0)?.codepoint).toBe("\u754c".codePointAt(0));
75
+ expect(term.readCell(2, 0)?.width).toBe(1);
76
+ expect(term.readCell(3, 0)?.width).toBe(2);
77
+ expect(term.readCell(6, 0)).toBeNull();
78
+ expect(term.readCell(0, 3)).toBeNull();
79
+ term.free();
80
+ },
81
+ );
45
82
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A live in-process terminal emulator backed by Ghostty's VT core (libghostty-vt)
3
- * through a flat FFI shim (native/src/shim.c). Feed it a child process's PTY
3
+ * through a flat FFI shim (native/src/shim.zig). Feed it a child process's PTY
4
4
  * output with `write`, then read the rendered screen cell-by-cell with `readCell`
5
5
  * to paint it anywhere - this is what lets a real `cc`/shell render inside an
6
6
  * OpenTUI box. Load with `loadGhosttyTerminals`, which returns null when no
@@ -30,7 +30,7 @@ export interface GhosttyCell {
30
30
  codepoint: number;
31
31
  fg: GhosttyColor;
32
32
  bg: GhosttyColor;
33
- /** 0 = wide-char trailing cell (skip it), 1 = normal, 2 = double-width lead. */
33
+ /** Ghostty width tag: 0 = normal, 1 = double-width lead, 2 = trailing cell. */
34
34
  width: number;
35
35
  bold: boolean;
36
36
  italic: boolean;
package/native/src/shim.c DELETED
@@ -1,107 +0,0 @@
1
- // Flat, FFI-trivial wrapper over libghostty-vt: hides the allocator vtable, the
2
- // by-value GhosttyPoint (which bun:ffi cannot pass), and the packed-cell decode,
3
- // exposing exactly what an OpenTUI paint loop needs - new/write/resize/cell/free.
4
- #include <stdint.h>
5
- #include <stddef.h>
6
- #include <stdbool.h>
7
- #include <string.h>
8
- #include "ghostty/vt/types.h"
9
- #include "ghostty/vt/allocator.h"
10
- #include "ghostty/vt/terminal.h"
11
- #include "ghostty/vt/point.h"
12
- #include "ghostty/vt/grid_ref.h"
13
- #include "ghostty/vt/screen.h"
14
- #include "ghostty/vt/style.h"
15
- #include "ghostty/vt/color.h"
16
-
17
- typedef struct {
18
- uint32_t codepoint; // 0 = blank
19
- uint8_t fg_r, fg_g, fg_b, fg_kind; // kind: 0 default, 1 palette (index in fg_r), 2 rgb
20
- uint8_t bg_r, bg_g, bg_b, bg_kind;
21
- uint8_t width; // 0 (wide trailing), 1, 2
22
- uint8_t flags; // bit0 bold, 1 italic, 2 underline, 3 inverse, 4 faint, 5 strikethrough
23
- uint16_t _pad;
24
- } CvtCell;
25
-
26
- void *cvt_new(uint16_t cols, uint16_t rows) {
27
- GhosttyTerminal t = NULL;
28
- // NULL allocator selects libghostty's default (libc malloc/free when linked).
29
- if (ghostty_terminal_new(NULL, &t, cols, rows) != GHOSTTY_SUCCESS) return NULL;
30
- return (void *)t;
31
- }
32
-
33
- void cvt_free(void *t) {
34
- if (t) ghostty_terminal_free((GhosttyTerminal)t);
35
- }
36
-
37
- void cvt_write(void *t, const uint8_t *data, size_t len) {
38
- ghostty_terminal_vt_write((GhosttyTerminal)t, data, len);
39
- }
40
-
41
- void cvt_resize(void *t, uint16_t cols, uint16_t rows) {
42
- // cell pixel size is irrelevant to the character grid; pass a nominal 1x1.
43
- ghostty_terminal_resize((GhosttyTerminal)t, cols, rows, 1, 1);
44
- }
45
-
46
- // Writes cursor state into out8: [0..1] col (u16), [2..3] row (u16), [4] visible (u8).
47
- void cvt_cursor(void *t, uint8_t *out8) {
48
- uint16_t cx = 0, cy = 0;
49
- bool vis = false;
50
- ghostty_terminal_get((GhosttyTerminal)t, GHOSTTY_TERMINAL_DATA_CURSOR_X, &cx);
51
- ghostty_terminal_get((GhosttyTerminal)t, GHOSTTY_TERMINAL_DATA_CURSOR_Y, &cy);
52
- ghostty_terminal_get((GhosttyTerminal)t, GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE, &vis);
53
- memcpy(out8, &cx, 2);
54
- memcpy(out8 + 2, &cy, 2);
55
- out8[4] = vis ? 1 : 0;
56
- }
57
-
58
- static void decode_color(const GhosttyStyleColor *c, uint8_t *r, uint8_t *g, uint8_t *b,
59
- uint8_t *kind) {
60
- if (c->tag == GHOSTTY_STYLE_COLOR_RGB) {
61
- *kind = 2;
62
- *r = c->value.rgb.r;
63
- *g = c->value.rgb.g;
64
- *b = c->value.rgb.b;
65
- } else if (c->tag == GHOSTTY_STYLE_COLOR_PALETTE) {
66
- *kind = 1;
67
- *r = (uint8_t)c->value.palette; // palette index in r; JS maps 0-255 -> RGB
68
- *g = 0;
69
- *b = 0;
70
- } else {
71
- *kind = 0;
72
- }
73
- }
74
-
75
- // Reads the viewport cell at (x, y). Returns 0 on success, -1 otherwise.
76
- int cvt_cell(void *t, uint16_t x, uint32_t y, CvtCell *out) {
77
- memset(out, 0, sizeof(*out));
78
- GhosttyPoint p;
79
- p.tag = GHOSTTY_POINT_TAG_VIEWPORT;
80
- p.value.coordinate.x = x;
81
- p.value.coordinate.y = y;
82
- GhosttyGridRef ref;
83
- if (ghostty_terminal_grid_ref((GhosttyTerminal)t, p, &ref) != GHOSTTY_SUCCESS) return -1;
84
- GhosttyCell cell;
85
- if (ghostty_grid_ref_cell(&ref, &cell) != GHOSTTY_SUCCESS) return -1;
86
- uint32_t cp = 0;
87
- ghostty_cell_get(cell, GHOSTTY_CELL_DATA_CODEPOINT, &cp);
88
- out->codepoint = cp;
89
- uint32_t wide = 0;
90
- ghostty_cell_get(cell, GHOSTTY_CELL_DATA_WIDE, &wide);
91
- out->width = (uint8_t)wide;
92
- GhosttyStyle st;
93
- ghostty_style_default(&st);
94
- if (ghostty_grid_ref_style(&ref, &st) == GHOSTTY_SUCCESS) {
95
- decode_color(&st.fg_color, &out->fg_r, &out->fg_g, &out->fg_b, &out->fg_kind);
96
- decode_color(&st.bg_color, &out->bg_r, &out->bg_g, &out->bg_b, &out->bg_kind);
97
- uint8_t f = 0;
98
- if (st.bold) f |= 1;
99
- if (st.italic) f |= 2;
100
- if (st.underline) f |= 4;
101
- if (st.inverse) f |= 8;
102
- if (st.faint) f |= 16;
103
- if (st.strikethrough) f |= 32;
104
- out->flags = f;
105
- }
106
- return 0;
107
- }