Skip to content

Commit b69a607

Browse files
mvanhornbenvinegar
authored andcommitted
feat: allow transparent background via config or flag
so terminal/IDE themes show through. Adds a `--transparent` CLI flag and a `transparent_background` config key. When set, the diff theme omits the background color and uses the terminal default. Fixes #245
1 parent be38878 commit b69a607

11 files changed

Lines changed: 146 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ All notable user-visible changes to Hunk are documented in this file.
66

77
### Added
88

9+
- Added a `--transparent-background` flag and `transparent_background` config option for translucent terminal setups.
10+
911
### Changed
1012

1113
### Fixed

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
### Git integration
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-background",
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-background"]);
110+
const opaque = await parseCli(["bun", "hunk", "diff", "--no-transparent-background"]);
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: 11 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,11 @@ 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(
78+
argv,
79+
"--transparent-background",
80+
"--no-transparent-background",
81+
),
7682
};
7783
}
7884

@@ -90,7 +96,9 @@ function applyCommonOptions(command: Command) {
9096
.option("--hunk-headers", "show hunk metadata rows")
9197
.option("--no-hunk-headers", "hide hunk metadata rows")
9298
.option("--agent-notes", "show agent notes by default")
93-
.option("--no-agent-notes", "hide agent notes by default");
99+
.option("--no-agent-notes", "hide agent notes by default")
100+
.option("--transparent-background", "let terminal background show through Hunk surfaces")
101+
.option("--no-transparent-background", "paint Hunk surfaces with the active theme");
94102
}
95103

96104
/** Attach auto-refresh support to review commands that can reopen their source input. */
@@ -152,6 +160,8 @@ function renderCliHelp() {
152160
" --wrap / --no-wrap wrap or truncate long diff lines",
153161
" --hunk-headers / --no-hunk-headers show or hide hunk metadata rows",
154162
" --agent-notes / --no-agent-notes show or hide agent notes by default",
163+
" --transparent-background / --no-transparent-background",
164+
" let terminal background show through Hunk surfaces",
155165
" --theme <theme> named theme override",
156166
"",
157167
"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,9 +88,37 @@ describe("config resolution", () => {
8788
wrapLines: true,
8889
hunkHeaders: false,
8990
agentNotes: true,
91+
transparentBackground: true,
9092
});
9193
});
9294

95+
test("accepts transparent background config and CLI overrides", () => {
96+
const home = createTempDir("hunk-config-home-");
97+
mkdirSync(join(home, ".config", "hunk"), { recursive: true });
98+
writeFileSync(join(home, ".config", "hunk", "config.toml"), "transparent_background = true\n");
99+
100+
const cwd = createTempDir("hunk-config-cwd-");
101+
const configured = resolveConfiguredCliInput(
102+
{
103+
kind: "vcs",
104+
staged: false,
105+
options: {},
106+
},
107+
{ cwd, env: { HOME: home } },
108+
);
109+
const overridden = resolveConfiguredCliInput(
110+
{
111+
kind: "vcs",
112+
staged: false,
113+
options: { transparentBackground: false },
114+
},
115+
{ cwd, env: { HOME: home } },
116+
);
117+
118+
expect(configured.input.options.transparentBackground).toBe(true);
119+
expect(overridden.input.options.transparentBackground).toBe(false);
120+
});
121+
93122
test("defaults unspecified themes to graphite, including piped pager-style patch input", () => {
94123
const home = createTempDir("hunk-config-home-");
95124
const cwd = createTempDir("hunk-config-cwd-");

src/core/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ function readConfigPreferences(source: Record<string, unknown>): CommonOptions {
6565
wrapLines: normalizeBoolean(source.wrap_lines),
6666
hunkHeaders: normalizeBoolean(source.hunk_headers),
6767
agentNotes: normalizeBoolean(source.agent_notes),
68+
transparentBackground:
69+
normalizeBoolean(source.transparentBackground) ??
70+
normalizeBoolean(source.transparent_background),
6871
};
6972
}
7073

@@ -83,6 +86,7 @@ function mergeOptions(base: CommonOptions, overrides: CommonOptions): CommonOpti
8386
wrapLines: overrides.wrapLines ?? base.wrapLines,
8487
hunkHeaders: overrides.hunkHeaders ?? base.hunkHeaders,
8588
agentNotes: overrides.agentNotes ?? base.agentNotes,
89+
transparentBackground: overrides.transparentBackground ?? base.transparentBackground,
8690
};
8791
}
8892

@@ -145,6 +149,7 @@ export function resolveConfiguredCliInput(
145149
wrapLines: DEFAULT_VIEW_PREFERENCES.wrapLines,
146150
hunkHeaders: DEFAULT_VIEW_PREFERENCES.showHunkHeaders,
147151
agentNotes: DEFAULT_VIEW_PREFERENCES.showAgentNotes,
152+
transparentBackground: false,
148153
};
149154

