Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions .changeset/optional-file-icons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Add an optional `file_icons` setting and matching CLI flags for Nerd Font file and folder icons.
2 changes: 2 additions & 0 deletions src/core/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe("parseCli", () => {
"--wrap",
"--no-hunk-headers",
"--agent-notes",
"--file-icons",
"--transparent-bg",
"--watch",
]);
Expand All @@ -118,6 +119,7 @@ describe("parseCli", () => {
wrapLines: true,
hunkHeaders: false,
agentNotes: true,
nerdFontIcons: true,
transparentBackground: true,
},
});
Expand Down
8 changes: 8 additions & 0 deletions src/core/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ function buildCommonOptions(
wrapLines: resolveBooleanFlag(argv, "--wrap", "--no-wrap"),
hunkHeaders: resolveBooleanFlag(argv, "--hunk-headers", "--no-hunk-headers"),
agentNotes: resolveBooleanFlag(argv, "--agent-notes", "--no-agent-notes"),
nerdFontIcons:
resolveBooleanFlag(argv, "--file-icons", "--no-file-icons") ??
resolveBooleanFlag(argv, "--nerd-font-icons", "--no-nerd-font-icons"),
transparentBackground: resolveBooleanFlag(argv, "--transparent-bg", "--no-transparent-bg"),
};
}
Expand All @@ -94,6 +97,10 @@ function applyCommonOptions(command: Command) {
.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")
.option("--file-icons", "show Nerd Font file and folder icons")
.option("--no-file-icons", "hide Nerd Font file and folder icons")
.option("--nerd-font-icons", "show Nerd Font file and folder icons")
.option("--no-nerd-font-icons", "hide Nerd Font file and folder icons")
.option("--transparent-bg", "let terminal background show through Hunk surfaces")
.option("--no-transparent-bg", "paint Hunk surfaces with the active theme");
}
Expand Down Expand Up @@ -157,6 +164,7 @@ function renderCliHelp() {
" --wrap / --no-wrap wrap or truncate long diff lines",
" --hunk-headers / --no-hunk-headers show or hide hunk metadata rows",
" --agent-notes / --no-agent-notes show or hide agent notes by default",
" --file-icons / --no-file-icons show or hide Nerd Font file/folder icons",
" --transparent-bg / --no-transparent-bg let terminal background show through Hunk surfaces",
" --theme <theme> named theme override",
"",
Expand Down
4 changes: 4 additions & 0 deletions src/core/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe("config resolution", () => {
[
'theme = "github-light-default"',
"wrap_lines = true",
"file_icons = true",
"",
"[pager]",
"hunk_headers = false",
Expand All @@ -94,6 +95,7 @@ describe("config resolution", () => {
lineNumbers: false,
wrapLines: true,
hunkHeaders: false,
nerdFontIcons: true,
agentNotes: true,
transparentBackground: true,
colorMoved: true,
Expand Down Expand Up @@ -487,6 +489,7 @@ describe("config resolution", () => {
"wrap_lines = true",
"hunk_headers = false",
"agent_notes = true",
"nerd_font_icons = true",
"copy_decorations = false",
].join("\n"),
);
Expand Down Expand Up @@ -514,6 +517,7 @@ describe("config resolution", () => {
expect(bootstrap.initialShowHunkHeaders).toBe(false);
expect(bootstrap.initialShowAgentNotes).toBe(true);
expect(bootstrap.initialCopyDecorations).toBe(false);
expect(bootstrap.initialNerdFontIcons).toBe(true);
});

test("loadAppBootstrap carries the configured custom theme into the UI bootstrap", async () => {
Expand Down
5 changes: 5 additions & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const DEFAULT_VIEW_PREFERENCES: PersistedViewPreferences = {
showHunkHeaders: true,
showAgentNotes: false,
copyDecorations: false,
nerdFontIcons: false,
};

interface ConfigResolutionOptions {
Expand Down Expand Up @@ -238,6 +239,7 @@ function readConfigPreferences(source: Record<string, unknown>): CommonOptions {
hunkHeaders: normalizeBoolean(source.hunk_headers),
agentNotes: normalizeBoolean(source.agent_notes),
copyDecorations: normalizeBoolean(source.copy_decorations),
nerdFontIcons: normalizeBoolean(source.file_icons) ?? normalizeBoolean(source.nerd_font_icons),
transparentBackground:
normalizeBoolean(source.transparentBackground) ??
normalizeBoolean(source.transparent_background),
Expand All @@ -261,6 +263,7 @@ function mergeOptions(base: CommonOptions, overrides: CommonOptions): CommonOpti
hunkHeaders: overrides.hunkHeaders ?? base.hunkHeaders,
agentNotes: overrides.agentNotes ?? base.agentNotes,
copyDecorations: overrides.copyDecorations ?? base.copyDecorations,
nerdFontIcons: overrides.nerdFontIcons ?? base.nerdFontIcons,
transparentBackground: overrides.transparentBackground ?? base.transparentBackground,
colorMoved: overrides.colorMoved ?? base.colorMoved,
};
Expand Down Expand Up @@ -327,6 +330,7 @@ export function resolveConfiguredCliInput(
hunkHeaders: DEFAULT_VIEW_PREFERENCES.showHunkHeaders,
agentNotes: DEFAULT_VIEW_PREFERENCES.showAgentNotes,
copyDecorations: DEFAULT_VIEW_PREFERENCES.copyDecorations,
nerdFontIcons: DEFAULT_VIEW_PREFERENCES.nerdFontIcons,
transparentBackground: false,
};

Expand Down Expand Up @@ -357,6 +361,7 @@ export function resolveConfiguredCliInput(
hunkHeaders: resolvedOptions.hunkHeaders ?? DEFAULT_VIEW_PREFERENCES.showHunkHeaders,
agentNotes: resolvedOptions.agentNotes ?? DEFAULT_VIEW_PREFERENCES.showAgentNotes,
copyDecorations: resolvedOptions.copyDecorations ?? DEFAULT_VIEW_PREFERENCES.copyDecorations,
nerdFontIcons: resolvedOptions.nerdFontIcons ?? DEFAULT_VIEW_PREFERENCES.nerdFontIcons,
transparentBackground: resolvedOptions.transparentBackground ?? false,
colorMoved: resolvedOptions.colorMoved,
};
Expand Down
1 change: 1 addition & 0 deletions src/core/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,5 +460,6 @@ export async function loadAppBootstrap(
initialShowHunkHeaders: input.options.hunkHeaders ?? true,
initialShowAgentNotes: input.options.agentNotes ?? false,
initialCopyDecorations: input.options.copyDecorations ?? false,
initialNerdFontIcons: input.options.nerdFontIcons ?? false,
};
}
3 changes: 3 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface CommonOptions {
hunkHeaders?: boolean;
agentNotes?: boolean;
copyDecorations?: boolean;
nerdFontIcons?: boolean;
transparentBackground?: boolean;
colorMoved?: boolean;
}
Expand Down Expand Up @@ -157,6 +158,7 @@ export interface PersistedViewPreferences {
showHunkHeaders: boolean;
showAgentNotes: boolean;
copyDecorations: boolean;
nerdFontIcons: boolean;
}

export interface HelpCommandInput {
Expand Down Expand Up @@ -366,4 +368,5 @@ export interface AppBootstrap {
initialShowHunkHeaders?: boolean;
initialShowAgentNotes?: boolean;
initialCopyDecorations?: boolean;
initialNerdFontIcons?: boolean;
}
16 changes: 16 additions & 0 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ function withCurrentViewOptions(
showAgentNotes: boolean;
showHunkHeaders: boolean;
showLineNumbers: boolean;
nerdFontIcons: boolean;
wrapLines: boolean;
},
): CliInput {
Expand All @@ -79,6 +80,7 @@ function withCurrentViewOptions(
agentNotes: view.showAgentNotes,
hunkHeaders: view.showHunkHeaders,
lineNumbers: view.showLineNumbers,
nerdFontIcons: view.nerdFontIcons,
wrapLines: view.wrapLines,
},
};
Expand Down Expand Up @@ -132,6 +134,7 @@ export function App({
const [showLineNumbers, setShowLineNumbers] = useState(bootstrap.initialShowLineNumbers ?? true);
const [wrapLines, setWrapLines] = useState(bootstrap.initialWrapLines ?? false);
const [copyDecorations, setCopyDecorations] = useState(bootstrap.initialCopyDecorations ?? false);
const [nerdFontIcons, setNerdFontIcons] = useState(bootstrap.initialNerdFontIcons ?? false);
const [codeHorizontalOffset, setCodeHorizontalOffset] = useState(0);
const [showHunkHeaders, setShowHunkHeaders] = useState(bootstrap.initialShowHunkHeaders ?? true);
const [themeSelectorState, setThemeSelectorState] = useState<ThemeSelectorState>({
Expand Down Expand Up @@ -374,6 +377,11 @@ export function App({
setShowLineNumbers((current) => !current);
};

/** Toggle optional Nerd Font file and folder icons without changing git status markers. */
const toggleNerdFontIcons = () => {
setNerdFontIcons((current) => !current);
};

/** Toggle whether mouse selection copies review decorations or only file content. */
const toggleCopyDecorations = () => {
setCopyDecorations((current) => !current);
Expand Down Expand Up @@ -519,6 +527,7 @@ export function App({
showAgentNotes,
showHunkHeaders,
showLineNumbers,
nerdFontIcons,
wrapLines,
});

Expand All @@ -540,6 +549,7 @@ export function App({
showAgentNotes,
showHunkHeaders,
showLineNumbers,
nerdFontIcons,
themeId,
wrapLines,
]);
Expand Down Expand Up @@ -743,6 +753,7 @@ export function App({
showHelp,
showHunkHeaders,
showLineNumbers,
nerdFontIcons,
renderSidebar,
toggleCopyDecorations,
toggleAgentNotes,
Expand All @@ -751,6 +762,7 @@ export function App({
toggleHelp,
toggleHunkHeaders,
toggleLineNumbers,
toggleNerdFontIcons,
toggleLineWrap,
toggleSidebar,
triggerEditSelectedFile,
Expand All @@ -774,12 +786,14 @@ export function App({
showHelp,
showHunkHeaders,
showLineNumbers,
nerdFontIcons,
renderSidebar,
toggleAgentNotes,
toggleFocusArea,
toggleHelp,
toggleHunkHeaders,
toggleLineNumbers,
toggleNerdFontIcons,
toggleLineWrap,
toggleSidebar,
triggerEditSelectedFile,
Expand Down Expand Up @@ -961,6 +975,7 @@ export function App({
entries={review.sidebarEntries}
scrollRef={sidebarScrollRef}
selectedFileId={selectedFile?.id}
nerdFontIcons={nerdFontIcons}
textWidth={sidebarTextWidth}
theme={activeTheme}
width={clampedSidebarWidth}
Expand Down Expand Up @@ -992,6 +1007,7 @@ export function App({
expandedGapsByFileId={review.expandedGapsByFileId}
files={filteredFiles}
pagerMode={pagerMode}
nerdFontIcons={nerdFontIcons}
screenLeft={diffPaneScreenLeft}
screenTop={diffPaneScreenTop}
headerLabelWidth={diffHeaderLabelWidth}
Expand Down
47 changes: 47 additions & 0 deletions src/ui/AppHost.interactions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { resolveTheme } from "./themes";

const { loadAppBootstrap } = await import("../core/loaders");
const { AppHost } = await import("./AppHost");
const { iconForFile } = await import("./lib/fileIcons");

const TEST_KEY_PAGE_UP = "\x1B[5~";
const TEST_KEY_PAGE_DOWN = "\x1B[6~";
Expand Down Expand Up @@ -1410,6 +1411,52 @@ describe("App interactions", () => {
}
});

test("View menu can toggle Nerd Font file icons", async () => {
const setup = await testRender(<AppHost bootstrap={createBootstrap()} />, {
width: 220,
height: 24,
});

try {
await flush(setup);

let frame = setup.captureCharFrame();
expect(frame).not.toContain(iconForFile("alpha.ts").icon);

await act(async () => {
await setup.mockInput.pressKey("F10");
});
await flush(setup);
await act(async () => {
await setup.mockInput.pressArrow("right");
});
await flush(setup);

for (let index = 0; index < 4; index += 1) {
await act(async () => {
await setup.mockInput.pressArrow("down");
});
await flush(setup);
}
Comment thread
eduwass marked this conversation as resolved.

frame = setup.captureCharFrame();
expect(frame).toContain("File icons");
expect(frame).toContain("[ ] File icons");

await act(async () => {
await setup.mockInput.pressEnter();
});
await flush(setup);

frame = setup.captureCharFrame();
expect(frame).toContain(iconForFile("alpha.ts").icon);
} finally {
await act(async () => {
setup.renderer.destroy();
});
}
});

test("reload shortcut reloads the current file diff from disk", async () => {
const dir = mkdtempSync(join(process.cwd(), ".hunk-reload-"));
const left = join(dir, "before.ts");
Expand Down
8 changes: 7 additions & 1 deletion src/ui/components/panes/DiffFileHeaderRow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DiffFile } from "../../../core/types";
import { iconForFile } from "../../lib/fileIcons";
import { fileLabelParts } from "../../lib/files";
import { fitText } from "../../lib/text";
import type { AppTheme } from "../../themes";
Expand All @@ -7,6 +8,7 @@ interface DiffFileHeaderRowProps {
file: DiffFile;
headerLabelWidth: number;
headerStatsWidth: number;
nerdFontIcons?: boolean;
theme: AppTheme;
onSelect?: () => void;
}
Expand All @@ -16,12 +18,15 @@ export function DiffFileHeaderRow({
file,
headerLabelWidth,
headerStatsWidth,
nerdFontIcons = false,
theme,
onSelect,
}: DiffFileHeaderRowProps) {
const additionsText = `+${file.stats.additions}${file.statsTruncated ? "+" : ""}`;
const deletionsText = `-${file.stats.deletions}`;
const { filename, stateLabel } = fileLabelParts(file);
const typeIcon = nerdFontIcons ? iconForFile(file.path) : null;
const iconWidth = typeIcon ? 2 : 0;

return (
<box
Expand All @@ -39,8 +44,9 @@ export function DiffFileHeaderRow({
>
{/* Clicking the file header jumps the main stream selection without collapsing to a single-file view. */}
<box style={{ flexDirection: "row" }}>
{typeIcon && <text fg={typeIcon.color}>{typeIcon.icon} </text>}
<text fg={theme.text}>
{fitText(filename, Math.max(1, headerLabelWidth - (stateLabel?.length ?? 0)))}
{fitText(filename, Math.max(1, headerLabelWidth - iconWidth - (stateLabel?.length ?? 0)))}
</text>
{stateLabel && <text fg={theme.muted}>{stateLabel}</text>}
</box>
Expand Down
4 changes: 4 additions & 0 deletions src/ui/components/panes/DiffPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export function DiffPane({
draftNoteFocused = false,
separatorWidth,
pagerMode = false,
nerdFontIcons = false,
copyDecorations = false,
screenLeft = 0,
screenTop = 0,
Expand Down Expand Up @@ -234,6 +235,7 @@ export function DiffPane({
draftNoteFocused?: boolean;
separatorWidth: number;
pagerMode?: boolean;
nerdFontIcons?: boolean;
copyDecorations?: boolean;
screenLeft?: number;
screenTop?: number;
Expand Down Expand Up @@ -1753,6 +1755,7 @@ export function DiffPane({
file={pinnedHeaderFile}
headerLabelWidth={headerLabelWidth}
headerStatsWidth={headerStatsWidth}
nerdFontIcons={nerdFontIcons}
theme={theme}
onSelect={() => onSelectFile(pinnedHeaderFile.id)}
/>
Expand Down Expand Up @@ -1809,6 +1812,7 @@ export function DiffPane({
file={file}
headerLabelWidth={headerLabelWidth}
headerStatsWidth={headerStatsWidth}
nerdFontIcons={nerdFontIcons}
layout={layout}
selectedHunkIndex={file.id === selectedFileId ? selectedHunkIndex : -1}
copySelectedRowRanges={copySelectedRowKeysByFile.get(file.id)}
Expand Down
Loading