import type { SettingItem, Component } from "@earendil-works/pi-tui"; import { CURSOR_MARKER } from "@earendil-works/pi-tui"; import { FOCUSED_STYLES, UNFOCUSED_STYLES, BLINK_RATES, FOCUS_PROVIDERS, CURSOR_MODES, type CursorConfig, type FocusedStyle, type UnfocusedStyle, type FocusProviderName, type CursorMode, } from "./defaults.ts"; import { transformFocused, transformUnfocused } from "./render.ts"; /** Build the SettingsList rows for the `/cursor` panel. Pure data — the entry * wires `SettingsList.onChange(id, newValue)` to `applyRowChange`. */ export function panelRows(cfg: CursorConfig, activeProviderLabel: string): SettingItem[] { return [ { id: "enabled", label: "Enabled", currentValue: cfg.enabled ? "on" : "off", values: ["on", "off"], description: "Master switch. Off → pi native block, no focus-awareness, no blink.", }, { id: "focusedStyle", label: "Focused style", currentValue: cfg.focusedStyle, values: [...FOCUSED_STYLES], description: "Cursor shape when the pane is active. bar hides the char at the cursor.", }, { id: "unfocusedStyle", label: "Unfocused style", currentValue: cfg.unfocusedStyle, values: [...UNFOCUSED_STYLES], description: "Cursor shape when the pane is inactive. hollow (▢) hides the char at the cursor." }, { id: "blink", label: "Blink", currentValue: cfg.blink ? "on" : "off", values: ["on", "off"], description: "Opt-in. Pauses when unfocused. Default off (pi is steady).", }, { id: "blinkRate", label: "Blink rate", currentValue: String(cfg.blinkRate), values: BLINK_RATES.map(String), description: "Blink interval in ms (used when blink is on).", }, { id: "focusProvider", label: "Focus provider", currentValue: cfg.focusProvider, values: [...FOCUS_PROVIDERS], description: "auto = detect (tmux > cmux > herdr > static). Manual override available.", }, { id: "cursorColor", label: "Cursor color", currentValue: cfg.cursorColor, values: ["accent", cfg.cursorColor === "accent" ? "accent" : cfg.cursorColor], description: "accent follows the pi theme; or set #RRGGBB via /cursor color.", }, { id: "cursorMode", label: "Cursor mode", currentValue: cfg.cursorMode, values: [...CURSOR_MODES], description: "fake = pi's fake cursor (default). hardware = native terminal cursor (Ghostty).", }, { id: "activeProvider", label: "Active provider", currentValue: activeProviderLabel, description: "Read-only: which adapter won detection + focus-events state.", }, ]; } /** Map a `SettingsList.onChange(id, newValue)` event to the next config. Pure. */ export function applyRowChange(cfg: CursorConfig, id: string, newValue: string): CursorConfig { switch (id) { case "enabled": return { ...cfg, enabled: newValue === "on" }; case "focusedStyle": return { ...cfg, focusedStyle: newValue as FocusedStyle }; case "unfocusedStyle": return { ...cfg, unfocusedStyle: newValue as UnfocusedStyle }; case "blink": return { ...cfg, blink: newValue === "on" }; case "blinkRate": return { ...cfg, blinkRate: Number(newValue) }; case "focusProvider": return { ...cfg, focusProvider: newValue as FocusProviderName }; case "cursorColor": return { ...cfg, cursorColor: newValue === "accent" || /^#[0-9a-fA-F]{6}$/.test(newValue) ? newValue : cfg.cursorColor }; case "cursorMode": return { ...cfg, cursorMode: newValue as CursorMode }; default: return cfg; // activeProvider is read-only } } /** A live preview line: renders a sample code line with the cursor in the * current focused (or unfocused) style. Reads `getCfg()` on each render so it * updates instantly as the user cycles a style/blink row. For the focused * preview, `getBlinkVisible()` drives the blink phase (so the preview blinks * in sync with the real editor cursor when blink is on). Uses the real ANSI * transforms — what the terminal shows is what ships. */ export function previewLine( getCfg: () => CursorConfig, focused: boolean, getBlinkVisible?: () => boolean, getTheme?: () => { getFgAnsi(color: string): string; getColorMode(): "truecolor" | "256color" }, ): Component { // Fallback no-color theme so previewLine works without a theme (T8 threads the real one). const fallbackTheme = { getFgAnsi: () => "", getColorMode: () => "256color" as const }; const sample = `const result = await fetch(url);${CURSOR_MARKER}\x1b[7m \x1b[0m`;; return { render(_width: number): string[] { const cfg = getCfg(); const theme = getTheme ? getTheme() : fallbackTheme; if (focused) { const blinkVisible = getBlinkVisible ? getBlinkVisible() : true; return transformFocused([sample], cfg, theme, blinkVisible, cfg.cursorMode); } return transformUnfocused([sample], cfg, theme); }, invalidate() {}, }; } /** Current display value for a row id, given a config (used to updateValue on the list). */ export function rowDisplayValue(id: string, cfg: CursorConfig): string { if (id === "enabled") return cfg.enabled ? "on" : "off"; if (id === "blink") return cfg.blink ? "on" : "off"; if (id === "blinkRate") return String(cfg.blinkRate); if (id === "focusedStyle") return cfg.focusedStyle; if (id === "unfocusedStyle") return cfg.unfocusedStyle; if (id === "focusProvider") return cfg.focusProvider; if (id === "cursorColor") return cfg.cursorColor; if (id === "cursorMode") return cfg.cursorMode; return cfg[id as keyof CursorConfig]?.toString() ?? ""; }