diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 91e5a2e..b9af21a 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -128,6 +128,13 @@ pub struct HikariConfig { #[serde(default)] pub trusted_workspaces: Vec, + + // Background image settings + #[serde(default)] + pub background_image_path: Option, + + #[serde(default = "default_background_image_opacity")] + pub background_image_opacity: f32, } impl Default for HikariConfig { @@ -163,6 +170,8 @@ impl Default for HikariConfig { use_worktree: false, disable_1m_context: false, trusted_workspaces: Vec::new(), + background_image_path: None, + background_image_opacity: 0.3, } } } @@ -199,6 +208,10 @@ fn default_discord_rpc_enabled() -> bool { true } +fn default_background_image_opacity() -> f32 { + 0.3 +} + #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[serde(rename_all = "lowercase")] pub enum BudgetAction { @@ -308,6 +321,8 @@ mod tests { use_worktree: true, disable_1m_context: false, trusted_workspaces: vec!["/home/naomi/projects/trusted".to_string()], + background_image_path: Some("/home/naomi/bg.png".to_string()), + background_image_opacity: 0.25, }; let json = serde_json::to_string(&config).unwrap(); diff --git a/src/lib/components/CompactMode.svelte b/src/lib/components/CompactMode.svelte index ca172e8..64fba0a 100644 --- a/src/lib/components/CompactMode.svelte +++ b/src/lib/components/CompactMode.svelte @@ -3,7 +3,7 @@ import { get } from "svelte/store"; import { claudeStore, isClaudeProcessing } from "$lib/stores/claude"; import { characterState, characterInfo } from "$lib/stores/character"; - import { isStreamerMode } from "$lib/stores/config"; + import { isStreamerMode, configStore } from "$lib/stores/config"; import { handleNewUserMessage } from "$lib/notifications/rules"; import { setSkipNextGreeting } from "$lib/tauri"; import type { CharacterState, CharacterStateInfo } from "$lib/types/states"; @@ -14,6 +14,9 @@ let { onExpand }: Props = $props(); + const configValues = configStore.config; + const hasBackgroundImage = $derived($configValues.background_image_path !== null); + let inputValue = $state(""); let isSubmitting = $state(false); let isConnected = $state(false); @@ -150,7 +153,10 @@ } -
+
diff --git a/src/lib/components/ConfigSidebar.svelte b/src/lib/components/ConfigSidebar.svelte index 330cb85..dd26102 100644 --- a/src/lib/components/ConfigSidebar.svelte +++ b/src/lib/components/ConfigSidebar.svelte @@ -13,6 +13,7 @@ import { claudeStore } from "$lib/stores/claude"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { invoke } from "@tauri-apps/api/core"; + import { open } from "@tauri-apps/plugin-dialog"; import CostSummary from "./CostSummary.svelte"; let config: HikariConfig = $state({ @@ -56,6 +57,8 @@ use_worktree: false, disable_1m_context: false, trusted_workspaces: [], + background_image_path: null, + background_image_opacity: 0.3, }); let showCustomThemeEditor = $state(false); @@ -242,6 +245,20 @@ await window.setAlwaysOnTop(enabled); await configStore.updateConfig({ always_on_top: enabled }); } + + async function pickBackgroundImage() { + const selected = await open({ + multiple: false, + filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "gif", "avif"] }], + }); + if (selected) { + config.background_image_path = selected; + } + } + + function clearBackgroundImage() { + config.background_image_path = null; + } @@ -915,6 +932,52 @@ expanded/collapsed to see reasoning details.

