mod cmd; mod model; mod pkg; mod pkg_store; mod util; mod download_handler; mod appdata; mod modules; mod profiles; use std::sync::OnceLock; use anyhow::anyhow; use closure::closure; use appdata::{AppData, ToggleAction}; use model::misc::Game; use pkg::PkgKey; use pkg_store::Payload; use tauri::{AppHandle, Listener, Manager, RunEvent}; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_cli::CliExt; use tauri_plugin_updater::UpdaterExt; use tokio::{fs, sync::Mutex, try_join}; static EXIT_REQUESTED: OnceLock<()> = OnceLock::new(); #[cfg_attr(mobile, tauri::mobile_entry_point)] pub async fn run(_args: Vec) { simple_logger::init_with_env().expect("Unable to initialize the logger"); log::info!( "Running from {}", std::env::current_dir() .unwrap_or_default() .to_str() .unwrap_or_default() ); let tauri = tauri::Builder::default() .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { let _ = app .get_webview_window("main") .expect("No main window") .set_focus(); if args.len() == 2 { deep_link(app.clone(), args); } })) .plugin(tauri_plugin_cli::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { let apph = app.handle(); util::init_dirs(&apph); let mut app_data = AppData::new(app.handle().clone()); let start_immediately; if let Ok(matches) = app.cli().matches() { let start_arg = matches.args.get("start").expect("Invalid argument configuration"); let game_arg = matches.args.get("game").expect("Invalid argument configuration"); let name_arg = matches.args.get("profile").expect("Invalid argument configuration"); log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg); if start_arg.occurrences > 0 { start_immediately = true; app_data.state.remain_open = false; } else { tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into())) .title("STARTLINER") .inner_size(720f64, 480f64) .min_inner_size(720f64, 480f64) .build()?; start_immediately = false; } if game_arg.occurrences == 1 && name_arg.occurrences == 1 { let game = game_arg.value.as_str().unwrap(); let name = name_arg.value.as_str().unwrap(); app_data.switch_profile( Game::from_str(game).ok_or_else(|| anyhow!("Invalid game"))?, name.to_owned() )?; } } else { return Err(anyhow!("Invalid command line arguments").into()); } app.manage(Mutex::new(app_data)); app.deep_link().register_all()?; log::debug!("\n{:?}\n{:?}\n{:?}", util::config_dir(), util::pkg_dir(), util::cache_dir()); tauri::async_runtime::spawn(async { let e = try_join!( fs::create_dir_all(util::config_dir()), fs::create_dir_all(util::pkg_dir()), fs::create_dir_all(util::cache_dir()) ); if let Err(e) = e { log::error!("Unable to create base directories: {}", e); std::process::exit(1); } }); app.listen("download-end", closure!(clone apph, |ev| { log::debug!("download-end triggered: {}", ev.payload()); let raw = ev.payload(); let key = PkgKey(raw[1..raw.len()-1].to_owned()); let apph = apph.clone(); tauri::async_runtime::spawn(async move { let mutex = apph.state::>(); let mut appd = mutex.lock().await; log::debug!("download-end install {:?}", appd.pkgs.install_package(&key, true, false).await); }); })); app.listen("launch-end", closure!(clone apph, |_| { log::debug!("launch-end triggered"); let apph = apph.clone(); tauri::async_runtime::spawn(async move { let mutex = apph.state::>(); let appd = mutex.lock().await; if !appd.state.remain_open { apph.exit(0); } }); })); app.listen("install-end-prelude", closure!(clone apph, |ev| { log::debug!("install-end-prelude triggered: {}", ev.payload()); let payload = serde_json::from_str::(ev.payload()); let apph = apph.clone(); if let Ok(payload) = payload { tauri::async_runtime::spawn(async move { let mutex = apph.state::>(); let mut appd = mutex.lock().await; log::debug!( "install-end-prelude toggle {:?}", appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf) ); use tauri::Emitter; log::debug!("install-end {:?}", apph.emit("install-end", payload)); }); } else { log::error!("install-end-prelude: invalid payload: {}", ev.payload()); } })); if start_immediately == true { let apph = apph.clone(); tauri::async_runtime::spawn(async move { let mtx = apph.state::>(); { let mut appd = mtx.lock().await; if let Err(e) = appd.pkgs.reload_all().await { log::error!("Unable to reload packages: {}", e); apph.exit(1); } } if let Err(e) = cmd::startline(apph.clone(), false).await { log::error!("Unable to launch: {}", e); apph.exit(1); } }); } else { let apph = apph.clone(); tauri::async_runtime::spawn(async move { update(apph).await.unwrap(); }); } Ok(()) }) .invoke_handler(tauri::generate_handler![ cmd::start_check, cmd::startline, cmd::kill, cmd::get_package, cmd::get_all_packages, cmd::get_game_packages, cmd::reload_all_packages, cmd::fetch_listings, cmd::install_package, cmd::delete_package, cmd::toggle_package, cmd::list_profiles, cmd::init_profile, cmd::load_profile, cmd::rename_profile, cmd::duplicate_profile, cmd::delete_profile, cmd::get_current_profile, cmd::sync_current_profile, cmd::save_current_profile, cmd::list_displays, cmd::list_platform_capabilities, cmd::list_directories, ]) .build(tauri::generate_context!()) .expect("error while building tauri application"); tauri.run(move |app, event| { match event { RunEvent::ExitRequested { api, code, .. } => { log::debug!("exit request {:?} {:?}", api, code); if EXIT_REQUESTED.get().is_none() { _= EXIT_REQUESTED.set(()); api.prevent_exit(); let app = app.clone(); tauri::async_runtime::spawn(async move { let mutex = app.state::>(); let appd = mutex.lock().await; if let Some(p) = &appd.profile { log::debug!("save: {:?}", p.save()); app.exit(0); } }); } }, RunEvent::Exit => { log::info!("exit"); } _ => {} } }); } fn deep_link(app: AppHandle, args: Vec) { let url = &args[1]; let proto = "rainycolor://"; if &url[..proto.len()] == proto { log::info!("Deep link: {}", url); let regex = regex::Regex::new( r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/" ).expect("Invalid regex"); if let Some(caps) = regex.captures(url) { if caps.len() == 3 { let app = app.clone(); let key = PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())); tauri::async_runtime::spawn(async move { let mutex = app.state::>(); let mut appd = mutex.lock().await; if appd.pkgs.is_offline() { log::warn!("Deep link installation failed: offline"); } else if let Err(e) = appd.pkgs.install_package(&key, true, true).await { log::warn!("Deep link installation failed: {}", e.to_string()); } }); } } } } async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> { if let Some(update) = app.updater()?.check().await? { let mut downloaded = 0; update .download_and_install( |chunk_length, content_length| { downloaded += chunk_length; log::debug!("downloaded {downloaded} from {content_length:?}"); }, || { log::info!("download finished"); }, ) .await?; log::info!("update installed"); app.restart(); } Ok(()) }