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

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.
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * An OpenTUI renderable that runs a real child process (a shell, cc, pi, codex)
3
3
  * on a PTY and paints its live screen into the box - the terminal-in-the-rail
4
- * primitive. It wires bun-pty (the child + tty) to a Ghostty VT emulator
4
+ * primitive. It wires the forkpty shim (child + tty, ../pty) to a Ghostty VT emulator
5
5
  * (ghostty-terminal.ts) and blits the emulator's cell grid every frame via
6
6
  * OptimizedBuffer.setCell. Register once with `registerTerminalPane`, then use
7
- * `<terminalPane command="cc" ... />` in the OpenTUI React tree.
7
+ * `<terminalPane command="claude" ... />` in the OpenTUI React tree.
8
8
  */
9
9
 
10
10
  import { Renderable, RGBA, createTextAttributes, type RenderContext } from "@opentui/core";
11
11
  import type { OptimizedBuffer } from "@opentui/core";
12
12
  import type { RenderableOptions } from "@opentui/core";
13
13
  import { extend } from "@opentui/react";
14
- import { spawn, type IPty } from "bun-pty";
14
+ import { spawn, ptyAvailable, type IPty } from "../pty";
15
15
  import {
16
16
  loadGhosttyTerminals,
17
17
  type GhosttyColor,
@@ -26,14 +26,15 @@ function factory(): GhosttyTerminalFactory | null {
26
26
  return ghosttyFactory;
27
27
  }
28
28
 
29
- /** Whether the embedded terminal can run here (the platform dylib is present). */
29
+ /** Whether the embedded terminal can run here: both native shims ship for this
30
+ * platform (the Ghostty VT renderer and the forkpty PTY). */
30
31
  export function embeddedTerminalAvailable(): boolean {
31
- return factory() !== null;
32
+ return factory() !== null && ptyAvailable();
32
33
  }
33
34
 
34
35
  /** Props for `<terminalPane>`: the child to run plus its cwd/env and a plan-context seed. */
35
36
  export interface TerminalPaneOptions extends RenderableOptions {
36
- /** The program to run, e.g. "cc" / "pi" / "codex" / a shell. */
37
+ /** The program to run, e.g. "claude" / "pi" / "codex" / a shell. */
37
38
  command?: string;
38
39
  args?: string[];
39
40
  cwd?: string;
@@ -85,7 +86,7 @@ export class TerminalPaneRenderable extends Renderable {
85
86
  env: (this.opts.env ?? process.env) as Record<string, string>,
86
87
  });
87
88
  this.pty.onData((data) => {
88
- // bun-pty streams a UTF-8-decoded string (split multibyte is handled); a
89
+ // the shim streams a UTF-8-decoded string (split multibyte is handled); a
89
90
  // rare non-UTF-8 byte arrives as U+FFFD - acceptable for agent TUIs.
90
91
  this.vt?.write(encoder.encode(data));
91
92
  if (this.pendingSeed !== undefined) {
@@ -120,7 +121,7 @@ export class TerminalPaneRenderable extends Renderable {
120
121
  for (let y = 0; y < this.height; y++) {
121
122
  for (let x = 0; x < this.width; x++) {
122
123
  const cell = this.vt.readCell(x, y);
123
- // width 0 = the trailing half of a wide glyph already painted at x-1: skip it.
124
+ // Skip an empty normal-width cell; the buffer already starts blank.
124
125
  if (cell && cell.width === 0 && cell.codepoint === 0) continue;
125
126
  const char = cell && cell.codepoint ? String.fromCodePoint(cell.codepoint) : " ";
126
127
  let fg = cell ? resolveColor(cell.fg, DEFAULT_FG) : DEFAULT_FG;
@@ -149,11 +150,17 @@ export class TerminalPaneRenderable extends Renderable {
149
150
  }
150
151
  }
151
152
 
152
- protected destroySelf(): void {
153
+ /** Kill the child and free the VT. Idempotent - the detach path calls this
154
+ * directly because the React reconciler removes a child without destroying it. */
155
+ shutdown(): void {
153
156
  this.pty?.kill();
154
157
  this.vt?.free();
155
158
  this.pty = null;
156
159
  this.vt = null;
160
+ }
161
+
162
+ protected destroySelf(): void {
163
+ this.shutdown();
157
164
  super.destroySelf();
158
165
  }
159
166
  }
@@ -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;
@@ -34,6 +34,9 @@ export type HintMode =
34
34
  | "walk"
35
35
  | "read-only";
36
36
 
37
+ /** Display glyph for the Agent-tab terminal detach chord (ctrl+], App-owned). */
38
+ export const AGENT_DETACH_HINT = "⌃]";
39
+
37
40
  export interface CheatsheetEntry {
38
41
  keys: string;
39
42
  label: string;
@@ -417,6 +420,9 @@ export class KeyBindings {
417
420
  build({ overlay: "none", spanMode: true }, "span", "Selection"),
418
421
  build({ overlay: "submit", spanMode: false }, "submit", "Submit"),
419
422
  build({ overlay: "walk", spanMode: false }, "walk", "Walk"),
423
+ // The Agent-tab terminal chord is owned by App (not the rebindable keymap),
424
+ // so it is listed statically rather than resolved from the active keys.
425
+ { title: "Agent terminal", entries: [{ keys: AGENT_DETACH_HINT, label: "detach" }] },
420
426
  ];
421
427
  this.context = saved;
422
428
  return sections.filter((section) => section.entries.length > 0);
@@ -0,0 +1,78 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ptyAvailable, spawn } from "./pty";
3
+
4
+ // The forkpty shim is a per-platform prebuilt; skip where none ships (e.g. CI
5
+ // without the built dylib), exactly as the caller degrades to a herdr split.
6
+ const ptyTest = ptyAvailable() ? test : test.skip;
7
+
8
+ /** Run a child on a PTY, collect its output, and resolve with the exit code. */
9
+ function runOnPty(file: string, args: string[]): Promise<{ output: string; exitCode: number }> {
10
+ return new Promise((resolve, reject) => {
11
+ const pty = spawn(file, args, { name: "xterm-256color", cols: 80, rows: 24 });
12
+ let output = "";
13
+ const timeout = setTimeout(() => reject(new Error("pty child never exited")), 5000);
14
+ pty.onData((chunk) => {
15
+ output += chunk;
16
+ });
17
+ pty.onExit(({ exitCode }) => {
18
+ clearTimeout(timeout);
19
+ resolve({ output, exitCode });
20
+ });
21
+ });
22
+ }
23
+
24
+ describe("pty", () => {
25
+ ptyTest("streams a child's output and reports its exit code", async () => {
26
+ // Act
27
+ const { output, exitCode } = await runOnPty("sh", ["-c", "printf 'PTY-OK'; exit 7"]);
28
+
29
+ // Assert
30
+ expect(output).toContain("PTY-OK");
31
+ expect(exitCode).toBe(7);
32
+ });
33
+
34
+ ptyTest("forwards written input to the child", async () => {
35
+ // Arrange - `cat` echoes stdin back through the tty until it closes
36
+ const pty = spawn("cat", [], { name: "xterm-256color", cols: 80, rows: 24 });
37
+ let output = "";
38
+ pty.onData((chunk) => {
39
+ output += chunk;
40
+ });
41
+
42
+ // Act
43
+ pty.write("ping\n");
44
+ await new Promise((resolve) => setTimeout(resolve, 200));
45
+ pty.kill();
46
+
47
+ // Assert - the tty echoes the written line back
48
+ expect(output).toContain("ping");
49
+ });
50
+
51
+ ptyTest("passes env through to the child", async () => {
52
+ // Act
53
+ const { output } = await runOnPty("sh", ["-c", "printf '%s' \"$CUELOOP_PTY_MARKER\""]);
54
+
55
+ // Assert - a child with no inherited marker prints nothing for it
56
+ expect(output).not.toContain("marker-value");
57
+
58
+ // Act - now inject the marker via env
59
+ const withEnv = await new Promise<string>((resolve, reject) => {
60
+ const pty = spawn("sh", ["-c", "printf '%s' \"$CUELOOP_PTY_MARKER\""], {
61
+ name: "xterm-256color",
62
+ env: { ...process.env, CUELOOP_PTY_MARKER: "marker-value" },
63
+ });
64
+ let collected = "";
65
+ const timeout = setTimeout(() => reject(new Error("no exit")), 5000);
66
+ pty.onData((chunk) => {
67
+ collected += chunk;
68
+ });
69
+ pty.onExit(() => {
70
+ clearTimeout(timeout);
71
+ resolve(collected);
72
+ });
73
+ });
74
+
75
+ // Assert
76
+ expect(withEnv).toContain("marker-value");
77
+ });
78
+ });
package/src/pty.ts ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * A pseudo-terminal for the embedded Agent-tab terminal, over cueloop's own
3
+ * forkpty(3) FFI shim (native/src/pty.zig). `spawn` returns a `Pty` with
4
+ * `onData`/`onExit` events and `write`/`resize`/`kill`; a poll loop drains the
5
+ * child's output and decodes it as streaming UTF-8. `ptyAvailable()` returns
6
+ * false when no prebuilt shim ships for this platform, so callers fall back to a
7
+ * herdr split, exactly like the Ghostty VT loader.
8
+ */
9
+
10
+ import { dlopen, FFIType, ptr } from "bun:ffi";
11
+ import { existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ const DEFAULT_COLS = 80;
15
+ const DEFAULT_ROWS = 24;
16
+ /** How long the read loop sleeps when the child produced no output this tick. */
17
+ const READ_IDLE_MS = 8;
18
+ const READ_BUFFER_BYTES = 4096;
19
+ /** cueloop_pty_read's sentinel: the child has exited and its output is drained. */
20
+ const CHILD_EXITED = -2;
21
+
22
+ export interface PtyForkOptions {
23
+ name?: string;
24
+ cols?: number;
25
+ rows?: number;
26
+ cwd?: string;
27
+ env?: Record<string, string>;
28
+ }
29
+
30
+ export interface ExitEvent {
31
+ exitCode: number;
32
+ }
33
+
34
+ export interface Disposable {
35
+ dispose(): void;
36
+ }
37
+
38
+ /** The pseudo-terminal surface the embedded terminal consumes. */
39
+ export interface IPty {
40
+ readonly pid: number;
41
+ readonly cols: number;
42
+ readonly rows: number;
43
+ readonly onData: (listener: (data: string) => void) => Disposable;
44
+ readonly onExit: (listener: (event: ExitEvent) => void) => Disposable;
45
+ write(data: string): void;
46
+ resize(cols: number, rows: number): void;
47
+ kill(signal?: string): void;
48
+ }
49
+
50
+ const PTY_SYMBOLS = {
51
+ cueloop_pty_spawn: {
52
+ args: [FFIType.ptr, FFIType.cstring, FFIType.ptr, FFIType.i32, FFIType.i32],
53
+ returns: FFIType.i32,
54
+ },
55
+ cueloop_pty_write: { args: [FFIType.i32, FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
56
+ cueloop_pty_read: { args: [FFIType.i32, FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
57
+ cueloop_pty_resize: { args: [FFIType.i32, FFIType.i32, FFIType.i32], returns: FFIType.i32 },
58
+ cueloop_pty_kill: { args: [FFIType.i32], returns: FFIType.i32 },
59
+ cueloop_pty_get_pid: { args: [FFIType.i32], returns: FFIType.i32 },
60
+ cueloop_pty_get_exit_code: { args: [FFIType.i32], returns: FFIType.i32 },
61
+ cueloop_pty_close: { args: [FFIType.i32], returns: FFIType.void },
62
+ } as const;
63
+
64
+ type PtyLib = ReturnType<typeof dlopen<typeof PTY_SYMBOLS>>["symbols"];
65
+
66
+ /** The prebuilt pty shim for this platform, or null when none ships for it. */
67
+ function nativeLibraryPath(): string | null {
68
+ const suffix = process.platform === "darwin" ? "dylib" : "so";
69
+ const dir = `${process.platform}-${process.arch}`;
70
+ const path = join(import.meta.dir, "..", "native", dir, `libcuelooppty.${suffix}`);
71
+ return existsSync(path) ? path : null;
72
+ }
73
+
74
+ /** One shared load of the pty shim; null when no dylib ships for the platform. */
75
+ let ptyLib: PtyLib | null | undefined;
76
+ function library(): PtyLib | null {
77
+ if (ptyLib !== undefined) return ptyLib;
78
+ const path = nativeLibraryPath();
79
+ if (!path) return (ptyLib = null);
80
+ try {
81
+ ptyLib = dlopen(path, PTY_SYMBOLS).symbols;
82
+ } catch (error) {
83
+ console.error(`cueloop: failed to load ${path}:`, error);
84
+ ptyLib = null;
85
+ }
86
+ return ptyLib;
87
+ }
88
+
89
+ /** True when this platform ships a prebuilt pty shim (embedding is possible). */
90
+ export function ptyAvailable(): boolean {
91
+ return library() !== null;
92
+ }
93
+
94
+ class EventEmitter<T> {
95
+ private listeners: ((value: T) => void)[] = [];
96
+ readonly event = (listener: (value: T) => void): Disposable => {
97
+ this.listeners.push(listener);
98
+ return {
99
+ dispose: () => {
100
+ const index = this.listeners.indexOf(listener);
101
+ if (index !== -1) this.listeners.splice(index, 1);
102
+ },
103
+ };
104
+ };
105
+ fire(value: T): void {
106
+ for (const listener of this.listeners) listener(value);
107
+ }
108
+ }
109
+
110
+ /** Pack tokens into the C shim's "tok\0tok\0...\0\0" (double-NUL-terminated) form. */
111
+ function packTokens(tokens: string[]): Buffer {
112
+ return Buffer.from(tokens.map((token) => `${token}\0`).join("") + "\0", "utf8");
113
+ }
114
+
115
+ class Pty implements IPty {
116
+ private handle: number;
117
+ private processId: number;
118
+ private columns: number;
119
+ private rowCount: number;
120
+ private closing = false;
121
+ private reading = false;
122
+ private readonly decoder = new TextDecoder("utf-8");
123
+ private readonly dataEvent = new EventEmitter<string>();
124
+ private readonly exitEvent = new EventEmitter<ExitEvent>();
125
+
126
+ constructor(
127
+ private readonly lib: PtyLib,
128
+ file: string,
129
+ args: string[],
130
+ options: PtyForkOptions,
131
+ ) {
132
+ this.columns = options.cols ?? DEFAULT_COLS;
133
+ this.rowCount = options.rows ?? DEFAULT_ROWS;
134
+ const cwd = options.cwd ?? process.cwd();
135
+ const env = options.env
136
+ ? Object.entries(options.env).map(([key, value]) => `${key}=${value}`)
137
+ : [];
138
+
139
+ const argvPacked = packTokens([file, ...args]);
140
+ const envPacked = packTokens(env);
141
+ this.handle = lib.cueloop_pty_spawn(
142
+ ptr(argvPacked),
143
+ Buffer.from(`${cwd}\0`, "utf8"),
144
+ ptr(envPacked),
145
+ this.columns,
146
+ this.rowCount,
147
+ );
148
+ if (this.handle < 0) throw new Error("PTY spawn failed");
149
+ this.processId = lib.cueloop_pty_get_pid(this.handle);
150
+ // Let the caller attach onData/onExit before the first bytes arrive.
151
+ queueMicrotask(() => void this.readLoop());
152
+ }
153
+
154
+ get pid(): number {
155
+ return this.processId;
156
+ }
157
+ get cols(): number {
158
+ return this.columns;
159
+ }
160
+ get rows(): number {
161
+ return this.rowCount;
162
+ }
163
+ get onData() {
164
+ return this.dataEvent.event;
165
+ }
166
+ get onExit() {
167
+ return this.exitEvent.event;
168
+ }
169
+
170
+ write(data: string): void {
171
+ if (this.closing) return;
172
+ const buffer = Buffer.from(data, "utf8");
173
+ this.lib.cueloop_pty_write(this.handle, ptr(buffer), buffer.length);
174
+ }
175
+
176
+ resize(cols: number, rows: number): void {
177
+ if (this.closing) return;
178
+ this.columns = cols;
179
+ this.rowCount = rows;
180
+ this.lib.cueloop_pty_resize(this.handle, cols, rows);
181
+ }
182
+
183
+ kill(): void {
184
+ if (this.closing) return;
185
+ this.closing = true;
186
+ this.lib.cueloop_pty_kill(this.handle);
187
+ this.lib.cueloop_pty_close(this.handle);
188
+ this.exitEvent.fire({ exitCode: 0 });
189
+ }
190
+
191
+ private async readLoop(): Promise<void> {
192
+ if (this.reading) return;
193
+ this.reading = true;
194
+ const buffer = Buffer.allocUnsafe(READ_BUFFER_BYTES);
195
+ while (!this.closing) {
196
+ const count = this.lib.cueloop_pty_read(this.handle, ptr(buffer), buffer.length);
197
+ if (count > 0) {
198
+ // Stream mode buffers a multibyte char split across reads (box-drawing etc.).
199
+ const text = this.decoder.decode(buffer.subarray(0, count), { stream: true });
200
+ if (text) this.dataEvent.fire(text);
201
+ } else if (count === CHILD_EXITED || count < 0) {
202
+ // Both the drained-exit sentinel and a hard read error end the session:
203
+ // flush the decoder, reap for the code, close, and fire the exit once.
204
+ const tail = this.decoder.decode();
205
+ if (tail) this.dataEvent.fire(tail);
206
+ const exitCode = this.lib.cueloop_pty_get_exit_code(this.handle);
207
+ this.lib.cueloop_pty_close(this.handle);
208
+ this.closing = true;
209
+ this.exitEvent.fire({ exitCode });
210
+ } else {
211
+ await new Promise((resolve) => setTimeout(resolve, READ_IDLE_MS));
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ /** Spawn `file` with `args` on a fresh PTY. Throws when no shim ships (gate on ptyAvailable). */
218
+ export function spawn(file: string, args: string[], options: PtyForkOptions = {}): IPty {
219
+ const lib = library();
220
+ if (!lib) throw new Error("cueloop: no pty shim for this platform");
221
+ return new Pty(lib, file, args, options);
222
+ }
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
- }