Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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