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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# runtime output
tmp
.hunk/latest.json
.hunk/config.toml
.pi/
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,62 @@ If you want a different install location, set `HUNK_INSTALL_DIR` before running
- `0` auto layout
- `t` cycle themes
- `a` toggle the agent panel
- `l` toggle line numbers
- `w` toggle line wrapping
- `m` toggle hunk metadata
- `[` / `]` move between hunks
- `space` / `b` page forward and backward
- `/` focus the file filter
- `tab` cycle focus regions
- `q` or `Esc` quit

## Configuration

Hunk reads layered TOML config with this precedence:

1. built-in defaults
2. global config: `$XDG_CONFIG_HOME/hunk/config.toml` or `~/.config/hunk/config.toml`
3. repo-local config: `.hunk/config.toml`
4. command-specific sections like `[git]`, `[diff]`, `[patch]`, `[difftool]`
5. `[pager]` when Hunk is running in pager mode
6. explicit CLI flags

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.

Example:

```toml
theme = "midnight"
mode = "auto"
line_numbers = true
wrap_lines = false
hunk_headers = true
agent_notes = false

[pager]
mode = "stack"
line_numbers = false

[diff]
mode = "split"
```

CLI overrides are available when you want one-off or pager-specific behavior:

```bash
hunk patch - --mode stack --no-line-numbers
hunk diff before.ts after.ts --theme paper --wrap
```

Supported persistent CLI overrides:

- `--mode <auto|split|stack>`
- `--theme <theme>`
- `--line-numbers` / `--no-line-numbers`
- `--wrap` / `--no-wrap`
- `--hunk-headers` / `--no-hunk-headers`
- `--agent-notes` / `--no-agent-notes`

## Agent sidecar format

Use `--agent-context <file>` to load a JSON sidecar and show agent rationale next to the diff.
Expand Down
94 changes: 58 additions & 36 deletions src/core/cli.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
import { Command } from "commander";
import type { CliInput, CommonOptions, LayoutMode } from "./types";

/** Validate one requested layout mode from CLI input. */
function parseLayoutMode(value: string): LayoutMode {
if (value === "auto" || value === "split" || value === "stack") {
return value;
}

throw new Error(`Invalid layout mode: ${value}`);
}

/** Read one paired positive/negative boolean flag directly from raw argv. */
function resolveBooleanFlag(argv: string[], enabledFlag: string, disabledFlag: string) {
let resolved: boolean | undefined;

for (const arg of argv) {
if (arg === enabledFlag) {
resolved = true;
continue;
}

if (arg === disabledFlag) {
resolved = false;
}
}

return resolved;
}

/** Normalize the flags shared by every input mode. */
function buildCommonOptions(options: {
mode?: LayoutMode;
theme?: string;
agentContext?: string;
pager?: boolean;
}): CommonOptions {
function buildCommonOptions(
options: {
mode?: LayoutMode;
theme?: string;
agentContext?: string;
pager?: boolean;
},
argv: string[],
): CommonOptions {
return {
mode: options.mode ?? "auto",
mode: options.mode,
theme: options.theme,
agentContext: options.agentContext,
pager: options.pager ?? false,
pager: options.pager ? true : undefined,
lineNumbers: resolveBooleanFlag(argv, "--line-numbers", "--no-line-numbers"),
wrapLines: resolveBooleanFlag(argv, "--wrap", "--no-wrap"),
hunkHeaders: resolveBooleanFlag(argv, "--hunk-headers", "--no-hunk-headers"),
agentNotes: resolveBooleanFlag(argv, "--agent-notes", "--no-agent-notes"),
};
}

Expand All @@ -22,7 +56,7 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
return {
kind: "git",
staged: false,
options: buildCommonOptions({}),
options: buildCommonOptions({}, argv),
};
}

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

applyCommonOptions(program.command("git"))
.argument("[range]", "revision or range to diff")
.option("--staged", "show staged changes instead of the working tree", false)
.option("--staged", "show staged changes instead of the working tree")
.action((range: string | undefined, options: Record<string, unknown>) => {
selected = {
kind: "git",
range,
staged: Boolean(options.staged),
options: buildCommonOptions({
mode: options.mode as LayoutMode | undefined,
theme: options.theme as string | undefined,
agentContext: options.agentContext as string | undefined,
pager: options.pager as boolean | undefined,
}),
options: buildCommonOptions(options, argv),
};
});

Expand All @@ -67,12 +104,7 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
kind: "diff",
left,
right,
options: buildCommonOptions({
mode: options.mode as LayoutMode | undefined,
theme: options.theme as string | undefined,
agentContext: options.agentContext as string | undefined,
pager: options.pager as boolean | undefined,
}),
options: buildCommonOptions(options, argv),
};
});

Expand All @@ -82,12 +114,7 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
selected = {
kind: "patch",
file,
options: buildCommonOptions({
mode: options.mode as LayoutMode | undefined,
theme: options.theme as string | undefined,
agentContext: options.agentContext as string | undefined,
pager: options.pager as boolean | undefined,
}),
options: buildCommonOptions(options, argv),
};
});

Expand All @@ -101,12 +128,7 @@ export async function parseCli(argv: string[]): Promise<CliInput> {
left,
right,
path,
options: buildCommonOptions({
mode: options.mode as LayoutMode | undefined,
theme: options.theme as string | undefined,
agentContext: options.agentContext as string | undefined,
pager: options.pager as boolean | undefined,
}),
options: buildCommonOptions(options, argv),
};
});

Expand Down
Loading
Loading