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
This commit is contained in:
Jason
2025-11-21 23:23:35 +08:00
parent 7fa0a7b166
commit ba336fc416
14 changed files with 178 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
use crate::error::AppError;
use auto_launch::AutoLaunch;
/// 初始化 AutoLaunch 实例
fn get_auto_launch() -> Result<AutoLaunch, AppError> {
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<bool, AppError> {
let auto_launch = get_auto_launch()?;
auto_launch
.is_enabled()
.map_err(|e| AppError::AutoLaunchCheckError(e.to_string()))
}

View File

@@ -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<bool, String> {
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<bool, String> {
crate::auto_launch::is_auto_launch_enabled().map_err(|e| e.to_string())
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -49,6 +49,9 @@ pub struct AppSettings {
pub gemini_config_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
/// 是否开机自启
#[serde(default)]
pub launch_on_startup: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub security: Option<SecuritySettings>,
/// 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(),