+ + +
+ Background Image + {#if config.background_image_path} +

+ {config.background_image_path.split("/").pop()} +

+ {/if} +
+ + {#if config.background_image_path} + + {/if} +
+ {#if config.background_image_path} +
+
+ + + {Math.round(config.background_image_opacity * 100)}% + +
+ +
+ {/if} +
diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index eee7695..bbc3d60 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -108,6 +108,8 @@ use_worktree: false, disable_1m_context: false, trusted_workspaces: [], + background_image_path: null, + background_image_opacity: 0.3, }); let streamerModeActive = $state(false); diff --git a/src/lib/stores/config.test.ts b/src/lib/stores/config.test.ts index 3087ee3..664858c 100644 --- a/src/lib/stores/config.test.ts +++ b/src/lib/stores/config.test.ts @@ -197,6 +197,8 @@ describe("config store", () => { use_worktree: false, disable_1m_context: false, trusted_workspaces: [], + background_image_path: null, + background_image_opacity: 0.3, }; expect(config.model).toBe("claude-sonnet-4"); @@ -246,6 +248,8 @@ describe("config store", () => { use_worktree: false, disable_1m_context: false, trusted_workspaces: [], + background_image_path: null, + background_image_opacity: 0.3, }; expect(config.model).toBeNull(); @@ -794,6 +798,8 @@ describe("config store", () => { use_worktree: false, disable_1m_context: false, trusted_workspaces: [], + background_image_path: null, + background_image_opacity: 0.3, }; const mockInvokeImpl = vi.mocked(invoke); diff --git a/src/lib/stores/config.ts b/src/lib/stores/config.ts index 3a7b4b4..174537d 100644 --- a/src/lib/stores/config.ts +++ b/src/lib/stores/config.ts @@ -53,6 +53,9 @@ export interface HikariConfig { disable_1m_context: boolean; // Workspaces the user has explicitly trusted trusted_workspaces: string[]; + // Background image settings + background_image_path: string | null; + background_image_opacity: number; } const defaultConfig: HikariConfig = { @@ -96,6 +99,8 @@ const defaultConfig: HikariConfig = { use_worktree: false, disable_1m_context: false, trusted_workspaces: [], + background_image_path: null, + background_image_opacity: 0.3, }; function createConfigStore() { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b8a1569..b4e720e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -12,6 +12,7 @@ setSkipNextGreeting, } from "$lib/tauri"; import { configStore, applyTheme, applyFontSize, isCompactMode } from "$lib/stores/config"; + import { readFile } from "@tauri-apps/plugin-fs"; import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications"; import { conversationsStore } from "$lib/stores/conversations"; import { claudeStore, isClaudeProcessing } from "$lib/stores/claude"; @@ -37,6 +38,45 @@ import { debugConsoleStore } from "$lib/stores/debugConsole"; import { initializeTodoListener, cleanupTodoListener } from "$lib/stores/todos"; + let backgroundDataUrl = $state(null); + let backgroundOpacity = $state(0.3); + + const configValues = configStore.config; + $effect(() => { + const cfg = $configValues; + backgroundOpacity = cfg.background_image_opacity; + if (cfg.background_image_path) { + void loadBackgroundImage(cfg.background_image_path); + } else { + backgroundDataUrl = null; + } + }); + + async function loadBackgroundImage(path: string) { + try { + const data = await readFile(path); + const chunks: string[] = []; + const chunkSize = 8192; + for (let i = 0; i < data.length; i += chunkSize) { + chunks.push(String.fromCharCode(...data.slice(i, i + chunkSize))); + } + const ext = path.split(".").pop()?.toLowerCase() ?? "png"; + const mimeMap: Record = { + jpg: "image/jpeg", + jpeg: "image/jpeg", + png: "image/png", + webp: "image/webp", + gif: "image/gif", + avif: "image/avif", + }; + const mime = mimeMap[ext] ?? "image/png"; + backgroundDataUrl = `data:${mime};base64,${btoa(chunks.join(""))}`; + } catch (error) { + console.error("Failed to load background image:", error); + backgroundDataUrl = null; + } + } + let initialized = false; let updateNotification: UpdateNotification | undefined = $state(undefined); let achievementPanelOpen = $state(false); @@ -473,16 +513,27 @@ }); +{#if backgroundDataUrl} +
+{/if} + {#if compactModeActive}
{:else} -
+
(achievementPanelOpen = !achievementPanelOpen)} onToggleCompact={enterCompactMode} @@ -491,8 +542,12 @@