Skip to content

Commit 7f4abac

Browse files
authored
feat: allow transparent background via config or flag (#322)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
1 parent ab7868b commit 7f4abac

11 files changed

Lines changed: 142 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All notable user-visible changes to Hunk are documented in this file.
99
- Show the newly selected theme in the footer status bar when switching themes.
1010
- Added Catppuccin Frappé and Macchiato as built-in themes, completing the four official Catppuccin flavors.
1111
- Added a Zenburn built-in theme (`theme = "zenburn"`), a warm low-contrast dark palette inspired by Jani Nurminen's original Zenburn. It also works as a custom-theme `base`.
12+
- Added a `--transparent-bg` flag and `transparent_background` config option for translucent terminal setups.
1213

1314
### Changed
1415

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,11 @@ exclude_untracked = false
127127
line_numbers = true
128128
wrap_lines = false
129129
agent_notes = false
130+
transparent_background = false
130131
```
131132

132133
`exclude_untracked` affects Git working-tree `hunk diff` sessions only.
134+
`transparent_background` can also be written as `transparentBackground`.
133135

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

src/core/cli.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ describe("parseCli", () => {
8383
"--wrap",
8484
"--no-hunk-headers",
8585
"--agent-notes",
86+
"--transparent-bg",
8687
"--watch",
8788
]);
8889

@@ -99,6 +100,25 @@ describe("parseCli", () => {
99100
wrapLines: true,
100101
hunkHeaders: false,
101102
agentNotes: true,
103+
transparentBackground: true,
104+
},
105+
});
106+
});
107+
108+
test("parses transparent background toggles", async () => {
109+
const transparent = await parseCli(["bun", "hunk", "diff", "--transparent-bg"]);
110+
const opaque = await parseCli(["bun", "hunk", "diff", "--no-transparent-bg"]);
111+
112+
expect(transparent).toMatchObject({
113+
kind: "vcs",
114+
options: {
115+
transparentBackground: true,
116+
},
117+
});
118+
expect(opaque).toMatchObject({
119+
kind: "vcs",
120+
options: {
121+
transparentBackground: false,
102122
},
103123
});
104124
});

src/core/cli.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function buildCommonOptions(
5959
agentContext?: string;
6060
pager?: boolean;
6161
watch?: boolean;
62+
transparentBackground?: boolean;
6263
},
6364
argv: string[],
6465
): CommonOptions {
@@ -73,6 +74,7 @@ function buildCommonOptions(
7374
wrapLines: resolveBooleanFlag(argv, "--wrap", "--no-wrap"),
7475
hunkHeaders: resolveBooleanFlag(argv, "--hunk-headers", "--no-hunk-headers"),
7576
agentNotes: resolveBooleanFlag(argv, "--agent-notes", "--no-agent-notes"),
77+
transparentBackground: resolveBooleanFlag(argv, "--transparent-bg", "--no-transparent-bg"),
7678
};
7779
}
7880

@@ -90,7 +92,9 @@ function applyCommonOptions(command: Command) {
9092
.option("--hunk-headers", "show hunk metadata rows")
9193
.option("--no-hunk-headers", "hide hunk metadata rows")
9294
.option("--agent-notes", "show agent notes by default")
93-
.option("--no-agent-notes", "hide agent notes by default");
95+
.option("--no-agent-notes", "hide agent notes by default")
96+
.option("--transparent-bg", "let terminal background show through Hunk surfaces")
97+
.option("--no-transparent-bg", "paint Hunk surfaces with the active theme");
9498
}
9599

96100
/** Attach auto-refresh support to review commands that can reopen their source input. */
@@ -152,6 +156,7 @@ function renderCliHelp() {
152156
" --wrap / --no-wrap wrap or truncate long diff lines",
153157
" --hunk-headers / --no-hunk-headers show or hide hunk metadata rows",
154158
" --agent-notes / --no-agent-notes show or hide agent notes by default",
159+
" --transparent-bg / --no-transparent-bg let terminal background show through Hunk surfaces",
155160
" --theme <theme> named theme override",
156161
"",
157162
"Git diff options:",

src/core/config.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe("config resolution", () => {
5858
[
5959
'theme = "graphite"',
6060
"line_numbers = false",
61+
"transparentBackground = true",
6162
"",
6263
"[patch]",
6364
'mode = "split"',
@@ -87,6 +88,7 @@ describe("config resolution", () => {
8788
wrapLines: true,
8889
hunkHeaders: false,
8990
agentNotes: true,
91+
transparentBackground: true,
9092
});
9193
});
9294

@@ -217,6 +219,33 @@ describe("config resolution", () => {
217219
).toThrow('Expected a [custom_theme] table when config selects theme = "custom".');
218220
});
219221

222+
test("accepts transparent background config and CLI overrides", () => {
223+
const home = createTempDir("hunk-config-home-");
224+
mkdirSync(join(home, ".config", "hunk"), { recursive: true });
225+
writeFileSync(join(home, ".config", "hunk", "config.toml"), "transparent_background = true\n");
226+
227+
const cwd = createTempDir("hunk-config-cwd-");
228+
const configured = resolveConfiguredCliInput(
229+
{
230+
kind: "vcs",
231+
staged: false,
232+
options: {},
233+
},
234+
{ cwd, env: { HOME: home } },
235+
);
236+
const overridden = resolveConfiguredCliInput(
237+
{
238+
kind: "vcs",
239+
staged: false,
240+
options: { transparentBackground: false },
241+
},
242+
{ cwd, env: { HOME: home } },
243+
);
244+
245+
expect(configured.input.options.transparentBackground).toBe(true);
246+
expect(overridden.input.options.transparentBackground).toBe(false);
247+
});
248+
220249
test("defaults unspecified themes to graphite, including piped pager-style patch input", () => {
221250
const home = createTempDir("hunk-config-home-");
222251
const cwd = createTempDir("hunk-config-cwd-");

src/core/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ function readConfigPreferences(source: Record<string, unknown>): CommonOptions {
236236
hunkHeaders: normalizeBoolean(source.hunk_headers),
237237
agentNotes: normalizeBoolean(source.agent_notes),
238238
copyDecorations: normalizeBoolean(source.copy_decorations),
239+
transparentBackground:
240+
normalizeBoolean(source.transparentBackground) ??
241+
normalizeBoolean(source.transparent_background),
239242
};
240243
}
241244

@@ -255,6 +258,7 @@ function mergeOptions(base: CommonOptions, overrides: CommonOptions): CommonOpti
255258
hunkHeaders: overrides.hunkHeaders ?? base.hunkHeaders,
256259
agentNotes: overrides.agentNotes ?? base.agentNotes,
257260
copyDecorations: overrides.copyDecorations ?? base.copyDecorations,
261+
transparentBackground: overrides.transparentBackground ?? base.transparentBackground,
258262
};
259263
}
260264

@@ -319,6 +323,7 @@ export function resolveConfiguredCliInput(
319323
hunkHeaders: DEFAULT_VIEW_PREFERENCES.showHunkHeaders,
320324
agentNotes: DEFAULT_VIEW_PREFERENCES.showAgentNotes,
321325
copyDecorations: DEFAULT_VIEW_PREFERENCES.copyDecorations,
326+
transparentBackground: false,
322327
};
323328

324329
if (userConfigPath) {
@@ -347,6 +352,7 @@ export function resolveConfiguredCliInput(
347352
hunkHeaders: resolvedOptions.hunkHeaders ?? DEFAULT_VIEW_PREFERENCES.showHunkHeaders,
348353
agentNotes: resolvedOptions.agentNotes ?? DEFAULT_VIEW_PREFERENCES.showAgentNotes,
349354
copyDecorations: resolvedOptions.copyDecorations ?? DEFAULT_VIEW_PREFERENCES.copyDecorations,
355+
transparentBackground: resolvedOptions.transparentBackground ?? false,
350356
};
351357

352358
if (resolvedOptions.theme === "custom" && !resolvedCustomTheme) {

src/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export interface CommonOptions {
8484
hunkHeaders?: boolean;
8585
agentNotes?: boolean;
8686
copyDecorations?: boolean;
87+
transparentBackground?: boolean;
8788
}
8889

8990
export interface CustomSyntaxColorsConfig {

src/ui/App.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { fileRowId } from "./lib/ids";
2828
import { openSelectedFileInEditor } from "./lib/openInEditor";
2929
import { resolveResponsiveLayout } from "./lib/responsive";
3030
import { resizeSidebarWidth } from "./lib/sidebar";
31-
import { availableThemes, resolveTheme } from "./themes";
31+
import { availableThemes, resolveTheme, withTransparentBackground } from "./themes";
3232

3333
type FocusArea = "files" | "filter" | "note";
3434
type ActiveAddNoteTarget = ActiveAddNoteAffordance & { fileId: string };
@@ -138,7 +138,14 @@ export function App({
138138
() => availableThemes(bootstrap.customTheme),
139139
[bootstrap.customTheme],
140140
);
141-
const activeTheme = resolveTheme(themeId, detectedThemeMode ?? null, bootstrap.customTheme);
141+
const baseTheme = resolveTheme(themeId, detectedThemeMode ?? null, bootstrap.customTheme);
142+
const activeTheme = useMemo(
143+
() =>
144+
bootstrap.input.options.transparentBackground
145+
? withTransparentBackground(baseTheme)
146+
: baseTheme,
147+
[baseTheme, bootstrap.input.options.transparentBackground],
148+
);
142149
const review = useReviewController({ files: bootstrap.changeset.files });
143150
const filteredFiles = review.visibleFiles;
144151
const selectedFile = review.selectedFile;

src/ui/diff/pierre.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,16 @@ function strengthenWordDiffBg(lineBg: string, signColor: string) {
240240

241241
/** Resolve the inline word-diff background, strengthening theme colors that are too subtle to see. */
242242
function wordDiffHighlightBg(kind: SplitLineCell["kind"], theme: AppTheme) {
243-
let cached = wordDiffBackgroundCache.get(theme.id);
243+
const cacheKey = [
244+
theme.id,
245+
theme.addedBg,
246+
theme.addedContentBg,
247+
theme.removedBg,
248+
theme.removedContentBg,
249+
theme.contextContentBg,
250+
theme.panelAlt,
251+
].join(":");
252+
let cached = wordDiffBackgroundCache.get(cacheKey);
244253
if (!cached) {
245254
const addition =
246255
hexColorDistance(theme.addedContentBg, theme.addedBg) >= MIN_WORD_DIFF_BG_DISTANCE
@@ -257,7 +266,7 @@ function wordDiffHighlightBg(kind: SplitLineCell["kind"], theme: AppTheme) {
257266
deletion,
258267
empty: theme.panelAlt,
259268
};
260-
wordDiffBackgroundCache.set(theme.id, cached);
269+
wordDiffBackgroundCache.set(cacheKey, cached);
261270
}
262271

263272
return cached[kind];

src/ui/themes.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { describe, expect, test } from "bun:test";
22
import { blendHex, hexColorDistance } from "./lib/color";
3-
import { CATPPUCCIN_PALETTES, resolveTheme } from "./themes";
3+
import {
4+
CATPPUCCIN_PALETTES,
5+
resolveTheme,
6+
TRANSPARENT_BACKGROUND,
7+
withTransparentBackground,
8+
} from "./themes";
49

510
describe("themes", () => {
611
test("resolves all Catppuccin flavors by theme id", () => {
@@ -125,4 +130,33 @@ describe("themes", () => {
125130
type: "#94bff3",
126131
});
127132
});
133+
134+
test("withTransparentBackground only swaps painted background fields", () => {
135+
const theme = resolveTheme("graphite", null);
136+
const transparent = withTransparentBackground(theme);
137+
138+
expect(transparent).toMatchObject({
139+
background: TRANSPARENT_BACKGROUND,
140+
panel: TRANSPARENT_BACKGROUND,
141+
panelAlt: TRANSPARENT_BACKGROUND,
142+
addedBg: TRANSPARENT_BACKGROUND,
143+
removedBg: TRANSPARENT_BACKGROUND,
144+
contextBg: TRANSPARENT_BACKGROUND,
145+
addedContentBg: TRANSPARENT_BACKGROUND,
146+
removedContentBg: TRANSPARENT_BACKGROUND,
147+
contextContentBg: TRANSPARENT_BACKGROUND,
148+
lineNumberBg: TRANSPARENT_BACKGROUND,
149+
selectedHunk: TRANSPARENT_BACKGROUND,
150+
noteBackground: TRANSPARENT_BACKGROUND,
151+
noteTitleBackground: TRANSPARENT_BACKGROUND,
152+
});
153+
expect(transparent.id).toBe(theme.id);
154+
expect(transparent.label).toBe(theme.label);
155+
expect(transparent.text).toBe(theme.text);
156+
expect(transparent.muted).toBe(theme.muted);
157+
expect(transparent.addedSignColor).toBe(theme.addedSignColor);
158+
expect(transparent.removedSignColor).toBe(theme.removedSignColor);
159+
expect(transparent.syntaxColors).toBe(theme.syntaxColors);
160+
expect(theme.background).not.toBe(TRANSPARENT_BACKGROUND);
161+
});
128162
});

0 commit comments

Comments
 (0)