import type { TUI } from "@earendil-works/pi-tui"; import type { SettingsManager } from "../../../core/settings-manager.ts"; import { detectTerminalBackgroundFromEnv, detectTerminalBackgroundTheme, detectTerminalThemeForAuto, initTheme, parseAutoThemeSetting, resolveThemeSetting, setTheme, setThemeInstance, type TerminalTheme, type Theme, } from "./theme.ts"; type ThemeResult = { success: boolean; error?: string }; export class InteractiveThemeController { private readonly ui: TUI; private readonly getSettingsManager: () => SettingsManager; private readonly showError: (message: string) => void; private readonly onChanged: () => void; private currentThemeSetting: string | undefined; private terminalTheme: TerminalTheme = detectTerminalBackgroundFromEnv().theme; private activeThemeName: string | undefined; private autoSyncEnabled = false; private terminalColorSchemeUnsubscribe: (() => void) | undefined; constructor( ui: TUI, options: { getSettingsManager: () => SettingsManager; showError: (message: string) => void; onChanged: () => void; initialThemeSetting?: string; }, ) { this.ui = ui; this.getSettingsManager = options.getSettingsManager; this.showError = options.showError; this.onChanged = options.onChanged; this.currentThemeSetting = options.initialThemeSetting; this.activeThemeName = resolveThemeSetting( this.currentThemeSetting ?? this.getSettingsManager().getThemeSetting(), this.terminalTheme, ); initTheme(this.activeThemeName, true); this.bindTerminalColorSchemeListener(); } rebindTui(): void { this.terminalColorSchemeUnsubscribe?.(); this.bindTerminalColorSchemeListener(); this.ui.setTerminalColorSchemeNotifications(this.autoSyncEnabled); } async applyFromSettings(): Promise { const settingsManager = this.getSettingsManager(); const themeSetting = this.currentThemeSetting ?? settingsManager.getThemeSetting(); const autoTheme = parseAutoThemeSetting(themeSetting); if (autoTheme) { this.terminalTheme = await detectTerminalThemeForAuto({ ui: this.ui, timeoutMs: 100 }); this.setAutoSync(true); this.applyThemeName(this.terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme, true); return; } this.setAutoSync(false); if (themeSetting !== undefined) { this.applyThemeName(themeSetting, true); return; } const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 }); this.terminalTheme = detection.theme; if (!this.applyThemeName(detection.theme).success) return; if (detection.confidence === "high") { settingsManager.setTheme(detection.theme); await settingsManager.flush(); } } getThemeSelection(): string | undefined { return this.currentThemeSetting ?? this.getSettingsManager().getThemeSetting() ?? this.activeThemeName; } setThemeName(themeName: string, showError = false): ThemeResult { this.setAutoSync(false); const result = this.applyThemeName(themeName, showError); if (result.success) { this.currentThemeSetting = themeName; } return result; } async setThemeSetting(themeSetting: string): Promise { this.currentThemeSetting = themeSetting; await this.applyFromSettings(); } setThemeInstance(themeInstance: Theme): ThemeResult { this.setAutoSync(false); setThemeInstance(themeInstance); this.activeThemeName = ""; this.notifyChanged(); return { success: true }; } preview(themeSettingOrName: string): void { const themeName = resolveThemeSetting(themeSettingOrName, this.terminalTheme) ?? this.activeThemeName; if (!themeName) return; if (setTheme(themeName, true).success) { this.ui.invalidate(); this.ui.requestRender(); } } disableAutoSync(): void { this.setAutoSync(false); } dispose(): void { this.setAutoSync(false); this.terminalColorSchemeUnsubscribe?.(); this.terminalColorSchemeUnsubscribe = undefined; } getTerminalTheme(): TerminalTheme { return this.terminalTheme; } private applyThemeName(themeName: string, showError = false): ThemeResult { const result = setTheme(themeName, true); this.activeThemeName = result.success ? themeName : "dark"; this.notifyChanged(); if (!result.success && showError) { this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`); } return result; } private notifyChanged(): void { this.ui.invalidate(); this.onChanged(); } private setAutoSync(enabled: boolean): void { if (this.autoSyncEnabled === enabled) return; this.autoSyncEnabled = enabled; this.ui.setTerminalColorSchemeNotifications(enabled); } private bindTerminalColorSchemeListener(): void { this.terminalColorSchemeUnsubscribe = this.ui.onTerminalColorSchemeChange((terminalTheme) => this.applyTerminalTheme(terminalTheme), ); } private applyTerminalTheme(terminalTheme: TerminalTheme): void { if (!this.autoSyncEnabled) return; this.terminalTheme = terminalTheme; const autoTheme = parseAutoThemeSetting(this.currentThemeSetting ?? this.getSettingsManager().getThemeSetting()); if (!autoTheme) { this.setAutoSync(false); return; } const themeName = terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme; if (themeName !== this.activeThemeName) { this.applyThemeName(themeName); } } }