@cueloop/client 0.1.0-alpha.67 → 0.1.0-alpha.68

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.67",
3
+ "version": "0.1.0-alpha.68",
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.67",
30
- "@cueloop/extension-api": "0.1.0-alpha.67",
31
- "@cueloop/integration-obsidian": "0.1.0-alpha.67",
32
- "@cueloop/schema": "0.1.0-alpha.67",
29
+ "@cueloop/daemon": "0.1.0-alpha.68",
30
+ "@cueloop/extension-api": "0.1.0-alpha.68",
31
+ "@cueloop/integration-obsidian": "0.1.0-alpha.68",
32
+ "@cueloop/schema": "0.1.0-alpha.68",
33
33
  "@opentui/core": "^0.5.8",
34
34
  "@opentui/keymap": "0.5.8",
35
35
  "@opentui/react": "^0.5.8",
@@ -21,10 +21,11 @@ describe("ghostty-terminal FFI", () => {
21
21
  term.write(new TextEncoder().encode("hi\x1b[1;32mGO\x1b[0m"));
22
22
  const cells = [0, 1, 2, 3].map((x) => term.readCell(x, 0));
23
23
 
24
- // Assert - glyphs decode, and the green run is bold on ANSI palette index 2
24
+ // Assert - glyphs decode, the row reads as text, and the green run is bold on ANSI palette index 2
25
25
  expect(
26
26
  cells.map((c) => (c?.codepoint ? String.fromCodePoint(c.codepoint) : " ")).join(""),
27
27
  ).toBe("hiGO");
28
+ expect(term.rowText(0, 80)).toBe("hiGO");
28
29
  expect(cells[2]!.bold).toBe(true);
29
30
  expect(cells[2]!.fg).toEqual({ kind: "palette", index: 2 });
30
31
  term.free();
@@ -60,25 +60,31 @@ function nativeLibraryPath(): string | null {
60
60
  return existsSync(path) ? path : null;
61
61
  }
62
62
 
63
+ /** One shared load of the VT library; null when no dylib ships or it failed to load. */
64
+ let ghosttyFactory: GhosttyTerminalFactory | null | undefined;
65
+
63
66
  /**
64
67
  * Open the Ghostty VT library for this platform, or return null when the prebuilt
65
68
  * dylib is missing (the caller degrades to a herdr split instead of embedding).
69
+ * The library is opened once per process; later calls return the same factory.
66
70
  */
67
71
  export function loadGhosttyTerminals(): GhosttyTerminalFactory | null {
72
+ if (ghosttyFactory !== undefined) return ghosttyFactory;
68
73
  const path = nativeLibraryPath();
69
74
 
70
- if (!path) return null;
75
+ if (!path) return (ghosttyFactory = null);
71
76
  try {
72
77
  const lib = dlopen(path, CVT_SYMBOLS);
73
78
 
74
- return new GhosttyTerminalFactory(lib.symbols);
79
+ ghosttyFactory = new GhosttyTerminalFactory(lib.symbols);
75
80
  } catch (error) {
76
81
  // The file exists but failed to load (bad arch, missing symbol) - a real
77
82
  // fault, not the expected no-prebuilt case; surface it, then degrade.
78
83
  console.error(`cueloop: failed to load ${path}:`, error);
79
-
80
- return null;
84
+ ghosttyFactory = null;
81
85
  }
86
+
87
+ return ghosttyFactory;
82
88
  }
83
89
 
84
90
  /** Creates `GhosttyTerminal`s that share one loaded copy of the VT library. */
@@ -146,6 +152,23 @@ export class GhosttyTerminal {
146
152
  };
147
153
  }
148
154
 
155
+ /**
156
+ * Row `y` as text: blank cells read as spaces, the trailing half of a wide
157
+ * glyph is skipped, and trailing spaces are trimmed.
158
+ */
159
+ rowText(y: number, cols: number): string {
160
+ let line = "";
161
+
162
+ for (let x = 0; x < cols; x++) {
163
+ const cell = this.readCell(x, y);
164
+
165
+ if (cell === null || cell.width === 2) continue;
166
+ line += cell.codepoint === 0 ? " " : String.fromCodePoint(cell.codepoint);
167
+ }
168
+
169
+ return line.trimEnd();
170
+ }
171
+
149
172
  /** The cursor's current viewport position and visibility. */
150
173
  readCursor(): GhosttyCursor {
151
174
  if (this.freed) return { x: 0, y: 0, visible: false };
@@ -14,6 +14,7 @@ import {
14
14
  frameRow,
15
15
  isolateUserConfig,
16
16
  locateText,
17
+ locateTextInFrame,
17
18
  pressKey,
18
19
  waitForText,
19
20
  type FrameLocation,
@@ -250,3 +251,14 @@ describe("dragText", () => {
250
251
  expect(log.mouseEvents).toContainEqual({ type: "up", x: 9, y: 3 });
251
252
  });
252
253
  });
254
+
255
+ describe("locateTextInFrame", () => {
256
+ test("finds the 0-based row and column of the first match, null when absent", () => {
257
+ // Arrange
258
+ const frame = "header\n ● draft here\nfooter";
259
+
260
+ // Act + Assert
261
+ expect(locateTextInFrame(frame, "draft")).toEqual({ row: 1, column: 4 });
262
+ expect(locateTextInFrame(frame, "missing")).toBeNull();
263
+ });
264
+ });
@@ -12,6 +12,16 @@ import type { TestRendererSetup } from "@opentui/core/testing";
12
12
  /** Pass budget for waits that include daemon or subprocess round-trips. */
13
13
  export const WAIT_PASSES = { maxPasses: 400 };
14
14
 
15
+ /**
16
+ * The CUELOOP_CONFIG path that isolates a test from the developer's real user
17
+ * config: a file inside the test home that does not exist unless the test
18
+ * writes it. Subprocess harnesses set this in the child env; in-process suites
19
+ * use `isolateUserConfig`.
20
+ */
21
+ export function isolatedUserConfigPath(home: string, fileName = "no-config.toml"): string {
22
+ return join(home, fileName);
23
+ }
24
+
15
25
  /**
16
26
  * Isolate the user config: point CUELOOP_CONFIG into the test home so
17
27
  * loadConfig never reads the developer's real ~/.config/cueloop (a persisted
@@ -20,10 +30,10 @@ export const WAIT_PASSES = { maxPasses: 400 };
20
30
  * defaults; pass a file name for suites that write and assert a config of
21
31
  * their own. Call in beforeEach; invoke the returned restore in afterEach.
22
32
  */
23
- export function isolateUserConfig(home: string, fileName = "no-config.toml"): () => void {
33
+ export function isolateUserConfig(home: string, fileName?: string): () => void {
24
34
  const priorUserConfig = process.env.CUELOOP_CONFIG;
25
35
 
26
- process.env.CUELOOP_CONFIG = join(home, fileName);
36
+ process.env.CUELOOP_CONFIG = isolatedUserConfigPath(home, fileName);
27
37
 
28
38
  return () => {
29
39
  if (priorUserConfig === undefined) delete process.env.CUELOOP_CONFIG;
@@ -114,17 +124,27 @@ export interface FrameLocation {
114
124
  column: number;
115
125
  }
116
126
 
117
- /** Locate the first visual line containing the needle: 0-based row and start column. */
118
- export function locateText(setup: TestRendererSetup, needle: string): FrameLocation {
119
- const frame = setup.captureCharFrame();
120
-
127
+ /** Locate the first line of a text frame containing the needle; null when absent. */
128
+ export function locateTextInFrame(frame: string, needle: string): FrameLocation | null {
121
129
  for (const [row, line] of frame.split("\n").entries()) {
122
130
  const column = line.indexOf(needle);
123
131
 
124
132
  if (column !== -1) return { row, column };
125
133
  }
126
134
 
127
- throw new Error(`locateText ${JSON.stringify(needle)} not found.\nframe:\n${frame}`);
135
+ return null;
136
+ }
137
+
138
+ /** Locate the first visual line containing the needle: 0-based row and start column. */
139
+ export function locateText(setup: TestRendererSetup, needle: string): FrameLocation {
140
+ const frame = setup.captureCharFrame();
141
+ const location = locateTextInFrame(frame, needle);
142
+
143
+ if (location === null) {
144
+ throw new Error(`locateText ${JSON.stringify(needle)} not found.\nframe:\n${frame}`);
145
+ }
146
+
147
+ return location;
128
148
  }
129
149
 
130
150
  /** The 0-based visual row of the needle - for relative vertical-layout assertions. */