/** * UI adapter over the schema. Reads `ui.options` declared inline in * settings-schema.ts and produces typed widget definitions for the * settings selector. * * To add a new setting to the UI: declare it in `settings-schema.ts` * with a `ui` block. If it needs a submenu, include `options: [...]` * (or `options: "runtime"` for runtime-injected lists like themes). */ import { TERMINAL } from "@oh-my-pi/pi-tui"; import { Settings } from "../../config/settings"; import { type AnyUiMetadata, getDefault, getEnumValues, getPathsForTab, getType, getUi, SETTING_TABS, type SettingPath, type SettingTab, type SubmenuOption, } from "../../config/settings-schema"; // ═══════════════════════════════════════════════════════════════════════════ // UI Definition Types // ═══════════════════════════════════════════════════════════════════════════ export type SettingValue = boolean | string; interface BaseSettingDef { path: SettingPath; label: string; description: string; tab: SettingTab; /** * Optional visibility predicate. When supplied and returning false, the * setting is hidden from the UI. Applies to every variant — booleans, * enums, submenus, and text inputs. */ condition?: () => boolean; } export interface BooleanSettingDef extends BaseSettingDef { type: "boolean"; } export interface EnumSettingDef extends BaseSettingDef { type: "enum"; values: readonly string[]; } type OptionList = ReadonlyArray; export interface SubmenuSettingDef extends BaseSettingDef { type: "submenu"; options: OptionList; onPreview?: (value: string) => void; onPreviewCancel?: (originalValue: string) => void; } export interface TextInputSettingDef extends BaseSettingDef { type: "text"; } export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef | TextInputSettingDef; // ═══════════════════════════════════════════════════════════════════════════ // Condition Functions // ═══════════════════════════════════════════════════════════════════════════ const CONDITIONS: Record boolean> = { hasImageProtocol: () => !!TERMINAL.imageProtocol, hindsightActive: () => { try { return Settings.instance.get("memory.backend") === "hindsight"; } catch { return false; } }, }; // ═══════════════════════════════════════════════════════════════════════════ // Schema to UI Conversion // ═══════════════════════════════════════════════════════════════════════════ function resolveOptions(ui: AnyUiMetadata): OptionList | "runtime" | undefined { if (!ui.options) return undefined; if (ui.options === "runtime") return "runtime"; return ui.options; } function pathToSettingDef(path: SettingPath): SettingDef | null { const ui = getUi(path); if (!ui) return null; const schemaType = getType(path); const condition = ui.condition ? CONDITIONS[ui.condition] : undefined; const base = { path, label: ui.label, description: ui.description, tab: ui.tab, condition }; if (schemaType === "boolean") { return { ...base, type: "boolean" }; } const options = resolveOptions(ui); if (schemaType === "enum") { if (options === undefined) { return { ...base, type: "enum", values: getEnumValues(path) ?? [] }; } // "runtime" is not a valid sentinel for enums — schema types prevent this, // but treat defensively as an empty submenu. return { ...base, type: "submenu", options: options === "runtime" ? [] : options }; } if (schemaType === "number") { // Numbers without options are intentionally hidden from the UI. if (!options || options === "runtime") return null; return { ...base, type: "submenu", options }; } if (schemaType === "string") { if (options === "runtime") { // Empty list now; the selector layer (theme handling, etc.) injects choices. return { ...base, type: "submenu", options: [] }; } if (options) { return { ...base, type: "submenu", options }; } return { ...base, type: "text" }; } return null; } // ═══════════════════════════════════════════════════════════════════════════ // Public API // ═══════════════════════════════════════════════════════════════════════════ /** Cache of generated definitions */ let cachedDefs: SettingDef[] | null = null; /** Get all setting definitions with UI */ export function getAllSettingDefs(): SettingDef[] { if (cachedDefs) return cachedDefs; const defs: SettingDef[] = []; for (const tab of SETTING_TABS) { for (const path of getPathsForTab(tab)) { const def = pathToSettingDef(path); if (def) defs.push(def); } } cachedDefs = defs; return defs; } /** Get settings for a specific tab */ export function getSettingsForTab(tab: SettingTab): SettingDef[] { return getAllSettingDefs().filter(def => def.tab === tab); } /** Get a setting definition by path */ export function getSettingDef(path: SettingPath): SettingDef | undefined { return getAllSettingDefs().find(def => def.path === path); } /** Get default value for display */ export function getDisplayDefault(path: SettingPath): string { const value = getDefault(path); if (value === undefined) return ""; if (typeof value === "boolean") return value ? "true" : "false"; return String(value); }