@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 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, registers the `/pretty` icon-style switch, and registers two FFF slash
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. (Activated by `pix-core`; not a standalone extension.)
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`** — stores the mode in `~/.pi/agent/pretty.json`;
37
- `initIconMode()` applies it on load.
38
- - **`/pretty`** the single switch: an overlay that previews each mode's
39
- glyphs live and persists the choice. One global knob governs every pix-*
40
- package (footer, paste chips, model picker, welcome banner, optimizer cell).
41
- Seeded from `PRETTY_ICONS` (`none`/`off` → `ascii`) when no choice is saved.
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
- "theme": "monokai", // syntax-highlight theme
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`). Overridden by `/pretty`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.7.14",
3
+ "version": "1.7.17",
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",
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: string,
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,
@@ -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
- "DIFF_BG_ADD_HL",
77
- hexToBg(dc.bgAddHighlight) || "\x1b[48;2;35;75;50m",
78
- );
79
- const BG_DEL_W = envBg(
80
- "DIFF_BG_DEL_HL",
81
- hexToBg(dc.bgDelHighlight) || "\x1b[48;2;80;35;35m",
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
- theme.getFgAnsi("toolDiffAdded") || FG_ADD,
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
- borderFg,
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
- del0.l.oldNum,
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
- leftLine &&
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,