@xynogen/pix-pretty 1.7.22 → 1.7.24
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/ansi.test.ts +3 -18
- package/src/ansi.ts +9 -41
- package/src/diff-render.ts +20 -91
- package/src/diff.test.ts +37 -4
- package/src/types.ts +1 -2
package/package.json
CHANGED
package/src/ansi.test.ts
CHANGED
|
@@ -2,24 +2,9 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import * as ansi from "./ansi.ts";
|
|
3
3
|
import { resolveBaseBackground } from "./ansi.ts";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
test("uses semantic success and error backgrounds from the active theme", () => {
|
|
9
|
-
resolveBaseBackground({
|
|
10
|
-
getBgAnsi: (key) => {
|
|
11
|
-
if (key === "toolSuccessBg") return bg(10, 20, 30);
|
|
12
|
-
if (key === "toolErrorBg") return bg(40, 50, 60);
|
|
13
|
-
return "";
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
expect(ansi.BG_BASE).toBe(bg(10, 20, 30));
|
|
17
|
-
expect(ansi.BG_ERROR).toBe(bg(40, 50, 60));
|
|
18
|
-
expect(ansi.RST).toContain(bg(10, 20, 30));
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("resets stale theme backgrounds when no raw theme accessor exists", () => {
|
|
22
|
-
resolveBaseBackground(undefined);
|
|
5
|
+
describe("tool surfaces", () => {
|
|
6
|
+
test("always preserves terminal background", () => {
|
|
7
|
+
resolveBaseBackground({ getBgAnsi: () => "\x1b[48;2;10;20;30m" });
|
|
23
8
|
expect(ansi.BG_BASE).toBe("\x1b[49m");
|
|
24
9
|
expect(ansi.BG_ERROR).toBe("\x1b[49m");
|
|
25
10
|
expect(ansi.RST).toBe("\x1b[0m");
|
package/src/ansi.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { BgTheme } from "./types.js";
|
|
2
|
-
|
|
3
1
|
export let RST = "\x1b[0m";
|
|
4
2
|
export const BOLD = "\x1b[1m";
|
|
5
3
|
|
|
@@ -13,47 +11,17 @@ export const FG_BLUE = "\x1b[38;2;100;140;220m";
|
|
|
13
11
|
const FG_MUTED = "\x1b[38;2;139;148;158m";
|
|
14
12
|
|
|
15
13
|
const BG_DEFAULT = "\x1b[49m";
|
|
16
|
-
export let BG_BASE = BG_DEFAULT;
|
|
17
|
-
export let BG_ERROR = BG_DEFAULT;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
function getThemeBgAnsi(theme: BgTheme, key: string): string | null {
|
|
26
|
-
try {
|
|
27
|
-
const bgAnsi = theme.getBgAnsi?.(key);
|
|
28
|
-
return bgAnsi && parseAnsiRgb(bgAnsi) ? bgAnsi : null;
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
14
|
+
export let BG_BASE = BG_DEFAULT;
|
|
15
|
+
export let BG_ERROR = BG_DEFAULT;
|
|
16
|
+
|
|
17
|
+
/** Tool and diff renderers always preserve the terminal background. */
|
|
18
|
+
export function resolveBaseBackground(_theme: unknown): void {
|
|
19
|
+
BG_BASE = BG_DEFAULT;
|
|
20
|
+
BG_ERROR = BG_DEFAULT;
|
|
21
|
+
RST = "\x1b[0m";
|
|
32
22
|
}
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
* Recompute on each render so runtime theme changes are respected. */
|
|
36
|
-
export function resolveBaseBackground(theme: BgTheme | null | undefined): void {
|
|
37
|
-
if (!theme?.getBgAnsi) {
|
|
38
|
-
BG_BASE = BG_DEFAULT;
|
|
39
|
-
BG_ERROR = BG_DEFAULT;
|
|
40
|
-
RST = "\x1b[0m";
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
BG_BASE =
|
|
45
|
-
getThemeBgAnsi(theme, "toolSuccessBg") ??
|
|
46
|
-
getThemeBgAnsi(theme, "toolPendingBg") ??
|
|
47
|
-
getThemeBgAnsi(theme, "toolBg") ??
|
|
48
|
-
getThemeBgAnsi(theme, "background") ??
|
|
49
|
-
BG_DEFAULT;
|
|
50
|
-
BG_ERROR = getThemeBgAnsi(theme, "toolErrorBg") ?? BG_BASE;
|
|
51
|
-
RST = `\x1b[0m${BG_BASE}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const ESC_RE = "\u001b";
|
|
55
|
-
|
|
56
|
-
export const ANSI_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([0-9;]*)m`, "g");
|
|
24
|
+
export const ANSI_CAPTURE_RE = /\x1b\[([0-9;]*)m/g;
|
|
57
25
|
|
|
58
26
|
// ---------------------------------------------------------------------------
|
|
59
27
|
// Low-contrast fix (same as pi-diff)
|
package/src/diff-render.ts
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
// vendored pretty extension's primitives (cli-highlight `hlBlock`, shared
|
|
4
4
|
// theme-aware `RST`/`BG_BASE` from ansi.ts).
|
|
5
5
|
//
|
|
6
|
-
// Engine note: pi-diff used Shiki's codeToANSI
|
|
7
|
-
//
|
|
8
|
-
// technique below works unchanged — diff backgrounds layer underneath and
|
|
9
|
-
// persist through fg switches.
|
|
6
|
+
// Engine note: pi-diff used Shiki's codeToANSI. pretty uses cli-highlight,
|
|
7
|
+
// shared with other pix-pretty renderers, and paints foreground tokens only.
|
|
10
8
|
|
|
11
9
|
import { pixConfig } from "@xynogen/pix-data/pix-config";
|
|
12
10
|
import * as Diff from "diff";
|
|
13
|
-
import { BG_BASE, BOLD, FG_DIM, FG_LNUM, FG_RULE, RST } from "./ansi.js";
|
|
11
|
+
import { BG_BASE, BOLD, FG_DIM, FG_GREEN, FG_LNUM, FG_RED, FG_RULE, RST } from "./ansi.js";
|
|
14
12
|
import { MAX_HL_CHARS, MAX_RENDER_LINES, WORD_DIFF_MIN_SIM } from "./config.js";
|
|
15
13
|
import type { DiffLine, ParsedDiff } from "./diff.js";
|
|
16
14
|
import { hlBlock } from "./highlight.js";
|
|
@@ -34,14 +32,9 @@ function envInt(name: string, fallback: number): number {
|
|
|
34
32
|
const DIM = "\x1b[2m";
|
|
35
33
|
const dc = pixConfig().pretty.diff;
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const FALLBACK_BG_DEL_W = "\x1b[48;2;80;35;35m";
|
|
41
|
-
const FALLBACK_BG_GUTTER_ADD = "\x1b[48;2;18;32;26m";
|
|
42
|
-
const FALLBACK_BG_GUTTER_DEL = "\x1b[48;2;38;22;22m";
|
|
43
|
-
const FG_ADD = "\x1b[38;2;100;180;120m";
|
|
44
|
-
const FG_DEL = "\x1b[38;2;200;100;100m";
|
|
35
|
+
// Diff add/remove share the canonical green/red from ansi.ts (single source).
|
|
36
|
+
const FG_ADD = FG_GREEN;
|
|
37
|
+
const FG_DEL = FG_RED;
|
|
45
38
|
const FG_STRIPE = "\x1b[38;2;40;40;40m"; // diagonal stripes on filler cells
|
|
46
39
|
|
|
47
40
|
const BORDER_BAR = "▌";
|
|
@@ -90,68 +83,17 @@ export const DEFAULT_DIFF_COLORS: DiffColors = {
|
|
|
90
83
|
fgAdd: FG_ADD,
|
|
91
84
|
fgDel: FG_DEL,
|
|
92
85
|
fgCtx: FG_DIM,
|
|
93
|
-
bgAdd:
|
|
94
|
-
bgDel:
|
|
95
|
-
bgAddHighlight:
|
|
96
|
-
bgDelHighlight:
|
|
97
|
-
bgGutterAdd:
|
|
98
|
-
bgGutterDel:
|
|
86
|
+
bgAdd: BG_BASE,
|
|
87
|
+
bgDel: BG_BASE,
|
|
88
|
+
bgAddHighlight: BG_BASE,
|
|
89
|
+
bgDelHighlight: BG_BASE,
|
|
90
|
+
bgGutterAdd: BG_BASE,
|
|
91
|
+
bgGutterDel: BG_BASE,
|
|
99
92
|
};
|
|
100
93
|
|
|
101
|
-
// --- contrast helpers -------------------------------------------------------
|
|
102
|
-
// The gutter (line number + sign) paints the diff fg over a dark gutter bg.
|
|
103
|
-
// A theme whose diff fg is itself dark renders the number/sign as black-on-
|
|
104
|
-
// black. We keep the theme's hue but lift its luminance until it clears a
|
|
105
|
-
// minimum contrast ratio against the gutter background it sits on.
|
|
106
|
-
|
|
107
|
-
type Rgb = [number, number, number];
|
|
108
|
-
|
|
109
|
-
function parseAnsiRgb(seq: string, kind: "38" | "48"): Rgb | null {
|
|
110
|
-
const m = seq.match(new RegExp(`\\x1b\\[${kind};2;(\\d+);(\\d+);(\\d+)m`));
|
|
111
|
-
if (!m) return null;
|
|
112
|
-
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function relLuminance([r, g, b]: Rgb): number {
|
|
116
|
-
const f = (c: number) => {
|
|
117
|
-
const s = c / 255;
|
|
118
|
-
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
119
|
-
};
|
|
120
|
-
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function contrastRatio(a: Rgb, b: Rgb): number {
|
|
124
|
-
const la = relLuminance(a);
|
|
125
|
-
const lb = relLuminance(b);
|
|
126
|
-
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
|
127
|
-
return (hi + 0.05) / (lo + 0.05);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Keep hue, raise lightness toward white until contrast >= min (or capped). */
|
|
131
|
-
function ensureContrast(fg: string, bgSeq: string, min = 3): string {
|
|
132
|
-
const rgb = parseAnsiRgb(fg, "38");
|
|
133
|
-
const bg = parseAnsiRgb(bgSeq, "48");
|
|
134
|
-
if (!rgb || !bg) return fg; // can't reason about it — leave theme value
|
|
135
|
-
if (contrastRatio(rgb, bg) >= min) return fg; // already legible
|
|
136
|
-
let [r, g, b] = rgb;
|
|
137
|
-
for (let i = 0; i < 12 && contrastRatio([r, g, b], bg) < min; i++) {
|
|
138
|
-
r = Math.round(r + (255 - r) * 0.25);
|
|
139
|
-
g = Math.round(g + (255 - g) * 0.25);
|
|
140
|
-
b = Math.round(b + (255 - b) * 0.25);
|
|
141
|
-
}
|
|
142
|
-
return `\x1b[38;2;${r};${g};${b}m`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Resolve diff fg colors from pi's theme (if it exposes getFgAnsi), falling
|
|
146
|
-
* back to hardcoded ANSI. BG_BASE is already kept in sync by ansi.ts's
|
|
147
|
-
* resolveBaseBackground (called from the tool renderers).
|
|
148
|
-
*
|
|
149
|
-
* Theme hue is preserved, but each add/del fg is contrast-checked against the
|
|
150
|
-
* gutter bg it is painted on and lifted if it would render too dark to read. */
|
|
151
94
|
type DiffTheme = {
|
|
152
95
|
fg?: FgTheme["fg"];
|
|
153
96
|
getFgAnsi?: (key: string) => string;
|
|
154
|
-
getBgAnsi?: (key: string) => string;
|
|
155
97
|
};
|
|
156
98
|
|
|
157
99
|
function themeFg(theme: DiffTheme, key: string, fallback: string): string {
|
|
@@ -162,32 +104,19 @@ function themeFg(theme: DiffTheme, key: string, fallback: string): string {
|
|
|
162
104
|
}
|
|
163
105
|
}
|
|
164
106
|
|
|
165
|
-
function tintedBackground(fg: string, strength: number, fallback: string): string {
|
|
166
|
-
const rgb = parseAnsiRgb(fg, "38");
|
|
167
|
-
if (!rgb) return fallback;
|
|
168
|
-
const [r, g, b] = rgb.map((channel) => Math.round(channel * strength)) as Rgb;
|
|
169
|
-
return `\x1b[48;2;${r};${g};${b}m`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
107
|
export function resolveDiffColors(theme?: DiffTheme): DiffColors {
|
|
173
108
|
if (!theme) return DEFAULT_DIFF_COLORS;
|
|
174
|
-
const rawFgAdd = themeFg(theme, "toolDiffAdded", FG_ADD);
|
|
175
|
-
const rawFgDel = themeFg(theme, "toolDiffRemoved", FG_DEL);
|
|
176
|
-
const bgAdd = tintedBackground(rawFgAdd, 0.2, FALLBACK_BG_ADD);
|
|
177
|
-
const bgDel = tintedBackground(rawFgDel, 0.2, FALLBACK_BG_DEL);
|
|
178
|
-
const bgGutterAdd = tintedBackground(rawFgAdd, 0.15, FALLBACK_BG_GUTTER_ADD);
|
|
179
|
-
const bgGutterDel = tintedBackground(rawFgDel, 0.15, FALLBACK_BG_GUTTER_DEL);
|
|
180
109
|
return {
|
|
181
|
-
fgAdd:
|
|
182
|
-
fgDel:
|
|
110
|
+
fgAdd: themeFg(theme, "toolDiffAdded", FG_ADD),
|
|
111
|
+
fgDel: themeFg(theme, "toolDiffRemoved", FG_DEL),
|
|
183
112
|
fgCtx: themeFg(theme, "toolDiffContext", FG_DIM),
|
|
184
113
|
theme: typeof theme.fg === "function" ? (theme as FgTheme) : undefined,
|
|
185
|
-
bgAdd,
|
|
186
|
-
bgDel,
|
|
187
|
-
bgAddHighlight:
|
|
188
|
-
bgDelHighlight:
|
|
189
|
-
bgGutterAdd,
|
|
190
|
-
bgGutterDel,
|
|
114
|
+
bgAdd: BG_BASE,
|
|
115
|
+
bgDel: BG_BASE,
|
|
116
|
+
bgAddHighlight: BG_BASE,
|
|
117
|
+
bgDelHighlight: BG_BASE,
|
|
118
|
+
bgGutterAdd: BG_BASE,
|
|
119
|
+
bgGutterDel: BG_BASE,
|
|
191
120
|
};
|
|
192
121
|
}
|
|
193
122
|
|
package/src/diff.test.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { parseDiff } from "./diff.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
diffThemeCacheKey,
|
|
5
|
+
renderDiffSummary,
|
|
6
|
+
renderUnified,
|
|
7
|
+
resolveDiffColors,
|
|
8
|
+
} from "./diff-render.js";
|
|
4
9
|
|
|
5
10
|
const OLD = "line1\nline2\nline3";
|
|
6
11
|
const NEW = "line1\nCHANGED\nline3";
|
|
12
|
+
const ANSI_RE = /\x1b\[[0-9;]*m|<\/?syntax\w+>/g;
|
|
7
13
|
|
|
8
14
|
describe("theme-derived diff rendering", () => {
|
|
9
15
|
const theme = {
|
|
@@ -16,13 +22,21 @@ describe("theme-derived diff rendering", () => {
|
|
|
16
22
|
},
|
|
17
23
|
};
|
|
18
24
|
|
|
19
|
-
it("
|
|
25
|
+
it("uses semantic foregrounds without tint backgrounds", () => {
|
|
20
26
|
const colors = resolveDiffColors(theme);
|
|
21
27
|
expect(colors.fgAdd).toBe("\x1b[38;2;120;210;150m");
|
|
22
28
|
expect(colors.fgDel).toBe("\x1b[38;2;230;120;130m");
|
|
23
29
|
expect(colors.fgCtx).toBe("\x1b[38;2;130;140;150m");
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
for (const key of [
|
|
31
|
+
"bgAdd",
|
|
32
|
+
"bgDel",
|
|
33
|
+
"bgAddHighlight",
|
|
34
|
+
"bgDelHighlight",
|
|
35
|
+
"bgGutterAdd",
|
|
36
|
+
"bgGutterDel",
|
|
37
|
+
] as const) {
|
|
38
|
+
expect(colors[key]).toBe("\x1b[49m");
|
|
39
|
+
}
|
|
26
40
|
});
|
|
27
41
|
|
|
28
42
|
it("includes semantic theme colors in cache identity", () => {
|
|
@@ -38,6 +52,25 @@ describe("theme-derived diff rendering", () => {
|
|
|
38
52
|
"<toolDiffContext>no changes</toolDiffContext>",
|
|
39
53
|
);
|
|
40
54
|
});
|
|
55
|
+
|
|
56
|
+
it("keeps the foreground-only diff style", async () => {
|
|
57
|
+
const rendered = await renderUnified(
|
|
58
|
+
parseDiff("const oldValue = 1;", "const newValue = 2;"),
|
|
59
|
+
"typescript",
|
|
60
|
+
80,
|
|
61
|
+
resolveDiffColors({ ...theme, fg: (_key, text) => text }),
|
|
62
|
+
);
|
|
63
|
+
const lines = rendered.replace(ANSI_RE, "").split("\n");
|
|
64
|
+
|
|
65
|
+
expect(lines).toHaveLength(4);
|
|
66
|
+
expect(lines[0]).toMatch(/^─+$/);
|
|
67
|
+
expect(lines[1]).toMatch(/^▌\s+1- │ const oldValue = 1;\s*$/);
|
|
68
|
+
expect(lines[2]).toMatch(/^▌\s+1\+ │ const newValue = 2;\s*$/);
|
|
69
|
+
expect(lines[3]).toMatch(/^─+$/);
|
|
70
|
+
expect(rendered).toContain(theme.getFgAnsi("toolDiffRemoved"));
|
|
71
|
+
expect(rendered).toContain(theme.getFgAnsi("toolDiffAdded"));
|
|
72
|
+
expect(rendered).not.toMatch(/\x1b\[48(?:;[^m]*)?m/);
|
|
73
|
+
});
|
|
41
74
|
});
|
|
42
75
|
|
|
43
76
|
describe("parseDiff baseLine", () => {
|
package/src/types.ts
CHANGED
|
@@ -28,8 +28,7 @@ export type BgTheme = { getBgAnsi?: (key: string) => string };
|
|
|
28
28
|
|
|
29
29
|
export type FgTheme = {
|
|
30
30
|
fg: (key: string, text: string) => string;
|
|
31
|
-
// Optional raw-ANSI
|
|
32
|
-
// generated tint backgrounds derive from these semantic theme colors.
|
|
31
|
+
// Optional raw-ANSI accessor exposed by Pi's theme for semantic foregrounds.
|
|
33
32
|
getFgAnsi?: (key: string) => string;
|
|
34
33
|
};
|
|
35
34
|
|