Compare commits

...

2 Commits

Author SHA1 Message Date
hikari b1a45ed00e feat: add high contrast mode for accessibility
- 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
2026-01-25 13:44:52 -08:00
hikari 457722dc3a feat: add minimize to system tray option
Add ability to minimize Hikari to the system tray when closing the window
instead of fully exiting. When enabled, clicking the close button hides
the window and shows a tray icon with "Show Hikari" and "Quit" options.

- Add tray module with system tray setup and menu handling
- Add minimize_to_tray config option in settings
- Handle window close event to hide instead of close when enabled
- Add tray icon click handler to restore window
2026-01-25 13:38:30 -08:00
11 changed files with 177 additions and 5 deletions
+1
View File
@@ -4180,6 +4180,7 @@ dependencies = [
"gtk", "gtk",
"heck 0.5.0", "heck 0.5.0",
"http", "http",
"image",
"jni", "jni",
"libc", "libc",
"log", "log",
+1 -1
View File
@@ -13,7 +13,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
+2 -1
View File
@@ -15,6 +15,7 @@
"notification:allow-request-permission", "notification:allow-request-permission",
"notification:allow-notify", "notification:allow-notify",
"clipboard-manager:default", "clipboard-manager:default",
"clipboard-manager:allow-read-image" "clipboard-manager:allow-read-image",
"core:tray:default"
] ]
} }
+13
View File
@@ -70,6 +70,9 @@ pub struct HikariConfig {
#[serde(default = "default_font_size")] #[serde(default = "default_font_size")]
pub font_size: u32, pub font_size: u32,
#[serde(default)]
pub minimize_to_tray: bool,
} }
impl Default for HikariConfig { impl Default for HikariConfig {
@@ -89,6 +92,7 @@ impl Default for HikariConfig {
update_checks_enabled: true, update_checks_enabled: true,
character_panel_width: None, character_panel_width: None,
font_size: 14, font_size: 14,
minimize_to_tray: false,
} }
} }
} }
@@ -119,6 +123,8 @@ pub enum Theme {
#[default] #[default]
Dark, Dark,
Light, Light,
#[serde(rename = "high-contrast")]
HighContrast,
} }
#[cfg(test)] #[cfg(test)]
@@ -140,6 +146,7 @@ mod tests {
assert!(config.update_checks_enabled); assert!(config.update_checks_enabled);
assert!(config.character_panel_width.is_none()); assert!(config.character_panel_width.is_none());
assert_eq!(config.font_size, 14); assert_eq!(config.font_size, 14);
assert!(!config.minimize_to_tray);
} }
#[test] #[test]
@@ -159,6 +166,7 @@ mod tests {
update_checks_enabled: true, update_checks_enabled: true,
character_panel_width: Some(400), character_panel_width: Some(400),
font_size: 16, font_size: 16,
minimize_to_tray: true,
}; };
let json = serde_json::to_string(&config).unwrap(); let json = serde_json::to_string(&config).unwrap();
@@ -179,8 +187,13 @@ mod tests {
fn test_theme_serialization() { fn test_theme_serialization() {
let dark = Theme::Dark; let dark = Theme::Dark;
let light = Theme::Light; let light = Theme::Light;
let high_contrast = Theme::HighContrast;
assert_eq!(serde_json::to_string(&dark).unwrap(), "\"dark\""); assert_eq!(serde_json::to_string(&dark).unwrap(), "\"dark\"");
assert_eq!(serde_json::to_string(&light).unwrap(), "\"light\""); assert_eq!(serde_json::to_string(&light).unwrap(), "\"light\"");
assert_eq!(
serde_json::to_string(&high_contrast).unwrap(),
"\"high-contrast\""
);
} }
} }
+24
View File
@@ -5,6 +5,7 @@ mod config;
mod notifications; mod notifications;
mod stats; mod stats;
mod temp_manager; mod temp_manager;
mod tray;
mod types; mod types;
mod vbs_notification; mod vbs_notification;
mod windows_toast; mod windows_toast;
@@ -15,7 +16,9 @@ use bridge_manager::create_shared_bridge_manager;
use commands::load_saved_achievements; use commands::load_saved_achievements;
use commands::*; use commands::*;
use notifications::*; use notifications::*;
use tauri::Manager;
use temp_manager::create_shared_temp_manager; use temp_manager::create_shared_temp_manager;
use tray::{setup_tray, should_minimize_to_tray};
use vbs_notification::*; use vbs_notification::*;
use windows_toast::*; use windows_toast::*;
use wsl_notifications::*; use wsl_notifications::*;
@@ -47,6 +50,27 @@ pub fn run() {
} }
} }
// Set up system tray
if let Err(e) = setup_tray(app.handle()) {
eprintln!("Failed to set up system tray: {}", e);
}
// Handle window close event for minimize to tray
let main_window = app.get_webview_window("main").unwrap();
main_window.on_window_event({
let app_handle = app.handle().clone();
move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if should_minimize_to_tray(&app_handle) {
api.prevent_close();
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.hide();
}
}
}
}
});
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
+68
View File
@@ -0,0 +1,68 @@
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
AppHandle, Manager,
};
use crate::config::HikariConfig;
pub fn setup_tray(app: &AppHandle) -> tauri::Result<()> {
let show_item = MenuItem::with_id(app, "show", "Show Hikari", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &quit_item])?;
let _tray = TrayIconBuilder::with_id("main")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.tooltip("Hikari - Claude Code Assistant")
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
"quit" => {
app.exit(0);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}
pub fn should_minimize_to_tray(app: &AppHandle) -> bool {
let config_path = app
.path()
.app_config_dir()
.ok()
.map(|p| p.join("hikari-config.json"));
if let Some(path) = config_path {
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(config) = serde_json::from_str::<HikariConfig>(&content) {
return config.minimize_to_tray;
}
}
}
false
}
+6
View File
@@ -22,6 +22,12 @@
], ],
"security": { "security": {
"csp": null "csp": null
},
"trayIcon": {
"id": "main",
"iconPath": "icons/32x32.png",
"iconAsTemplate": false,
"tooltip": "Hikari - Claude Code Assistant"
} }
}, },
"bundle": { "bundle": {
+30
View File
@@ -61,6 +61,36 @@
--hljs-meta: #64748b; --hljs-meta: #64748b;
} }
[data-theme="high-contrast"] {
--bg-primary: #000000;
--bg-secondary: #0a0a0a;
--bg-terminal: #000000;
--bg-hover: #1a1a1a;
--bg-code: #0a0a0a;
--accent-primary: #ff4d6d;
--accent-secondary: #ff85a1;
--text-primary: #ffffff;
--text-secondary: #e0e0e0;
--text-tertiary: #b0b0b0;
--border-color: #ffffff;
/* Terminal specific colors - bright and saturated */
--terminal-user: #00ffff;
--terminal-tool: #ff00ff;
--terminal-tool-name: #ffaaff;
--terminal-error: #ff5555;
/* Syntax highlighting colors (high contrast) */
--hljs-keyword: #ff66ff;
--hljs-string: #66ff66;
--hljs-number: #ffff00;
--hljs-comment: #aaaaaa;
--hljs-function: #ff99ff;
--hljs-type: #00ffff;
--hljs-variable: #ffaa00;
--hljs-meta: #cccccc;
}
html, html,
body { body {
margin: 0; margin: 0;
+28 -2
View File
@@ -23,6 +23,7 @@
notifications_enabled: true, notifications_enabled: true,
notification_volume: 0.7, notification_volume: 0.7,
always_on_top: false, always_on_top: false,
minimize_to_tray: false,
update_checks_enabled: true, update_checks_enabled: true,
character_panel_width: null, character_panel_width: null,
font_size: 14, font_size: 14,
@@ -404,7 +405,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
onclick={() => handleThemeChange("dark")} onclick={() => handleThemeChange("dark")}
class="flex-1 px-4 py-2 rounded-lg border transition-colors {config.theme === 'dark' class="flex-1 px-3 py-2 rounded-lg border transition-colors {config.theme === 'dark'
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white' ? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}" : 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
> >
@@ -412,12 +413,22 @@
</button> </button>
<button <button
onclick={() => handleThemeChange("light")} onclick={() => handleThemeChange("light")}
class="flex-1 px-4 py-2 rounded-lg border transition-colors {config.theme === 'light' class="flex-1 px-3 py-2 rounded-lg border transition-colors {config.theme === 'light'
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white' ? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}" : 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
> >
Light Light
</button> </button>
<button
onclick={() => handleThemeChange("high-contrast")}
class="flex-1 px-3 py-2 rounded-lg border transition-colors {config.theme ===
'high-contrast'
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
title="High contrast mode for improved accessibility"
>
High Contrast
</button>
</div> </div>
</div> </div>
@@ -477,6 +488,21 @@
</p> </p>
</div> </div>
<!-- Minimize to Tray Toggle -->
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
bind:checked={config.minimize_to_tray}
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
/>
<span class="text-sm text-[var(--text-primary)]">Minimize to system tray</span>
</label>
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
Hide to tray instead of closing when you click the X button
</p>
</div>
<!-- Update Checks Toggle --> <!-- Update Checks Toggle -->
<div class="mb-4"> <div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer"> <label class="flex items-center gap-3 cursor-pointer">
+1
View File
@@ -49,6 +49,7 @@
update_checks_enabled: true, update_checks_enabled: true,
character_panel_width: null, character_panel_width: null,
font_size: 14, font_size: 14,
minimize_to_tray: false,
}); });
onMount(async () => { onMount(async () => {
+3 -1
View File
@@ -1,7 +1,7 @@
import { writable, derived } from "svelte/store"; import { writable, derived } from "svelte/store";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
export type Theme = "dark" | "light"; export type Theme = "dark" | "light" | "high-contrast";
export interface HikariConfig { export interface HikariConfig {
model: string | null; model: string | null;
@@ -15,6 +15,7 @@ export interface HikariConfig {
notifications_enabled: boolean; notifications_enabled: boolean;
notification_volume: number; notification_volume: number;
always_on_top: boolean; always_on_top: boolean;
minimize_to_tray: boolean;
update_checks_enabled: boolean; update_checks_enabled: boolean;
character_panel_width: number | null; character_panel_width: number | null;
font_size: number; font_size: number;
@@ -32,6 +33,7 @@ const defaultConfig: HikariConfig = {
notifications_enabled: true, notifications_enabled: true,
notification_volume: 0.7, notification_volume: 0.7,
always_on_top: false, always_on_top: false,
minimize_to_tray: false,
update_checks_enabled: true, update_checks_enabled: true,
character_panel_width: null, character_panel_width: null,
font_size: 14, font_size: 14,