pub mod db; pub mod vault; pub mod settings; pub mod connections; pub mod credentials; pub mod ssh; pub mod sftp; pub mod rdp; pub mod theme; pub mod workspace; pub mod commands; use std::path::PathBuf; use std::sync::Mutex; use db::Database; use vault::VaultService; use credentials::CredentialService; use settings::SettingsService; use connections::ConnectionService; use sftp::SftpService; use ssh::session::SshService; use rdp::RdpService; use theme::ThemeService; use workspace::WorkspaceService; /// Application state shared across all Tauri commands via State. pub struct AppState { pub db: Database, pub vault: Mutex>, pub settings: SettingsService, pub connections: ConnectionService, pub credentials: Mutex>, pub ssh: SshService, pub sftp: SftpService, pub rdp: RdpService, pub theme: ThemeService, pub workspace: WorkspaceService, } impl AppState { pub fn new(data_dir: PathBuf) -> Result> { std::fs::create_dir_all(&data_dir)?; let db_path = data_dir.join("wraith.db"); let database = Database::open(&db_path)?; database.migrate()?; let settings = SettingsService::new(database.clone()); let connections = ConnectionService::new(database.clone()); let ssh = SshService::new(database.clone()); let sftp = SftpService::new(); let rdp = RdpService::new(); let theme = ThemeService::new(database.clone()); // WorkspaceService shares the same SettingsService interface; we clone // the Database to construct a second SettingsService for the workspace // module so it can remain self-contained. let workspace_settings = SettingsService::new(database.clone()); let workspace = WorkspaceService::new(workspace_settings); Ok(Self { db: database, vault: Mutex::new(None), settings, connections, credentials: Mutex::new(None), ssh, sftp, rdp, theme, workspace, }) } /// Returns true if the vault has never been set up. pub fn is_first_run(&self) -> bool { self.settings.get("vault_salt").unwrap_or_default().is_empty() } /// Returns true if the vault is currently unlocked. pub fn is_unlocked(&self) -> bool { self.vault.lock().unwrap().is_some() } } /// Determine the data directory for Wraith. pub fn data_directory() -> PathBuf { // Windows: %APPDATA%\Wraith if let Ok(appdata) = std::env::var("APPDATA") { return PathBuf::from(appdata).join("Wraith"); } // macOS/Linux: XDG_DATA_HOME or ~/.local/share/wraith if let Ok(home) = std::env::var("HOME") { if let Ok(xdg) = std::env::var("XDG_DATA_HOME") { return PathBuf::from(xdg).join("wraith"); } return PathBuf::from(home).join(".local").join("share").join("wraith"); } // Fallback PathBuf::from(".") } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let data_dir = data_directory(); let app_state = AppState::new(data_dir) .expect("Failed to initialize application state"); // Seed built-in themes (INSERT OR IGNORE — safe to call on every boot). app_state.theme.seed_builtins(); // Crash recovery detection: log dirty shutdowns so they can be acted on. if app_state.workspace.was_clean_shutdown() { app_state .workspace .clear_clean_shutdown() .unwrap_or_else(|e| eprintln!("workspace: failed to clear clean-shutdown flag: {e}")); } else { // No clean-shutdown flag found — either first run or a crash/kill. // Only log if a snapshot exists (i.e. there were open tabs last time). if app_state.workspace.load().is_some() { eprintln!("workspace: dirty shutdown detected — a previous session may not have exited cleanly"); } } tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .manage(app_state) .invoke_handler(tauri::generate_handler![ commands::vault::is_first_run, commands::vault::create_vault, commands::vault::unlock, commands::vault::is_unlocked, commands::settings::get_setting, commands::settings::set_setting, commands::connections::list_connections, commands::connections::create_connection, commands::connections::get_connection, commands::connections::update_connection, commands::connections::delete_connection, commands::connections::list_groups, commands::connections::create_group, commands::connections::delete_group, commands::connections::rename_group, commands::connections::search_connections, commands::credentials::list_credentials, commands::credentials::create_password, commands::credentials::create_ssh_key, commands::credentials::delete_credential, commands::ssh_commands::connect_ssh, commands::ssh_commands::connect_ssh_with_key, commands::ssh_commands::ssh_write, commands::ssh_commands::ssh_resize, commands::ssh_commands::disconnect_ssh, commands::ssh_commands::list_ssh_sessions, commands::sftp_commands::sftp_list, commands::sftp_commands::sftp_read_file, commands::sftp_commands::sftp_write_file, commands::sftp_commands::sftp_mkdir, commands::sftp_commands::sftp_delete, commands::sftp_commands::sftp_rename, commands::rdp_commands::connect_rdp, commands::rdp_commands::rdp_get_frame, commands::rdp_commands::rdp_send_mouse, commands::rdp_commands::rdp_send_key, commands::rdp_commands::disconnect_rdp, commands::rdp_commands::list_rdp_sessions, commands::theme_commands::list_themes, commands::theme_commands::get_theme, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }