Skip to content

Commit c3861bf

Browse files
authored
feat: persist Hunk view preferences (#7)
* feat: persist Hunk view preferences * fix: preserve TOML arrays when persisting config
1 parent 0a2a4f8 commit c3861bf

11 files changed

Lines changed: 793 additions & 51 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3838
# runtime output
3939
tmp
4040
.hunk/latest.json
41+
.hunk/config.toml
4142
.pi/

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,62 @@ If you want a different install location, set `HUNK_INSTALL_DIR` before running
5151
- `0` auto layout
5252
- `t` cycle themes
5353
- `a` toggle the agent panel
54+
- `l` toggle line numbers
55+
- `w` toggle line wrapping
56+
- `m` toggle hunk metadata
5457
- `[` / `]` move between hunks
58+
- `space` / `b` page forward and backward
5559
- `/` focus the file filter
5660
- `tab` cycle focus regions
5761
- `q` or `Esc` quit
5862

63+
## Configuration
64+
65+
Hunk reads layered TOML config with this precedence:
66+
67+
1. built-in defaults
68+
2. global config: `$XDG_CONFIG_HOME/hunk/config.toml` or `~/.config/hunk/config.toml`
69+
3. repo-local config: `.hunk/config.toml`
70+
4. command-specific sections like `[git]`, `[diff]`, `[patch]`, `[difftool]`
71+
5. `[pager]` when Hunk is running in pager mode
72+
6. explicit CLI flags
73+
74+
When you change persistent view settings inside Hunk, it writes them back to `.hunk/config.toml` in the current repo when possible, or to the global config file outside a repo.
75+
76+
Example:
77+
78+
```toml
79+
theme = "midnight"
80+
mode = "auto"
81+
line_numbers = true
82+
wrap_lines = false
83+
hunk_headers = true
84+
agent_notes = false
85+
86+
[pager]
87+
mode = "stack"
88+
line_numbers = false
89+
90+
[diff]
91+
mode = "split"
92+
```
93+
94+
CLI overrides are available when you want one-off or pager-specific behavior:
95+
96+
```bash
97+
hunk patch - --mode stack --no-line-numbers
98+
hunk diff before.ts after.ts --theme paper --wrap
99+
```
100+
101+
Supported persistent CLI overrides:
102+
103+
- `--mode <auto|split|stack>`
104+
- `--theme <theme>`
105+
- `--line-numbers` / `--no-line-numbers`
106+
- `--wrap` / `--no-wrap`
107+
- `--hunk-headers` / `--no-hunk-headers`
108+
- `--agent-notes` / `--no-agent-notes`
109+
59110
## Agent sidecar format
60111

61112
Use `--agent-context <file>` to load a JSON sidecar and show agent rationale next to the diff.

src/core/cli.ts

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,52 @@
11
import { Command } from "commander";
22
import type { CliInput, CommonOptions, LayoutMode } from "./types";
33

4+
/** Validate one requested layout mode from CLI input. */
5+
function parseLayoutMode(value: string): LayoutMode {
6+
if (value === "auto" || value === "split" || value === "stack") {
7+
return value;
8+
}
9+
10+
throw new Error(`Invalid layout mode: ${value}`);
11+
}
12+
13+
/** Read one paired positive/negative boolean flag directly from raw argv. */
14+
function resolveBooleanFlag(argv: string[], enabledFlag: string, disabledFlag: string) {
15+
let resolved: boolean | undefined;
16+
17+
for (const arg of argv) {
18+
if (arg === enabledFlag) {
19+
resolved = true;
20+
continue;
21+
}
22+
23+
if (arg === disabledFlag) {
24+
resolved = false;
25+
}
26+
}
27+
28+
return resolved;
29+
}
30+
431
/** Normalize the flags shared by every input mode. */
5-
function buildCommonOptions(options: {
6-
mode?: LayoutMode;
7-
theme?: string;
8-
agentContext?: string;
9-
pager?: boolean;
10-
}): CommonOptions {
32+
function buildCommonOptions(
33+
options: {
34+
mode?: LayoutMode;
35+
theme?: string;
36+
agentContext?: string;
37+
pager?: boolean;
38+
},
39+
argv: string[],
40+
): CommonOptions {
1141
return {
12-
mode: options.mode ?? "auto",
42+
mode: options.mode,
1343
theme: options.theme,
1444
agentContext: options.agentContext,
15-
pager: options.pager ?? false,
45+
pager: options.pager ? true : undefined,
46+
lineNumbers: resolveBooleanFlag(argv, "--line-numbers", "--no-line-numbers"),
47+
wrapLines: resolveBooleanFlag(argv, "--wrap", "--no-wrap"),
48+
hunkHeaders: resolveBooleanFlag(argv, "--hunk-headers", "--no-hunk-headers"),
49+
agentNotes: resolveBooleanFlag(argv, "--agent-notes", "--no-agent-notes"),
1650
};
1751
}
1852

@@ -22,7 +56,7 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
2256
return {
2357
kind: "git",
2458
staged: false,
25-
options: buildCommonOptions({}),
59+
options: buildCommonOptions({}, argv),
2660
};
2761
}
2862

@@ -37,25 +71,28 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
3771
/** Attach the shared mode/theme/agent-context flags to a subcommand. */
3872
const applyCommonOptions = (command: Command) =>
3973
command
40-
.option("--mode <mode>", "layout mode: auto, split, stack", "auto")
74+
.option("--mode <mode>", "layout mode: auto, split, stack", parseLayoutMode)
4175
.option("--theme <theme>", "named theme override")
4276
.option("--agent-context <path>", "JSON sidecar with agent rationale")
43-
.option("--pager", "use pager-style chrome and controls", false);
77+
.option("--pager", "use pager-style chrome and controls")
78+
.option("--line-numbers", "show line numbers")
79+
.option("--no-line-numbers", "hide line numbers")
80+
.option("--wrap", "wrap long diff lines")
81+
.option("--no-wrap", "truncate long diff lines to one row")
82+
.option("--hunk-headers", "show hunk metadata rows")
83+
.option("--no-hunk-headers", "hide hunk metadata rows")
84+
.option("--agent-notes", "show agent notes by default")
85+
.option("--no-agent-notes", "hide agent notes by default");
4486

4587
applyCommonOptions(program.command("git"))
4688
.argument("[range]", "revision or range to diff")
47-
.option("--staged", "show staged changes instead of the working tree", false)
89+
.option("--staged", "show staged changes instead of the working tree")
4890
.action((range: string | undefined, options: Record<string, unknown>) => {
4991
selected = {
5092
kind: "git",
5193
range,
5294
staged: Boolean(options.staged),
53-
options: buildCommonOptions({
54-
mode: options.mode as LayoutMode | undefined,
55-
theme: options.theme as string | undefined,
56-
agentContext: options.agentContext as string | undefined,
57-
pager: options.pager as boolean | undefined,
58-
}),
95+
options: buildCommonOptions(options, argv),
5996
};
6097
});
6198

@@ -67,12 +104,7 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
67104
kind: "diff",
68105
left,
69106
right,
70-
options: buildCommonOptions({
71-
mode: options.mode as LayoutMode | undefined,
72-
theme: options.theme as string | undefined,
73-
agentContext: options.agentContext as string | undefined,
74-
pager: options.pager as boolean | undefined,
75-
}),
107+
options: buildCommonOptions(options, argv),
76108
};
77109
});
78110

@@ -82,12 +114,7 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
82114
selected = {
83115
kind: "patch",
84116
file,
85-
options: buildCommonOptions({
86-
mode: options.mode as LayoutMode | undefined,
87-
theme: options.theme as string | undefined,
88-
agentContext: options.agentContext as string | undefined,
89-
pager: options.pager as boolean | undefined,
90-
}),
117+
options: buildCommonOptions(options, argv),
91118
};
92119
});
93120

@@ -101,12 +128,7 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
101128
left,
102129
right,
103130
path,
104-
options: buildCommonOptions({
105-
mode: options.mode as LayoutMode | undefined,
106-
theme: options.theme as string | undefined,
107-
agentContext: options.agentContext as string | undefined,
108-
pager: options.pager as boolean | undefined,
109-
}),
131+
options: buildCommonOptions(options, argv),
110132
};
111133
});
112134

0 commit comments

Comments
 (0)