//! Tauri commands: thin wrappers over `Launcher` (see docs/CONTRACT.md). use tauri::{AppHandle, State}; use tauri_plugin_dialog::DialogExt; use tauri_plugin_opener::OpenerExt; use crate::launcher::{Launcher, Market, Status}; use crate::market; use crate::settings::Settings; use crate::stack::StackEvent; use crate::sync::Conflict; type CmdResult = Result; #[derive(serde::Serialize)] pub struct SignedIn { username: String, } #[tauri::command] pub async fn release_plan(launcher: State<'_, Launcher>) -> CmdResult { Ok(launcher.release_plan().await) } #[tauri::command] pub async fn release_run( launcher: State<'_, Launcher>, password: String, with_api: bool, with_mcp: bool, with_web: bool, ) -> CmdResult<()> { launcher.release_run(password, with_api, with_mcp, with_web).await } #[tauri::command] pub async fn refresh_run( launcher: State<'_, Launcher>, password: String, pg_password: String, tile_root: String, ) -> CmdResult<()> { launcher.refresh_run(password, pg_password, tile_root).await } #[tauri::command] pub fn release_console(launcher: State<'_, Launcher>, since: usize) -> CmdResult { Ok(launcher.release_console(since)) } #[tauri::command] pub async fn get_status(launcher: State<'_, Launcher>) -> CmdResult { Ok(launcher.status().await) } /// The marketplace: every tile set and source file of the version, required ones first. With /// `drive`, what that drive can install instead of the DHP. #[tauri::command] pub async fn get_market(app: AppHandle, launcher: State<'_, Launcher>, drive: Option) -> CmdResult { launcher.market(&app, drive.map(std::path::PathBuf::from)).await } /// Choose the optional items (their keys) for an install about to run, without touching what /// is installed. `drive` names the drive whose items these are. #[tauri::command] pub async fn set_market( app: AppHandle, launcher: State<'_, Launcher>, selected: Vec, drive: Option, ) -> CmdResult<()> { launcher.choose_market(&app, selected, drive.map(std::path::PathBuf::from)).await } /// Choose the optional items (their keys) and apply the choice; see `Launcher::apply_market`. #[tauri::command] pub async fn finish_market(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> { launcher.finish_market(&app).await } #[tauri::command] pub async fn check_market(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult> { launcher.check_market(&app).await } #[tauri::command] pub async fn apply_market(app: AppHandle, launcher: State<'_, Launcher>, selected: Vec) -> CmdResult<()> { launcher.apply_market(&app, selected).await } #[tauri::command] pub async fn refresh(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult { Ok(launcher.refresh(&app).await) } #[tauri::command] pub async fn sign_in(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult { launcher.sign_in(&app).await.map(|username| SignedIn { username }) } #[tauri::command] pub async fn cancel_sign_in(launcher: State<'_, Launcher>) -> CmdResult<()> { launcher.cancel_sign_in(); Ok(()) } #[tauri::command] pub async fn sign_out(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult { Ok(launcher.sign_out(&app).await) } #[tauri::command] pub async fn get_notes(launcher: State<'_, Launcher>, version: Option) -> CmdResult { launcher.notes(version).await } /// Links in patch notes open in the system browser; the launcher window never navigates. #[tauri::command] pub async fn open_external(app: AppHandle, url: String) -> CmdResult<()> { let parsed = url::Url::parse(&url).map_err(|_| "That link is not a valid address.".to_string())?; if !matches!(parsed.scheme(), "http" | "https") { return Err("Only web links can be opened.".to_string()); } app .opener() .open_url(parsed.as_str(), None::<&str>) .map_err(|e| format!("cannot open the browser: {e}")) } #[tauri::command] pub async fn start_install(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> { launcher.start_install(&app, None).await } #[tauri::command] pub async fn start_install_from_drive(app: AppHandle, launcher: State<'_, Launcher>, path: String) -> CmdResult<()> { launcher.start_install(&app, Some(std::path::PathBuf::from(path))).await } #[tauri::command] pub async fn cancel_install(launcher: State<'_, Launcher>) -> CmdResult<()> { launcher.cancel_install() } #[tauri::command] pub async fn launch(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> { launcher.launch(&app).await } #[tauri::command] pub async fn stop(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> { launcher.stop(&app).await } #[tauri::command] pub async fn get_stack(launcher: State<'_, Launcher>) -> CmdResult> { Ok(launcher.stack_states()) } #[tauri::command] pub async fn sync_tiles(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> { launcher.sync_tiles(&app).await } #[tauri::command] pub async fn sync_now(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> { launcher.sync_now(&app).await } #[tauri::command] pub async fn get_sync_conflicts(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult> { launcher.sync_conflicts(&app).await } /// `choice`: "theirs", "mine", "value" (with `value`) or "dismiss" (refused edits only). #[tauri::command] pub async fn resolve_sync_conflict( app: AppHandle, launcher: State<'_, Launcher>, tbl: String, key: serde_json::Value, choice: String, value: Option, ) -> CmdResult> { launcher.resolve_sync_conflict(&app, tbl, key, choice, value).await } #[tauri::command] pub async fn get_settings(launcher: State<'_, Launcher>) -> CmdResult { Ok(launcher.settings().await) } #[tauri::command] pub async fn set_settings(app: AppHandle, launcher: State<'_, Launcher>, settings: Settings) -> CmdResult { launcher.set_settings(&app, settings).await } #[tauri::command] pub async fn pick_folder(app: AppHandle) -> CmdResult> { // The native dialog blocks its thread, so keep it off the async runtime's workers. tauri::async_runtime::spawn_blocking(move || { app .dialog() .file() .blocking_pick_folder() .and_then(|picked| picked.into_path().ok()) .map(|path| path.display().to_string()) }) .await .map_err(|e| e.to_string()) }