@xynogen/pix-pretty 1.18.1 → 1.18.3
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/confirm.ts +4 -4
- package/src/gate-overlay.test.ts +43 -1
- package/src/gate-overlay.ts +26 -8
- package/src/modal-frame.ts +3 -3
- package/src/progress.ts +1 -1
- package/src/utils.test.ts +10 -0
- package/src/utils.ts +3 -3
package/package.json
CHANGED
package/src/confirm.ts
CHANGED
|
@@ -97,7 +97,7 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
|
|
|
97
97
|
const updateCountdown = () => {
|
|
98
98
|
const remaining = Math.max(0, Math.ceil((deadlineMs - Date.now()) / SECOND_MS));
|
|
99
99
|
countdownLine =
|
|
100
|
-
theme.fg("
|
|
100
|
+
theme.fg("muted", "Auto-cancel in ") +
|
|
101
101
|
theme.fg(remaining <= COUNTDOWN_WARN_S ? accent : "muted", `${remaining}s`);
|
|
102
102
|
};
|
|
103
103
|
updateCountdown();
|
|
@@ -121,12 +121,12 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
|
|
|
121
121
|
render: (w: number) => {
|
|
122
122
|
const mw = modalWidth(w);
|
|
123
123
|
const inner = mw - 4;
|
|
124
|
-
const footer = [theme.fg("
|
|
124
|
+
const footer = [theme.fg("muted", "─".repeat(inner))];
|
|
125
125
|
if (countdownLine !== undefined) footer.push(countdownLine);
|
|
126
126
|
footer.push(...selectList.render(inner));
|
|
127
127
|
footer.push("");
|
|
128
128
|
footer.push(
|
|
129
|
-
theme.fg("
|
|
129
|
+
theme.fg("muted", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc cancel"),
|
|
130
130
|
);
|
|
131
131
|
|
|
132
132
|
const result = frameModal({
|
|
@@ -141,7 +141,7 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
|
|
|
141
141
|
bg: (s) => theme.bg("customMessageBg", s),
|
|
142
142
|
fg: (s) => theme.fg("text", s),
|
|
143
143
|
overflowLine: ({ page, totalPages }) =>
|
|
144
|
-
theme.fg("
|
|
144
|
+
theme.fg("muted", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
|
|
145
145
|
});
|
|
146
146
|
pager.sync(result);
|
|
147
147
|
return result.lines;
|
package/src/gate-overlay.test.ts
CHANGED
|
@@ -31,6 +31,7 @@ interface Wired {
|
|
|
31
31
|
function makeUI(
|
|
32
32
|
onReady: (comp: Wired, finish: (v: unknown) => void) => void,
|
|
33
33
|
onOptions?: (options: unknown) => void,
|
|
34
|
+
renderTheme = theme,
|
|
34
35
|
): OverlayUI {
|
|
35
36
|
return {
|
|
36
37
|
custom: async <T>(
|
|
@@ -47,7 +48,7 @@ function makeUI(
|
|
|
47
48
|
const done = (v: T) => {
|
|
48
49
|
resolved = v;
|
|
49
50
|
};
|
|
50
|
-
const comp = cb({ requestRender: () => {} },
|
|
51
|
+
const comp = cb({ requestRender: () => {} }, renderTheme, undefined, done);
|
|
51
52
|
comp.render(80); // initialise render path
|
|
52
53
|
onReady(comp, done as (v: unknown) => void);
|
|
53
54
|
return resolved;
|
|
@@ -136,6 +137,47 @@ describe("showOverlay — confirm mode", () => {
|
|
|
136
137
|
expect(joined).toContain("body-line-x");
|
|
137
138
|
});
|
|
138
139
|
|
|
140
|
+
test("uses semantic colors for transfer details", async () => {
|
|
141
|
+
const coloredTheme = {
|
|
142
|
+
fg: (color: string, text: string) => `<${color}>${text}</${color}>`,
|
|
143
|
+
bg: (_color: string, text: string) => text,
|
|
144
|
+
bold: (text: string) => text,
|
|
145
|
+
};
|
|
146
|
+
let captured: string[] = [];
|
|
147
|
+
await showOverlay(
|
|
148
|
+
makeUI(
|
|
149
|
+
(comp) => {
|
|
150
|
+
captured = comp.render(100);
|
|
151
|
+
comp.handleInput(ENTER);
|
|
152
|
+
},
|
|
153
|
+
undefined,
|
|
154
|
+
coloredTheme,
|
|
155
|
+
),
|
|
156
|
+
{
|
|
157
|
+
mode: "confirm",
|
|
158
|
+
title: "SSH FILE TRANSFER",
|
|
159
|
+
body: [
|
|
160
|
+
"Intent: Copy a release artifact",
|
|
161
|
+
"Host: deploy@example.com",
|
|
162
|
+
"Direction: Download",
|
|
163
|
+
"From: /srv/releases/app.tar.gz",
|
|
164
|
+
"To: /tmp/app.tar.gz",
|
|
165
|
+
"Mode: Single item",
|
|
166
|
+
"Warning: existing destination may be overwritten",
|
|
167
|
+
"Auth: SSH key (no password)",
|
|
168
|
+
],
|
|
169
|
+
timeoutMs: 0,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
const joined = captured.join("\n");
|
|
173
|
+
expect(joined).toContain("<dim>Host:</dim> <accent>deploy@example.com</accent>");
|
|
174
|
+
expect(joined).toContain("<dim>Direction:</dim> <warning>Download</warning>");
|
|
175
|
+
expect(joined).toContain("<dim>From:</dim> <text>/srv/releases/app.tar.gz</text>");
|
|
176
|
+
expect(joined).toContain("<dim>To:</dim> <accent>/tmp/app.tar.gz</accent>");
|
|
177
|
+
expect(joined).toContain("<warning>Warning: existing destination may be overwritten</warning>");
|
|
178
|
+
expect(joined).toContain("<dim>Auth:</dim> <success>SSH key (no password)</success>");
|
|
179
|
+
});
|
|
180
|
+
|
|
139
181
|
test("wraps a long body command instead of truncating it", async () => {
|
|
140
182
|
// A command far wider than any modal width — must survive in full, wrapped.
|
|
141
183
|
const longCmd = `echo ${"pix-gate-installed-or-linked ".repeat(8)}done`;
|
package/src/gate-overlay.ts
CHANGED
|
@@ -177,10 +177,28 @@ function buildSections(opts: {
|
|
|
177
177
|
const header = [theme.fg(accent, theme.bold(config.title))];
|
|
178
178
|
const body = (config.body ?? []).map((line) => {
|
|
179
179
|
if (line.startsWith("Intent:")) return theme.fg("text", line.slice(7).trimStart());
|
|
180
|
-
if (line.startsWith("Command:")) return theme.fg("
|
|
181
|
-
return theme.fg("
|
|
180
|
+
if (line.startsWith("Command:")) return theme.fg("text", line.slice(8).trimStart());
|
|
181
|
+
if (line.startsWith("Warning:")) return theme.fg("warning", line);
|
|
182
|
+
if (line.startsWith("(") && line.endsWith(")")) return theme.fg("muted", line);
|
|
183
|
+
|
|
184
|
+
const separator = line.indexOf(":");
|
|
185
|
+
if (separator < 1) return theme.fg("text", line);
|
|
186
|
+
const label = line.slice(0, separator + 1);
|
|
187
|
+
const value = line.slice(separator + 1).trimStart();
|
|
188
|
+
const valueColors: Record<string, string> = {
|
|
189
|
+
"Host:": "accent",
|
|
190
|
+
"Direction:": "warning",
|
|
191
|
+
"From:": "text",
|
|
192
|
+
"To:": "accent",
|
|
193
|
+
"Mode:": "muted",
|
|
194
|
+
"Auth:": "success",
|
|
195
|
+
};
|
|
196
|
+
const valueColor = valueColors[label];
|
|
197
|
+
return valueColor
|
|
198
|
+
? `${theme.fg("dim", label)} ${theme.fg(valueColor, value)}`
|
|
199
|
+
: theme.fg("text", line);
|
|
182
200
|
});
|
|
183
|
-
const footer = [theme.fg("
|
|
201
|
+
const footer = [theme.fg("muted", "─".repeat(inner))];
|
|
184
202
|
|
|
185
203
|
if (countdownLine !== undefined) footer.push(countdownLine);
|
|
186
204
|
|
|
@@ -188,14 +206,14 @@ function buildSections(opts: {
|
|
|
188
206
|
if (stage === "select") {
|
|
189
207
|
footer.push(...selectList.render(inner));
|
|
190
208
|
footer.push("");
|
|
191
|
-
footer.push(theme.fg("
|
|
209
|
+
footer.push(theme.fg("muted", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc deny"));
|
|
192
210
|
} else {
|
|
193
211
|
const label = config.mode === "sudo" ? (config.passwordLabel ?? "Sudo password:") : "Password:";
|
|
194
|
-
footer.push(theme.fg("
|
|
212
|
+
footer.push(theme.fg("dim", label));
|
|
195
213
|
if (passwordStatus) footer.push(theme.fg("error", passwordStatus));
|
|
196
214
|
footer.push(...maskedInput.render(inner));
|
|
197
215
|
footer.push("");
|
|
198
|
-
footer.push(theme.fg("
|
|
216
|
+
footer.push(theme.fg("muted", "←→/PgUp/PgDn inspect • enter confirm • esc cancel"));
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
return { header, body, footer };
|
|
@@ -284,7 +302,7 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
|
|
|
284
302
|
// Arm the dead-man's switch (only when a timeout was requested).
|
|
285
303
|
if (timeoutMs > 0) {
|
|
286
304
|
const urgencyColor = (s: number): string =>
|
|
287
|
-
s <= 5 ? "error" : s <= 15 ? "warning" : "
|
|
305
|
+
s <= 5 ? "error" : s <= 15 ? "warning" : "muted";
|
|
288
306
|
const countdownText = (s: number): string => {
|
|
289
307
|
const color = urgencyColor(s);
|
|
290
308
|
const text = `auto-deny in ${s}s`;
|
|
@@ -371,7 +389,7 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
|
|
|
371
389
|
bg: (s) => theme.bg("customMessageBg", s),
|
|
372
390
|
fg: (s) => theme.fg("text", s),
|
|
373
391
|
overflowLine: ({ page, totalPages }) =>
|
|
374
|
-
theme.fg("
|
|
392
|
+
theme.fg("muted", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
|
|
375
393
|
});
|
|
376
394
|
pager.sync(result);
|
|
377
395
|
return result.lines;
|
package/src/modal-frame.ts
CHANGED
|
@@ -546,14 +546,14 @@ interface FgTheme {
|
|
|
546
546
|
|
|
547
547
|
/**
|
|
548
548
|
* Canonical SelectList theme for interactive overlays.
|
|
549
|
-
* accent = active/selected,
|
|
549
|
+
* accent = active/selected, dim = descriptions, muted = scroll/hints, warning = no-match.
|
|
550
550
|
*/
|
|
551
551
|
export function selectListTheme(theme: FgTheme, accent = "accent"): SelectListThemeConfig {
|
|
552
552
|
return {
|
|
553
553
|
selectedPrefix: (t) => theme.fg(accent, t),
|
|
554
554
|
selectedText: (t) => theme.fg(accent, t),
|
|
555
|
-
description: (t) => theme.fg("
|
|
556
|
-
scrollInfo: (t) => theme.fg("
|
|
555
|
+
description: (t) => theme.fg("dim", t),
|
|
556
|
+
scrollInfo: (t) => theme.fg("muted", t),
|
|
557
557
|
noMatch: (t) => theme.fg("warning", t),
|
|
558
558
|
};
|
|
559
559
|
}
|
package/src/progress.ts
CHANGED
|
@@ -92,7 +92,7 @@ export function openProgress(ui: ProgressUI, title: string, accent = "accent"):
|
|
|
92
92
|
maxHeight: terminalModalHeight(tui.terminal?.rows),
|
|
93
93
|
minHeight: MIN_MODAL_HEIGHT,
|
|
94
94
|
header: [theme.fg(accent, theme.bold(title))],
|
|
95
|
-
body: [`${theme.fg(accent, SPINNER[frame] ?? "")} ${theme.fg("
|
|
95
|
+
body: [`${theme.fg(accent, SPINNER[frame] ?? "")} ${theme.fg("dim", labelValue)}`],
|
|
96
96
|
color: (s) => theme.fg(accent, s),
|
|
97
97
|
bg: (s) => theme.bg("customMessageBg", s),
|
|
98
98
|
}).lines;
|
package/src/utils.test.ts
CHANGED
|
@@ -300,6 +300,16 @@ describe("formatJson", () => {
|
|
|
300
300
|
describe("collapsed tool rows", () => {
|
|
301
301
|
const rowTheme = { fg: (_key: string, text: string) => text, bold: (text: string) => text };
|
|
302
302
|
|
|
303
|
+
it("uses dim for the target and muted for tertiary metadata", () => {
|
|
304
|
+
const taggedTheme = {
|
|
305
|
+
fg: (key: string, text: string) => `<${key}>${text}</${key}>`,
|
|
306
|
+
bold: (text: string) => text,
|
|
307
|
+
};
|
|
308
|
+
expect(formatCollapsedToolRow(taggedTheme, "read", "src/a.ts", "12 lines")).toContain(
|
|
309
|
+
"<dim>src/a.ts</dim> <muted>·</muted> <muted>12 lines</muted>",
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
303
313
|
it("renders a consistent status, tool, target, and metadata row", () => {
|
|
304
314
|
// The status marker is width-normalized to 2 cells (padIcon) so wide glyphs
|
|
305
315
|
// align with narrow ones; a 1-cell `✓` therefore carries one pad space.
|
package/src/utils.ts
CHANGED
|
@@ -293,8 +293,8 @@ export function formatCollapsedToolRow(
|
|
|
293
293
|
const icon = padIcon(COLLAPSED_TOOL_GLYPH[status]);
|
|
294
294
|
const parts = [
|
|
295
295
|
`${theme.fg(status, icon)} ${theme.fg("toolTitle", theme.bold(tool))}`,
|
|
296
|
-
target ? theme.fg("
|
|
297
|
-
meta ? `${theme.fg("
|
|
296
|
+
target ? theme.fg("dim", target) : "",
|
|
297
|
+
meta ? `${theme.fg("muted", "·")} ${theme.fg("muted", meta)}` : "",
|
|
298
298
|
].filter(Boolean);
|
|
299
299
|
return parts.join(" ");
|
|
300
300
|
}
|
|
@@ -406,7 +406,7 @@ export function renderDimPreview(
|
|
|
406
406
|
const header = opts.header ? ` ${theme.fg("dim", opts.header)}` : undefined;
|
|
407
407
|
const overflow =
|
|
408
408
|
lines.length > maxLines
|
|
409
|
-
? ` ${theme.fg("
|
|
409
|
+
? ` ${theme.fg("muted", `… ${pluralize(lines.length - maxLines, "more line")}`)}`
|
|
410
410
|
: undefined;
|
|
411
411
|
|
|
412
412
|
if (opts.frame) {
|