generated from nhcarrigan/template
b1a45ed00e
- Add HighContrast variant to Theme enum in Rust backend - Add high-contrast CSS theme with pure black/white for max contrast - Use bright saturated colors for syntax highlighting - Add High Contrast button to theme selector with accessibility tooltip - Follows WCAG guidelines for contrast ratios Closes #20
177 lines
5.4 KiB
TypeScript
177 lines
5.4 KiB
TypeScript
import { writable, derived } from "svelte/store";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
export type Theme = "dark" | "light" | "high-contrast";
|
|
|
|
export interface HikariConfig {
|
|
model: string | null;
|
|
api_key: string | null;
|
|
custom_instructions: string | null;
|
|
mcp_servers_json: string | null;
|
|
auto_granted_tools: string[];
|
|
theme: Theme;
|
|
greeting_enabled: boolean;
|
|
greeting_custom_prompt: string | null;
|
|
notifications_enabled: boolean;
|
|
notification_volume: number;
|
|
always_on_top: boolean;
|
|
minimize_to_tray: boolean;
|
|
update_checks_enabled: boolean;
|
|
character_panel_width: number | null;
|
|
font_size: number;
|
|
}
|
|
|
|
const defaultConfig: HikariConfig = {
|
|
model: null,
|
|
api_key: null,
|
|
custom_instructions: null,
|
|
mcp_servers_json: null,
|
|
auto_granted_tools: [],
|
|
theme: "dark",
|
|
greeting_enabled: true,
|
|
greeting_custom_prompt: null,
|
|
notifications_enabled: true,
|
|
notification_volume: 0.7,
|
|
always_on_top: false,
|
|
minimize_to_tray: false,
|
|
update_checks_enabled: true,
|
|
character_panel_width: null,
|
|
font_size: 14,
|
|
};
|
|
|
|
function createConfigStore() {
|
|
const config = writable<HikariConfig>(defaultConfig);
|
|
const isLoading = writable<boolean>(true);
|
|
const isSidebarOpen = writable<boolean>(false);
|
|
const saveError = writable<string | null>(null);
|
|
|
|
async function loadConfig() {
|
|
isLoading.set(true);
|
|
try {
|
|
const savedConfig = await invoke<HikariConfig>("get_config");
|
|
config.set(savedConfig);
|
|
} catch (error) {
|
|
console.error("Failed to load config:", error);
|
|
config.set(defaultConfig);
|
|
} finally {
|
|
isLoading.set(false);
|
|
}
|
|
}
|
|
|
|
async function saveConfig(newConfig: HikariConfig) {
|
|
saveError.set(null);
|
|
try {
|
|
await invoke("save_config", { config: newConfig });
|
|
config.set(newConfig);
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
saveError.set(errorMessage);
|
|
console.error("Failed to save config:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function updateConfig(updates: Partial<HikariConfig>) {
|
|
let currentConfig: HikariConfig = defaultConfig;
|
|
config.subscribe((c) => (currentConfig = c))();
|
|
const newConfig = { ...currentConfig, ...updates };
|
|
await saveConfig(newConfig);
|
|
}
|
|
|
|
return {
|
|
config: { subscribe: config.subscribe },
|
|
isLoading: { subscribe: isLoading.subscribe },
|
|
isSidebarOpen: { subscribe: isSidebarOpen.subscribe },
|
|
saveError: { subscribe: saveError.subscribe },
|
|
|
|
loadConfig,
|
|
saveConfig,
|
|
updateConfig,
|
|
|
|
openSidebar: () => isSidebarOpen.set(true),
|
|
closeSidebar: () => isSidebarOpen.set(false),
|
|
toggleSidebar: () => isSidebarOpen.update((open) => !open),
|
|
|
|
setTheme: async (theme: Theme) => {
|
|
await updateConfig({ theme });
|
|
applyTheme(theme);
|
|
},
|
|
|
|
setFontSize: async (size: number) => {
|
|
const clampedSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size));
|
|
await updateConfig({ font_size: clampedSize });
|
|
applyFontSize(clampedSize);
|
|
},
|
|
|
|
increaseFontSize: async () => {
|
|
let currentConfig: HikariConfig = defaultConfig;
|
|
config.subscribe((c) => (currentConfig = c))();
|
|
const newSize = Math.min(MAX_FONT_SIZE, currentConfig.font_size + 2);
|
|
await updateConfig({ font_size: newSize });
|
|
applyFontSize(newSize);
|
|
},
|
|
|
|
decreaseFontSize: async () => {
|
|
let currentConfig: HikariConfig = defaultConfig;
|
|
config.subscribe((c) => (currentConfig = c))();
|
|
const newSize = Math.max(MIN_FONT_SIZE, currentConfig.font_size - 2);
|
|
await updateConfig({ font_size: newSize });
|
|
applyFontSize(newSize);
|
|
},
|
|
|
|
resetFontSize: async () => {
|
|
await updateConfig({ font_size: DEFAULT_FONT_SIZE });
|
|
applyFontSize(DEFAULT_FONT_SIZE);
|
|
},
|
|
|
|
addAutoGrantedTool: async (tool: string) => {
|
|
let currentConfig: HikariConfig = defaultConfig;
|
|
config.subscribe((c) => (currentConfig = c))();
|
|
if (!currentConfig.auto_granted_tools.includes(tool)) {
|
|
const newTools = [...currentConfig.auto_granted_tools, tool];
|
|
await updateConfig({ auto_granted_tools: newTools });
|
|
}
|
|
},
|
|
|
|
removeAutoGrantedTool: async (tool: string) => {
|
|
let currentConfig: HikariConfig = defaultConfig;
|
|
config.subscribe((c) => (currentConfig = c))();
|
|
const newTools = currentConfig.auto_granted_tools.filter((t) => t !== tool);
|
|
await updateConfig({ auto_granted_tools: newTools });
|
|
},
|
|
|
|
getConfig: (): HikariConfig => {
|
|
let currentConfig: HikariConfig = defaultConfig;
|
|
config.subscribe((c) => (currentConfig = c))();
|
|
return currentConfig;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function applyTheme(theme: Theme) {
|
|
if (typeof document !== "undefined") {
|
|
document.documentElement.setAttribute("data-theme", theme);
|
|
}
|
|
}
|
|
|
|
const MIN_FONT_SIZE = 10;
|
|
const MAX_FONT_SIZE = 24;
|
|
const DEFAULT_FONT_SIZE = 14;
|
|
|
|
export function applyFontSize(size: number) {
|
|
if (typeof document !== "undefined") {
|
|
const clampedSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size));
|
|
document.documentElement.style.setProperty("--terminal-font-size", `${clampedSize}px`);
|
|
}
|
|
}
|
|
|
|
export function clampFontSize(size: number): number {
|
|
return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size));
|
|
}
|
|
|
|
export { MIN_FONT_SIZE, MAX_FONT_SIZE, DEFAULT_FONT_SIZE };
|
|
|
|
export const configStore = createConfigStore();
|
|
|
|
export const isDarkTheme = derived(configStore.config, ($config) => $config.theme === "dark");
|