Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/preserve-captured-pager-color.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 7 additions & 4 deletions src/app/startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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);
});

Expand Down Expand Up @@ -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);
});

Expand All @@ -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);
});

Expand Down
29 changes: 15 additions & 14 deletions src/app/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type StartupPlan =
| {
kind: "passthrough";
text: string;
preserveColor: boolean;
}
| {
kind: "static-diff-pager";
Expand Down Expand Up @@ -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",
Expand All @@ -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 {
Expand All @@ -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();
}

Expand Down
4 changes: 3 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
43 changes: 43 additions & 0 deletions test/cli/entrypoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"], {
Expand Down Expand Up @@ -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"], {
Expand Down