@xynogen/pix-runtime 0.16.0 → 0.16.1
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 +1 -1
- package/src/exec.ts +7 -3
- package/src/os.ts +15 -6
- package/src/pix-command.ts +59 -7
package/package.json
CHANGED
package/src/exec.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface ToolExecOptions {
|
|
|
97
97
|
onStatus?: (status: ToolStatus) => void;
|
|
98
98
|
/** Test seam for the host OS. */
|
|
99
99
|
host?: HostPlatform;
|
|
100
|
-
/** Cap on captured stdout/stderr bytes each (default 50 MB). */
|
|
100
|
+
/** Cap on captured stdout/stderr bytes each (default 50 MB). Past it the child is stopped. */
|
|
101
101
|
maxBuffer?: number;
|
|
102
102
|
/** Args are already quoted for the target (cmd.exe `/c` lines); Node must not re-quote. */
|
|
103
103
|
verbatim?: boolean;
|
|
@@ -111,7 +111,7 @@ export interface ToolRunResult {
|
|
|
111
111
|
/** Raw stdout for binary output (clipboard images, archives). */
|
|
112
112
|
stdoutBytes: Buffer;
|
|
113
113
|
timedOut: boolean;
|
|
114
|
-
/** Output passed `maxBuffer
|
|
114
|
+
/** Output passed `maxBuffer`; the child was stopped and output cut. Never a complete result. */
|
|
115
115
|
truncated: boolean;
|
|
116
116
|
/** The binary that ran and where it was found. */
|
|
117
117
|
tool: ResolvedTool;
|
|
@@ -220,7 +220,11 @@ function collect(
|
|
|
220
220
|
let truncated = false;
|
|
221
221
|
const push = (list: Buffer[], chunk: Buffer, len: number): number => {
|
|
222
222
|
const room = cap - len;
|
|
223
|
-
|
|
223
|
+
// Like pi.exec: stop a runaway child instead of draining output nobody keeps.
|
|
224
|
+
if (chunk.length > room) {
|
|
225
|
+
truncated = true;
|
|
226
|
+
stop();
|
|
227
|
+
}
|
|
224
228
|
if (room <= 0) return len;
|
|
225
229
|
list.push(chunk.length > room ? chunk.subarray(0, room) : chunk);
|
|
226
230
|
return len + Math.min(chunk.length, room);
|
package/src/os.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function openCommand(
|
|
|
46
46
|
target: string,
|
|
47
47
|
host: HostPlatform,
|
|
48
48
|
app?: string,
|
|
49
|
-
): { name: string; args: string[]; verbatim?: boolean } {
|
|
49
|
+
): { name: string; args: string[]; verbatim?: boolean; cwd?: string } {
|
|
50
50
|
if (host.os === "darwin") return { name: "open", args: app ? ["-a", app, target] : [target] };
|
|
51
51
|
if (host.os === "win32") {
|
|
52
52
|
// `start` is a cmd.exe builtin; "" is its window-title argument.
|
|
@@ -54,8 +54,9 @@ export function openCommand(
|
|
|
54
54
|
return { name: "cmd", args: ["/d", "/s", "/c", `"${parts.join(" ")}"`], verbatim: true };
|
|
55
55
|
}
|
|
56
56
|
if (host.wsl && !app) {
|
|
57
|
-
// WSL: hand the target to Windows so the user's default browser opens.
|
|
58
|
-
|
|
57
|
+
// WSL: hand the target to Windows so the user's default browser opens. Windows programs
|
|
58
|
+
// cannot start from a long `\\wsl.localhost\...` cwd, so start from `/`.
|
|
59
|
+
return { name: "wslview", args: [target], cwd: "/" };
|
|
59
60
|
}
|
|
60
61
|
return { name: app ?? "xdg-open", args: [target] };
|
|
61
62
|
}
|
|
@@ -71,6 +72,7 @@ export async function openTarget(target: string, opts: OpenOptions = {}): Promis
|
|
|
71
72
|
if (cmd.name === "wslview" && !resolveTool("wslview", opts))
|
|
72
73
|
cmd = openCommand(target, { ...host, wsl: false });
|
|
73
74
|
const result = await runTool(cmd.name, cmd.args, {
|
|
75
|
+
cwd: cmd.cwd,
|
|
74
76
|
env: opts.env,
|
|
75
77
|
host,
|
|
76
78
|
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
@@ -140,10 +142,16 @@ export function pickPreferredMime(types: readonly string[]): string | null {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
/** Output bytes when the tool exists and exits 0; null otherwise (probing, never throws). */
|
|
143
|
-
function probe(
|
|
145
|
+
function probe(
|
|
146
|
+
name: string,
|
|
147
|
+
args: string[],
|
|
148
|
+
timeoutMs: number,
|
|
149
|
+
opts: OsOptions,
|
|
150
|
+
cwd?: string,
|
|
151
|
+
): Buffer | null {
|
|
144
152
|
if (!resolveTool(name, opts)) return null;
|
|
145
153
|
try {
|
|
146
|
-
const r = runToolSync(name, args, { env: opts.env, host: opts.host, timeoutMs });
|
|
154
|
+
const r = runToolSync(name, args, { env: opts.env, host: opts.host, timeoutMs, cwd });
|
|
147
155
|
return r.code === 0 ? r.stdoutBytes : null;
|
|
148
156
|
} catch {
|
|
149
157
|
return null;
|
|
@@ -203,7 +211,8 @@ function viaPowerShell(host: HostPlatform, opts: OsOptions): ClipboardImage | nu
|
|
|
203
211
|
"if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }",
|
|
204
212
|
].join("; ");
|
|
205
213
|
const args = ["-NoProfile", "-STA", "-Command", script];
|
|
206
|
-
|
|
214
|
+
// From WSL, Windows programs cannot start from a long `\\wsl.localhost\...` cwd.
|
|
215
|
+
const out = probe("powershell", args, POWERSHELL_TIMEOUT_MS, opts, host.wsl ? "/" : undefined);
|
|
207
216
|
if (out?.toString("utf-8").trim() !== "ok") return null;
|
|
208
217
|
const bytes = readFileSync(tmpFile);
|
|
209
218
|
return bytes.length ? { bytes, mimeType: "image/png" } : null;
|
package/src/pix-command.ts
CHANGED
|
@@ -42,6 +42,21 @@ function row<T>(r: SettingRow<T>): SettingRow<unknown> {
|
|
|
42
42
|
return r as SettingRow<unknown>;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Pi-owned theme row: not stored in pix.json, applied through `ctx.ui.setTheme`. */
|
|
46
|
+
interface ThemeRow {
|
|
47
|
+
kind: "theme";
|
|
48
|
+
section: string;
|
|
49
|
+
label: string;
|
|
50
|
+
values: readonly string[];
|
|
51
|
+
current: () => string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Row = SettingRow<unknown> | ThemeRow;
|
|
55
|
+
|
|
56
|
+
function isThemeRow(row: Row): row is ThemeRow {
|
|
57
|
+
return "kind" in row;
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
/** Render a token count as a compact label: 1_000_000 → "1M", 150_000 → "150k". */
|
|
46
61
|
function formatTokens(tokens: number): string {
|
|
47
62
|
return tokens % 1_000_000 === 0 ? `${tokens / 1_000_000}M` : `${tokens / 1000}k`;
|
|
@@ -291,12 +306,32 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
291
306
|
if (data && typeof data === "object" && "registered" in data) data.registered = true;
|
|
292
307
|
});
|
|
293
308
|
pi.registerCommand("pix", {
|
|
294
|
-
description: "pix: open shared settings (edit pix.json)",
|
|
295
|
-
|
|
309
|
+
description: "pix: open shared settings (edit pix.json); /pix <tab> opens that tab",
|
|
310
|
+
getArgumentCompletions: (prefix: string) => {
|
|
311
|
+
const p = prefix.trim().toLowerCase();
|
|
312
|
+
const items = [...TABS, ...packageSettings(pi).map((entry) => entry.label)]
|
|
313
|
+
.map((label) => label.toLowerCase())
|
|
314
|
+
.filter((name) => name.startsWith(p))
|
|
315
|
+
.map((name) => ({ value: name, label: name }));
|
|
316
|
+
return items.length ? items : null;
|
|
317
|
+
},
|
|
318
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
296
319
|
// Re-parse pix.json so section parsers remain the single validation source.
|
|
297
320
|
await runtime.reload({ origin: "command", source: "pix" });
|
|
298
321
|
const panels = packageSettings(pi);
|
|
299
322
|
const settings = visibleSettings(pi);
|
|
323
|
+
// Pi owns the active theme (settings.json); `/pix` only cycles it through `ctx.ui.setTheme`.
|
|
324
|
+
const themeNames = (ctx.ui.getAllThemes?.() ?? []).map((t) => t.name).sort();
|
|
325
|
+
const themeRow: ThemeRow | undefined =
|
|
326
|
+
themeNames.length > 1
|
|
327
|
+
? {
|
|
328
|
+
kind: "theme",
|
|
329
|
+
section: "Pretty",
|
|
330
|
+
label: "theme",
|
|
331
|
+
values: themeNames,
|
|
332
|
+
current: () => ctx.ui.theme.name ?? "",
|
|
333
|
+
}
|
|
334
|
+
: undefined;
|
|
300
335
|
// SAFETY: Runtime UI supports custom overlays beyond the published command context type.
|
|
301
336
|
const ui = ctx.ui as unknown as {
|
|
302
337
|
custom?: (f: unknown, opts?: unknown) => Promise<unknown>;
|
|
@@ -338,7 +373,8 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
338
373
|
),
|
|
339
374
|
...panels.map((entry) => entry.label),
|
|
340
375
|
].sort((a, b) => tabRank(a) - tabRank(b));
|
|
341
|
-
|
|
376
|
+
const wanted = (args ?? "").trim().toLowerCase();
|
|
377
|
+
let tab = tabs.find((name) => name.toLowerCase() === wanted) ?? "Settings";
|
|
342
378
|
let embedded:
|
|
343
379
|
| { render(width: number): string[]; handleInput(data: string): void; dispose?(): void }
|
|
344
380
|
| undefined;
|
|
@@ -429,11 +465,27 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
429
465
|
mountPanel();
|
|
430
466
|
};
|
|
431
467
|
|
|
432
|
-
|
|
433
|
-
|
|
468
|
+
// `/pix <tab>` opens straight into that tab.
|
|
469
|
+
if (tab === "Binaries") binariesTab().refresh();
|
|
470
|
+
mountPanel();
|
|
471
|
+
|
|
472
|
+
const rows = (): Row[] => {
|
|
473
|
+
const list = settings.filter((r) => (r.section === "Footer") === (tab === "Footer"));
|
|
474
|
+
return tab === "Settings" && themeRow ? [themeRow, ...list] : list;
|
|
475
|
+
};
|
|
434
476
|
const cycle = (direction: -1 | 1) => {
|
|
435
477
|
const row = rows()[selected];
|
|
436
478
|
if (!row) return;
|
|
479
|
+
if (isThemeRow(row)) {
|
|
480
|
+
const cur = row.values.indexOf(row.current());
|
|
481
|
+
const start = cur === -1 && direction === -1 ? 0 : cur;
|
|
482
|
+
const next = row.values[(start + direction + row.values.length) % row.values.length];
|
|
483
|
+
const result = next ? ctx.ui.setTheme(next) : undefined;
|
|
484
|
+
if (result && !result.success)
|
|
485
|
+
ui.notify(`pix: theme ${next}: ${result.error ?? "failed"}`, "error");
|
|
486
|
+
tui.requestRender();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
437
489
|
// Functional updater: the runtime queue serializes writes, and the
|
|
438
490
|
// callback sees the latest committed value — so rapid presses each
|
|
439
491
|
// advance exactly one step instead of collapsing on a stale snapshot.
|
|
@@ -538,8 +590,8 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
538
590
|
const sel = i === selected;
|
|
539
591
|
const cursor = sel ? theme.fg("accent", "→") : " ";
|
|
540
592
|
const label = theme.fg(sel ? "accent" : "text", row.label.padEnd(labelW));
|
|
541
|
-
const value = row.read(runtime.get(row.handle));
|
|
542
|
-
const isDefault = value === row.read(row.handle.defaults);
|
|
593
|
+
const value = isThemeRow(row) ? row.current() : row.read(runtime.get(row.handle));
|
|
594
|
+
const isDefault = isThemeRow(row) || value === row.read(row.handle.defaults);
|
|
543
595
|
settingBodyLines[i] = body.length;
|
|
544
596
|
body.push(`${cursor} ${label} ${theme.fg(isDefault ? "dim" : "success", value)}`);
|
|
545
597
|
}
|