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/shiki-syntax-themes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Refresh Hunk's built-in theme system, default to `github-dark-default`, and simplify theme selection around one `theme` setting with `View -> Themes…` / `t` opening the selector. Custom themes can inherit from any built-in theme with `custom_theme.base` while keeping explicit syntax color overrides, and removed theme ids such as `graphite` and `paper` remain accepted as compatibility aliases.
5 changes: 5 additions & 0 deletions .changeset/theme-contrast-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Improve generated theme contrast checks for built-in themes, including diff rows, metadata, chrome, and fallback token colors.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ jobs:
- name: Typecheck
run: bun run typecheck

- name: Theme contrast check
run: bun run test:theme-contrast

- name: Test suite
run: bun run test

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ jobs:
- name: Typecheck
run: bun run typecheck

- name: Theme contrast check
run: bun run test:theme-contrast

- name: Test suite
run: bun test ./src ./packages ./scripts ./test/cli ./test/session

Expand Down Expand Up @@ -136,6 +139,9 @@ jobs:
- name: Typecheck
run: bun run typecheck

- name: Theme contrast check
run: bun run test:theme-contrast

- name: Test suite
run: bun run test

Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ You can persist preferences to a config file:
Example:

```toml
theme = "graphite" # auto, graphite, midnight, paper, ember, catppuccin-latte, catppuccin-frappe, catppuccin-macchiato, catppuccin-mocha, zenburn, custom
theme = "github-dark-default" # any built-in theme id, auto, or custom
mode = "auto" # auto, split, stack
vcs = "git" # git, jj, sl
watch = false
Expand All @@ -130,17 +130,18 @@ agent_notes = false
transparent_background = false
```

`theme = "auto"` and `--theme auto` query the terminal background at startup, choose `paper` for light backgrounds and `graphite` for dark backgrounds, and fall back to `graphite` if the terminal does not answer.
`theme = "auto"` and `--theme auto` query the terminal background at startup, choose `github-light-default` for light backgrounds and `github-dark-default` for dark backgrounds, and fall back to `github-dark-default` if the terminal does not answer.
Older theme ids such as `graphite` and `paper` remain accepted as compatibility aliases.
`exclude_untracked` affects Git/Sapling working-tree `hunk diff` sessions only.
`transparent_background` can also be written as `transparentBackground`.

Custom themes can inherit from any built-in base theme and override only the colors you care about:
Custom themes can inherit from any built-in theme and override only the colors you care about:

```toml
theme = "custom"

[custom_theme]
base = "graphite" # graphite, midnight, paper, ember, catppuccin-latte, catppuccin-frappe, catppuccin-macchiato, catppuccin-mocha, zenburn
base = "catppuccin-mocha"
label = "My Theme"
accent = "#7fd1ff"
panel = "#10161d"
Expand All @@ -150,9 +151,11 @@ noteBorder = "#c49bff"
keyword = "#8ed4ff"
string = "#c7b4ff"
comment = "#6e85a7"
operator = "#7fd1ff"
variable = "#eef4ff"
```

All custom theme colors must use `#rrggbb` hex values.
All custom theme colors must use `#rrggbb` hex values. Press `t` in the app, or choose `View -> Themes…`, to open the theme selector.

