diff --git a/.changeset/preserve-captured-pager-color.md b/.changeset/preserve-captured-pager-color.md new file mode 100644 index 000000000..faaa62a1b --- /dev/null +++ b/.changeset/preserve-captured-pager-color.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Keep Git's colors in non-diff `hunk pager` output for captured pager hosts, so LazyGit's branch log renders in its normal per-branch palette instead of a single color. diff --git a/src/app/startup.test.ts b/src/app/startup.test.ts index 009f6db12..d2e4cfeca 100644 --- a/src/app/startup.test.ts +++ b/src/app/startup.test.ts @@ -135,7 +135,8 @@ describe("startup planning", () => { }, }); - expect(plan).toEqual({ kind: "passthrough", text }); + // Captured hosts draw this text themselves, so Git's colors have to survive. + expect(plan).toEqual({ kind: "passthrough", text, preserveColor: true }); expect(loaded).toBe(false); }); @@ -155,7 +156,8 @@ describe("startup planning", () => { }, }); - expect(plan).toEqual({ kind: "passthrough", text }); + // A genuinely dumb terminal would print the escape sequences as literal text. + expect(plan).toEqual({ kind: "passthrough", text, preserveColor: false }); expect(loaded).toBe(false); }); @@ -216,13 +218,14 @@ describe("startup planning", () => { readStdinText: async () => patchText, looksLikePatchInputImpl: () => true, stdoutIsTTY: false, + env: { TERM: "xterm-256color" }, loadAppBootstrapImpl: async () => { loaded = true; throw new Error("unreachable"); }, }); - expect(plan).toEqual({ kind: "passthrough", text: patchText }); + expect(plan).toEqual({ kind: "passthrough", text: patchText, preserveColor: false }); expect(loaded).toBe(false); }); @@ -242,7 +245,7 @@ describe("startup planning", () => { }, }); - expect(plan).toEqual({ kind: "passthrough", text: patchText }); + expect(plan).toEqual({ kind: "passthrough", text: patchText, preserveColor: false }); expect(loaded).toBe(false); }); diff --git a/src/app/startup.ts b/src/app/startup.ts index 41e59b616..870b08650 100644 --- a/src/app/startup.ts +++ b/src/app/startup.ts @@ -46,6 +46,7 @@ export type StartupPlan = | { kind: "passthrough"; text: string; + preserveColor: boolean; } | { kind: "static-diff-pager"; @@ -154,6 +155,7 @@ export async function prepareStartupPlan( if (parsedCliInput.kind === "pager") { const stdinText = await readStdinText(); const pagerOptions = parsedCliInput.options; + const capturedPagerHost = isCapturedPagerHost(env); const staticPagerPlan = () => { const staticPatchInput: CliInput = { kind: "patch", @@ -179,13 +181,18 @@ export async function prepareStartupPlan( : staticPlan; }; + // Captured hosts render Hunk's stdout in their own panel, so passed-through text keeps + // the color Git already put in it. + const passthroughPlan = { + kind: "passthrough" as const, + text: stdinText, + preserveColor: capturedPagerHost, + }; + if (!looksLikePatchInputImpl(stdinText)) { // Dumb-terminal and captured pager hosts cannot safely own an interactive text pager. if (env.TERM === "dumb") { - return { - kind: "passthrough", - text: stdinText, - }; + return passthroughPlan; } return { @@ -195,22 +202,16 @@ export async function prepareStartupPlan( } if (!stdoutIsTTY) { - return { - kind: "passthrough", - text: stdinText, - }; + return passthroughPlan; } - if (env.TERM === "dumb" && !isCapturedPagerHost(env)) { - return { - kind: "passthrough", - text: stdinText, - }; + if (env.TERM === "dumb" && !capturedPagerHost) { + return passthroughPlan; } // Captured pager hosts like LazyGit can provide a PTY while advertising TERM=dumb. // In that mode, emit static colored diff output instead of launching the TUI. - if (isCapturedPagerHost(env)) { + if (capturedPagerHost) { return staticPagerPlan(); } diff --git a/src/main.tsx b/src/main.tsx index 66149683a..28f0d2d7d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -49,7 +49,9 @@ async function main() { } if (startupPlan.kind === "passthrough") { - process.stdout.write(sanitizeTerminalText(startupPlan.text)); + process.stdout.write( + sanitizeTerminalText(startupPlan.text, { preserveAnsiStyle: startupPlan.preserveColor }), + ); process.exit(0); } diff --git a/test/cli/entrypoint.test.ts b/test/cli/entrypoint.test.ts index da5686ec3..b00a0efa2 100644 --- a/test/cli/entrypoint.test.ts +++ b/test/cli/entrypoint.test.ts @@ -18,6 +18,15 @@ function git(cwd: string, ...args: string[]) { } } +/** Drop the ambient markers that would make Hunk treat this run as a captured pager host. */ +function uncapturedPagerEnv() { + return Object.fromEntries( + Object.entries(process.env).filter( + ([key]) => key !== "LV" && key !== "GIT_PAGER" && !key.startsWith("LAZYGIT"), + ), + ); +} + describe("CLI entrypoint contracts", () => { test("bare hunk prints standard help without terminal takeover sequences", () => { const proc = Bun.spawnSync(["bun", "run", "src/main.tsx"], { @@ -225,6 +234,40 @@ describe("CLI entrypoint contracts", () => { expect(stdout).not.toContain("\u001b[?1049h"); }); + test("general pager mode keeps Git colors in non-diff stdin for captured pager hosts", () => { + const coloredLog = "\u001b[33m*\u001b[m \u001b[32mabc1234\u001b[m feat: thing\n"; + const proc = Bun.spawnSync(["bun", "run", "src/main.tsx", "pager"], { + cwd: process.cwd(), + stdin: Buffer.from(coloredLog), + stdout: "pipe", + stderr: "pipe", + env: { ...uncapturedPagerEnv(), TERM: "dumb", LAZYGIT_NEW_DIR_FILE: "/tmp/lazygit-dir" }, + }); + + const stdout = Buffer.from(proc.stdout).toString("utf8"); + + expect(proc.exitCode).toBe(0); + expect(Buffer.from(proc.stderr).toString("utf8")).toBe(""); + expect(stdout).toBe(coloredLog); + }); + + test("general pager mode strips colors from non-diff stdin outside captured pager hosts", () => { + const coloredLog = "\u001b[33m*\u001b[m \u001b[32mabc1234\u001b[m feat: thing\n"; + const proc = Bun.spawnSync(["bun", "run", "src/main.tsx", "pager"], { + cwd: process.cwd(), + stdin: Buffer.from(coloredLog), + stdout: "pipe", + stderr: "pipe", + env: { ...uncapturedPagerEnv(), TERM: "dumb" }, + }); + + const stdout = Buffer.from(proc.stdout).toString("utf8"); + + expect(proc.exitCode).toBe(0); + expect(Buffer.from(proc.stderr).toString("utf8")).toBe(""); + expect(stdout).toBe("* abc1234 feat: thing\n"); + }); + test("general pager mode passes diff stdin through when stdout is not a terminal", () => { const patchText = "diff --git a/a.ts b/a.ts\n@@ -1 +1 @@\n-old\n+new\n"; const proc = Bun.spawnSync(["bun", "run", "src/main.tsx", "pager"], {