150155
if (userConfigPath) {
@@ -174,6 +179,7 @@ export function resolveConfiguredCliInput(
174179
wrapLines: resolvedOptions.wrapLines ?? DEFAULT_VIEW_PREFERENCES.wrapLines,
175180
hunkHeaders: resolvedOptions.hunkHeaders ?? DEFAULT_VIEW_PREFERENCES.showHunkHeaders,
176181
agentNotes: resolvedOptions.agentNotes ?? DEFAULT_VIEW_PREFERENCES.showAgentNotes,
182+
transparentBackground: resolvedOptions.transparentBackground ?? false,
177183
};
178184

179185
return {

src/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface CommonOptions {
7979
wrapLines?: boolean;
8080
hunkHeaders?: boolean;
8181
agentNotes?: boolean;
82+
transparentBackground?: boolean;
8283
}
8384

8485
export interface PersistedViewPreferences {

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 { resolveTheme, THEMES } from "./themes";
31+
import { resolveTheme, THEMES, withTransparentBackground } from "./themes";
3232

3333
type FocusArea = "files" | "filter" | "note";
3434
type ActiveAddNoteTarget = ActiveAddNoteAffordance & { fileId: string };
@@ -128,7 +128,14 @@ export function App({
128128
const [sessionNoticeText, setSessionNoticeText] = useState<string | null>(null);
129129
const sessionNoticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
130130

131-
const activeTheme = resolveTheme(themeId, detectedThemeMode ?? null);
131+
const baseTheme = resolveTheme(themeId, detectedThemeMode ?? null);
132+
const activeTheme = useMemo(
133+
() =>
134+
bootstrap.input.options.transparentBackground
135+
? withTransparentBackground(baseTheme)
136+
: baseTheme,
137+
[baseTheme, bootstrap.input.options.transparentBackground],
138+
);
132139
const review = useReviewController({ files: bootstrap.changeset.files });
133140
const filteredFiles = review.visibleFiles;
134141
const selectedFile = review.selectedFile;

src/ui/diff/pierre.ts

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

196196
/** Resolve the inline word-diff background, strengthening theme colors that are too subtle to see. */
197197
function wordDiffHighlightBg(kind: SplitLineCell["kind"], theme: AppTheme) {
198-
let cached = wordDiffBackgroundCache.get(theme.id);
198+
const cacheKey = [
199+
theme.id,
200+
theme.addedBg,
201+
theme.addedContentBg,
202+
theme.removedBg,
203+
theme.removedContentBg,
204+
theme.contextContentBg,
205+
theme.panelAlt,
206+
].join(":");
207+
let cached = wordDiffBackgroundCache.get(cacheKey);
199208
if (!cached) {
200209
const addition =
201210
hexColorDistance(theme.addedContentBg, theme.addedBg) >= MIN_WORD_DIFF_BG_DISTANCE
@@ -212,7 +221,7 @@ function wordDiffHighlightBg(kind: SplitLineCell["kind"], theme: AppTheme) {
212221
deletion,
213222
empty: theme.panelAlt,
214223
};
215-
wordDiffBackgroundCache.set(theme.id, cached);
224+
wordDiffBackgroundCache.set(cacheKey, cached);
216225
}
217226

218227
return cached[kind];

src/ui/themes.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { resolveTheme, TRANSPARENT_BACKGROUND, withTransparentBackground } from "./themes";
3+
4+
describe("themes", () => {
5+
test("withTransparentBackground only swaps painted background fields", () => {
6+
const theme = resolveTheme("graphite", null);
7+
const transparent = withTransparentBackground(theme);
8+
9+
expect(transparent).toMatchObject({
10+
background: TRANSPARENT_BACKGROUND,
11+
panel: TRANSPARENT_BACKGROUND,
12+
panelAlt: TRANSPARENT_BACKGROUND,
13+
addedBg: TRANSPARENT_BACKGROUND,
14+
removedBg: TRANSPARENT_BACKGROUND,
15+
contextBg: TRANSPARENT_BACKGROUND,
16+
addedContentBg: TRANSPARENT_BACKGROUND,
17+
removedContentBg: TRANSPARENT_BACKGROUND,
18+
contextContentBg: TRANSPARENT_BACKGROUND,
19+
lineNumberBg: TRANSPARENT_BACKGROUND,
20+
selectedHunk: TRANSPARENT_BACKGROUND,
21+
noteBackground: TRANSPARENT_BACKGROUND,
22+
noteTitleBackground: TRANSPARENT_BACKGROUND,
23+
});
24+
expect(transparent.id).toBe(theme.id);
25+
expect(transparent.label).toBe(theme.label);
26+
expect(transparent.text).toBe(theme.text);
27+
expect(transparent.muted).toBe(theme.muted);
28+
expect(transparent.addedSignColor).toBe(theme.addedSignColor);
29+
expect(transparent.removedSignColor).toBe(theme.removedSignColor);
30+
expect(transparent.syntaxColors).toBe(theme.syntaxColors);
31+
expect(theme.background).not.toBe(TRANSPARENT_BACKGROUND);
32+
});
33+
});

0 commit comments

Comments
 (0)