@xynogen/pix-pretty 1.19.0 → 1.20.1
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 +5 -3
- package/src/test-utils.ts +100 -0
- package/src/utils.ts +7 -2
- package/src/ansi.test.ts +0 -12
- package/src/dependency-security.test.ts +0 -32
- package/src/diff.test.ts +0 -117
- package/src/gate-overlay.test.ts +0 -382
- package/src/highlight.test.ts +0 -36
- package/src/icon-catalog.test.ts +0 -97
- package/src/icon-persist.test.ts +0 -59
- package/src/icons.test.ts +0 -35
- package/src/index.test.ts +0 -13
- package/src/modal-frame.test.ts +0 -445
- package/src/utils.test.ts +0 -576
- package/src/widget-format.test.ts +0 -127
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-pretty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.1",
|
|
4
4
|
"description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, diff rendering, and FFF search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -25,13 +25,15 @@
|
|
|
25
25
|
"./resize": "./src/resize.ts",
|
|
26
26
|
"./context": "./src/tools/context.ts",
|
|
27
27
|
"./gate-overlay": "./src/gate-overlay.ts",
|
|
28
|
-
"./modal-frame": "./src/modal-frame.ts"
|
|
28
|
+
"./modal-frame": "./src/modal-frame.ts",
|
|
29
|
+
"./test-utils": "./src/test-utils.ts"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"test": "bun test"
|
|
32
33
|
},
|
|
33
34
|
"files": [
|
|
34
35
|
"src",
|
|
36
|
+
"!src/**/*.test.*",
|
|
35
37
|
"README.md",
|
|
36
38
|
"LICENSE"
|
|
37
39
|
],
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
"@xynogen/pix-runtime": "^0.8.0",
|
|
63
65
|
"chalk": "^4.1.2",
|
|
64
66
|
"cli-highlight": "^2.1.11",
|
|
65
|
-
"@ff-labs/fff-node": "^0.
|
|
67
|
+
"@ff-labs/fff-node": "^0.10.6",
|
|
66
68
|
"diff": "^8.0.3"
|
|
67
69
|
},
|
|
68
70
|
"peerDependencies": {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Shared test harness for tool renderers. Test-only — every pix tool package
|
|
2
|
+
// rebuilt these same mocks (MockTextComponent, capture-pi, theme, render ctx)
|
|
3
|
+
// by hand; this collapses them to one import. Pure and Pi-host-agnostic.
|
|
4
|
+
|
|
5
|
+
import type { CursorStore, FffState } from "./fff.js";
|
|
6
|
+
import type { ToolContext } from "./tools/context.js";
|
|
7
|
+
import type { PiPrettyApi, RenderContextLike, TextComponentCtor, ThemeLike } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/** In-memory TextComponent: stores text, splits on render. */
|
|
10
|
+
export class MockTextComponent {
|
|
11
|
+
private text: string;
|
|
12
|
+
constructor(text = "") {
|
|
13
|
+
this.text = text;
|
|
14
|
+
}
|
|
15
|
+
setText(value: string): void {
|
|
16
|
+
this.text = value;
|
|
17
|
+
}
|
|
18
|
+
getText(): string {
|
|
19
|
+
return this.text;
|
|
20
|
+
}
|
|
21
|
+
render(_width?: number): string[] {
|
|
22
|
+
return this.text.split("\n");
|
|
23
|
+
}
|
|
24
|
+
invalidate(): void {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The tool object a registrar hands to `registerTool`, with the render hooks tests poke. */
|
|
28
|
+
export interface CapturedTool {
|
|
29
|
+
name?: string;
|
|
30
|
+
renderCall?: (...args: unknown[]) => MockTextComponent;
|
|
31
|
+
renderResult?: (...args: unknown[]) => MockTextComponent;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A PiPrettyApi that captures every registered tool. `tool` is the last one
|
|
37
|
+
* registered (Object.assign'd so render hooks are directly callable); `names`
|
|
38
|
+
* lists all registered tool names in order.
|
|
39
|
+
*/
|
|
40
|
+
export function capturePi(): { pi: PiPrettyApi; tool: CapturedTool; names: string[] } {
|
|
41
|
+
const tool: CapturedTool = {};
|
|
42
|
+
const names: string[] = [];
|
|
43
|
+
const pi: PiPrettyApi = {
|
|
44
|
+
registerTool(t: unknown) {
|
|
45
|
+
const name = (t as { name?: string }).name;
|
|
46
|
+
if (name) names.push(name);
|
|
47
|
+
Object.assign(tool, t);
|
|
48
|
+
},
|
|
49
|
+
registerCommand() {},
|
|
50
|
+
on() {},
|
|
51
|
+
};
|
|
52
|
+
return { pi, tool, names };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build the ToolContext every registrar expects; override any field. */
|
|
56
|
+
export function makeToolContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
57
|
+
return {
|
|
58
|
+
cwd: process.cwd(),
|
|
59
|
+
sp: (p: string) => p,
|
|
60
|
+
// SAFETY: MockTextComponent implements the setText/getText/render/invalidate surface
|
|
61
|
+
// TextComponentCtor requires; the ctor arity differs but callers only use `new C(text)`.
|
|
62
|
+
TextComponent: MockTextComponent as unknown as TextComponentCtor,
|
|
63
|
+
fffState: { module: null, finder: null, partialIndex: false, dbDir: null } as FffState,
|
|
64
|
+
// SAFETY: tool renderers only call store()/get() on the cursor store; this stub covers both.
|
|
65
|
+
cursorStore: { store: () => "", get: () => undefined } as unknown as CursorStore,
|
|
66
|
+
...overrides,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Theme mock. Default is passthrough (`fg`/`bold` return the raw value).
|
|
72
|
+
* Pass `tag: true` to wrap `dim`/`muted` foregrounds as `<key>value</key>`,
|
|
73
|
+
* which lets tests assert on visual-hierarchy roles.
|
|
74
|
+
*/
|
|
75
|
+
export function makeTheme({ tag = false }: { tag?: boolean } = {}): ThemeLike {
|
|
76
|
+
return {
|
|
77
|
+
fg: (key: string, value: string) =>
|
|
78
|
+
tag && (key === "dim" || key === "muted") ? `<${key}>${value}</${key}>` : value,
|
|
79
|
+
bold: (value: string) => value,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a RenderContextLike with sane defaults; override expanded/isError/state/etc.
|
|
85
|
+
* `state` is intentionally loose (`Record<string, unknown>`) because renderers stash
|
|
86
|
+
* booleans/numbers/timers there at runtime that the strict `state` type doesn't model.
|
|
87
|
+
*/
|
|
88
|
+
export function makeRenderCtx(
|
|
89
|
+
overrides: Partial<Omit<RenderContextLike, "state">> & { state?: Record<string, unknown> } = {},
|
|
90
|
+
): RenderContextLike {
|
|
91
|
+
// SAFETY: loose state bag matches runtime renderer usage; the strict RenderContextLike
|
|
92
|
+
// state type only lists string keys, but renderers read/write timers and flags there.
|
|
93
|
+
return {
|
|
94
|
+
expanded: false,
|
|
95
|
+
isError: false,
|
|
96
|
+
invalidate: () => {},
|
|
97
|
+
state: {},
|
|
98
|
+
...overrides,
|
|
99
|
+
} as unknown as RenderContextLike;
|
|
100
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
} from "./types.js";
|
|
25
25
|
|
|
26
26
|
export function renderToolError(error: string, theme: FgTheme): string {
|
|
27
|
-
return fillToolBackground(
|
|
27
|
+
return fillToolBackground(theme.fg("error", error), BG_ERROR);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function normalizeLineEndings(text: string): string {
|
|
@@ -324,8 +324,13 @@ export function renderCollapsedToolRow(
|
|
|
324
324
|
target: string,
|
|
325
325
|
meta = "",
|
|
326
326
|
status: CollapsedToolStatus = "success",
|
|
327
|
+
width?: number,
|
|
327
328
|
): string {
|
|
328
|
-
return fillToolBackground(
|
|
329
|
+
return fillToolBackground(
|
|
330
|
+
formatCollapsedToolRow(theme, tool, target, meta, status),
|
|
331
|
+
BG_BASE,
|
|
332
|
+
width,
|
|
333
|
+
);
|
|
329
334
|
}
|
|
330
335
|
|
|
331
336
|
/** Hide renderCall after its paired result has auto-collapsed. */
|
package/src/ansi.test.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import * as ansi from "./ansi.ts";
|
|
3
|
-
import { resolveBaseBackground } from "./ansi.ts";
|
|
4
|
-
|
|
5
|
-
describe("tool surfaces", () => {
|
|
6
|
-
test("always preserves terminal background", () => {
|
|
7
|
-
resolveBaseBackground({ getBgAnsi: () => "\x1b[48;2;10;20;30m" });
|
|
8
|
-
expect(ansi.BG_BASE).toBe("\x1b[49m");
|
|
9
|
-
expect(ansi.BG_ERROR).toBe("\x1b[49m");
|
|
10
|
-
expect(ansi.RST).toBe("\x1b[0m");
|
|
11
|
-
});
|
|
12
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
-
const repoRoot = dirname(dirname(packageRoot));
|
|
8
|
-
|
|
9
|
-
interface PackageManifest {
|
|
10
|
-
dependencies?: Record<string, string>;
|
|
11
|
-
overrides?: Record<string, string>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function readManifest(path: string): PackageManifest {
|
|
15
|
-
try {
|
|
16
|
-
return JSON.parse(readFileSync(path, "utf8")) as PackageManifest;
|
|
17
|
-
} catch (cause) {
|
|
18
|
-
throw new Error(`Unable to read package manifest: ${path}`, { cause });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("dependency security floors", () => {
|
|
23
|
-
it("uses a jsdiff release without GHSA-73rr-hh4g-fpgx", () => {
|
|
24
|
-
const manifest = readManifest(join(packageRoot, "package.json"));
|
|
25
|
-
expect(manifest.dependencies?.diff).toBe("^8.0.3");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("pins protobufjs above GHSA-j3f2-48v5-ccww", () => {
|
|
29
|
-
const manifest = readManifest(join(repoRoot, "package.json"));
|
|
30
|
-
expect(manifest.overrides?.protobufjs).toBe("7.6.5");
|
|
31
|
-
});
|
|
32
|
-
});
|
package/src/diff.test.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { parseDiff } from "./diff.js";
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_DIFF_COLORS,
|
|
5
|
-
diffThemeCacheKey,
|
|
6
|
-
renderDiffSummary,
|
|
7
|
-
renderUnified,
|
|
8
|
-
resolveDiffColors,
|
|
9
|
-
} from "./diff-render.js";
|
|
10
|
-
|
|
11
|
-
const OLD = "line1\nline2\nline3";
|
|
12
|
-
const NEW = "line1\nCHANGED\nline3";
|
|
13
|
-
const ANSI_RE = /\x1b\[[0-9;]*m|<\/?syntax\w+>/g;
|
|
14
|
-
|
|
15
|
-
describe("theme-derived diff rendering", () => {
|
|
16
|
-
const theme = {
|
|
17
|
-
fg: (key: string, text: string) => `<${key}>${text}</${key}>`,
|
|
18
|
-
getFgAnsi: (key: string) => {
|
|
19
|
-
if (key === "toolDiffAdded") return "\x1b[38;2;120;210;150m";
|
|
20
|
-
if (key === "toolDiffRemoved") return "\x1b[38;2;230;120;130m";
|
|
21
|
-
if (key === "toolDiffContext") return "\x1b[38;2;130;140;150m";
|
|
22
|
-
return "";
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
it("uses semantic foregrounds", () => {
|
|
27
|
-
const colors = resolveDiffColors(theme);
|
|
28
|
-
expect(colors.fgAdd).toBe("\x1b[38;2;120;210;150m");
|
|
29
|
-
expect(colors.fgDel).toBe("\x1b[38;2;230;120;130m");
|
|
30
|
-
expect(colors.fgCtx).toBe("\x1b[38;2;130;140;150m");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Regression: these six slots were all set to BG_BASE (\x1b[49m), which
|
|
34
|
-
// dropped the faint green/red row tint and left only the gutter chips
|
|
35
|
-
// colored. A diff must read as add/remove bands at a glance.
|
|
36
|
-
it("gives changed rows a faint tint background", () => {
|
|
37
|
-
for (const colors of [DEFAULT_DIFF_COLORS, resolveDiffColors(theme)]) {
|
|
38
|
-
for (const key of [
|
|
39
|
-
"bgAdd",
|
|
40
|
-
"bgDel",
|
|
41
|
-
"bgAddHighlight",
|
|
42
|
-
"bgDelHighlight",
|
|
43
|
-
"bgGutterAdd",
|
|
44
|
-
"bgGutterDel",
|
|
45
|
-
] as const) {
|
|
46
|
-
expect(colors[key]).not.toBe("\x1b[49m");
|
|
47
|
-
expect(colors[key]).toMatch(/^\x1b\[48;2;\d+;\d+;\d+m$/);
|
|
48
|
-
}
|
|
49
|
-
// Word-diff emphasis must be distinguishable from the row tint.
|
|
50
|
-
expect(colors.bgAddHighlight).not.toBe(colors.bgAdd);
|
|
51
|
-
expect(colors.bgDelHighlight).not.toBe(colors.bgDel);
|
|
52
|
-
// Add and remove must never collide.
|
|
53
|
-
expect(colors.bgAdd).not.toBe(colors.bgDel);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("includes semantic theme colors in cache identity", () => {
|
|
58
|
-
const changed = { ...theme, getFgAnsi: () => "\x1b[38;2;1;2;3m" };
|
|
59
|
-
expect(diffThemeCacheKey(theme)).not.toBe(diffThemeCacheKey(changed));
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("colors persisted plain summaries only at render time", () => {
|
|
63
|
-
expect(renderDiffSummary("+3 -2", theme)).toBe(
|
|
64
|
-
"<toolDiffAdded>+3</toolDiffAdded> <toolDiffRemoved>-2</toolDiffRemoved>",
|
|
65
|
-
);
|
|
66
|
-
expect(renderDiffSummary("no changes", theme)).toBe(
|
|
67
|
-
"<toolDiffContext>no changes</toolDiffContext>",
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("emits tint backgrounds on changed rows", async () => {
|
|
72
|
-
const rendered = await renderUnified(
|
|
73
|
-
parseDiff("const oldValue = 1;", "const newValue = 2;"),
|
|
74
|
-
"typescript",
|
|
75
|
-
80,
|
|
76
|
-
resolveDiffColors({ ...theme, fg: (_key, text) => text }),
|
|
77
|
-
);
|
|
78
|
-
const { bgAdd, bgDel } = resolveDiffColors(theme);
|
|
79
|
-
expect(rendered).toContain(bgDel);
|
|
80
|
-
expect(rendered).toContain(bgAdd);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("keeps gutter numbering and rule layout intact", async () => {
|
|
84
|
-
const rendered = await renderUnified(
|
|
85
|
-
parseDiff("const oldValue = 1;", "const newValue = 2;"),
|
|
86
|
-
"typescript",
|
|
87
|
-
80,
|
|
88
|
-
resolveDiffColors({ ...theme, fg: (_key, text) => text }),
|
|
89
|
-
);
|
|
90
|
-
const lines = rendered.replace(ANSI_RE, "").split("\n");
|
|
91
|
-
|
|
92
|
-
expect(lines).toHaveLength(4);
|
|
93
|
-
expect(lines[0]).toMatch(/^─+$/);
|
|
94
|
-
expect(lines[1]).toMatch(/^▌\s+1- │ const oldValue = 1;\s*$/);
|
|
95
|
-
expect(lines[2]).toMatch(/^▌\s+1\+ │ const newValue = 2;\s*$/);
|
|
96
|
-
expect(lines[3]).toMatch(/^─+$/);
|
|
97
|
-
expect(rendered).toContain(theme.getFgAnsi("toolDiffRemoved"));
|
|
98
|
-
expect(rendered).toContain(theme.getFgAnsi("toolDiffAdded"));
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("parseDiff baseLine", () => {
|
|
103
|
-
it("is snippet-relative when baseLine omitted (default 0)", () => {
|
|
104
|
-
const { lines } = parseDiff(OLD, NEW);
|
|
105
|
-
const del = lines.find((l) => l.type === "del");
|
|
106
|
-
expect(del?.oldNum).toBe(2); // line2 is the 2nd line of the snippet
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("shifts gutter numbers to absolute when baseLine given", () => {
|
|
110
|
-
// Snippet begins at file line 84 → snippet line 2 becomes file line 85.
|
|
111
|
-
const { lines } = parseDiff(OLD, NEW, 3, 84);
|
|
112
|
-
const del = lines.find((l) => l.type === "del");
|
|
113
|
-
const add = lines.find((l) => l.type === "add");
|
|
114
|
-
expect(del?.oldNum).toBe(85);
|
|
115
|
-
expect(add?.newNum).toBe(85);
|
|
116
|
-
});
|
|
117
|
-
});
|