diff --git a/src/ui/diff/PierreDiffView.tsx b/src/ui/diff/PierreDiffView.tsx index 388ee43..c948367 100644 --- a/src/ui/diff/PierreDiffView.tsx +++ b/src/ui/diff/PierreDiffView.tsx @@ -82,9 +82,33 @@ function trimSpans(spans: RenderSpan[], width: number) { }; } -/** Render the left-edge hunk marker without changing row width. */ -function marker(selected: boolean) { - return selected ? "▌" : " "; +/** Parse a hex color string into RGB components. */ +function hexToRgb(hex: string) { + const n = parseInt(hex.slice(1), 16); + return { r: (n >> 16) & 0xff, g: (n >> 8) & 0xff, b: n & 0xff }; +} + +/** Blend a color toward a background at a given ratio (0 = bg, 1 = fg). */ +function blendHex(fg: string, bg: string, ratio: number) { + const f = hexToRgb(fg); + const b = hexToRgb(bg); + const mix = (a: number, z: number) => Math.round(z + (a - z) * ratio); + const r = mix(f.r, b.r); + const g = mix(f.g, b.g); + const bl = mix(f.b, b.b); + return `#${((r << 16) | (g << 8) | bl).toString(16).padStart(6, "0")}`; +} + +const INACTIVE_RAIL_BLEND = 0.35; + +/** Dim a rail color for inactive hunks by blending toward the panel background. */ +function dimRailColor(color: string, theme: AppTheme) { + return blendHex(color, theme.panel, INACTIVE_RAIL_BLEND); +} + +/** The rail marker is always visible. */ +function marker() { + return "▌"; } /** Return the neutral active-hunk rail color for the current theme. */ @@ -93,26 +117,30 @@ function neutralRailColor(theme: AppTheme) { } /** Pick the stack-view rail color for one rendered row. */ -function stackRailColor(kind: StackLineCell["kind"], theme: AppTheme) { - if (kind === "addition") { - return theme.addedSignColor; - } +function stackRailColor(kind: StackLineCell["kind"], theme: AppTheme, selected: boolean) { + let color: string; - if (kind === "deletion") { - return theme.removedSignColor; + if (kind === "addition") { + color = theme.addedSignColor; + } else if (kind === "deletion") { + color = theme.removedSignColor; + } else { + color = neutralRailColor(theme); } - return neutralRailColor(theme); + return selected ? color : dimRailColor(color, theme); } /** Pick the left split-view rail color from the old-side cell state. */ -function splitLeftRailColor(kind: SplitLineCell["kind"], theme: AppTheme) { - return kind === "deletion" ? theme.removedSignColor : neutralRailColor(theme); +function splitLeftRailColor(kind: SplitLineCell["kind"], theme: AppTheme, selected: boolean) { + const color = kind === "deletion" ? theme.removedSignColor : neutralRailColor(theme); + return selected ? color : dimRailColor(color, theme); } /** Pick the right split-view rail color from the new-side cell state. */ -function splitRightRailColor(kind: SplitLineCell["kind"], theme: AppTheme) { - return kind === "addition" ? theme.addedSignColor : neutralRailColor(theme); +function splitRightRailColor(kind: SplitLineCell["kind"], theme: AppTheme, selected: boolean) { + const color = kind === "addition" ? theme.addedSignColor : neutralRailColor(theme); + return selected ? color : dimRailColor(color, theme); } /** Pick split-view colors from the semantic diff cell kind. */ @@ -510,8 +538,8 @@ function renderHeaderRow( }} > - - {marker(selected)} + + {marker()} {label} @@ -534,8 +562,8 @@ function renderHeaderRow( > - - {marker(selected)} + + {marker()} {label} @@ -684,13 +712,13 @@ function renderRow( const leftWidth = Math.max(0, markerWidth + Math.floor(usableWidth / 2)); const rightWidth = Math.max(0, separatorWidth + usableWidth - Math.floor(usableWidth / 2)); const leftPrefix = { - text: marker(selected), - fg: selected ? splitLeftRailColor(row.left.kind, theme) : theme.panel, + text: marker(), + fg: splitLeftRailColor(row.left.kind, theme, selected), bg: theme.panel, }; const rightPrefix = { - text: selected ? "▌" : "│", - fg: selected ? splitRightRailColor(row.right.kind, theme) : theme.border, + text: "▌", + fg: splitRightRailColor(row.right.kind, theme, selected), bg: theme.panel, }; @@ -744,8 +772,8 @@ function renderRow( } } else if (row.type === "stack-line") { const prefix = { - text: marker(selected), - fg: selected ? stackRailColor(row.cell.kind, theme) : theme.panel, + text: marker(), + fg: stackRailColor(row.cell.kind, theme, selected), bg: theme.panel, }; diff --git a/test/app-responsive.test.tsx b/test/app-responsive.test.tsx index a5f8030..29db33f 100644 --- a/test/app-responsive.test.tsx +++ b/test/app-responsive.test.tsx @@ -140,16 +140,16 @@ describe("responsive shell", () => { expect(full).toContain("M alpha.ts"); expect(full).not.toContain("Changeset summary"); expect(full).toContain("drag divider resize"); - expect(full).toContain("│"); + expect(full).toMatch(/▌.*▌/); expect(medium).not.toContain("Files"); expect(medium).not.toContain("Changeset summary"); - expect(medium).toContain("│"); + expect(medium).toMatch(/▌.*▌/); expect(medium).not.toContain("drag divider resize"); expect(tight).not.toContain("Files"); expect(tight).not.toContain("Changeset summary"); - expect(tight).not.toContain("│"); + expect(tight).not.toMatch(/▌.*▌/); expect(tight).not.toContain("drag divider resize"); }); @@ -159,12 +159,12 @@ describe("responsive shell", () => { expect(forcedSplit).not.toContain("Files"); expect(forcedSplit).not.toContain("Changeset summary"); - expect(forcedSplit).toContain("│"); + expect(forcedSplit).toMatch(/▌.*▌/); expect(forcedSplit).not.toContain("drag divider resize"); expect(forcedStack).not.toContain("Files"); expect(forcedStack).not.toContain("Changeset summary"); - expect(forcedStack).not.toContain("│"); + expect(forcedStack).not.toMatch(/▌.*▌/); expect(forcedStack).not.toContain("drag divider resize"); }); @@ -175,12 +175,12 @@ describe("responsive shell", () => { expect(wide).not.toContain("File View Navigate Theme Agent Help"); expect(wide).not.toContain("F10 menu"); expect(wide).not.toContain("M alpha.ts"); - expect(wide).toContain("│"); + expect(wide).toMatch(/▌.*▌/); expect(narrow).not.toContain("File View Navigate Theme Agent Help"); expect(narrow).not.toContain("F10 menu"); expect(narrow).not.toContain("M alpha.ts"); - expect(narrow).not.toContain("│"); + expect(narrow).not.toMatch(/▌.*▌/); }); test("filter focus suppresses global shortcut keys like quit", async () => {