@xynogen/pix-pretty 1.18.2 → 1.18.4
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 +41 -0
- package/src/gate-overlay.ts +9 -9
- 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
|
@@ -158,6 +158,7 @@ describe("showOverlay — confirm mode", () => {
|
|
|
158
158
|
title: "SSH FILE TRANSFER",
|
|
159
159
|
body: [
|
|
160
160
|
"Intent: Copy a release artifact",
|
|
161
|
+
"Command: scp app.tar.gz host:/tmp",
|
|
161
162
|
"Host: deploy@example.com",
|
|
162
163
|
"Direction: Download",
|
|
163
164
|
"From: /srv/releases/app.tar.gz",
|
|
@@ -170,6 +171,8 @@ describe("showOverlay — confirm mode", () => {
|
|
|
170
171
|
},
|
|
171
172
|
);
|
|
172
173
|
const joined = captured.join("\n");
|
|
174
|
+
expect(joined).toContain("<dim>Intent:</dim> <text>Copy a release artifact</text>");
|
|
175
|
+
expect(joined).toContain("<dim>Command:</dim> <dim>scp app.tar.gz host:/tmp</dim>");
|
|
173
176
|
expect(joined).toContain("<dim>Host:</dim> <accent>deploy@example.com</accent>");
|
|
174
177
|
expect(joined).toContain("<dim>Direction:</dim> <warning>Download</warning>");
|
|
175
178
|
expect(joined).toContain("<dim>From:</dim> <text>/srv/releases/app.tar.gz</text>");
|
|
@@ -178,6 +181,44 @@ describe("showOverlay — confirm mode", () => {
|
|
|
178
181
|
expect(joined).toContain("<dim>Auth:</dim> <success>SSH key (no password)</success>");
|
|
179
182
|
});
|
|
180
183
|
|
|
184
|
+
// Regression guard (readability fix, pix-pretty 1.18.4): Intent:/Command:
|
|
185
|
+
// lines must go through the label/value colour map — a <dim> label plus a
|
|
186
|
+
// semantic value — never a bare bright value with the label stripped off.
|
|
187
|
+
// Pre-1.18.4 they were sliced and dumped as bright <text> with no label,
|
|
188
|
+
// which was hard to read on the modal background. Command value must be <dim>.
|
|
189
|
+
test("Intent/Command body lines keep a dim label and never drop it", async () => {
|
|
190
|
+
const coloredTheme = {
|
|
191
|
+
fg: (color: string, text: string) => `<${color}>${text}</${color}>`,
|
|
192
|
+
bg: (_color: string, text: string) => text,
|
|
193
|
+
bold: (text: string) => text,
|
|
194
|
+
};
|
|
195
|
+
let captured: string[] = [];
|
|
196
|
+
await showOverlay(
|
|
197
|
+
makeUI(
|
|
198
|
+
(comp) => {
|
|
199
|
+
captured = comp.render(100);
|
|
200
|
+
comp.handleInput(ENTER);
|
|
201
|
+
},
|
|
202
|
+
undefined,
|
|
203
|
+
coloredTheme,
|
|
204
|
+
),
|
|
205
|
+
{
|
|
206
|
+
mode: "sudo",
|
|
207
|
+
title: "ROOT COMMAND REQUEST",
|
|
208
|
+
accent: "error",
|
|
209
|
+
body: ["Intent: install a package", "Command: apt install foo"],
|
|
210
|
+
timeoutMs: 0,
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
const joined = captured.join("\n");
|
|
214
|
+
// Command label + value both dim; intent value stays readable text but is
|
|
215
|
+
// always prefixed by a dim label (never a bare label-less value).
|
|
216
|
+
expect(joined).toContain("<dim>Command:</dim> <dim>apt install foo</dim>");
|
|
217
|
+
expect(joined).toContain("<dim>Intent:</dim> <text>install a package</text>");
|
|
218
|
+
// The pre-1.18.4 bug: label stripped, value dumped bright with no dim label.
|
|
219
|
+
expect(joined).not.toContain("<text>apt install foo</text>"); // command value is dim, not text
|
|
220
|
+
});
|
|
221
|
+
|
|
181
222
|
test("wraps a long body command instead of truncating it", async () => {
|
|
182
223
|
// A command far wider than any modal width — must survive in full, wrapped.
|
|
183
224
|
const longCmd = `echo ${"pix-gate-installed-or-linked ".repeat(8)}done`;
|
package/src/gate-overlay.ts
CHANGED
|
@@ -176,16 +176,16 @@ function buildSections(opts: {
|
|
|
176
176
|
const inner = width - 4; // CHROME = 2 border + 2 padding
|
|
177
177
|
const header = [theme.fg(accent, theme.bold(config.title))];
|
|
178
178
|
const body = (config.body ?? []).map((line) => {
|
|
179
|
-
if (line.startsWith("Intent:")) return theme.fg("text", line.slice(7).trimStart());
|
|
180
|
-
if (line.startsWith("Command:")) return theme.fg("dim", line.slice(8).trimStart());
|
|
181
179
|
if (line.startsWith("Warning:")) return theme.fg("warning", line);
|
|
182
|
-
if (line.startsWith("(") && line.endsWith(")")) return theme.fg("
|
|
180
|
+
if (line.startsWith("(") && line.endsWith(")")) return theme.fg("muted", line);
|
|
183
181
|
|
|
184
182
|
const separator = line.indexOf(":");
|
|
185
183
|
if (separator < 1) return theme.fg("text", line);
|
|
186
184
|
const label = line.slice(0, separator + 1);
|
|
187
185
|
const value = line.slice(separator + 1).trimStart();
|
|
188
186
|
const valueColors: Record<string, string> = {
|
|
187
|
+
"Intent:": "text",
|
|
188
|
+
"Command:": "dim",
|
|
189
189
|
"Host:": "accent",
|
|
190
190
|
"Direction:": "warning",
|
|
191
191
|
"From:": "text",
|
|
@@ -198,7 +198,7 @@ function buildSections(opts: {
|
|
|
198
198
|
? `${theme.fg("dim", label)} ${theme.fg(valueColor, value)}`
|
|
199
199
|
: theme.fg("text", line);
|
|
200
200
|
});
|
|
201
|
-
const footer = [theme.fg("
|
|
201
|
+
const footer = [theme.fg("muted", "─".repeat(inner))];
|
|
202
202
|
|
|
203
203
|
if (countdownLine !== undefined) footer.push(countdownLine);
|
|
204
204
|
|
|
@@ -206,14 +206,14 @@ function buildSections(opts: {
|
|
|
206
206
|
if (stage === "select") {
|
|
207
207
|
footer.push(...selectList.render(inner));
|
|
208
208
|
footer.push("");
|
|
209
|
-
footer.push(theme.fg("
|
|
209
|
+
footer.push(theme.fg("muted", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc deny"));
|
|
210
210
|
} else {
|
|
211
211
|
const label = config.mode === "sudo" ? (config.passwordLabel ?? "Sudo password:") : "Password:";
|
|
212
|
-
footer.push(theme.fg("
|
|
212
|
+
footer.push(theme.fg("dim", label));
|
|
213
213
|
if (passwordStatus) footer.push(theme.fg("error", passwordStatus));
|
|
214
214
|
footer.push(...maskedInput.render(inner));
|
|
215
215
|
footer.push("");
|
|
216
|
-
footer.push(theme.fg("
|
|
216
|
+
footer.push(theme.fg("muted", "←→/PgUp/PgDn inspect • enter confirm • esc cancel"));
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
return { header, body, footer };
|
|
@@ -302,7 +302,7 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
|
|
|
302
302
|
// Arm the dead-man's switch (only when a timeout was requested).
|
|
303
303
|
if (timeoutMs > 0) {
|
|
304
304
|
const urgencyColor = (s: number): string =>
|
|
305
|
-
s <= 5 ? "error" : s <= 15 ? "warning" : "
|
|
305
|
+
s <= 5 ? "error" : s <= 15 ? "warning" : "muted";
|
|
306
306
|
const countdownText = (s: number): string => {
|
|
307
307
|
const color = urgencyColor(s);
|
|
308
308
|
const text = `auto-deny in ${s}s`;
|
|
@@ -389,7 +389,7 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
|
|
|
389
389
|
bg: (s) => theme.bg("customMessageBg", s),
|
|
390
390
|
fg: (s) => theme.fg("text", s),
|
|
391
391
|
overflowLine: ({ page, totalPages }) =>
|
|
392
|
-
theme.fg("
|
|
392
|
+
theme.fg("muted", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
|
|
393
393
|
});
|
|
394
394
|
pager.sync(result);
|
|
395
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) {
|