@xynogen/pix-pretty 1.18.0 → 1.18.2
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/diff-render.ts +3 -6
- package/src/gate-overlay.test.ts +43 -1
- package/src/gate-overlay.ts +19 -1
- package/src/utils.test.ts +32 -3
- package/src/utils.ts +10 -10
package/package.json
CHANGED
package/src/diff-render.ts
CHANGED
|
@@ -49,8 +49,6 @@ const ANSI_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([^m]*)m`, "g");
|
|
|
49
49
|
// Terminal bounds + thresholds
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
|
|
52
|
-
const MAX_TERM_WIDTH = 210;
|
|
53
|
-
|
|
54
52
|
const MAX_PREVIEW_LINES = envInt("PRETTY_MAX_PREVIEW_LINES", 80);
|
|
55
53
|
|
|
56
54
|
const SPLIT_MIN_WIDTH = envInt("DIFF_SPLIT_MIN_WIDTH", dc.splitMinWidth || 150);
|
|
@@ -158,8 +156,8 @@ function tabs(s: string): string {
|
|
|
158
156
|
function termW(): number {
|
|
159
157
|
// Single source of truth: utils.termW caches, falls back to tty ioctl, and
|
|
160
158
|
// invalidates on resize. Diff layout needs a hard floor of 80 cols for the
|
|
161
|
-
// split-view column math
|
|
162
|
-
return Math.max(80,
|
|
159
|
+
// split-view column math (no ceiling — fill the true width on ultrawide).
|
|
160
|
+
return Math.max(80, utilsTermW());
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
/** Pad/truncate `s` to exactly `w` visible chars. ANSI-aware. */
|
|
@@ -557,8 +555,7 @@ export async function renderUnified(
|
|
|
557
555
|
if (l.type === "sep") {
|
|
558
556
|
const gap = l.newNum;
|
|
559
557
|
const label = gap && gap > 0 ? ` ${gap} unmodified lines ` : "···";
|
|
560
|
-
const
|
|
561
|
-
const pad = Math.max(0, totalW - label.length - 2);
|
|
558
|
+
const pad = Math.max(0, tw - label.length - 2);
|
|
562
559
|
const half1 = Math.floor(pad / 2);
|
|
563
560
|
const half2 = pad - half1;
|
|
564
561
|
out.push(`${BG_BASE}${FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${RST}`);
|
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
|
@@ -178,7 +178,25 @@ function buildSections(opts: {
|
|
|
178
178
|
const body = (config.body ?? []).map((line) => {
|
|
179
179
|
if (line.startsWith("Intent:")) return theme.fg("text", line.slice(7).trimStart());
|
|
180
180
|
if (line.startsWith("Command:")) return theme.fg("dim", line.slice(8).trimStart());
|
|
181
|
-
return theme.fg("
|
|
181
|
+
if (line.startsWith("Warning:")) return theme.fg("warning", line);
|
|
182
|
+
if (line.startsWith("(") && line.endsWith(")")) return theme.fg("dim", 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
201
|
const footer = [theme.fg("dim", "─".repeat(inner))];
|
|
184
202
|
|
package/src/utils.test.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ruleFrame,
|
|
16
16
|
sectionRule,
|
|
17
17
|
setResultDetails,
|
|
18
|
+
termW,
|
|
18
19
|
viewportText,
|
|
19
20
|
} from "./utils.js";
|
|
20
21
|
|
|
@@ -160,6 +161,35 @@ function plain(text: string): string {
|
|
|
160
161
|
return text.replace(ANSI, "");
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
describe("termW", () => {
|
|
165
|
+
// termW() caches and invalidates on a stdout 'resize' event. Set columns then
|
|
166
|
+
// emit resize so the next call re-reads.
|
|
167
|
+
function setCols(cols: number): void {
|
|
168
|
+
(process.stdout as { columns?: number }).columns = cols;
|
|
169
|
+
process.stdout.emit("resize");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
it("returns the true terminal width with no upper clamp (ultrawide)", () => {
|
|
173
|
+
const orig = process.stdout.columns;
|
|
174
|
+
try {
|
|
175
|
+
setCols(384); // wider than the old 210 cap
|
|
176
|
+
expect(termW()).toBe(384);
|
|
177
|
+
} finally {
|
|
178
|
+
setCols(orig ?? 80);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("floors width at 1 for a degenerate column count", () => {
|
|
183
|
+
const orig = process.stdout.columns;
|
|
184
|
+
try {
|
|
185
|
+
setCols(0); // falsy — falls through resolution chain, never < 1
|
|
186
|
+
expect(termW()).toBeGreaterThanOrEqual(1);
|
|
187
|
+
} finally {
|
|
188
|
+
setCols(orig ?? 80);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
163
193
|
describe("ruleFrame", () => {
|
|
164
194
|
it("wraps body with a rule top and bottom, then footer below the close", () => {
|
|
165
195
|
const out = ruleFrame(["a", "b"], ["… +3 more"], 10);
|
|
@@ -469,10 +499,9 @@ describe("sectionRule", () => {
|
|
|
469
499
|
expect(sectionRule("\x1b[31m=== x ===\x1b[0m", tag, 20)).toBeNull();
|
|
470
500
|
});
|
|
471
501
|
|
|
472
|
-
it("
|
|
473
|
-
// Ask for 400 columns — the visible width must stay capped (<= 72).
|
|
502
|
+
it("fills the full requested width so it aligns with the tool frame", () => {
|
|
474
503
|
const out = sectionRule("=== x ===", tag, 400) ?? "";
|
|
475
504
|
const visible = out.replace(/<\/?[a-z]+>/g, "");
|
|
476
|
-
expect([...visible].length).
|
|
505
|
+
expect([...visible].length).toBe(400);
|
|
477
506
|
});
|
|
478
507
|
});
|
package/src/utils.ts
CHANGED
|
@@ -52,6 +52,9 @@ export function fillToolBackground(text: string, bg = BG_BASE, width?: number):
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function viewportTextConstructor(TextComponent: TextComponentCtor): TextComponentCtor {
|
|
55
|
+
// SAFETY: a `new`-able ctor and a plain factory returning the same instance
|
|
56
|
+
// shape are interchangeable to callers here; TS can't see the returned
|
|
57
|
+
// ViewportText satisfies TextComponentCtor's construct signature.
|
|
55
58
|
return function ViewportTextComponent(text = "") {
|
|
56
59
|
const component = viewportText(TextComponent);
|
|
57
60
|
component.setText(text);
|
|
@@ -451,7 +454,9 @@ export function termW(): number {
|
|
|
451
454
|
Number.parseInt(process.env.COLUMNS ?? "", 10) ||
|
|
452
455
|
_readTtyColumns() ||
|
|
453
456
|
120;
|
|
454
|
-
|
|
457
|
+
// No upper clamp: frames must fill the true terminal width so our rules align
|
|
458
|
+
// with Pi's native full-width UI on ultrawide displays.
|
|
459
|
+
_cachedTermW = Math.max(1, raw);
|
|
455
460
|
|
|
456
461
|
return _cachedTermW;
|
|
457
462
|
}
|
|
@@ -500,26 +505,21 @@ export function rule(w: number, paint?: RulePaint): string {
|
|
|
500
505
|
/** Matches an `=== label ===` separator line (a common shell/echo idiom). */
|
|
501
506
|
const SECTION_RE = /^\s*={2,}\s*(.+?)\s*={2,}\s*$/;
|
|
502
507
|
|
|
503
|
-
/** Max width of a section divider — a full-width rule on an ultra-wide
|
|
504
|
-
* terminal reads as noise, so cap it regardless of available columns. */
|
|
505
|
-
const SECTION_RULE_MAX = 72;
|
|
506
|
-
|
|
507
508
|
/** Fixed dash count before a left-aligned section label. */
|
|
508
509
|
const SECTION_RULE_LEAD = 4;
|
|
509
510
|
|
|
510
511
|
/**
|
|
511
512
|
* If `line` is an `=== label ===` separator, render it as a left-aligned
|
|
512
513
|
* section divider (`──── label ───────────`); otherwise return null. Callers
|
|
513
|
-
* fall back to their normal per-line rendering on null.
|
|
514
|
-
*
|
|
515
|
-
*
|
|
514
|
+
* fall back to their normal per-line rendering on null. Fills the full `width`
|
|
515
|
+
* so the divider aligns with the surrounding tool frame. Label and rule both
|
|
516
|
+
* use the muted role — theme-driven, ANSI-safe.
|
|
516
517
|
*/
|
|
517
518
|
export function sectionRule(line: string, theme: FgTheme, width: number): string | null {
|
|
518
519
|
const m = SECTION_RE.exec(line);
|
|
519
520
|
if (!m || hasAnsi(line)) return null;
|
|
520
521
|
const label = ` ${m[1]} `;
|
|
521
|
-
const
|
|
522
|
-
const trail = w - SECTION_RULE_LEAD - visibleWidth(label);
|
|
522
|
+
const trail = width - SECTION_RULE_LEAD - visibleWidth(label);
|
|
523
523
|
// Label fits → lead rule + trailing fill. Too long to fill → don't force a
|
|
524
524
|
// full-width rule; wrap it snugly with 2 dashes each side (`── long text ──`).
|
|
525
525
|
const [lead, tail] = trail >= 2 ? [SECTION_RULE_LEAD, trail] : [2, 2];
|