@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, FFF search, and paste chip formatting",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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", () => {
@@ -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 { Input, type SelectItem, SelectList } from "@earendil-works/pi-tui";
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
- /** Auto-deny after this many ms. 0 = no timeout. Default 30_000. */
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
- lines.push(theme.fg(accent, theme.bold(config.title)));
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
- lines.push(theme.fg("text", line));
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
- if (timer !== undefined) clearTimeout(timer);
331
- resolve(result ?? { action: "timeout" });
296
+ resolve(result ?? { action: "denied" });
332
297
  });
333
298
  });
334
299
  }