diff --git a/src/core/config.ts b/src/core/config.ts index 93996867..1988ce8e 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -20,7 +20,7 @@ interface HunkConfigResolution { input: CliInput; globalConfigPath?: string; repoConfigPath?: string; - persistencePath?: string; + } function isRecord(value: unknown): value is Record { @@ -178,113 +178,8 @@ export function resolveConfiguredCliInput( }, globalConfigPath: userConfigPath, repoConfigPath, - persistencePath: repoConfigPath ?? userConfigPath, }; } -/** Return whether an array contains only TOML table objects. */ -function isRecordArray(value: unknown): value is Array> { - return Array.isArray(value) && value.every(isRecord); -} - -/** Serialize one inline TOML value, including scalar arrays. */ -function serializeTomlValue(value: unknown): string | undefined { - if (typeof value === "string") { - return JSON.stringify(value); - } - - if (typeof value === "boolean" || typeof value === "number") { - return String(value); - } - - if (Array.isArray(value) && !isRecordArray(value)) { - const serializedItems = value.map((item) => serializeTomlValue(item)); - if (serializedItems.some((item) => item === undefined)) { - return undefined; - } - - return `[${serializedItems.join(", ")}]`; - } - - return undefined; -} - -/** Render one TOML object recursively while keeping scalar keys above child tables. */ -function serializeTomlObject(source: Record, sectionName?: string, arrayTable = false): string[] { - const lines: string[] = []; - const scalarEntries: Array<[string, string]> = []; - const tableEntries: Array<[string, Record]> = []; - const arrayTableEntries: Array<[string, Array>]> = []; - - for (const [key, value] of Object.entries(source)) { - if (value === undefined) { - continue; - } - - if (isRecord(value)) { - tableEntries.push([key, value]); - continue; - } - - if (isRecordArray(value)) { - arrayTableEntries.push([key, value]); - continue; - } - - const serialized = serializeTomlValue(value); - if (serialized !== undefined) { - scalarEntries.push([key, serialized]); - } - } - - if (sectionName) { - lines.push(`${arrayTable ? "[[" : "["}${sectionName}${arrayTable ? "]]" : "]"}`); - } - - for (const [key, value] of scalarEntries) { - lines.push(`${key} = ${value}`); - } - - for (const [key, value] of tableEntries) { - if (lines.length > 0) { - lines.push(""); - } - - lines.push(...serializeTomlObject(value, sectionName ? `${sectionName}.${key}` : key)); - } - - for (const [key, values] of arrayTableEntries) { - for (const value of values) { - if (lines.length > 0) { - lines.push(""); - } - - lines.push(...serializeTomlObject(value, sectionName ? `${sectionName}.${key}` : key, true)); - } - } - - return lines; -} - -/** Persist the current view defaults while preserving any existing profile sections. */ -export function persistViewPreferences(path: string, preferences: PersistedViewPreferences) { - const existing = readTomlRecord(path); - - existing.mode = preferences.mode; - existing.line_numbers = preferences.showLineNumbers; - existing.wrap_lines = preferences.wrapLines; - existing.hunk_headers = preferences.showHunkHeaders; - existing.agent_notes = preferences.showAgentNotes; - - if (preferences.theme) { - existing.theme = preferences.theme; - } else { - delete existing.theme; - } - - fs.mkdirSync(dirname(path), { recursive: true }); - fs.writeFileSync(path, `${serializeTomlObject(existing).join("\n").trim()}\n`); -} - export const CONFIG_DEFAULTS = DEFAULT_VIEW_PREFERENCES; export const CONFIG_SECTION_KEYS = CONFIG_SECTION_NAMES; diff --git a/src/main.tsx b/src/main.tsx index f81feb70..a692e793 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,7 +3,7 @@ import { createCliRenderer } from "@opentui/core"; import { createRoot } from "@opentui/react"; import { parseCli } from "./core/cli"; -import { persistViewPreferences, resolveConfiguredCliInput } from "./core/config"; +import { resolveConfiguredCliInput } from "./core/config"; import { loadAppBootstrap } from "./core/loaders"; import { looksLikePatchInput, pagePlainText } from "./core/pager"; import { shutdownSession } from "./core/shutdown"; @@ -70,8 +70,5 @@ root.render( persistViewPreferences(configured.persistencePath!, preferences) : undefined - } />, ); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index fee7179d..1ca27351 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,7 +1,7 @@ import { MouseButton, type KeyEvent, type MouseEvent as TuiMouseEvent, type ScrollBoxRenderable } from "@opentui/core"; import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"; import { Suspense, lazy, startTransition, useDeferredValue, useEffect, useRef, useState } from "react"; -import type { AppBootstrap, LayoutMode, PersistedViewPreferences } from "../core/types"; +import type { AppBootstrap, LayoutMode } from "../core/types"; import { MenuBar } from "./components/chrome/MenuBar"; import { MENU_ORDER, buildMenuSpecs, menuWidth, nextMenuItemIndex, type MenuEntry, type MenuId } from "./components/chrome/menu"; import { StatusBar } from "./components/chrome/StatusBar"; @@ -29,11 +29,9 @@ function clamp(value: number, min: number, max: number) { export function App({ bootstrap, onQuit = () => process.exit(0), - onPreferencesChange, }: { bootstrap: AppBootstrap; onQuit?: () => void; - onPreferencesChange?: (preferences: PersistedViewPreferences) => void; }) { const FILES_MIN_WIDTH = 22; const DIFF_MIN_WIDTH = 48; @@ -45,7 +43,6 @@ export function App({ const terminal = useTerminalDimensions(); const filesScrollRef = useRef(null); const diffScrollRef = useRef(null); - const didPersistPreferences = useRef(false); const [layoutMode, setLayoutMode] = useState(bootstrap.initialMode); const [themeId, setThemeId] = useState(() => resolveTheme(bootstrap.initialTheme, renderer.themeMode).id); const [showAgentNotes, setShowAgentNotes] = useState(bootstrap.initialShowAgentNotes ?? false); @@ -68,26 +65,6 @@ export function App({ const pagerMode = Boolean(bootstrap.input.options.pager); const activeTheme = resolveTheme(themeId, renderer.themeMode); - useEffect(() => { - if (!onPreferencesChange) { - return; - } - - if (!didPersistPreferences.current) { - didPersistPreferences.current = true; - return; - } - - onPreferencesChange({ - mode: layoutMode, - theme: themeId, - showLineNumbers, - wrapLines, - showHunkHeaders, - showAgentNotes, - }); - }, [layoutMode, onPreferencesChange, showAgentNotes, showHunkHeaders, showLineNumbers, themeId, wrapLines]); - const filteredFiles = bootstrap.changeset.files.filter((file) => { if (!deferredFilter.trim()) { return true; diff --git a/test/app-interactions.test.tsx b/test/app-interactions.test.tsx index 88a7a328..6faa53d2 100644 --- a/test/app-interactions.test.tsx +++ b/test/app-interactions.test.tsx @@ -450,35 +450,4 @@ describe("App interactions", () => { } }); - test("view preference changes are emitted for persistence after interaction", async () => { - const onPreferencesChange = mock(() => undefined); - const setup = await testRender( - , - { width: 220, height: 24 }, - ); - - try { - await flush(setup); - expect(onPreferencesChange).not.toHaveBeenCalled(); - - await act(async () => { - await setup.mockInput.typeText("l"); - }); - await flush(setup); - - expect(onPreferencesChange).toHaveBeenCalledTimes(1); - expect(onPreferencesChange.mock.calls[0]?.[0]).toMatchObject({ - mode: "split", - theme: "midnight", - showLineNumbers: false, - wrapLines: false, - showHunkHeaders: true, - showAgentNotes: false, - }); - } finally { - await act(async () => { - setup.renderer.destroy(); - }); - } - }); }); diff --git a/test/config.test.ts b/test/config.test.ts index b566575c..7ef2617e 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { CliInput } from "../src/core/types"; -import { persistViewPreferences, resolveConfiguredCliInput } from "../src/core/config"; +import { resolveConfiguredCliInput } from "../src/core/config"; import { loadAppBootstrap } from "../src/core/loaders"; const tempDirs: string[] = []; @@ -81,7 +81,6 @@ describe("config resolution", () => { }); expect(resolved.repoConfigPath).toBe(join(repo, ".hunk", "config.toml")); - expect(resolved.persistencePath).toBe(join(repo, ".hunk", "config.toml")); expect(resolved.input.options).toMatchObject({ pager: true, mode: "stack", @@ -103,82 +102,6 @@ describe("config resolution", () => { }); expect(resolved.repoConfigPath).toBeUndefined(); - expect(resolved.persistencePath).toBe(join(home, ".config", "hunk", "config.toml")); - }); - - test("persists top-level preferences without discarding profile sections", () => { - const repo = createTempDir("hunk-config-repo-"); - const configPath = join(repo, ".hunk", "config.toml"); - - mkdirSync(join(repo, ".hunk"), { recursive: true }); - writeFileSync( - configPath, - [ - 'recent_themes = ["paper", "midnight"]', - '', - '[pager]', - 'mode = "stack"', - '', - '[git]', - 'wrap_lines = true', - ].join('\n'), - ); - - persistViewPreferences(configPath, { - mode: "split", - theme: "midnight", - showLineNumbers: false, - wrapLines: true, - showHunkHeaders: false, - showAgentNotes: true, - }); - - const parsed = Bun.TOML.parse(readFileSync(configPath, "utf8")) as Record; - expect(parsed.mode).toBe("split"); - expect(parsed.theme).toBe("midnight"); - expect(parsed.line_numbers).toBe(false); - expect(parsed.wrap_lines).toBe(true); - expect(parsed.hunk_headers).toBe(false); - expect(parsed.agent_notes).toBe(true); - expect(parsed.recent_themes).toEqual(["paper", "midnight"]); - expect((parsed.pager as Record).mode).toBe("stack"); - expect((parsed.git as Record).wrap_lines).toBe(true); - }); - - test("preserves TOML array-of-table sections when persisting view preferences", () => { - const repo = createTempDir("hunk-config-repo-"); - const configPath = join(repo, ".hunk", "config.toml"); - - mkdirSync(join(repo, ".hunk"), { recursive: true }); - writeFileSync( - configPath, - [ - 'theme = "paper"', - '', - '[[bookmarks]]', - 'path = "src/a.ts"', - 'hunk = 0', - '', - '[[bookmarks]]', - 'path = "src/b.ts"', - 'hunk = 2', - ].join('\n'), - ); - - persistViewPreferences(configPath, { - mode: "split", - theme: "paper", - showLineNumbers: true, - wrapLines: false, - showHunkHeaders: true, - showAgentNotes: false, - }); - - const parsed = Bun.TOML.parse(readFileSync(configPath, "utf8")) as Record; - expect(parsed.bookmarks).toEqual([ - { path: "src/a.ts", hunk: 0 }, - { path: "src/b.ts", hunk: 2 }, - ]); }); test("command-specific config sections also apply to show mode", () => {