| import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event"; |
| import { LazyStore } from "@tauri-apps/plugin-store"; |
| import { |
| DEFAULT_AUTOCOMPLETE_MODEL, |
| DEFAULT_MODEL_ID, |
| LMSTUDIO_DEFAULT_BASE_URL, |
| OPENAI_COMPATIBLE_DEFAULT_BASE_URL, |
| type AutocompleteProviderId, |
| type ModelId, |
| } from "@/modules/ai/config"; |
| import type { KeyBinding, ShortcutId } from "@/modules/shortcuts/shortcuts"; |
|
|
| export type ThemePref = "system" | "light" | "dark"; |
|
|
| export const EDITOR_THEMES = [ |
| "atomone", |
| "aura", |
| "copilot", |
| "github-dark", |
| "github-light", |
| "nord", |
| "tokyo-night", |
| "xcode-dark", |
| "xcode-light", |
| ] as const; |
|
|
| export type EditorThemeId = (typeof EDITOR_THEMES)[number]; |
|
|
| export const EDITOR_THEME_LABELS: Record<EditorThemeId, string> = { |
| atomone: "Atom One", |
| aura: "Aura", |
| copilot: "Copilot", |
| "github-dark": "GitHub Dark", |
| "github-light": "GitHub Light", |
| nord: "Nord", |
| "tokyo-night": "Tokyo Night", |
| "xcode-dark": "Xcode Dark", |
| "xcode-light": "Xcode Light", |
| }; |
|
|
| export type Preferences = { |
| theme: ThemePref; |
| defaultModelId: ModelId; |
| editorTheme: EditorThemeId; |
| customInstructions: string; |
| autostart: boolean; |
| restoreWindowState: boolean; |
| autocompleteEnabled: boolean; |
| autocompleteProvider: AutocompleteProviderId; |
| autocompleteModelId: string; |
| lmstudioBaseURL: string; |
| lmstudioModelId: string; |
| openaiCompatibleBaseURL: string; |
| openaiCompatibleModelId: string; |
| favoriteModelIds: string[]; |
| recentModelIds: string[]; |
| vimMode: boolean; |
| showHidden: boolean; |
| terminalWebglEnabled: boolean; |
| terminalFontSize: number; |
| shortcuts: Record<ShortcutId, KeyBinding[]>; |
| }; |
|
|
| const STORE_PATH = "terax-settings.json"; |
| const KEY_THEME = "theme"; |
| const KEY_DEFAULT_MODEL = "defaultModelId"; |
| const KEY_EDITOR_THEME = "editorTheme"; |
| const KEY_CUSTOM_INSTRUCTIONS = "customInstructions"; |
| const KEY_AUTOSTART = "autostart"; |
| const KEY_RESTORE_WINDOW = "restoreWindowState"; |
| const KEY_AUTOCOMPLETE_ENABLED = "autocompleteEnabled"; |
| const KEY_AUTOCOMPLETE_PROVIDER = "autocompleteProvider"; |
| const KEY_AUTOCOMPLETE_MODEL = "autocompleteModelId"; |
| const KEY_LMSTUDIO_BASE_URL = "lmstudioBaseURL"; |
| const KEY_LMSTUDIO_MODEL_ID = "lmstudioModelId"; |
| const KEY_OPENAI_COMPAT_BASE_URL = "openaiCompatibleBaseURL"; |
| const KEY_OPENAI_COMPAT_MODEL_ID = "openaiCompatibleModelId"; |
| const KEY_FAVORITE_MODELS = "favoriteModelIds"; |
| const KEY_RECENT_MODELS = "recentModelIds"; |
| const KEY_VIM_MODE = "vimMode"; |
| const KEY_SHOW_HIDDEN = "showHidden"; |
| const LEGACY_KEY_SHOW_HIDDEN_DIRS = "showHiddenDirectories"; |
| const KEY_TERMINAL_WEBGL_ENABLED = "terminalWebglEnabled"; |
| const KEY_TERMINAL_FONT_SIZE = "terminalFontSize"; |
| const KEY_SHORTCUTS = "shortcuts"; |
|
|
| export const TERMINAL_FONT_SIZE_DEFAULT = 14; |
| export const TERMINAL_FONT_SIZE_MIN = 8; |
| export const TERMINAL_FONT_SIZE_MAX = 32; |
|
|
| export const TERMINAL_FONT_SIZES = [ |
| 10, 12, 13, 14, 15, 16, 18, 20, 22, 24, |
| ] as const; |
|
|
| export const DEFAULT_PREFERENCES: Preferences = { |
| theme: "system", |
| defaultModelId: DEFAULT_MODEL_ID, |
| editorTheme: "atomone", |
| customInstructions: "", |
| autostart: false, |
| restoreWindowState: true, |
| autocompleteEnabled: false, |
| autocompleteProvider: "cerebras", |
| autocompleteModelId: DEFAULT_AUTOCOMPLETE_MODEL.cerebras ?? "", |
| lmstudioBaseURL: LMSTUDIO_DEFAULT_BASE_URL, |
| lmstudioModelId: "", |
| openaiCompatibleBaseURL: OPENAI_COMPATIBLE_DEFAULT_BASE_URL, |
| openaiCompatibleModelId: "", |
| favoriteModelIds: [], |
| recentModelIds: [], |
| vimMode: false, |
| showHidden: false, |
| terminalWebglEnabled: true, |
| terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT, |
| shortcuts: {} as Record<ShortcutId, KeyBinding[]>, |
| }; |
|
|
| const store = new LazyStore(STORE_PATH, { defaults: {}, autoSave: 200 }); |
|
|
| |
| |
| |
| |
| const PREFS_CHANGED_EVENT = "terax://prefs-changed"; |
|
|
| async function writePref<T>(key: string, value: T): Promise<void> { |
| await store.set(key, value); |
| await store.save(); |
| await emit(PREFS_CHANGED_EVENT, { key, value }); |
| } |
|
|
| export async function loadPreferences(): Promise<Preferences> { |
| |
| |
| const entries = await store.entries(); |
| const map = new Map<string, unknown>(entries); |
| const get = <T>(k: string): T | undefined => map.get(k) as T | undefined; |
| return { |
| theme: get<ThemePref>(KEY_THEME) ?? DEFAULT_PREFERENCES.theme, |
| defaultModelId: |
| get<ModelId>(KEY_DEFAULT_MODEL) ?? DEFAULT_PREFERENCES.defaultModelId, |
| editorTheme: |
| get<EditorThemeId>(KEY_EDITOR_THEME) ?? DEFAULT_PREFERENCES.editorTheme, |
| customInstructions: |
| get<string>(KEY_CUSTOM_INSTRUCTIONS) ?? |
| DEFAULT_PREFERENCES.customInstructions, |
| autostart: get<boolean>(KEY_AUTOSTART) ?? DEFAULT_PREFERENCES.autostart, |
| restoreWindowState: |
| get<boolean>(KEY_RESTORE_WINDOW) ?? |
| DEFAULT_PREFERENCES.restoreWindowState, |
| autocompleteEnabled: |
| get<boolean>(KEY_AUTOCOMPLETE_ENABLED) ?? |
| DEFAULT_PREFERENCES.autocompleteEnabled, |
| autocompleteProvider: |
| get<AutocompleteProviderId>(KEY_AUTOCOMPLETE_PROVIDER) ?? |
| DEFAULT_PREFERENCES.autocompleteProvider, |
| autocompleteModelId: |
| get<string>(KEY_AUTOCOMPLETE_MODEL) ?? |
| DEFAULT_PREFERENCES.autocompleteModelId, |
| lmstudioBaseURL: |
| get<string>(KEY_LMSTUDIO_BASE_URL) ?? DEFAULT_PREFERENCES.lmstudioBaseURL, |
| lmstudioModelId: |
| get<string>(KEY_LMSTUDIO_MODEL_ID) ?? DEFAULT_PREFERENCES.lmstudioModelId, |
| openaiCompatibleBaseURL: |
| get<string>(KEY_OPENAI_COMPAT_BASE_URL) ?? |
| DEFAULT_PREFERENCES.openaiCompatibleBaseURL, |
| openaiCompatibleModelId: |
| get<string>(KEY_OPENAI_COMPAT_MODEL_ID) ?? |
| DEFAULT_PREFERENCES.openaiCompatibleModelId, |
| favoriteModelIds: |
| get<string[]>(KEY_FAVORITE_MODELS) ?? |
| DEFAULT_PREFERENCES.favoriteModelIds, |
| recentModelIds: |
| get<string[]>(KEY_RECENT_MODELS) ?? DEFAULT_PREFERENCES.recentModelIds, |
| vimMode: get<boolean>(KEY_VIM_MODE) ?? DEFAULT_PREFERENCES.vimMode, |
| showHidden: |
| get<boolean>(KEY_SHOW_HIDDEN) ?? |
| get<boolean>(LEGACY_KEY_SHOW_HIDDEN_DIRS) ?? |
| DEFAULT_PREFERENCES.showHidden, |
| terminalWebglEnabled: |
| get<boolean>(KEY_TERMINAL_WEBGL_ENABLED) ?? |
| DEFAULT_PREFERENCES.terminalWebglEnabled, |
| terminalFontSize: |
| get<number>(KEY_TERMINAL_FONT_SIZE) ?? |
| DEFAULT_PREFERENCES.terminalFontSize, |
| shortcuts: |
| get<Record<ShortcutId, KeyBinding[]>>(KEY_SHORTCUTS) ?? |
| DEFAULT_PREFERENCES.shortcuts, |
| }; |
| } |
|
|
| export async function setTheme(value: ThemePref): Promise<void> { |
| await writePref(KEY_THEME, value); |
| } |
|
|
| export async function setDefaultModel(value: ModelId): Promise<void> { |
| await writePref(KEY_DEFAULT_MODEL, value); |
| } |
|
|
| export async function setEditorTheme(value: EditorThemeId): Promise<void> { |
| await writePref(KEY_EDITOR_THEME, value); |
| } |
|
|
| export async function setCustomInstructions(value: string): Promise<void> { |
| await writePref(KEY_CUSTOM_INSTRUCTIONS, value); |
| } |
|
|
| export async function setAutostart(value: boolean): Promise<void> { |
| await writePref(KEY_AUTOSTART, value); |
| } |
|
|
| export async function setRestoreWindowState(value: boolean): Promise<void> { |
| await writePref(KEY_RESTORE_WINDOW, value); |
| } |
|
|
| export async function setAutocompleteEnabled(value: boolean): Promise<void> { |
| await writePref(KEY_AUTOCOMPLETE_ENABLED, value); |
| } |
|
|
| export async function setAutocompleteProvider( |
| value: AutocompleteProviderId |
| ): Promise<void> { |
| await writePref(KEY_AUTOCOMPLETE_PROVIDER, value); |
| } |
|
|
| export async function setAutocompleteModelId(value: string): Promise<void> { |
| await writePref(KEY_AUTOCOMPLETE_MODEL, value); |
| } |
|
|
| export async function setLmstudioBaseURL(value: string): Promise<void> { |
| await writePref(KEY_LMSTUDIO_BASE_URL, value); |
| } |
|
|
| export async function setLmstudioModelId(value: string): Promise<void> { |
| await writePref(KEY_LMSTUDIO_MODEL_ID, value); |
| } |
|
|
| export async function setOpenaiCompatibleBaseURL(value: string): Promise<void> { |
| await writePref(KEY_OPENAI_COMPAT_BASE_URL, value); |
| } |
|
|
| export async function setOpenaiCompatibleModelId(value: string): Promise<void> { |
| await writePref(KEY_OPENAI_COMPAT_MODEL_ID, value); |
| } |
|
|
| export async function setFavoriteModelIds(value: string[]): Promise<void> { |
| await writePref(KEY_FAVORITE_MODELS, value); |
| } |
|
|
| export async function setRecentModelIds(value: string[]): Promise<void> { |
| await writePref(KEY_RECENT_MODELS, value); |
| } |
|
|
| export async function setVimMode(value: boolean): Promise<void> { |
| await writePref(KEY_VIM_MODE, value); |
| } |
|
|
| export async function setShowHidden(value: boolean): Promise<void> { |
| await writePref(KEY_SHOW_HIDDEN, value); |
| } |
|
|
| export async function setTerminalWebglEnabled(value: boolean): Promise<void> { |
| await writePref(KEY_TERMINAL_WEBGL_ENABLED, value); |
| } |
|
|
| export async function setTerminalFontSize(value: number): Promise<void> { |
| const clamped = Number.isFinite(value) |
| ? Math.min( |
| TERMINAL_FONT_SIZE_MAX, |
| Math.max(TERMINAL_FONT_SIZE_MIN, Math.round(value)), |
| ) |
| : TERMINAL_FONT_SIZE_DEFAULT; |
| await writePref(KEY_TERMINAL_FONT_SIZE, clamped); |
| } |
|
|
| export async function setShortcuts( |
| value: Record<ShortcutId, KeyBinding[]> | {} |
| ): Promise<void> { |
| await store.set(KEY_SHORTCUTS, value); |
| await store.save(); |
| } |
|
|
| export async function resetShortcuts(): Promise<void> { |
| await store.set(KEY_SHORTCUTS, DEFAULT_PREFERENCES.shortcuts); |
| await store.save(); |
| } |
|
|
| export type PrefKey = keyof Preferences; |
|
|
| |
| export async function onPreferencesChange( |
| cb: (key: PrefKey, value: unknown) => void, |
| ): Promise<UnlistenFn> { |
| const map: Record<string, PrefKey> = { |
| [KEY_THEME]: "theme", |
| [KEY_DEFAULT_MODEL]: "defaultModelId", |
| [KEY_EDITOR_THEME]: "editorTheme", |
| [KEY_CUSTOM_INSTRUCTIONS]: "customInstructions", |
| [KEY_AUTOSTART]: "autostart", |
| [KEY_RESTORE_WINDOW]: "restoreWindowState", |
| [KEY_AUTOCOMPLETE_ENABLED]: "autocompleteEnabled", |
| [KEY_AUTOCOMPLETE_PROVIDER]: "autocompleteProvider", |
| [KEY_AUTOCOMPLETE_MODEL]: "autocompleteModelId", |
| [KEY_LMSTUDIO_BASE_URL]: "lmstudioBaseURL", |
| [KEY_LMSTUDIO_MODEL_ID]: "lmstudioModelId", |
| [KEY_OPENAI_COMPAT_BASE_URL]: "openaiCompatibleBaseURL", |
| [KEY_OPENAI_COMPAT_MODEL_ID]: "openaiCompatibleModelId", |
| [KEY_FAVORITE_MODELS]: "favoriteModelIds", |
| [KEY_RECENT_MODELS]: "recentModelIds", |
| [KEY_VIM_MODE]: "vimMode", |
| [KEY_SHOW_HIDDEN]: "showHidden", |
| [KEY_TERMINAL_WEBGL_ENABLED]: "terminalWebglEnabled", |
| [KEY_TERMINAL_FONT_SIZE]: "terminalFontSize", |
| [KEY_SHORTCUTS]: "shortcuts", |
| }; |
| |
| |
| const unsubLocal = await store.onChange<unknown>((key, value) => { |
| const mapped = map[key]; |
| if (mapped) cb(mapped, value); |
| }); |
| const unsubEvent = await listen<{ key: string; value: unknown }>( |
| PREFS_CHANGED_EVENT, |
| (e) => { |
| const mapped = map[e.payload.key]; |
| if (mapped) cb(mapped, e.payload.value); |
| }, |
| ); |
| return () => { |
| unsubLocal(); |
| unsubEvent(); |
| }; |
| } |
|
|
| |
| |
| const KEYS_CHANGED_EVENT = "terax://ai-keys-changed"; |
|
|
| export async function emitKeysChanged(): Promise<void> { |
| await emit(KEYS_CHANGED_EVENT); |
| } |
|
|
| export function onKeysChanged(cb: () => void): Promise<UnlistenFn> { |
| return listen(KEYS_CHANGED_EVENT, () => cb()); |
| } |
|
|