use std::error::Error; use std::time::Duration; use once_cell::sync::OnceCell; use rfd::{ MessageDialog, MessageLevel, }; use sqlx::{ SqlitePool, sqlite::SqlitePoolOptions, sqlite::SqliteConnectOptions, }; use tauri::{ App, AppHandle, async_runtime as rt, Manager, RunEvent, WindowEvent, }; use tauri::menu::MenuItem; use crate::{ config::{self, AppConfig}, credentials::Session, ipc, server::Server, errors::*, shortcuts, state::AppState, tray, }; pub static APP: OnceCell = OnceCell::new(); pub fn run() -> tauri::Result<()> { tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { show_main_window(app) .error_popup("Failed to show main window") })) .plugin(tauri_plugin_global_shortcut::Builder::default().build()) .invoke_handler(tauri::generate_handler![ ipc::unlock, ipc::respond, ipc::get_session_status, ipc::signal_activity, ipc::save_credentials, ipc::get_config, ipc::save_config, ipc::launch_terminal, ipc::get_setup_errors, ]) .setup(|app| { let res = rt::block_on(setup(app)); if let Err(ref e) = res { MessageDialog::new() .set_level(MessageLevel::Error) .set_title("Creddy failed to start") .set_description(format!("{e}")) .show(); } res }) .build(tauri::generate_context!())? .run(|app, run_event| { if let RunEvent::WindowEvent { event, .. } = run_event { if let WindowEvent::CloseRequested { api, .. } = event { let _ = hide_main_window(app); api.prevent_close(); } } }); Ok(()) } pub async fn connect_db() -> Result { let conn_opts = SqliteConnectOptions::new() .filename(config::get_or_create_db_path()?) .create_if_missing(true); let pool_opts = SqlitePoolOptions::new(); let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?; sqlx::migrate!().run(&pool).await?; Ok(pool) } async fn setup(app: &mut App) -> Result<(), Box> { APP.set(app.handle().clone()).unwrap(); tray::setup(app)?; // get_or_create_db_path doesn't create the actual db file, just the directory let is_first_launch = !config::get_or_create_db_path()?.exists(); let pool = connect_db().await?; let mut setup_errors: Vec = vec![]; let mut conf = match AppConfig::load(&pool).await { Ok(c) => c, Err(SetupError::ConfigParseError(_)) => { setup_errors.push( "Could not load configuration from database. Reverting to defaults.".into() ); AppConfig::default() }, err => err?, }; let session = Session::load(&pool).await?; Server::start(app.handle().clone())?; config::set_auto_launch(conf.start_on_login)?; if let Err(_e) = config::set_auto_launch(conf.start_on_login) { setup_errors.push("Error: Failed to manage autolaunch.".into()); } // if hotkeys fail to register, disable them so that this error doesn't have to keep showing up if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) { conf.hotkeys.disable_all(); conf.save(&pool).await?; setup_errors.push("Failed to register hotkeys. Hotkey settings have been disabled.".into()); } let desktop_is_gnome = std::env::var("XDG_CURRENT_DESKTOP") .map(|names| names.split(':').any(|n| n == "GNOME")) .unwrap_or(false); // if session is empty, this is probably the first launch, so don't autohide if !conf.start_minimized || is_first_launch { show_main_window(&app.handle())?; } let state = AppState::new(conf, session, pool, setup_errors, desktop_is_gnome); app.manage(state); // make sure we do this after managing app state, so that it doesn't panic start_auto_locker(app.app_handle().clone()); Ok(()) } fn start_auto_locker(app: AppHandle) { rt::spawn(async move { let state = app.state::(); loop { // this gives our session-timeout a minimum resolution of 10s, which seems fine? let delay = Duration::from_secs(10); tokio::time::sleep(delay).await; if state.should_auto_lock().await { state.lock().await.error_popup("Failed to lock Creddy"); } } }); } pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> { let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; w.show()?; let show_hide = app.state::>(); show_hide.set_text("Hide")?; Ok(()) } pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> { let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; w.hide()?; let show_hide = app.state::>(); show_hide.set_text("Show")?; Ok(()) } pub fn toggle_main_window(app: &AppHandle) -> Result<(), WindowError> { let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; if w.is_visible()? { hide_main_window(app) } else { show_main_window(app) } }