use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ClaudeStartOptions { #[serde(default)] pub working_dir: String, #[serde(default)] pub model: Option, #[serde(default)] pub api_key: Option, #[serde(default)] pub custom_instructions: Option, #[serde(default)] pub mcp_servers_json: Option, #[serde(default)] pub allowed_tools: Vec, #[serde(default)] pub skip_greeting: bool, #[serde(default)] pub resume_session_id: Option, #[serde(default)] pub use_worktree: bool, #[serde(default)] pub disable_1m_context: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct HikariConfig { #[serde(default)] pub model: Option, #[serde(default)] pub api_key: Option, #[serde(default)] pub custom_instructions: Option, #[serde(default)] pub mcp_servers_json: Option, #[serde(default)] pub auto_granted_tools: Vec, #[serde(default)] pub theme: Theme, #[serde(default = "default_greeting_enabled")] pub greeting_enabled: bool, #[serde(default)] pub greeting_custom_prompt: Option, #[serde(default = "default_notifications_enabled")] pub notifications_enabled: bool, #[serde(default = "default_notification_volume")] pub notification_volume: f32, #[serde(default)] pub always_on_top: bool, #[serde(default = "default_update_checks_enabled")] pub update_checks_enabled: bool, #[serde(default)] pub character_panel_width: Option, #[serde(default = "default_font_size")] pub font_size: u32, #[serde(default)] pub streamer_mode: bool, #[serde(default)] pub streamer_hide_paths: bool, #[serde(default)] pub compact_mode: bool, // Profile fields #[serde(default)] pub profile_name: Option, #[serde(default)] pub profile_avatar_path: Option, #[serde(default)] pub profile_bio: Option, // Custom theme colors #[serde(default)] pub custom_theme_colors: CustomThemeColors, // Token budget settings #[serde(default)] pub budget_enabled: bool, #[serde(default)] pub session_token_budget: Option, #[serde(default)] pub session_cost_budget: Option, #[serde(default = "default_budget_action")] pub budget_action: BudgetAction, #[serde(default = "default_budget_warning_threshold")] pub budget_warning_threshold: f32, #[serde(default = "default_discord_rpc_enabled")] pub discord_rpc_enabled: bool, #[serde(default)] pub use_worktree: bool, #[serde(default)] pub disable_1m_context: bool, #[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 { fn default() -> Self { Self { model: None, api_key: None, custom_instructions: None, mcp_servers_json: None, auto_granted_tools: Vec::new(), theme: Theme::default(), greeting_enabled: true, greeting_custom_prompt: None, notifications_enabled: true, notification_volume: 0.7, always_on_top: false, update_checks_enabled: true, character_panel_width: None, font_size: 14, streamer_mode: false, streamer_hide_paths: false, compact_mode: false, profile_name: None, profile_avatar_path: None, profile_bio: None, custom_theme_colors: CustomThemeColors::default(), budget_enabled: false, session_token_budget: None, session_cost_budget: None, budget_action: BudgetAction::Warn, budget_warning_threshold: 0.8, discord_rpc_enabled: true, use_worktree: false, disable_1m_context: false, trusted_workspaces: Vec::new(), background_image_path: None, background_image_opacity: 0.3, } } } fn default_update_checks_enabled() -> bool { true } fn default_greeting_enabled() -> bool { true } fn default_notifications_enabled() -> bool { true } fn default_notification_volume() -> f32 { 0.7 } fn default_font_size() -> u32 { 14 } fn default_budget_action() -> BudgetAction { BudgetAction::Warn } fn default_budget_warning_threshold() -> f32 { 0.8 } 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 { #[default] Warn, Block, } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Theme { #[default] Dark, Light, #[serde(rename = "high-contrast")] HighContrast, Custom, } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct CustomThemeColors { #[serde(default)] pub bg_primary: Option, #[serde(default)] pub bg_secondary: Option, #[serde(default)] pub bg_terminal: Option, #[serde(default)] pub accent_primary: Option, #[serde(default)] pub accent_secondary: Option, #[serde(default)] pub text_primary: Option, #[serde(default)] pub text_secondary: Option, #[serde(default)] pub border_color: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_config() { let config = HikariConfig::default(); assert!(config.model.is_none()); assert!(config.api_key.is_none()); assert!(config.custom_instructions.is_none()); assert!(config.mcp_servers_json.is_none()); assert!(config.auto_granted_tools.is_empty()); assert_eq!(config.theme, Theme::Dark); assert!(config.greeting_enabled); assert!(config.greeting_custom_prompt.is_none()); assert!(!config.always_on_top); assert!(config.update_checks_enabled); assert!(config.character_panel_width.is_none()); assert_eq!(config.font_size, 14); assert!(!config.streamer_mode); assert!(!config.streamer_hide_paths); assert!(!config.compact_mode); assert!(config.profile_name.is_none()); assert!(config.profile_avatar_path.is_none()); assert!(config.profile_bio.is_none()); assert_eq!(config.custom_theme_colors, CustomThemeColors::default()); assert!(!config.budget_enabled); assert!(config.session_token_budget.is_none()); assert!(config.session_cost_budget.is_none()); assert_eq!(config.budget_action, BudgetAction::Warn); assert!((config.budget_warning_threshold - 0.8).abs() < f32::EPSILON); assert!(config.discord_rpc_enabled); assert!(!config.use_worktree); assert!(!config.disable_1m_context); assert!(config.trusted_workspaces.is_empty()); } #[test] fn test_config_serialization() { let config = HikariConfig { model: Some("claude-sonnet-4-20250514".to_string()), api_key: None, custom_instructions: Some("Be helpful".to_string()), mcp_servers_json: None, auto_granted_tools: vec!["Read".to_string(), "Glob".to_string()], theme: Theme::Light, greeting_enabled: true, greeting_custom_prompt: Some("Hello!".to_string()), notifications_enabled: true, notification_volume: 0.7, always_on_top: true, update_checks_enabled: true, character_panel_width: Some(400), font_size: 16, streamer_mode: false, streamer_hide_paths: false, compact_mode: false, profile_name: Some("Test User".to_string()), profile_avatar_path: None, profile_bio: Some("A test bio".to_string()), custom_theme_colors: CustomThemeColors::default(), budget_enabled: true, session_token_budget: Some(100000), session_cost_budget: Some(1.50), budget_action: BudgetAction::Block, budget_warning_threshold: 0.75, discord_rpc_enabled: true, 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(); let deserialized: HikariConfig = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.model, config.model); assert_eq!(deserialized.custom_instructions, config.custom_instructions); assert_eq!(deserialized.auto_granted_tools, config.auto_granted_tools); assert_eq!(deserialized.theme, Theme::Light); assert!(deserialized.greeting_enabled); assert_eq!( deserialized.greeting_custom_prompt, Some("Hello!".to_string()) ); } #[test] fn test_theme_serialization() { let dark = Theme::Dark; let light = Theme::Light; let high_contrast = Theme::HighContrast; assert_eq!(serde_json::to_string(&dark).unwrap(), "\"dark\""); assert_eq!(serde_json::to_string(&light).unwrap(), "\"light\""); assert_eq!( serde_json::to_string(&high_contrast).unwrap(), "\"high-contrast\"" ); let custom = Theme::Custom; assert_eq!(serde_json::to_string(&custom).unwrap(), "\"custom\""); } }