@xynogen/pix-pretty 1.7.3 → 1.7.5
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/gate-overlay.test.ts +17 -0
- package/src/gate-overlay.ts +17 -52
package/package.json
CHANGED
package/src/gate-overlay.test.ts
CHANGED
|
@@ -117,6 +117,23 @@ describe("showOverlay — confirm mode", () => {
|
|
|
117
117
|
expect(joined).toContain("MY TITLE");
|
|
118
118
|
expect(joined).toContain("body-line-x");
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
test("wraps a long body command instead of truncating it", async () => {
|
|
122
|
+
// A command far wider than any modal width — must survive in full, wrapped.
|
|
123
|
+
const longCmd = `echo ${"pix-gate-installed-or-linked ".repeat(8)}done`;
|
|
124
|
+
let captured: string[] = [];
|
|
125
|
+
await showOverlay(
|
|
126
|
+
makeUI((comp) => {
|
|
127
|
+
captured = comp.render(80);
|
|
128
|
+
comp.handleInput(ENTER);
|
|
129
|
+
}),
|
|
130
|
+
{ mode: "confirm", title: "T", body: [longCmd], timeoutMs: 0 },
|
|
131
|
+
);
|
|
132
|
+
// Every whitespace-delimited token of the command appears somewhere in the
|
|
133
|
+
// frame — nothing was dropped by truncation.
|
|
134
|
+
const joined = captured.join("\n");
|
|
135
|
+
for (const tok of longCmd.split(" ")) expect(joined).toContain(tok);
|
|
136
|
+
});
|
|
120
137
|
});
|
|
121
138
|
|
|
122
139
|
describe("showOverlay — sudo mode", () => {
|
package/src/gate-overlay.ts
CHANGED
|
@@ -14,12 +14,17 @@
|
|
|
14
14
|
* - Single source of truth for the overlay look across pix-gate and pix-sudo.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Input,
|
|
19
|
+
type SelectItem,
|
|
20
|
+
SelectList,
|
|
21
|
+
wrapTextWithAnsi,
|
|
22
|
+
} from "@earendil-works/pi-tui";
|
|
18
23
|
import { frameLines, modalWidth, selectListTheme } from "./modal-frame.js";
|
|
19
24
|
|
|
20
25
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
21
26
|
|
|
22
|
-
export type OverlayAction = "approved" | "denied" | "timeout";
|
|
27
|
+
export type OverlayAction = "approved" | "denied" | "timeout"; // ponytail: timeout kept for back-compat; never emitted now
|
|
23
28
|
|
|
24
29
|
export interface OverlayResult {
|
|
25
30
|
action: OverlayAction;
|
|
@@ -40,7 +45,7 @@ interface BaseConfig {
|
|
|
40
45
|
title: string;
|
|
41
46
|
/** Optional body lines under the title. */
|
|
42
47
|
body?: string[];
|
|
43
|
-
/**
|
|
48
|
+
/** @deprecated No-op — timeout removed. Dialog waits indefinitely for user input. */
|
|
44
49
|
timeoutMs?: number;
|
|
45
50
|
/**
|
|
46
51
|
* Choices shown in the SelectList.
|
|
@@ -96,10 +101,6 @@ export interface OverlayUI {
|
|
|
96
101
|
|
|
97
102
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
98
103
|
|
|
99
|
-
const SECOND_MS = 1_000;
|
|
100
|
-
const COUNTDOWN_WARN_S = 5;
|
|
101
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
102
|
-
|
|
103
104
|
const DEFAULT_CHOICES: OverlayChoice[] = [
|
|
104
105
|
{ value: "yes", label: "Allow", description: "Proceed" },
|
|
105
106
|
{ value: "no", label: "Deny", description: "Block" },
|
|
@@ -143,12 +144,15 @@ function buildLines(opts: {
|
|
|
143
144
|
const inner = width - 4; // CHROME = 2 border + 2 padding
|
|
144
145
|
const lines: string[] = [];
|
|
145
146
|
|
|
146
|
-
// Title
|
|
147
|
-
|
|
147
|
+
// Title — wrap so a long reason/command isn't truncated by the frame.
|
|
148
|
+
for (const t of wrapTextWithAnsi(config.title, inner)) {
|
|
149
|
+
lines.push(theme.fg(accent, theme.bold(t)));
|
|
150
|
+
}
|
|
148
151
|
|
|
149
|
-
// Body
|
|
152
|
+
// Body — wrap each line so long commands wrap instead of getting cut off.
|
|
150
153
|
for (const line of config.body ?? []) {
|
|
151
|
-
|
|
154
|
+
const wrapped = line === "" ? [""] : wrapTextWithAnsi(line, inner);
|
|
155
|
+
for (const w of wrapped) lines.push(theme.fg("text", w));
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
// Divider after title/body
|
|
@@ -214,17 +218,10 @@ export function showOverlay(
|
|
|
214
218
|
config: OverlayConfig,
|
|
215
219
|
): Promise<OverlayResult> {
|
|
216
220
|
const accent = config.accent ?? "accent";
|
|
217
|
-
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
218
221
|
const choices = config.choices ?? DEFAULT_CHOICES;
|
|
219
222
|
const approveVal = config.approveValue ?? "yes";
|
|
220
223
|
|
|
221
224
|
return new Promise((resolve) => {
|
|
222
|
-
const controller = new AbortController();
|
|
223
|
-
const timer =
|
|
224
|
-
timeoutMs > 0
|
|
225
|
-
? setTimeout(() => controller.abort(), timeoutMs)
|
|
226
|
-
: undefined;
|
|
227
|
-
|
|
228
225
|
ui.custom<OverlayResult>(
|
|
229
226
|
(tui, theme, _kb, done) => {
|
|
230
227
|
type Stage = "select" | "password";
|
|
@@ -245,35 +242,8 @@ export function showOverlay(
|
|
|
245
242
|
);
|
|
246
243
|
const maskedInput = new MaskedInput();
|
|
247
244
|
|
|
248
|
-
// ── countdown ───────────────────────────────────────────────────
|
|
249
|
-
let ticker: ReturnType<typeof setInterval> | undefined;
|
|
250
|
-
if (timeoutMs > 0) {
|
|
251
|
-
const deadlineMs = Date.now() + timeoutMs;
|
|
252
|
-
const updateCountdown = () => {
|
|
253
|
-
const remaining = Math.max(
|
|
254
|
-
0,
|
|
255
|
-
Math.ceil((deadlineMs - Date.now()) / SECOND_MS),
|
|
256
|
-
);
|
|
257
|
-
countdownLine =
|
|
258
|
-
theme.fg("dim", "Auto-deny in ") +
|
|
259
|
-
theme.fg(
|
|
260
|
-
remaining <= COUNTDOWN_WARN_S ? accent : "muted",
|
|
261
|
-
`${remaining}s`,
|
|
262
|
-
);
|
|
263
|
-
};
|
|
264
|
-
updateCountdown();
|
|
265
|
-
ticker = setInterval(() => {
|
|
266
|
-
updateCountdown();
|
|
267
|
-
tui.requestRender();
|
|
268
|
-
}, SECOND_MS);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
245
|
// ── finish ───────────────────────────────────────────────────────
|
|
272
|
-
const finish = (result: OverlayResult) =>
|
|
273
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
274
|
-
if (ticker !== undefined) clearInterval(ticker);
|
|
275
|
-
done(result);
|
|
276
|
-
};
|
|
246
|
+
const finish = (result: OverlayResult) => done(result);
|
|
277
247
|
|
|
278
248
|
// ── event wiring ─────────────────────────────────────────────────
|
|
279
249
|
selectList.onSelect = (item) => {
|
|
@@ -292,10 +262,6 @@ export function showOverlay(
|
|
|
292
262
|
finish({ action: "approved", password: pw });
|
|
293
263
|
maskedInput.onEscape = () => finish({ action: "denied" });
|
|
294
264
|
|
|
295
|
-
controller.signal.addEventListener("abort", () =>
|
|
296
|
-
finish({ action: "timeout" }),
|
|
297
|
-
);
|
|
298
|
-
|
|
299
265
|
// ── component interface ──────────────────────────────────────────
|
|
300
266
|
return {
|
|
301
267
|
render: (w) => {
|
|
@@ -327,8 +293,7 @@ export function showOverlay(
|
|
|
327
293
|
},
|
|
328
294
|
{ overlay: true },
|
|
329
295
|
).then((result) => {
|
|
330
|
-
|
|
331
|
-
resolve(result ?? { action: "timeout" });
|
|
296
|
+
resolve(result ?? { action: "denied" });
|
|
332
297
|
});
|
|
333
298
|
});
|
|
334
299
|
}
|