use std::net::Ipv4Addr; use std::path::PathBuf; use auto_launch::AutoLaunchBuilder; use is_terminal::IsTerminal; use serde::{Serialize, Deserialize}; use sqlx::SqlitePool; use tauri::{ Manager, GlobalShortcutManager, async_runtime as rt, }; use crate::errors::*; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TermConfig { pub name: String, // we call it exec because it isn't always the actual path, // in some cases it's just the name and relies on path-searching // it's a string because it can come from the frontend as json pub exec: String, pub args: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Hotkey { pub keys: String, pub enabled: bool, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct HotkeysConfig { // tauri uses strings to represent keybinds, so we will as well pub show_window: Hotkey, pub launch_terminal: Hotkey, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppConfig { #[serde(default = "default_listen_addr")] pub listen_addr: Ipv4Addr, #[serde(default = "default_listen_port")] pub listen_port: u16, #[serde(default = "default_rehide_ms")] pub rehide_ms: u64, #[serde(default = "default_start_minimized")] pub start_minimized: bool, #[serde(default = "default_start_on_login")] pub start_on_login: bool, #[serde(default = "default_term_config")] pub terminal: TermConfig, #[serde(default = "default_hotkey_config")] pub hotkeys: HotkeysConfig, } impl Default for AppConfig { fn default() -> Self { AppConfig { listen_addr: default_listen_addr(), listen_port: default_listen_port(), rehide_ms: default_rehide_ms(), start_minimized: default_start_minimized(), start_on_login: default_start_on_login(), terminal: default_term_config(), hotkeys: default_hotkey_config(), } } } impl AppConfig { pub async fn load(pool: &SqlitePool) -> Result { let res = sqlx::query!("SELECT * from config where name = 'main'") .fetch_optional(pool) .await?; let row = match res { Some(row) => row, None => return Ok(AppConfig::default()), }; Ok(serde_json::from_str(&row.data)?) } pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> { let data = serde_json::to_string(self).unwrap(); sqlx::query( "INSERT INTO config (name, data) VALUES ('main', ?) ON CONFLICT (name) DO UPDATE SET data = ?" ) .bind(&data) .bind(&data) .execute(pool) .await?; Ok(()) } } pub fn set_auto_launch(is_configured: bool) -> Result<(), SetupError> { let path_buf = std::env::current_exe() .map_err(|e| auto_launch::Error::Io(e))?; let path = path_buf .to_string_lossy(); let auto = AutoLaunchBuilder::new() .set_app_name("Creddy") .set_app_path(&path) .build()?; let is_enabled = auto.is_enabled()?; if is_configured && !is_enabled { auto.enable()?; } else if !is_configured && is_enabled { auto.disable()?; } Ok(()) } pub fn get_or_create_db_path() -> Result { let mut path = dirs::data_dir() .ok_or(DataDirError::NotFound)?; path.push("Creddy"); std::fs::create_dir_all(&path)?; if cfg!(debug_assertions) && std::io::stdout().is_terminal() { path.push("creddy.dev.db"); } else { path.push("creddy.db"); } Ok(path) } fn default_listen_port() -> u16 { if cfg!(debug_assertions) { 12_345 } else { 19_923 } } fn default_term_config() -> TermConfig { #[cfg(windows)] { let shell = if which::which("pwsh.exe").is_ok() { "pwsh.exe".to_string() } else { "powershell.exe".to_string() }; let (exec, args) = if cfg!(debug_assertions) { ("conhost.exe".to_string(), vec![shell.clone()]) } else { (shell.clone(), vec![]) }; TermConfig { name: shell, exec, args } } #[cfg(unix)] { for bin in ["gnome-terminal", "konsole"] { if let Ok(_) = which::which(bin) { return TermConfig { name: bin.into(), exec: bin.into(), args: vec![], } } } return TermConfig { name: "gnome-terminal".into(), exec: "gnome-terminal".into(), args: vec![], }; } } fn default_hotkey_config() -> HotkeysConfig { HotkeysConfig { show_window: Hotkey {keys: "alt+shift+C".into(), enabled: true}, launch_terminal: Hotkey {keys: "alt+shift+T".into(), enabled: true}, } } // note: will panic if called before APP is set pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> { let app = crate::app::APP.get().unwrap(); let mut manager = app.global_shortcut_manager(); manager.unregister_all()?; if hotkeys.show_window.enabled { let handle = app.app_handle(); manager.register( &hotkeys.show_window.keys, move || { handle.get_window("main") .map(|w| w.show().error_popup("Failed to show")) .ok_or(HandlerError::NoMainWindow) .error_popup("No main window"); }, )?; } if hotkeys.launch_terminal.enabled { // register() doesn't take an async fn, so we have to use spawn manager.register( &hotkeys.launch_terminal.keys, || { rt::spawn(async { crate::terminal::launch(false) .await .error_popup("Failed to launch"); }); } )?; } Ok(()) } fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST } fn default_rehide_ms() -> u64 { 1000 } // start minimized and on login only in production mode fn default_start_minimized() -> bool { !cfg!(debug_assertions) } fn default_start_on_login() -> bool { !cfg!(debug_assertions) }