Skip to content
Open
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/tidy-hounds-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Accept a `[theme]` table that names one theme per terminal background, so `dark` and `light` terminals each get a theme you chose instead of only Hunk's GitHub defaults. `fallback` covers terminals that never report a background.
19 changes: 19 additions & 0 deletions docs/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ Hunk chooses `github-light-default` for light backgrounds and
`github-dark-default` for dark backgrounds, falling back to
`github-dark-default` when the terminal does not answer.

To pick the two themes yourself, write `theme` as a table instead of an id:

```toml
[theme]
dark = "catppuccin-mocha" # required
light = "catppuccin-latte" # required
fallback = "github-dark-default" # optional
```

Hunk queries the terminal background the same way `auto` does, then draws
`dark` or `light`. `fallback` covers sessions where Hunk never gets an answer:
terminals that ignore the query, and captured pager hosts such as LazyGit,
where Hunk never asks. Without it those sessions use `dark`. Both sides accept
any built-in id, a custom theme id, and the compatibility aliases.

A `--theme <id>` flag overrides the table for that run, and picking a theme in
the app (`t`, or `View -> Themes…`) replaces the pair with the single id you
chose — the save-on-quit prompt shows that before writing anything.

Older theme ids such as `graphite` and `paper` remain accepted as compatibility
aliases.

Expand Down
57 changes: 57 additions & 0 deletions src/app/startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,63 @@ describe("startup planning", () => {
expect(opened).toBe(1);
});

test("detects the terminal background for an adaptive theme pair", async () => {
const cliInput: CliInput = {
kind: "patch",
file: "-",
options: {
theme: { dark: "vitesse-dark", light: "one-light" },
pager: true,
},
};
const controllingTerminal = { stdin: {} as never, close: () => {} };
let probes = 0;

const plan = await prepareStartupPlan(["bun", "hunk", "patch", "-"], {
parseCliImpl: async () => cliInput as ParsedCliInput,
resolveRuntimeCliInputImpl: (input) => input,
resolveConfiguredCliInputImpl: (input) => createTestConfigResolution(input),
loadAppBootstrapImpl: async (input) => createBootstrap(input),
openControllingTerminalImpl: () => controllingTerminal,
detectTerminalThemeModeFromBackgroundImpl: async () => {
probes += 1;
return "light";
},
stdinIsTTY: false,
stdoutIsTTY: true,
stdout: { write: () => true } as never,
});

expect(plan).toMatchObject({ kind: "app", bootstrap: { initialThemeMode: "light" } });
expect(probes).toBe(1);
});

test("skips the background probe when one theme covers every terminal", async () => {
const cliInput: CliInput = {
kind: "patch",
file: "-",
options: { theme: "dracula", pager: true },
};
let probes = 0;

await prepareStartupPlan(["bun", "hunk", "patch", "-", "--theme", "dracula"], {
parseCliImpl: async () => cliInput as ParsedCliInput,
resolveRuntimeCliInputImpl: (input) => input,
resolveConfiguredCliInputImpl: (input) => createTestConfigResolution(input),
loadAppBootstrapImpl: async (input) => createBootstrap(input),
openControllingTerminalImpl: () => ({ stdin: {} as never, close: () => {} }),
detectTerminalThemeModeFromBackgroundImpl: async () => {
probes += 1;
return "dark";
},
stdinIsTTY: false,
stdoutIsTTY: true,
stdout: { write: () => true } as never,
});

expect(probes).toBe(0);
});

test("opens the controlling terminal for piped patch startup", async () => {
const cliInput: CliInput = {
kind: "patch",
Expand Down
3 changes: 2 additions & 1 deletion src/app/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { loadAppBootstrap } from "../core/changeset/loaders";
import { looksLikePatchInput } from "../core/process/pager";
import { sanitizeTerminalText } from "../lib/terminalText";
import { detectTerminalThemeModeFromBackground } from "../core/theme/detection";
import { themeSelectionNeedsTerminalMode } from "../core/theme/selection";
import {
openControllingTerminal,
resolveRuntimeCliInput,
Expand Down Expand Up @@ -485,7 +486,7 @@ export async function prepareStartupPlan(
}

let initialThemeMode: AppBootstrap["initialThemeMode"];
if (cliInput.options.theme === "auto" && stdoutIsTTY) {
if (themeSelectionNeedsTerminalMode(cliInput.options.theme) && stdoutIsTTY) {
const themeInput = controllingTerminal?.stdin ?? (stdinIsTTY ? process.stdin : null);
if (themeInput) {
initialThemeMode =
Expand Down
3 changes: 2 additions & 1 deletion src/core/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { CliInput, CursorLine, LayoutMode, SidebarVisibility } from "./run/
import type { UserKeyBinding } from "./run/config";
import type { StartupNotice } from "./process/startupNotice";
import type { TerminalThemeMode } from "./theme/detection";
import type { ThemeSelection } from "./theme/selection";
import type { VcsCatalog } from "./vcs/types";

/** Where a review was loaded from, retained so the session can reload and watch it. */
Expand All @@ -39,7 +40,7 @@ export interface AppBootstrap<ExtensionState = unknown> {
reloadContext: ReloadContext;
changeset: Changeset;
initialMode: LayoutMode;
initialTheme?: string;
initialTheme?: ThemeSelection;
initialThemeMode?: TerminalThemeMode;
/** Selectable custom themes for this session, in menu order. */
customThemes?: readonly NamedCustomThemeConfig[];
Expand Down
3 changes: 2 additions & 1 deletion src/core/run/commandInputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
ExtensionVcsStashShowInput,
} from "../../extension-api/types";
import type { InstallSource } from "../install/installSource";
import type { ThemeSelection } from "../theme/selection";

export type LayoutMode = "auto" | "split" | "stack";
export type CursorLine = "row" | "number" | "off";
Expand All @@ -24,7 +25,7 @@ export interface CommonOptions {
mode?: LayoutMode;
cursorLine?: CursorLine;
vcs?: VcsMode;
theme?: string;
theme?: ThemeSelection;
agentContext?: string;
pager?: boolean;
watch?: boolean;
Expand Down
Loading
Loading