From ba336fc416b9c7971bbbda1d862d1e38faadc42a Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 21 Nov 2025 23:23:35 +0800 Subject: [PATCH] feat(settings): add auto-launch on system startup feature Implement auto-launch functionality with proper state synchronization and error handling across Windows, macOS, and Linux platforms. Key changes: - Add auto_launch module using auto-launch crate 0.5 - Define typed errors (AutoLaunchPathError, AutoLaunchEnableError, etc.) - Sync system state with settings.json on app startup - Only call system API when auto-launch state actually changes - Add UI toggle in Window Settings panel - Add i18n support for auto-launch settings (en/zh) Implementation details: - Settings file (settings.json) is the single source of truth - On startup, system state is synced to match settings.json - Error handling uses Rust type system with proper error propagation - Frontend optimized to avoid unnecessary system API calls Platform support: - Windows: HKEY_CURRENT_USER registry modification - macOS: AppleScript-based launch item (configurable to Launch Agent) - Linux: XDG autostart desktop file --- src-tauri/Cargo.lock | 41 ++++++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/auto_launch.rs | 39 ++++++++++++++++++++ src-tauri/src/commands/settings.rs | 17 +++++++++ src-tauri/src/error.rs | 8 +++++ src-tauri/src/lib.rs | 27 ++++++++++++++ src-tauri/src/settings.rs | 4 +++ src/components/settings/WindowSettings.tsx | 7 ++++ src/hooks/useSettings.ts | 15 ++++++++ src/hooks/useSettingsForm.ts | 3 ++ src/i18n/locales/en.json | 3 ++ src/i18n/locales/zh.json | 3 ++ src/lib/api/settings.ts | 8 +++++ src/types.ts | 2 ++ 14 files changed, 178 insertions(+) create mode 100644 src-tauri/src/auto_launch.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7bcb55c..c8b8f84 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -291,6 +291,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -598,6 +609,7 @@ name = "cc-switch" version = "3.7.0" dependencies = [ "anyhow", + "auto-launch", "chrono", "dirs 5.0.1", "futures", @@ -982,6 +994,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1000,6 +1021,17 @@ dependencies = [ "dirs-sys 0.5.0", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -6397,6 +6429,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.52.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2a2dd53..530d258 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,6 +34,7 @@ tauri-plugin-updater = "2" tauri-plugin-dialog = "2" tauri-plugin-store = "2" tauri-plugin-deep-link = "2" +auto-launch = "0.5" dirs = "5.0" toml = "0.8" toml_edit = "0.22" diff --git a/src-tauri/src/auto_launch.rs b/src-tauri/src/auto_launch.rs new file mode 100644 index 0000000..43bef82 --- /dev/null +++ b/src-tauri/src/auto_launch.rs @@ -0,0 +1,39 @@ +use crate::error::AppError; +use auto_launch::AutoLaunch; + +/// 初始化 AutoLaunch 实例 +fn get_auto_launch() -> Result { + let app_name = "CC Switch"; + let app_path = std::env::current_exe().map_err(AppError::AutoLaunchPathError)?; + + let auto_launch = AutoLaunch::new(app_name, &app_path.to_string_lossy(), false, &[] as &[&str]); + Ok(auto_launch) +} + +/// 启用开机自启 +pub fn enable_auto_launch() -> Result<(), AppError> { + let auto_launch = get_auto_launch()?; + auto_launch + .enable() + .map_err(|e| AppError::AutoLaunchEnableError(e.to_string()))?; + log::info!("Auto-launch enabled"); + Ok(()) +} + +/// 禁用开机自启 +pub fn disable_auto_launch() -> Result<(), AppError> { + let auto_launch = get_auto_launch()?; + auto_launch + .disable() + .map_err(|e| AppError::AutoLaunchDisableError(e.to_string()))?; + log::info!("Auto-launch disabled"); + Ok(()) +} + +/// 检查是否已启用开机自启 +pub fn is_auto_launch_enabled() -> Result { + let auto_launch = get_auto_launch()?; + auto_launch + .is_enabled() + .map_err(|e| AppError::AutoLaunchCheckError(e.to_string())) +} diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index ee76526..eeca097 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -37,3 +37,20 @@ pub async fn set_app_config_dir_override( crate::app_store::set_app_config_dir_to_store(&app, path.as_deref())?; Ok(true) } + +/// 设置开机自启 +#[tauri::command] +pub async fn set_auto_launch(enabled: bool) -> Result { + if enabled { + crate::auto_launch::enable_auto_launch().map_err(|e| e.to_string())?; + } else { + crate::auto_launch::disable_auto_launch().map_err(|e| e.to_string())?; + } + Ok(true) +} + +/// 获取开机自启状态 +#[tauri::command] +pub async fn get_auto_launch_status() -> Result { + crate::auto_launch::is_auto_launch_enabled().map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index d9b0262..2912d8e 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -50,6 +50,14 @@ pub enum AppError { zh: String, en: String, }, + #[error("Failed to get application path for auto-launch: {0}")] + AutoLaunchPathError(#[source] std::io::Error), + #[error("Failed to enable auto-launch: {0}")] + AutoLaunchEnableError(String), + #[error("Failed to disable auto-launch: {0}")] + AutoLaunchDisableError(String), + #[error("Failed to check auto-launch status: {0}")] + AutoLaunchCheckError(String), } impl AppError { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0ed21ab..5ac537e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod app_config; mod app_store; +mod auto_launch; mod claude_mcp; mod claude_plugin; mod codex_config; @@ -559,6 +560,30 @@ pub fn run() { // 启动阶段不再无条件保存,避免意外覆盖用户配置。 + // 同步开机自启状态:以 settings.json 为准,保持系统项一致 + { + let settings = crate::settings::get_settings(); + let system_enabled = crate::auto_launch::is_auto_launch_enabled().unwrap_or(false); + + if settings.launch_on_startup != system_enabled { + log::info!( + "开机自启状态不一致:settings={}, system={},以 settings 为准", + settings.launch_on_startup, + system_enabled + ); + + let sync_result = if settings.launch_on_startup { + crate::auto_launch::enable_auto_launch() + } else { + crate::auto_launch::disable_auto_launch() + }; + + if let Err(e) = sync_result { + log::warn!("同步开机自启状态失败: {}", e); + } + } + } + // 注册 deep-link URL 处理器(使用正确的 DeepLinkExt API) log::info!("=== Registering deep-link URL handler ==="); @@ -653,6 +678,8 @@ pub fn run() { commands::get_settings, commands::save_settings, commands::restart_app, + commands::set_auto_launch, + commands::get_auto_launch_status, commands::check_for_updates, commands::is_portable_mode, commands::get_claude_plugin_status, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 75b4c3c..518e1cb 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -49,6 +49,9 @@ pub struct AppSettings { pub gemini_config_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub language: Option, + /// 是否开机自启 + #[serde(default)] + pub launch_on_startup: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub security: Option, /// Claude 自定义端点列表 @@ -77,6 +80,7 @@ impl Default for AppSettings { codex_config_dir: None, gemini_config_dir: None, language: None, + launch_on_startup: false, security: None, custom_endpoints_claude: HashMap::new(), custom_endpoints_codex: HashMap::new(), diff --git a/src/components/settings/WindowSettings.tsx b/src/components/settings/WindowSettings.tsx index 94ec79a..06f0d39 100644 --- a/src/components/settings/WindowSettings.tsx +++ b/src/components/settings/WindowSettings.tsx @@ -19,6 +19,13 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {

+ onChange({ launchOnStartup: value })} + /> + { + return await invoke("set_auto_launch", { enabled }); + }, + + async getAutoLaunchStatus(): Promise { + return await invoke("get_auto_launch_status"); + }, }; diff --git a/src/types.ts b/src/types.ts index b4cb57f..f94a737 100644 --- a/src/types.ts +++ b/src/types.ts @@ -101,6 +101,8 @@ export interface Settings { geminiConfigDir?: string; // 首选语言(可选,默认中文) language?: "en" | "zh"; + // 是否开机自启 + launchOnStartup?: boolean; // Claude 自定义端点列表 customEndpointsClaude?: Record; // Codex 自定义端点列表