@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 +5 -5
- package/src/ghostty-terminal.test.ts +2 -1
- package/src/ghostty-terminal.ts +27 -4
- package/src/test-support.test.ts +12 -0
- package/src/test-support.ts +27 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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.
|
|
30
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
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();
|
package/src/ghostty-terminal.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/src/test-support.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/test-support.ts
CHANGED
|
@@ -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
|
|
33
|
+
export function isolateUserConfig(home: string, fileName?: string): () => void {
|
|
24
34
|
const priorUserConfig = process.env.CUELOOP_CONFIG;
|
|
25
35
|
|
|
26
|
-
process.env.CUELOOP_CONFIG =
|
|
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
|
|
118
|
-
export function
|
|
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
|
-
|
|
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. */
|