Skip to content

Commit e098b3a

Browse files
benvinegarclaude
andauthored
feat(ui): prompt to save view preferences on quit (#468)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent d8ded5b commit e098b3a

14 files changed

Lines changed: 952 additions & 9 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hunkdiff": minor
3+
---
4+
5+
Offer to save changed view preferences to the user config on quit, including theme, layout, line numbers, wrapping, hunk headers, agent notes, and copy decorations. The prompt also includes a “never ask” option that persists `prompt_save_view_preferences = false`.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ CLI input
4646

4747
- `App` should remain the orchestration shell for app state, navigation, layout mode, theme, filtering, and pane coordination.
4848
- Pane rendering should live in dedicated components.
49+
- Confirmation prompts with a small set of choices should reuse `ConfirmDialog` (body rows plus a clickable key-legend action row) instead of composing `ModalFrame` with a hand-rolled footer; keyboard handling for its actions stays in `useAppKeyboardShortcuts`.
4950
- New UI work should extend existing components or add new ones, not grow `App` back into a monolith.
5051
- Shared formatting, ids, and small derivations belong in helper modules, not repeated inline.
5152
- Prefer one implementation path per feature instead of separate "old" and "new" codepaths that duplicate behavior.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,14 @@ line_numbers = true
131131
wrap_lines = false
132132
menu_bar = true
133133
agent_notes = false
134+
prompt_save_view_preferences = true
134135
transparent_background = false
135136
```
136137

137138
`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.
138139
Older theme ids such as `graphite` and `paper` remain accepted as compatibility aliases.
139140
`exclude_untracked` affects Git/Sapling working-tree `hunk diff` sessions only.
141+
`prompt_save_view_preferences = false` disables the quit prompt for saving changed view preferences.
140142
`transparent_background` can also be written as `transparentBackground`.
141143

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

src/core/config.test.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { afterEach, describe, expect, test } from "bun:test";
2-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import type { CliInput } from "./types";
6-
import { resolveConfiguredCliInput } from "./config";
6+
import {
7+
diffPersistedViewPreferences,
8+
resolveConfiguredCliInput,
9+
saveGlobalViewPreferences,
10+
saveViewPreferencesPromptPreference,
11+
} from "./config";
712
import { loadAppBootstrap } from "./loaders";
813

914
const tempDirs: string[] = [];
@@ -46,6 +51,109 @@ afterEach(() => {
4651
cleanupTempDirs();
4752
});
4853

54+
describe("config persistence", () => {
55+
test("writes accepted view preferences to user config without disturbing tables", () => {
56+
const home = createTempDir("hunk-save-config-home-");
57+
const configPath = join(home, ".config", "hunk", "config.toml");
58+
mkdirSync(join(home, ".config", "hunk"), { recursive: true });
59+
writeFileSync(
60+
configPath,
61+
[
62+
"# personal defaults",
63+
'theme = "github-dark-default"',
64+
"wrap_lines = false",
65+
"",
66+
"[custom_theme]",
67+
'label = "Keep me"',
68+
].join("\n"),
69+
);
70+
71+
const savedPath = saveGlobalViewPreferences(
72+
{
73+
mode: "split",
74+
theme: "dracula",
75+
showLineNumbers: false,
76+
wrapLines: true,
77+
showHunkHeaders: false,
78+
showMenuBar: false,
79+
showAgentNotes: true,
80+
copyDecorations: true,
81+
},
82+
{ env: { HOME: home } },
83+
);
84+
85+
expect(savedPath).toBe(configPath);
86+
expect(readFileSync(configPath, "utf8")).toBe(
87+
[
88+
"# personal defaults",
89+
'theme = "dracula"',
90+
"wrap_lines = true",
91+
'mode = "split"',
92+
"line_numbers = false",
93+
"hunk_headers = false",
94+
"menu_bar = false",
95+
"agent_notes = true",
96+
"copy_decorations = true",
97+
"",
98+
"[custom_theme]",
99+
'label = "Keep me"',
100+
"",
101+
].join("\n"),
102+
);
103+
});
104+
105+
test("writes the view preferences prompt setting without disturbing tables", () => {
106+
const home = createTempDir("hunk-save-config-home-");
107+
const configPath = join(home, ".config", "hunk", "config.toml");
108+
mkdirSync(join(home, ".config", "hunk"), { recursive: true });
109+
writeFileSync(configPath, ["# personal defaults", "", "[custom_theme]"].join("\n"));
110+
111+
const savedPath = saveViewPreferencesPromptPreference(false, { env: { HOME: home } });
112+
113+
expect(savedPath).toBe(configPath);
114+
expect(readFileSync(configPath, "utf8")).toBe(
115+
[
116+
"# personal defaults",
117+
"prompt_save_view_preferences = false",
118+
"",
119+
"[custom_theme]",
120+
"",
121+
].join("\n"),
122+
);
123+
});
124+
125+
test("diffs view preference snapshots as the TOML assignments a save would rewrite", () => {
126+
const initial = {
127+
mode: "auto",
128+
theme: "github-dark-default",
129+
showLineNumbers: false,
130+
wrapLines: false,
131+
showHunkHeaders: false,
132+
showMenuBar: true,
133+
showAgentNotes: true,
134+
copyDecorations: false,
135+
} as const;
136+
137+
expect(diffPersistedViewPreferences(initial, { ...initial })).toEqual([]);
138+
expect(
139+
diffPersistedViewPreferences(initial, {
140+
...initial,
141+
mode: "split",
142+
theme: "github-dark-dimmed",
143+
showLineNumbers: true,
144+
}),
145+
).toEqual([
146+
{
147+
configKey: "theme",
148+
previousValue: '"github-dark-default"',
149+
nextValue: '"github-dark-dimmed"',
150+
},
151+
{ configKey: "mode", previousValue: '"auto"', nextValue: '"split"' },
152+
{ configKey: "line_numbers", previousValue: "false", nextValue: "true" },
153+
]);
154+
});
155+
});
156+
49157
describe("config resolution", () => {
50158
test("merges global, repo, pager, command, and CLI overrides in the right order", () => {
51159
const home = createTempDir("hunk-config-home-");
@@ -60,6 +168,7 @@ describe("config resolution", () => {
60168
"line_numbers = false",
61169
"transparentBackground = true",
62170
"color_moved = true",
171+
"prompt_save_view_preferences = false",
63172
"",
64173
"[patch]",
65174
'mode = "split"',
@@ -88,6 +197,7 @@ describe("config resolution", () => {
88197
});
89198

90199
expect(resolved.repoConfigPath).toBe(join(repo, ".hunk", "config.toml"));
200+
expect(resolved.viewPreferencesConfigPath).toBe(join(repo, ".hunk", "config.toml"));
91201
expect(resolved.input.options).toMatchObject({
92202
pager: true,
93203
mode: "stack",
@@ -97,6 +207,7 @@ describe("config resolution", () => {
97207
menuBar: false,
98208
hunkHeaders: false,
99209
agentNotes: true,
210+
promptSaveViewPreferences: false,
100211
transparentBackground: true,
101212
colorMoved: true,
102213
});
@@ -296,6 +407,7 @@ describe("config resolution", () => {
296407
});
297408

298409
expect(resolved.repoConfigPath).toBeUndefined();
410+
expect(resolved.viewPreferencesConfigPath).toBe(join(home, ".config", "hunk", "config.toml"));
299411
expect(resolved.input.options.theme).toBe("github-dark-default");
300412
});
301413

0 commit comments

Comments
 (0)