@xynogen/pix-pretty 1.7.14 → 1.7.17
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/README.md +12 -10
- package/package.json +1 -1
- package/src/ansi.ts +4 -12
- package/src/config.ts +5 -72
- package/src/confirm.ts +6 -24
- package/src/diff-render.ts +42 -174
- package/src/diff.ts +1 -2
- package/src/fff.ts +3 -9
- package/src/gate-overlay.test.ts +2 -6
- package/src/gate-overlay.ts +5 -26
- package/src/highlight.ts +3 -6
- package/src/icon-catalog.ts +11 -8
- package/src/icon-persist.test.ts +10 -2
- package/src/icon-persist.ts +19 -44
- package/src/index.ts +2 -16
- package/src/modal-frame.ts +2 -6
- package/src/progress.ts +1 -5
- package/src/renderers.ts +115 -18
- package/src/types-diff.d.ts +1 -5
- package/src/types.ts +3 -12
- package/src/utils.test.ts +3 -11
- package/src/utils.ts +6 -23
- package/src/commands/pretty.ts +0 -135
package/README.md
CHANGED
|
@@ -9,9 +9,10 @@ consume. It does not register user-facing tools itself — the tool renderers
|
|
|
9
9
|
(`pix-read`, `pix-bash`, `pix-ls`, `pix-find`, `pix-grep`, `pix-edit`,
|
|
10
10
|
`pix-write`) import from it. The extension entry point (`src/index.ts`) only
|
|
11
11
|
initializes the syntax-highlight theme from Pi settings, clears the highlight
|
|
12
|
-
cache,
|
|
12
|
+
cache, seeds the icon mode from `pix.json`, and registers two FFF slash
|
|
13
13
|
commands (`/fff-health`, `/fff-rescan`) once `pix-grep` has brought the FFF
|
|
14
|
-
finder online.
|
|
14
|
+
finder online. The `/pix` settings command lives in `pix-data`.
|
|
15
|
+
(Activated by `pix-core`; not a standalone extension.)
|
|
15
16
|
|
|
16
17
|
### Rendering
|
|
17
18
|
|
|
@@ -33,12 +34,12 @@ problem on terminals without a Nerd Font, becomes a one-file edit here.
|
|
|
33
34
|
mode. Modes: `nerd` (Nerd Font PUA, default), `unicode` (standard BMP glyphs,
|
|
34
35
|
no patched font needed), `ascii` (plain letters). Also `iconFor(key, mode)`,
|
|
35
36
|
`getIconMode()`, `setIconMode()`, `ICON_KEYS`, `ICON_MODES`.
|
|
36
|
-
- **`./icon-persist`** —
|
|
37
|
-
`initIconMode()` applies it on load.
|
|
38
|
-
- **`/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
- **`./icon-persist`** — reads/writes the icon mode via `pix.json`
|
|
38
|
+
(`pretty.icons`); `initIconMode()` applies it on load.
|
|
39
|
+
- **`/pix`** (in `pix-data`) — unified settings overlay that includes the icon
|
|
40
|
+
mode switch. One global knob governs every pix-* package (footer, paste
|
|
41
|
+
chips, model picker, welcome banner, optimizer cell). Seeded from
|
|
42
|
+
`PRETTY_ICONS` (`none`/`off` → `ascii`) when no choice is saved.
|
|
42
43
|
|
|
43
44
|
### Shared overlay
|
|
44
45
|
|
|
@@ -70,7 +71,7 @@ Configuration is read from **`~/.pi/agent/pix.json`** (the unified config file h
|
|
|
70
71
|
```jsonc
|
|
71
72
|
{
|
|
72
73
|
"pretty": {
|
|
73
|
-
"
|
|
74
|
+
"syntaxTheme": "monokai", // syntax-highlight theme
|
|
74
75
|
"icons": "nerd", // nerd | unicode | ascii
|
|
75
76
|
"maxPreviewLines": 50,
|
|
76
77
|
"diffColors": true
|
|
@@ -85,8 +86,9 @@ Configuration is read from **`~/.pi/agent/pix.json`** (the unified config file h
|
|
|
85
86
|
- `PRETTY_MAX_PREVIEW_LINES` — max lines in preview output
|
|
86
87
|
- `PRETTY_CACHE_LIMIT` — FFF cache size limit
|
|
87
88
|
- `PRETTY_ICONS` — default icon mode when none is persisted: `nerd` (default),
|
|
88
|
-
`unicode`, `ascii`, or `none`/`off` (→ `ascii`).
|
|
89
|
+
`unicode`, `ascii`, or `none`/`off` (→ `ascii`).
|
|
89
90
|
Note: this seeds the file-icon helpers AND the semantic icon catalog.
|
|
91
|
+
Overridden by the `/pix` settings command.
|
|
90
92
|
- `PRETTY_MAX_RENDER_LINES` — max lines in edit/write diff render (default: 150)
|
|
91
93
|
- `PRETTY_FFF_DIR` — override FFF state dir (default: `~/.cache/pi/fff`)
|
|
92
94
|
|
package/package.json
CHANGED
package/src/ansi.ts
CHANGED
|
@@ -17,12 +17,8 @@ export let BG_BASE = BG_DEFAULT; // tool box success/base bg — updated from th
|
|
|
17
17
|
export let BG_ERROR = BG_DEFAULT; // tool box error bg — updated from theme's toolErrorBg
|
|
18
18
|
|
|
19
19
|
/** Parse an ANSI 24-bit color escape into { r, g, b }. Handles both fg (38;2) and bg (48;2). */
|
|
20
|
-
function parseAnsiRgb(
|
|
21
|
-
ansi
|
|
22
|
-
): { r: number; g: number; b: number } | null {
|
|
23
|
-
const m = ansi.match(
|
|
24
|
-
new RegExp(`${ESC_RE}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`),
|
|
25
|
-
);
|
|
20
|
+
function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
|
|
21
|
+
const m = ansi.match(new RegExp(`${ESC_RE}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`));
|
|
26
22
|
return m ? { r: +(m[1] ?? 0), g: +(m[2] ?? 0), b: +(m[3] ?? 0) } : null;
|
|
27
23
|
}
|
|
28
24
|
|
|
@@ -40,10 +36,7 @@ function getThemeBgAnsi(theme: BgTheme, key: string): string | null {
|
|
|
40
36
|
export function resolveBaseBackground(theme: BgTheme | null | undefined): void {
|
|
41
37
|
if (!theme?.getBgAnsi) return;
|
|
42
38
|
|
|
43
|
-
BG_BASE =
|
|
44
|
-
getThemeBgAnsi(theme, "toolBg") ??
|
|
45
|
-
getThemeBgAnsi(theme, "background") ??
|
|
46
|
-
BG_DEFAULT;
|
|
39
|
+
BG_BASE = getThemeBgAnsi(theme, "toolBg") ?? getThemeBgAnsi(theme, "background") ?? BG_DEFAULT;
|
|
47
40
|
BG_ERROR = getThemeBgAnsi(theme, "toolErrorBg") ?? BG_BASE;
|
|
48
41
|
RST = `\x1b[0m${BG_BASE}`;
|
|
49
42
|
}
|
|
@@ -61,8 +54,7 @@ function isLowContrastShikiFg(params: string): boolean {
|
|
|
61
54
|
if (params === "38;5;0" || params === "38;5;8") return true;
|
|
62
55
|
if (!params.startsWith("38;2;")) return false;
|
|
63
56
|
const parts = params.split(";").map(Number);
|
|
64
|
-
if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n)))
|
|
65
|
-
return false;
|
|
57
|
+
if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n))) return false;
|
|
66
58
|
const [, , r = 0, g = 0, b = 0] = parts;
|
|
67
59
|
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
68
60
|
return luminance < 72;
|
package/src/config.ts
CHANGED
|
@@ -1,51 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
1
|
import { pixConfig } from "@xynogen/pix-data/pix-config";
|
|
5
|
-
import type { BundledTheme } from "./types.js";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_THEME: BundledTheme = "github-dark";
|
|
8
|
-
|
|
9
|
-
export function getDefaultAgentDir(): string | undefined {
|
|
10
|
-
const home = process.env.HOME ?? "";
|
|
11
|
-
return home ? join(home, ".pi/agent") : undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function readThemeFromSettings(agentDir?: string): BundledTheme | undefined {
|
|
15
|
-
const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
|
|
16
|
-
if (!resolvedAgentDir) return undefined;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const settings = JSON.parse(
|
|
20
|
-
readFileSync(join(resolvedAgentDir, "settings.json"), "utf8"),
|
|
21
|
-
) as {
|
|
22
|
-
theme?: unknown;
|
|
23
|
-
};
|
|
24
|
-
return typeof settings.theme === "string"
|
|
25
|
-
? (settings.theme as BundledTheme)
|
|
26
|
-
: undefined;
|
|
27
|
-
} catch {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function resolvePrettyTheme(agentDir?: string): BundledTheme {
|
|
33
|
-
// Precedence: env → pix.json → settings.json → default
|
|
34
|
-
return (
|
|
35
|
-
(process.env.PRETTY_THEME as BundledTheme | undefined) ??
|
|
36
|
-
(pixConfig().pretty.theme as BundledTheme) ??
|
|
37
|
-
readThemeFromSettings(agentDir) ??
|
|
38
|
-
DEFAULT_THEME
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export let THEME: BundledTheme = resolvePrettyTheme();
|
|
43
|
-
|
|
44
|
-
export function setPrettyTheme(agentDir?: string): void {
|
|
45
|
-
const resolvedTheme = resolvePrettyTheme(agentDir);
|
|
46
|
-
if (resolvedTheme === THEME) return;
|
|
47
|
-
THEME = resolvedTheme;
|
|
48
|
-
}
|
|
49
2
|
|
|
50
3
|
export function envInt(name: string, fallback: number): number {
|
|
51
4
|
const v = Number.parseInt(process.env[name] ?? "", 10);
|
|
@@ -53,11 +6,7 @@ export function envInt(name: string, fallback: number): number {
|
|
|
53
6
|
}
|
|
54
7
|
|
|
55
8
|
// Precedence for numeric config: env var → pix.json → hardcoded default
|
|
56
|
-
function pixOrEnvInt(
|
|
57
|
-
envName: string,
|
|
58
|
-
pixValue: number,
|
|
59
|
-
fallback: number,
|
|
60
|
-
): number {
|
|
9
|
+
function pixOrEnvInt(envName: string, pixValue: number, fallback: number): number {
|
|
61
10
|
const env = process.env[envName];
|
|
62
11
|
if (env) {
|
|
63
12
|
const v = Number.parseInt(env, 10);
|
|
@@ -68,30 +17,14 @@ function pixOrEnvInt(
|
|
|
68
17
|
|
|
69
18
|
const pc = pixConfig().pretty;
|
|
70
19
|
|
|
71
|
-
export const MAX_HL_CHARS = pixOrEnvInt(
|
|
72
|
-
"PRETTY_MAX_HL_CHARS",
|
|
73
|
-
pc.maxHighlightChars,
|
|
74
|
-
80_000,
|
|
75
|
-
);
|
|
20
|
+
export const MAX_HL_CHARS = pixOrEnvInt("PRETTY_MAX_HL_CHARS", pc.maxHighlightChars, 80_000);
|
|
76
21
|
|
|
77
|
-
export const MAX_PREVIEW_LINES = pixOrEnvInt(
|
|
78
|
-
"PRETTY_MAX_PREVIEW_LINES",
|
|
79
|
-
pc.maxPreviewLines,
|
|
80
|
-
80,
|
|
81
|
-
);
|
|
22
|
+
export const MAX_PREVIEW_LINES = pixOrEnvInt("PRETTY_MAX_PREVIEW_LINES", pc.maxPreviewLines, 80);
|
|
82
23
|
|
|
83
|
-
export const CACHE_LIMIT = pixOrEnvInt(
|
|
84
|
-
"PRETTY_CACHE_LIMIT",
|
|
85
|
-
pc.cacheLimit,
|
|
86
|
-
128,
|
|
87
|
-
);
|
|
24
|
+
export const CACHE_LIMIT = pixOrEnvInt("PRETTY_CACHE_LIMIT", pc.cacheLimit, 128);
|
|
88
25
|
|
|
89
26
|
// --- Diff rendering limits (edit/write tools) ---
|
|
90
|
-
export const MAX_RENDER_LINES = pixOrEnvInt(
|
|
91
|
-
"PRETTY_MAX_RENDER_LINES",
|
|
92
|
-
pc.maxRenderLines,
|
|
93
|
-
150,
|
|
94
|
-
);
|
|
27
|
+
export const MAX_RENDER_LINES = pixOrEnvInt("PRETTY_MAX_RENDER_LINES", pc.maxRenderLines, 150);
|
|
95
28
|
|
|
96
29
|
// Word-level emphasis only when paired del/add lines are at least this similar.
|
|
97
30
|
export const WORD_DIFF_MIN_SIM = 0.15;
|
package/src/confirm.ts
CHANGED
|
@@ -55,19 +55,13 @@ const COUNTDOWN_WARN_S = 5;
|
|
|
55
55
|
/**
|
|
56
56
|
* Show a Yes/No overlay. Resolves true on confirm, false otherwise.
|
|
57
57
|
*/
|
|
58
|
-
export function confirmOverlay(
|
|
59
|
-
ui: ConfirmUI,
|
|
60
|
-
opts: ConfirmOptions,
|
|
61
|
-
): Promise<boolean> {
|
|
58
|
+
export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boolean> {
|
|
62
59
|
const accent = opts.accent ?? "accent";
|
|
63
60
|
const timeoutMs = opts.timeoutMs ?? 0;
|
|
64
61
|
|
|
65
62
|
return new Promise((resolve) => {
|
|
66
63
|
const controller = new AbortController();
|
|
67
|
-
const timer =
|
|
68
|
-
timeoutMs > 0
|
|
69
|
-
? setTimeout(() => controller.abort(), timeoutMs)
|
|
70
|
-
: undefined;
|
|
64
|
+
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
|
|
71
65
|
|
|
72
66
|
ui.custom<boolean>(
|
|
73
67
|
(tui, theme, _kb, done) => {
|
|
@@ -87,25 +81,15 @@ export function confirmOverlay(
|
|
|
87
81
|
},
|
|
88
82
|
];
|
|
89
83
|
|
|
90
|
-
const selectList = new SelectList(
|
|
91
|
-
choices,
|
|
92
|
-
choices.length,
|
|
93
|
-
selectListTheme(theme, accent),
|
|
94
|
-
);
|
|
84
|
+
const selectList = new SelectList(choices, choices.length, selectListTheme(theme, accent));
|
|
95
85
|
|
|
96
86
|
if (timeoutMs > 0) {
|
|
97
87
|
const deadlineMs = Date.now() + timeoutMs;
|
|
98
88
|
const updateCountdown = () => {
|
|
99
|
-
const remaining = Math.max(
|
|
100
|
-
0,
|
|
101
|
-
Math.ceil((deadlineMs - Date.now()) / SECOND_MS),
|
|
102
|
-
);
|
|
89
|
+
const remaining = Math.max(0, Math.ceil((deadlineMs - Date.now()) / SECOND_MS));
|
|
103
90
|
countdownLine =
|
|
104
91
|
theme.fg("dim", "Auto-cancel in ") +
|
|
105
|
-
theme.fg(
|
|
106
|
-
remaining <= COUNTDOWN_WARN_S ? accent : "muted",
|
|
107
|
-
`${remaining}s`,
|
|
108
|
-
);
|
|
92
|
+
theme.fg(remaining <= COUNTDOWN_WARN_S ? accent : "muted", `${remaining}s`);
|
|
109
93
|
};
|
|
110
94
|
updateCountdown();
|
|
111
95
|
ticker = setInterval(() => {
|
|
@@ -148,9 +132,7 @@ export function confirmOverlay(
|
|
|
148
132
|
for (const l of selectList.render(inner)) lines.push(l);
|
|
149
133
|
|
|
150
134
|
lines.push("");
|
|
151
|
-
lines.push(
|
|
152
|
-
theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
|
|
153
|
-
);
|
|
135
|
+
lines.push(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"));
|
|
154
136
|
|
|
155
137
|
return frameLines({
|
|
156
138
|
width: mw,
|
package/src/diff-render.ts
CHANGED
|
@@ -72,31 +72,13 @@ const dc = pixConfig().pretty.diff;
|
|
|
72
72
|
// Precedence: env → pix.json → hardcoded default
|
|
73
73
|
const BG_ADD = envBg("DIFF_BG_ADD", hexToBg(dc.bgAdd) || "\x1b[48;2;22;38;32m");
|
|
74
74
|
const BG_DEL = envBg("DIFF_BG_DEL", hexToBg(dc.bgDel) || "\x1b[48;2;45;25;25m");
|
|
75
|
-
const BG_ADD_W = envBg(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
const BG_GUTTER_ADD = envBg(
|
|
84
|
-
"DIFF_BG_GUTTER_ADD",
|
|
85
|
-
hexToBg(dc.bgGutterAdd) || "\x1b[48;2;18;32;26m",
|
|
86
|
-
);
|
|
87
|
-
const BG_GUTTER_DEL = envBg(
|
|
88
|
-
"DIFF_BG_GUTTER_DEL",
|
|
89
|
-
hexToBg(dc.bgGutterDel) || "\x1b[48;2;38;22;22m",
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const FG_ADD = envFg(
|
|
93
|
-
"DIFF_FG_ADD",
|
|
94
|
-
hexToFg(dc.fgAdd) || "\x1b[38;2;100;180;120m",
|
|
95
|
-
);
|
|
96
|
-
const FG_DEL = envFg(
|
|
97
|
-
"DIFF_FG_DEL",
|
|
98
|
-
hexToFg(dc.fgDel) || "\x1b[38;2;200;100;100m",
|
|
99
|
-
);
|
|
75
|
+
const BG_ADD_W = envBg("DIFF_BG_ADD_HL", hexToBg(dc.bgAddHighlight) || "\x1b[48;2;35;75;50m");
|
|
76
|
+
const BG_DEL_W = envBg("DIFF_BG_DEL_HL", hexToBg(dc.bgDelHighlight) || "\x1b[48;2;80;35;35m");
|
|
77
|
+
const BG_GUTTER_ADD = envBg("DIFF_BG_GUTTER_ADD", hexToBg(dc.bgGutterAdd) || "\x1b[48;2;18;32;26m");
|
|
78
|
+
const BG_GUTTER_DEL = envBg("DIFF_BG_GUTTER_DEL", hexToBg(dc.bgGutterDel) || "\x1b[48;2;38;22;22m");
|
|
79
|
+
|
|
80
|
+
const FG_ADD = envFg("DIFF_FG_ADD", hexToFg(dc.fgAdd) || "\x1b[38;2;100;180;120m");
|
|
81
|
+
const FG_DEL = envFg("DIFF_FG_DEL", hexToFg(dc.fgDel) || "\x1b[38;2;200;100;100m");
|
|
100
82
|
const FG_STRIPE = "\x1b[38;2;40;40;40m"; // diagonal stripes on filler cells
|
|
101
83
|
|
|
102
84
|
const BORDER_BAR = "▌";
|
|
@@ -115,10 +97,7 @@ const MAX_TERM_WIDTH = 210;
|
|
|
115
97
|
const MAX_PREVIEW_LINES = envInt("PRETTY_MAX_PREVIEW_LINES", 80);
|
|
116
98
|
|
|
117
99
|
const SPLIT_MIN_WIDTH = envInt("DIFF_SPLIT_MIN_WIDTH", dc.splitMinWidth || 150);
|
|
118
|
-
const SPLIT_MIN_CODE_WIDTH = envInt(
|
|
119
|
-
"DIFF_SPLIT_MIN_CODE_WIDTH",
|
|
120
|
-
dc.splitMinCodeWidth || 60,
|
|
121
|
-
);
|
|
100
|
+
const SPLIT_MIN_CODE_WIDTH = envInt("DIFF_SPLIT_MIN_CODE_WIDTH", dc.splitMinCodeWidth || 60);
|
|
122
101
|
const SPLIT_MAX_WRAP_RATIO = 0.2;
|
|
123
102
|
const SPLIT_MAX_WRAP_LINES = 8;
|
|
124
103
|
|
|
@@ -192,20 +171,12 @@ function ensureContrast(fg: string, bgSeq: string, min = 3): string {
|
|
|
192
171
|
*
|
|
193
172
|
* Theme hue is preserved, but each add/del fg is contrast-checked against the
|
|
194
173
|
* gutter bg it is painted on and lifted if it would render too dark to read. */
|
|
195
|
-
export function resolveDiffColors(theme?: {
|
|
196
|
-
getFgAnsi?: (key: string) => string;
|
|
197
|
-
}): DiffColors {
|
|
174
|
+
export function resolveDiffColors(theme?: { getFgAnsi?: (key: string) => string }): DiffColors {
|
|
198
175
|
if (!theme?.getFgAnsi) return DEFAULT_DIFF_COLORS;
|
|
199
176
|
try {
|
|
200
177
|
return {
|
|
201
|
-
fgAdd: ensureContrast(
|
|
202
|
-
|
|
203
|
-
BG_GUTTER_ADD,
|
|
204
|
-
),
|
|
205
|
-
fgDel: ensureContrast(
|
|
206
|
-
theme.getFgAnsi("toolDiffRemoved") || FG_DEL,
|
|
207
|
-
BG_GUTTER_DEL,
|
|
208
|
-
),
|
|
178
|
+
fgAdd: ensureContrast(theme.getFgAnsi("toolDiffAdded") || FG_ADD, BG_GUTTER_ADD),
|
|
179
|
+
fgDel: ensureContrast(theme.getFgAnsi("toolDiffRemoved") || FG_DEL, BG_GUTTER_DEL),
|
|
209
180
|
fgCtx: theme.getFgAnsi("toolDiffContext") || FG_DIM,
|
|
210
181
|
};
|
|
211
182
|
} catch {
|
|
@@ -214,9 +185,7 @@ export function resolveDiffColors(theme?: {
|
|
|
214
185
|
}
|
|
215
186
|
|
|
216
187
|
/** Stable cache key for the resolved diff theme colors. */
|
|
217
|
-
export function diffThemeCacheKey(theme?: {
|
|
218
|
-
getFgAnsi?: (key: string) => string;
|
|
219
|
-
}): string {
|
|
188
|
+
export function diffThemeCacheKey(theme?: { getFgAnsi?: (key: string) => string }): string {
|
|
220
189
|
const c = resolveDiffColors(theme);
|
|
221
190
|
return `${c.fgAdd}|${c.fgDel}|${c.fgCtx}|${BG_BASE}`;
|
|
222
191
|
}
|
|
@@ -266,9 +235,7 @@ function fit(s: string, w: number): string {
|
|
|
266
235
|
vis++;
|
|
267
236
|
i++;
|
|
268
237
|
}
|
|
269
|
-
return w > 2
|
|
270
|
-
? `${s.slice(0, i)}${RST}${FG_DIM}›${RST}`
|
|
271
|
-
: `${s.slice(0, i)}${RST}`;
|
|
238
|
+
return w > 2 ? `${s.slice(0, i)}${RST}${FG_DIM}›${RST}` : `${s.slice(0, i)}${RST}`;
|
|
272
239
|
}
|
|
273
240
|
|
|
274
241
|
/** Extract last active fg + bg ANSI codes from a string (for wrap continuations). */
|
|
@@ -293,12 +260,7 @@ function ansiState(s: string): string {
|
|
|
293
260
|
}
|
|
294
261
|
|
|
295
262
|
/** Wrap ANSI-encoded string into rows of `w` visible chars. */
|
|
296
|
-
function wrapAnsi(
|
|
297
|
-
s: string,
|
|
298
|
-
w: number,
|
|
299
|
-
maxRows = adaptiveWrapRows(),
|
|
300
|
-
fillBg = "",
|
|
301
|
-
): string[] {
|
|
263
|
+
function wrapAnsi(s: string, w: number, maxRows = adaptiveWrapRows(), fillBg = ""): string[] {
|
|
302
264
|
if (w <= 0) return [""];
|
|
303
265
|
const plain = strip(s);
|
|
304
266
|
if (plain.length <= w) {
|
|
@@ -375,12 +337,7 @@ function stripes(w: number, _rowOffset: number): string {
|
|
|
375
337
|
|
|
376
338
|
/** Right-aligned line number. `noReset` keeps the active bg alive (no
|
|
377
339
|
* trailing RST) so the caller can build one bg-continuous gutter segment. */
|
|
378
|
-
function lnum(
|
|
379
|
-
n: number | null,
|
|
380
|
-
w: number,
|
|
381
|
-
fg = FG_LNUM,
|
|
382
|
-
noReset = false,
|
|
383
|
-
): string {
|
|
340
|
+
function lnum(n: number | null, w: number, fg = FG_LNUM, noReset = false): string {
|
|
384
341
|
if (n === null) return " ".repeat(w);
|
|
385
342
|
const v = String(n);
|
|
386
343
|
const pad = " ".repeat(Math.max(0, w - v.length));
|
|
@@ -402,20 +359,8 @@ function buildGutter(opts: {
|
|
|
402
359
|
nw: number;
|
|
403
360
|
continuation: boolean;
|
|
404
361
|
}): string {
|
|
405
|
-
const {
|
|
406
|
-
|
|
407
|
-
gutterBg,
|
|
408
|
-
bodyBg,
|
|
409
|
-
num,
|
|
410
|
-
numFg,
|
|
411
|
-
signFg,
|
|
412
|
-
sign,
|
|
413
|
-
nw,
|
|
414
|
-
continuation,
|
|
415
|
-
} = opts;
|
|
416
|
-
const border = borderFg
|
|
417
|
-
? `${gutterBg}${borderFg}${BORDER_BAR}`
|
|
418
|
-
: `${BG_BASE} `;
|
|
362
|
+
const { borderFg, gutterBg, bodyBg, num, numFg, signFg, sign, nw, continuation } = opts;
|
|
363
|
+
const border = borderFg ? `${gutterBg}${borderFg}${BORDER_BAR}` : `${BG_BASE} `;
|
|
419
364
|
const numCell = continuation
|
|
420
365
|
? " ".repeat(nw + 2)
|
|
421
366
|
: `${lnum(num, nw, numFg, true)}${signFg}${sign} `;
|
|
@@ -505,8 +450,7 @@ function injectBg(
|
|
|
505
450
|
continue;
|
|
506
451
|
}
|
|
507
452
|
}
|
|
508
|
-
while (ri < ranges.length && vis >= (ranges[ri] as [number, number])[1])
|
|
509
|
-
ri++;
|
|
453
|
+
while (ri < ranges.length && vis >= (ranges[ri] as [number, number])[1]) ri++;
|
|
510
454
|
const want =
|
|
511
455
|
ri < ranges.length &&
|
|
512
456
|
vis >= (ranges[ri] as [number, number])[0] &&
|
|
@@ -523,10 +467,7 @@ function injectBg(
|
|
|
523
467
|
}
|
|
524
468
|
|
|
525
469
|
/** Simple word diff (no syntax hl) — fallback when highlighting is unavailable. */
|
|
526
|
-
function plainWordDiff(
|
|
527
|
-
oldText: string,
|
|
528
|
-
newText: string,
|
|
529
|
-
): { old: string; new: string } {
|
|
470
|
+
function plainWordDiff(oldText: string, newText: string): { old: string; new: string } {
|
|
530
471
|
const parts = Diff.diffWords(oldText, newText);
|
|
531
472
|
let o = "";
|
|
532
473
|
let n = "";
|
|
@@ -551,18 +492,13 @@ function at<T>(arr: T[], i: number): T {
|
|
|
551
492
|
// Split-vs-unified decision
|
|
552
493
|
// ---------------------------------------------------------------------------
|
|
553
494
|
|
|
554
|
-
function shouldUseSplit(
|
|
555
|
-
diff: ParsedDiff,
|
|
556
|
-
tw: number,
|
|
557
|
-
maxRows = MAX_PREVIEW_LINES,
|
|
558
|
-
): boolean {
|
|
495
|
+
function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVIEW_LINES): boolean {
|
|
559
496
|
if (!diff.lines.length) return false;
|
|
560
497
|
if (tw < SPLIT_MIN_WIDTH) return false;
|
|
561
498
|
|
|
562
499
|
const nw = Math.max(
|
|
563
500
|
2,
|
|
564
|
-
String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0))
|
|
565
|
-
.length,
|
|
501
|
+
String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length,
|
|
566
502
|
);
|
|
567
503
|
const half = Math.floor((tw - 1) / 2);
|
|
568
504
|
const gw = nw + 5;
|
|
@@ -599,10 +535,7 @@ export async function renderUnified(
|
|
|
599
535
|
|
|
600
536
|
const vis = diff.lines.slice(0, max);
|
|
601
537
|
const tw = termW();
|
|
602
|
-
const nw = Math.max(
|
|
603
|
-
2,
|
|
604
|
-
String(Math.max(...vis.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length,
|
|
605
|
-
);
|
|
538
|
+
const nw = Math.max(2, String(Math.max(...vis.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
|
606
539
|
const gw = nw + 5;
|
|
607
540
|
const cw = Math.max(20, tw - gw);
|
|
608
541
|
const canHL = diff.chars <= MAX_HL_CHARS && vis.length <= MAX_RENDER_LINES;
|
|
@@ -650,8 +583,7 @@ export async function renderUnified(
|
|
|
650
583
|
const contGutter = buildGutter({ ...gutterArgs, continuation: true });
|
|
651
584
|
const rows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), bodyBg);
|
|
652
585
|
out.push(`${gutter}${rows[0]}${RST}`);
|
|
653
|
-
for (let r = 1; r < rows.length; r++)
|
|
654
|
-
out.push(`${contGutter}${rows[r]}${RST}`);
|
|
586
|
+
for (let r = 1; r < rows.length; r++) out.push(`${contGutter}${rows[r]}${RST}`);
|
|
655
587
|
}
|
|
656
588
|
|
|
657
589
|
while (idx < vis.length) {
|
|
@@ -664,23 +596,14 @@ export async function renderUnified(
|
|
|
664
596
|
const pad = Math.max(0, totalW - label.length - 2);
|
|
665
597
|
const half1 = Math.floor(pad / 2);
|
|
666
598
|
const half2 = pad - half1;
|
|
667
|
-
out.push(
|
|
668
|
-
`${BG_BASE}${FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${RST}`,
|
|
669
|
-
);
|
|
599
|
+
out.push(`${BG_BASE}${FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${RST}`);
|
|
670
600
|
idx++;
|
|
671
601
|
continue;
|
|
672
602
|
}
|
|
673
603
|
|
|
674
604
|
if (l.type === "ctx") {
|
|
675
605
|
const hl = oldHL[oI] ?? l.content;
|
|
676
|
-
emitRow(
|
|
677
|
-
l.newNum,
|
|
678
|
-
" ",
|
|
679
|
-
BG_BASE,
|
|
680
|
-
dc.fgCtx,
|
|
681
|
-
`${BG_BASE}${DIM}${hl}`,
|
|
682
|
-
BG_BASE,
|
|
683
|
-
);
|
|
606
|
+
emitRow(l.newNum, " ", BG_BASE, dc.fgCtx, `${BG_BASE}${DIM}${hl}`, BG_BASE);
|
|
684
607
|
oI++;
|
|
685
608
|
nI++;
|
|
686
609
|
idx++;
|
|
@@ -705,9 +628,7 @@ export async function renderUnified(
|
|
|
705
628
|
}
|
|
706
629
|
|
|
707
630
|
const isPaired = dels.length === 1 && adds.length === 1;
|
|
708
|
-
const wd = isPaired
|
|
709
|
-
? wordDiffAnalysis(at(dels, 0).l.content, at(adds, 0).l.content)
|
|
710
|
-
: null;
|
|
631
|
+
const wd = isPaired ? wordDiffAnalysis(at(dels, 0).l.content, at(adds, 0).l.content) : null;
|
|
711
632
|
const wdBalanced = wd && wd.oldRanges.length > 0 && wd.newRanges.length > 0;
|
|
712
633
|
|
|
713
634
|
if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
|
|
@@ -715,30 +636,11 @@ export async function renderUnified(
|
|
|
715
636
|
const add0 = at(adds, 0);
|
|
716
637
|
const delBody = injectBg(del0.hl, wd.oldRanges, BG_DEL, BG_DEL_W);
|
|
717
638
|
const addBody = injectBg(add0.hl, wd.newRanges, BG_ADD, BG_ADD_W);
|
|
718
|
-
emitRow(
|
|
719
|
-
|
|
720
|
-
"-",
|
|
721
|
-
BG_GUTTER_DEL,
|
|
722
|
-
`${dc.fgDel}${BOLD}`,
|
|
723
|
-
delBody,
|
|
724
|
-
BG_DEL,
|
|
725
|
-
);
|
|
726
|
-
emitRow(
|
|
727
|
-
add0.l.newNum,
|
|
728
|
-
"+",
|
|
729
|
-
BG_GUTTER_ADD,
|
|
730
|
-
`${dc.fgAdd}${BOLD}`,
|
|
731
|
-
addBody,
|
|
732
|
-
BG_ADD,
|
|
733
|
-
);
|
|
639
|
+
emitRow(del0.l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, delBody, BG_DEL);
|
|
640
|
+
emitRow(add0.l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, addBody, BG_ADD);
|
|
734
641
|
continue;
|
|
735
642
|
}
|
|
736
|
-
if (
|
|
737
|
-
isPaired &&
|
|
738
|
-
wdBalanced &&
|
|
739
|
-
wd.similarity >= WORD_DIFF_MIN_SIM &&
|
|
740
|
-
!canHL
|
|
741
|
-
) {
|
|
643
|
+
if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
|
|
742
644
|
const del0 = at(dels, 0);
|
|
743
645
|
const add0 = at(adds, 0);
|
|
744
646
|
const pwd = plainWordDiff(del0.l.content, add0.l.content);
|
|
@@ -763,33 +665,17 @@ export async function renderUnified(
|
|
|
763
665
|
|
|
764
666
|
for (const d of dels) {
|
|
765
667
|
const body = canHL ? `${BG_DEL}${d.hl}` : `${BG_DEL}${d.l.content}`;
|
|
766
|
-
emitRow(
|
|
767
|
-
d.l.oldNum,
|
|
768
|
-
"-",
|
|
769
|
-
BG_GUTTER_DEL,
|
|
770
|
-
`${dc.fgDel}${BOLD}`,
|
|
771
|
-
body,
|
|
772
|
-
BG_DEL,
|
|
773
|
-
);
|
|
668
|
+
emitRow(d.l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, body, BG_DEL);
|
|
774
669
|
}
|
|
775
670
|
for (const a of adds) {
|
|
776
671
|
const body = canHL ? `${BG_ADD}${a.hl}` : `${BG_ADD}${a.l.content}`;
|
|
777
|
-
emitRow(
|
|
778
|
-
a.l.newNum,
|
|
779
|
-
"+",
|
|
780
|
-
BG_GUTTER_ADD,
|
|
781
|
-
`${dc.fgAdd}${BOLD}`,
|
|
782
|
-
body,
|
|
783
|
-
BG_ADD,
|
|
784
|
-
);
|
|
672
|
+
emitRow(a.l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, body, BG_ADD);
|
|
785
673
|
}
|
|
786
674
|
}
|
|
787
675
|
|
|
788
676
|
out.push(rule(tw));
|
|
789
677
|
if (diff.lines.length > vis.length) {
|
|
790
|
-
out.push(
|
|
791
|
-
`${BG_BASE}${FG_DIM} … ${diff.lines.length - vis.length} more lines${RST}`,
|
|
792
|
-
);
|
|
678
|
+
out.push(`${BG_BASE}${FG_DIM} … ${diff.lines.length - vis.length} more lines${RST}`);
|
|
793
679
|
}
|
|
794
680
|
return out.join("\n");
|
|
795
681
|
}
|
|
@@ -805,8 +691,7 @@ export async function renderSplit(
|
|
|
805
691
|
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
806
692
|
): Promise<string> {
|
|
807
693
|
const tw = termW();
|
|
808
|
-
if (!shouldUseSplit(diff, tw, max))
|
|
809
|
-
return renderUnified(diff, language, max, dc);
|
|
694
|
+
if (!shouldUseSplit(diff, tw, max)) return renderUnified(diff, language, max, dc);
|
|
810
695
|
if (!diff.lines.length) return "";
|
|
811
696
|
|
|
812
697
|
type Row = { left: DiffLine | null; right: DiffLine | null };
|
|
@@ -834,21 +719,18 @@ export async function renderSplit(
|
|
|
834
719
|
i++;
|
|
835
720
|
}
|
|
836
721
|
const n = Math.max(dels.length, adds.length);
|
|
837
|
-
for (let j = 0; j < n; j++)
|
|
838
|
-
rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
|
|
722
|
+
for (let j = 0; j < n; j++) rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
|
|
839
723
|
}
|
|
840
724
|
|
|
841
725
|
const vis = rows.slice(0, max);
|
|
842
726
|
const half = Math.floor((tw - 1) / 2);
|
|
843
727
|
const nw = Math.max(
|
|
844
728
|
2,
|
|
845
|
-
String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0))
|
|
846
|
-
.length,
|
|
729
|
+
String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length,
|
|
847
730
|
);
|
|
848
731
|
const gw = nw + 5;
|
|
849
732
|
const cw = Math.max(12, half - gw);
|
|
850
|
-
const canHL =
|
|
851
|
-
diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;
|
|
733
|
+
const canHL = diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;
|
|
852
734
|
|
|
853
735
|
const leftSrc: string[] = [];
|
|
854
736
|
const rightSrc: string[] = [];
|
|
@@ -920,9 +802,7 @@ export async function renderSplit(
|
|
|
920
802
|
|
|
921
803
|
// Split view's non-bordered context rows lead with a space before bg;
|
|
922
804
|
// buildGutter handles bordered rows, so feed the same border convention.
|
|
923
|
-
const splitBorder = borderFg
|
|
924
|
-
? `${gBg}${borderFg}${BORDER_BAR}`
|
|
925
|
-
: ` ${BG_BASE}`;
|
|
805
|
+
const splitBorder = borderFg ? `${gBg}${borderFg}${BORDER_BAR}` : ` ${BG_BASE}`;
|
|
926
806
|
const numCell = `${lnum(num, nw, numFg, true)}${sFg}${BOLD}${sign} `;
|
|
927
807
|
const gutter = `${splitBorder}${gBg}${numCell}${FG_RULE}│${cBg} ${RST}`;
|
|
928
808
|
const contGutter = `${splitBorder}${gBg}${" ".repeat(nw + 2)}${FG_RULE}│${cBg} ${RST}`;
|
|
@@ -936,14 +816,8 @@ export async function renderSplit(
|
|
|
936
816
|
for (const r of vis) {
|
|
937
817
|
const leftLine = r.left;
|
|
938
818
|
const rightLine = r.right;
|
|
939
|
-
const paired =
|
|
940
|
-
|
|
941
|
-
rightLine &&
|
|
942
|
-
leftLine.type === "del" &&
|
|
943
|
-
rightLine.type === "add";
|
|
944
|
-
const wd = paired
|
|
945
|
-
? wordDiffAnalysis(leftLine.content, rightLine.content)
|
|
946
|
-
: null;
|
|
819
|
+
const paired = leftLine && rightLine && leftLine.type === "del" && rightLine.type === "add";
|
|
820
|
+
const wd = paired ? wordDiffAnalysis(leftLine.content, rightLine.content) : null;
|
|
947
821
|
|
|
948
822
|
let lResult: HalfResult;
|
|
949
823
|
let rResult: HalfResult;
|
|
@@ -961,13 +835,9 @@ export async function renderSplit(
|
|
|
961
835
|
rResult = halfBuild(rightLine, pwd.new, null, "right");
|
|
962
836
|
} else {
|
|
963
837
|
const lhl =
|
|
964
|
-
leftLine && leftLine.type !== "sep"
|
|
965
|
-
? (leftHL[lI++] ?? leftLine?.content ?? "")
|
|
966
|
-
: "";
|
|
838
|
+
leftLine && leftLine.type !== "sep" ? (leftHL[lI++] ?? leftLine?.content ?? "") : "";
|
|
967
839
|
const rhl =
|
|
968
|
-
rightLine && rightLine.type !== "sep"
|
|
969
|
-
? (rightHL[rI++] ?? rightLine?.content ?? "")
|
|
970
|
-
: "";
|
|
840
|
+
rightLine && rightLine.type !== "sep" ? (rightHL[rI++] ?? rightLine?.content ?? "") : "";
|
|
971
841
|
lResult = halfBuild(leftLine, lhl, null, "left");
|
|
972
842
|
rResult = halfBuild(rightLine, rhl, null, "right");
|
|
973
843
|
}
|
|
@@ -987,9 +857,7 @@ export async function renderSplit(
|
|
|
987
857
|
|
|
988
858
|
out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
|
|
989
859
|
if (rows.length > vis.length) {
|
|
990
|
-
out.push(
|
|
991
|
-
`${BG_BASE}${FG_DIM} … ${rows.length - vis.length} more lines${RST}`,
|
|
992
|
-
);
|
|
860
|
+
out.push(`${BG_BASE}${FG_DIM} … ${rows.length - vis.length} more lines${RST}`);
|
|
993
861
|
}
|
|
994
862
|
return out.join("\n");
|
|
995
863
|
}
|
package/src/diff.ts
CHANGED
|
@@ -42,8 +42,7 @@ export function parseDiff(
|
|
|
42
42
|
for (let hi = 0; hi < patch.hunks.length; hi++) {
|
|
43
43
|
if (hi > 0) {
|
|
44
44
|
const prev = at(patch.hunks, hi - 1);
|
|
45
|
-
const gap =
|
|
46
|
-
at(patch.hunks, hi).oldStart - (prev.oldStart + prev.oldLines);
|
|
45
|
+
const gap = at(patch.hunks, hi).oldStart - (prev.oldStart + prev.oldLines);
|
|
47
46
|
lines.push({
|
|
48
47
|
type: "sep",
|
|
49
48
|
oldNum: null,
|