### Git integration

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"release:version": "bunx @changesets/cli@2.31.0 version",
"prepare": "simple-git-hooks",
"test": "\"${npm_execpath:-bun}\" test ./src ./packages ./scripts ./test/cli ./test/session",
"test:theme-contrast": "bun test src/ui/themes.test.ts --test-name-pattern contrast",
"test:integration": "\"${npm_execpath:-bun}\" test ./test/pty",
"test:tty-smoke": "HUNK_RUN_TTY_SMOKE=1 \"${npm_execpath:-bun}\" test ./test/smoke",
"check:pack": "bun run ./scripts/check-pack.ts",
Expand Down
8 changes: 4 additions & 4 deletions src/core/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe("parseCli", () => {
"--mode",
"split",
"--theme",
"paper",
"github-light-default",
"--agent-context",
"notes.json",
"--no-line-numbers",
Expand All @@ -111,7 +111,7 @@ describe("parseCli", () => {
staged: false,
options: {
mode: "split",
theme: "paper",
theme: "github-light-default",
agentContext: "notes.json",
watch: true,
lineNumbers: false,
Expand Down Expand Up @@ -227,12 +227,12 @@ describe("parseCli", () => {
});

test("parses general pager mode", async () => {
const parsed = await parseCli(["bun", "hunk", "pager", "--theme", "paper"]);
const parsed = await parseCli(["bun", "hunk", "pager", "--theme", "github-light-default"]);

expect(parsed).toMatchObject({
kind: "pager",
options: {
theme: "paper",
theme: "github-light-default",
},
});
});
Expand Down
71 changes: 42 additions & 29 deletions src/core/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe("config resolution", () => {
writeFileSync(
join(home, ".config", "hunk", "config.toml"),
[
'theme = "graphite"',
'theme = "github-dark-default"',
"line_numbers = false",
"transparentBackground = true",
"color_moved = true",
Expand All @@ -72,7 +72,13 @@ describe("config resolution", () => {
mkdirSync(join(repo, ".hunk"), { recursive: true });
writeFileSync(
join(repo, ".hunk", "config.toml"),
['theme = "paper"', "wrap_lines = true", "", "[pager]", "hunk_headers = false"].join("\n"),
[
'theme = "github-light-default"',
"wrap_lines = true",
"",
"[pager]",
"hunk_headers = false",
].join("\n"),
);

const resolved = resolveConfiguredCliInput(createPatchPagerInput({ agentNotes: true }), {
Expand All @@ -84,7 +90,7 @@ describe("config resolution", () => {
expect(resolved.input.options).toMatchObject({
pager: true,
mode: "stack",
theme: "paper",
theme: "github-light-default",
lineNumbers: false,
wrapLines: true,
hunkHeaders: false,
Expand All @@ -106,7 +112,7 @@ describe("config resolution", () => {
'theme = "custom"',
"",
"[custom_theme]",
'base = "midnight"',
'base = "github-dark-default"',
'label = "Global Custom"',
'accent = "#123456"',
"",
Expand Down Expand Up @@ -137,7 +143,7 @@ describe("config resolution", () => {

expect(resolved.input.options.theme).toBe("custom");
expect(resolved.customTheme).toEqual({
base: "midnight",
base: "github-dark-default",
label: "Repo Custom",
accent: "#123456",
panel: "#654321",
Expand All @@ -148,30 +154,39 @@ describe("config resolution", () => {
});
});

test.each([
"graphite",
"midnight",
"paper",
"ember",
"catppuccin-latte",
"catppuccin-frappe",
"catppuccin-macchiato",
"catppuccin-mocha",
"zenburn",
])("accepts custom theme base id: %s", (base) => {
test.each(["github-dark-default", "github-light-default", "dracula", "catppuccin-mocha"])(
"accepts custom theme base id: %s",
(base) => {
const home = createTempDir("hunk-config-home-");
mkdirSync(join(home, ".config", "hunk"), { recursive: true });
writeFileSync(
join(home, ".config", "hunk", "config.toml"),
["[custom_theme]", `base = "${base}"`].join("\n"),
);

const resolved = resolveConfiguredCliInput(createPatchPagerInput(), {
cwd: createTempDir("hunk-config-cwd-"),
env: { HOME: home },
});

expect(resolved.customTheme).toEqual({ base });
},
);

test("normalizes legacy custom theme base ids", () => {
const home = createTempDir("hunk-config-home-");
mkdirSync(join(home, ".config", "hunk"), { recursive: true });
writeFileSync(
join(home, ".config", "hunk", "config.toml"),
["[custom_theme]", `base = "${base}"`].join("\n"),
["[custom_theme]", 'base = "graphite"'].join("\n"),
);

const resolved = resolveConfiguredCliInput(createPatchPagerInput(), {
cwd: createTempDir("hunk-config-cwd-"),
env: { HOME: home },
});

expect(resolved.customTheme).toEqual({ base });
expect(resolved.customTheme).toEqual({ base: "github-dark-default" });
});

test("rejects invalid custom theme base ids", () => {
Expand All @@ -187,9 +202,7 @@ describe("config resolution", () => {
cwd: createTempDir("hunk-config-cwd-"),
env: { HOME: home },
}),
).toThrow(
"Expected custom_theme.base to be one of: graphite, midnight, paper, ember, catppuccin-latte, catppuccin-frappe, catppuccin-macchiato, catppuccin-mocha, zenburn.",
);
).toThrow("Expected custom_theme.base to be a built-in theme id.");
});

test("rejects invalid custom theme color values", () => {
Expand Down Expand Up @@ -248,7 +261,7 @@ describe("config resolution", () => {
expect(overridden.input.options.transparentBackground).toBe(false);
});

test("defaults unspecified themes to graphite, including piped pager-style patch input", () => {
test("defaults unspecified themes to github-dark-default, including piped pager-style patch input", () => {
const home = createTempDir("hunk-config-home-");
const cwd = createTempDir("hunk-config-cwd-");

Expand All @@ -258,7 +271,7 @@ describe("config resolution", () => {
});

expect(resolved.repoConfigPath).toBeUndefined();
expect(resolved.input.options.theme).toBe("graphite");
expect(resolved.input.options.theme).toBe("github-dark-default");
});

test("command-specific config sections also apply to show mode", () => {
Expand Down Expand Up @@ -469,7 +482,7 @@ describe("config resolution", () => {
writeFileSync(
join(home, ".config", "hunk", "config.toml"),
[
'theme = "paper"',
'theme = "github-light-default"',
"line_numbers = false",
"wrap_lines = true",
"hunk_headers = false",
Expand All @@ -495,7 +508,7 @@ describe("config resolution", () => {
const bootstrap = await loadAppBootstrap(resolved.input);

expect(bootstrap.initialMode).toBe("auto");
expect(bootstrap.initialTheme).toBe("paper");
expect(bootstrap.initialTheme).toBe("github-light-default");
expect(bootstrap.initialShowLineNumbers).toBe(false);
expect(bootstrap.initialWrapLines).toBe(true);
expect(bootstrap.initialShowHunkHeaders).toBe(false);
Expand All @@ -515,7 +528,7 @@ describe("config resolution", () => {
'theme = "custom"',
"",
"[custom_theme]",
'base = "paper"',
'base = "catppuccin-mocha"',
'accent = "#7755aa"',
"",
"[custom_theme.syntax]",
Expand All @@ -541,15 +554,15 @@ describe("config resolution", () => {

expect(bootstrap.initialTheme).toBe("custom");
expect(bootstrap.customTheme).toEqual({
base: "paper",
base: "catppuccin-mocha",
accent: "#7755aa",
syntax: {
comment: "#998877",
},
});
});

test("loadAppBootstrap exposes graphite when no theme is configured", async () => {
test("loadAppBootstrap exposes github-dark-default when no theme is configured", async () => {
const home = createTempDir("hunk-config-home-");
const repo = createTempDir("hunk-config-repo-");
createRepo(repo);
Expand All @@ -570,6 +583,6 @@ describe("config resolution", () => {
);
const bootstrap = await loadAppBootstrap(resolved.input);

expect(bootstrap.initialTheme).toBe("graphite");
expect(bootstrap.initialTheme).toBe("github-dark-default");
});
});
Loading