mod cmd; mod model; mod pkg; mod pkg_store; mod util; mod download_handler; mod appdata; mod modules; mod profiles; mod patcher; 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, Emitter, Listener, Manager, RunEvent}; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_cli::CliExt; 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) { 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()); log::info!( "running from {}", std::env::current_dir() .unwrap_or_default() .to_str() .unwrap_or_default() ); 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; } else { open_window(apph.clone())?; 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| { let raw = ev.payload(); log::debug!("download-end triggered: {}", raw); 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; let res = appd.pkgs.install_package(&key, true, false).await; log::debug!("download-end install {:?}", res); }); })); 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| { let payload = serde_json::from_str::(ev.payload()); log::debug!("install-end-prelude triggered: {:?}", 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; let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf); log::debug!( "install-end-prelude toggle {:?}", res ); use tauri::Emitter; let res = apph.emit("install-end", payload); log::debug!("install-end {:?}", res); }); } 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); _ = open_window(apph.clone()); // stupid but effective std::thread::sleep(std::time::Duration::from_secs(3)); _ = apph.emit("launch-error", e.to_string()); } else { let mut appd = mtx.lock().await; appd.state.remain_open = false; log::info!("started quietly"); } }); } 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::load_segatools_ini, cmd::create_shortcut, cmd::get_global_config, cmd::set_global_config, cmd::list_displays, cmd::list_platform_capabilities, cmd::list_directories, cmd::file_exists, cmd::open_file, cmd::get_changelog, cmd::list_com_ports, cmd::list_patches, ]) .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 { let res = p.save(); log::debug!("save: {:?}", res); 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<()> { let mutex = app.state::>(); { let appd = mutex.lock().await; if !appd.cfg.enable_autoupdates { log::info!("skipping auto-update"); return Ok(()); } } #[cfg(not(debug_assertions))] { use tauri_plugin_updater::UpdaterExt; use tauri::Emitter; if let Some(update) = app.updater()?.check().await? { let mut downloaded = 0; update.download_and_install( |chunk_length, content_length| { downloaded += chunk_length; _ = app.emit("update-progress", (downloaded as f64) / (content_length.unwrap_or(u64::MAX) as f64)); }, || { log::info!("download finished"); }, ) .await?; log::info!("update installed"); app.restart(); } } // One day I will write proper tests // #[cfg(debug_assertions)] // { // use tauri::Emitter; // std::thread::sleep(std::time::Duration::from_millis(5000)); // let mut downloaded = 0; // while downloaded < 500 { // std::thread::sleep(std::time::Duration::from_millis(10)); // downloaded += 1; // _ = app.emit("update-progress", (downloaded as f32) / 500f32); // } // app.restart(); // } log::info!("ending auto-update check"); Ok(()) } fn open_window(apph: AppHandle) -> anyhow::Result<()> { let config = apph.config().clone(); tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into())) .title(format!("STARTLINER {}", config.version.unwrap_or_default())) .inner_size(900f64, 600f64) .min_inner_size(900f64, 600f64) .build()?; Ok(()) }