import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { getAgentDir } from "@earendil-works/pi-coding-agent"; import { DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES, normalizeFullscreenWheelScrollLines, } from "./fullscreen-scroll.ts"; import type { IconMode } from "./icons.ts"; export type SettingsLanguage = "en" | "zh"; export type CursorStyle = "block" | "bar" | "underline"; export type { IconMode } from "./icons.ts"; export interface FooterSegments { cwd: boolean; sessionName: boolean; gitBranch: boolean; gitStatus: boolean; gitCommit: boolean; runtime: boolean; context: boolean; tokens: boolean; cost: boolean; extensionStatuses: boolean; } export interface TelemetryConfig { enabled: boolean; tps: boolean; ttft: boolean; duration: boolean; tokens: boolean; stalls: boolean; cost: boolean; } export interface FullscreenConfig { wheelScrollLines: number; } export interface OpenTuiConfig { enabled: boolean; settingsLanguage: SettingsLanguage; cursorStyle: CursorStyle; fullscreen: FullscreenConfig; icons: { mode: IconMode; }; footerSegments: FooterSegments; telemetry: TelemetryConfig; } export const DEFAULT_CONFIG: OpenTuiConfig = { enabled: true, settingsLanguage: "en", cursorStyle: "block", fullscreen: { wheelScrollLines: DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES, }, icons: { mode: "auto", }, footerSegments: { cwd: true, sessionName: false, gitBranch: true, gitStatus: true, gitCommit: false, runtime: true, context: true, tokens: true, cost: true, extensionStatuses: true, }, telemetry: { enabled: true, tps: true, ttft: true, duration: true, tokens: true, stalls: true, cost: true, }, }; export function getConfigPath(): string { const agentDir = getAgentDir(); return join(agentDir, "open-tui.json"); } function deepMerge(base: T, override: unknown): T { if (typeof base !== "object" || base === null || Array.isArray(base)) { return (override as T) ?? base; } if (typeof override !== "object" || override === null || Array.isArray(override)) { return base; } const result = { ...(base as Record) }; const overrideRec = override as Record; for (const key of Object.keys(overrideRec)) { const baseVal = (base as Record)[key]; const overVal = overrideRec[key]; if (typeof baseVal === "object" && baseVal !== null && !Array.isArray(baseVal) && typeof overVal === "object" && overVal !== null && !Array.isArray(overVal)) { result[key] = deepMerge(baseVal, overVal); } else if (overVal !== undefined) { result[key] = overVal; } } return result as T; } export function ensureConfigExists(): void { const path = getConfigPath(); if (existsSync(path)) return; try { const agentDir = getAgentDir(); if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true }); writeFileSync(path, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf8"); } catch { // ponytail: silent fallback — config creation is best-effort } } export function loadConfig(notify?: (msg: string, level: "warning" | "info") => void): OpenTuiConfig { const path = getConfigPath(); if (!existsSync(path)) { ensureConfigExists(); return structuredClone(DEFAULT_CONFIG); } try { const raw = readFileSync(path, "utf8"); const parsed: unknown = JSON.parse(raw); const config = deepMerge(DEFAULT_CONFIG, parsed); if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") { config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage; } if (config.cursorStyle !== "block" && config.cursorStyle !== "bar" && config.cursorStyle !== "underline") { config.cursorStyle = DEFAULT_CONFIG.cursorStyle; } config.fullscreen.wheelScrollLines = normalizeFullscreenWheelScrollLines( config.fullscreen.wheelScrollLines, DEFAULT_CONFIG.fullscreen.wheelScrollLines, ); return config; } catch (err) { notify?.(`open-tui config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning"); return structuredClone(DEFAULT_CONFIG); } } export function saveConfig(config: OpenTuiConfig): void { const path = getConfigPath(); try { const agentDir = getAgentDir(); if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true }); writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf8"); } catch { // ponytail: silent fallback — config save is best-effort } }