/** * pi-shimmer-quotes-spinner — Configuration module * * Manages user preferences persisted as JSON in the Pi agent directory. * All config fields have safe defaults so the extension works out of the box. */ import { getAgentDir } from "@earendil-works/pi-coding-agent"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; // ─── Types ────────────────────────────────────────────────────────────── export type SpinnerStyle = "shimmer" | "classic" | "minimal" | "dots" | "braille"; export type QuoteMode = "inline" | "multiline"; export type TextIntensity = "dim" | "normal" | "bright"; export interface SQConfig { /** Spinner animation style */ spinnerStyle: SpinnerStyle; /** Animation speed multiplier (0.25 – 4.0) */ animationSpeed: number; /** How quotes are displayed relative to the working message */ quoteMode: QuoteMode; /** Milliseconds between quote rotations */ quoteInterval: number; /** Where to source quotes from */ quoteSource: "user" | "bundled" | "all"; /** Enable color-sweep shimmer on the verb text */ enableShimmer: boolean; /** Enable custom spinner indicator frames (otherwise Pi default) */ enableCustomSpinner: boolean; /** Use built-in fallback quotes when user quotes file is empty/missing */ enableFallbackQuotes: boolean; /** Verb & UI text brightness */ textIntensity: TextIntensity; /** Master toggle */ enabled: boolean; } // ─── Defaults ─────────────────────────────────────────────────────────── export const DEFAULTS: SQConfig = { spinnerStyle: "shimmer", animationSpeed: 1.0, quoteMode: "inline", quoteInterval: 8_000, quoteSource: "all", enableShimmer: true, enableCustomSpinner: true, enableFallbackQuotes: true, textIntensity: "normal", enabled: true, }; const VALID_STYLES = new Set(["shimmer", "classic", "minimal", "dots", "braille"]); const VALID_INTENSITIES = new Set(["dim", "normal", "bright"]); // ─── Paths ────────────────────────────────────────────────────────────── function extensionDir(): string { return join(getAgentDir(), "pi-shimmer-quotes-spinner"); } function configFile(): string { return join(extensionDir(), "config.json"); } // ─── Load / Save ──────────────────────────────────────────────────────── export function loadConfig(): SQConfig { try { const raw = JSON.parse(readFileSync(configFile(), "utf8")) as Partial; return { ...DEFAULTS, ...raw, spinnerStyle: VALID_STYLES.has(raw.spinnerStyle ?? ("" as SpinnerStyle)) ? raw.spinnerStyle! : DEFAULTS.spinnerStyle, quoteMode: raw.quoteMode === "multiline" ? "multiline" : "inline", textIntensity: VALID_INTENSITIES.has(raw.textIntensity ?? ("" as TextIntensity)) ? raw.textIntensity! : DEFAULTS.textIntensity, animationSpeed: clamp(raw.animationSpeed ?? DEFAULTS.animationSpeed, 0.25, 4.0), quoteInterval: clamp(raw.quoteInterval ?? DEFAULTS.quoteInterval, 1_000, 60_000), }; } catch { return { ...DEFAULTS }; } } export function saveConfig(config: SQConfig): void { try { mkdirSync(extensionDir(), { recursive: true }); writeFileSync(configFile(), JSON.stringify(config, null, 2) + "\n", "utf8"); } catch { // Best-effort persistence } } export function getQuotesFileDir(): string { return extensionDir(); } // ─── Helpers ──────────────────────────────────────────────────────────── function clamp(val: number, min: number, max: number): number { return Math.max(min, Math.min(max, val)); }