//! Theseus state management system use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf, sync::Arc}; use tokio::sync::OnceCell; use tokio::sync::RwLock; use uuid::Uuid; use crate::state::SafeProcesses; pub mod emit; // Global event state // Stores the Tauri app handle and other event-related state variables static EVENT_STATE: OnceCell> = OnceCell::const_new(); pub struct EventState { /// Tauri app #[cfg(feature = "tauri")] pub app: tauri::AppHandle, pub loading_bars: RwLock>, } impl EventState { #[cfg(feature = "tauri")] pub async fn init(app: tauri::AppHandle) -> crate::Result> { EVENT_STATE .get_or_try_init(|| async { Ok(Arc::new(Self { app, loading_bars: RwLock::new(HashMap::new()), })) }) .await .map(Arc::clone) } #[cfg(not(feature = "tauri"))] pub async fn init() -> crate::Result> { EVENT_STATE .get_or_try_init(|| async { Ok(Arc::new(Self { loading_bars: RwLock::new(HashMap::new()), })) }) .await .map(Arc::clone) } #[cfg(feature = "tauri")] pub async fn get() -> crate::Result> { Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone()) } // Initialization requires no app handle in non-tauri mode, so we can just use the same function #[cfg(not(feature = "tauri"))] pub async fn get() -> crate::Result> { Self::init().await } // Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date pub async fn list_progress_bars() -> crate::Result> { let value = Self::get().await?; let read = value.loading_bars.read().await; let mut display_list: HashMap = HashMap::new(); for (uuid, loading_bar) in read.iter() { display_list.insert(*uuid, loading_bar.clone()); } Ok(display_list) } #[cfg(feature = "tauri")] pub async fn get_main_window() -> crate::Result> { use tauri::Manager; let value = Self::get().await?; Ok(value.app.get_window("main")) } } #[derive(Serialize, Debug, Clone)] pub struct LoadingBar { // loading_bar_uuid not be used directly by external functions as it may not reflect the current state of the loading bar/hashmap pub loading_bar_uuid: Uuid, pub message: String, pub total: f64, pub current: f64, #[serde(skip)] pub last_sent: f64, pub bar_type: LoadingBarType, #[cfg(feature = "cli")] #[serde(skip)] pub cli_progress_bar: indicatif::ProgressBar, } #[derive(Serialize, Debug, Clone)] pub struct LoadingBarId(Uuid); // When Loading bar id is dropped, we should remove it from the hashmap impl Drop for LoadingBarId { fn drop(&mut self) { let loader_uuid = self.0; tokio::spawn(async move { if let Ok(event_state) = EventState::get().await { let mut bars = event_state.loading_bars.write().await; #[cfg(any(feature = "tauri", feature = "cli"))] if let Some(bar) = bars.remove(&loader_uuid) { #[cfg(feature = "tauri")] { let loader_uuid = bar.loading_bar_uuid; let event = bar.bar_type.clone(); let fraction = bar.current / bar.total; use tauri::Manager; let _ = event_state.app.emit_all( "loading", LoadingPayload { fraction: None, message: "Completed".to_string(), event, loader_uuid, }, ); tracing::trace!( "Exited at {fraction} for loading bar: {:?}", loader_uuid ); } // Emit event to indicatif progress bar arc #[cfg(feature = "cli")] { let cli_progress_bar = bar.cli_progress_bar; cli_progress_bar.finish(); } } #[cfg(not(any(feature = "tauri", feature = "cli")))] bars.remove(&loader_uuid); } let _ = SafeProcesses::complete( crate::state::ProcessType::LoadingBar, loader_uuid, ) .await; }); } } #[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum LoadingBarType { StateInit, JavaDownload { version: u32, }, PackFileDownload { profile_path: PathBuf, pack_name: String, icon: Option, pack_version: String, }, PackDownload { profile_path: PathBuf, pack_name: String, icon: Option, pack_id: Option, pack_version: Option, }, MinecraftDownload { profile_path: PathBuf, profile_name: String, }, ProfileUpdate { profile_path: PathBuf, profile_name: String, }, ZipExtract { profile_path: PathBuf, profile_name: String, }, ConfigChange { new_path: PathBuf, }, } #[derive(Serialize, Clone)] pub struct LoadingPayload { pub event: LoadingBarType, pub loader_uuid: Uuid, pub fraction: Option, // by convention, if optional, it means the loading is done pub message: String, } #[derive(Serialize, Clone)] pub struct OfflinePayload { pub offline: bool, } #[derive(Serialize, Clone)] pub struct WarningPayload { pub message: String, } #[derive(Serialize, Clone)] #[serde(tag = "event")] pub enum CommandPayload { InstallMod { id: String, }, InstallVersion { id: String, }, InstallModpack { id: String, }, RunMRPack { // run or install .mrpack path: PathBuf, }, } #[derive(Serialize, Clone)] pub struct ProcessPayload { pub uuid: Uuid, // processes in state are going to be identified by UUIDs, as they might change to different processes pub pid: u32, pub event: ProcessPayloadType, pub message: String, } #[derive(Serialize, Clone, Debug)] #[serde(rename_all = "snake_case")] pub enum ProcessPayloadType { Launched, Updated, // eg: if the MinecraftChild changes to its post-command process instead of the Minecraft process Finished, } #[derive(Serialize, Clone)] pub struct ProfilePayload { pub uuid: Uuid, pub path: PathBuf, pub name: String, pub event: ProfilePayloadType, } #[derive(Serialize, Clone)] #[serde(rename_all = "snake_case")] pub enum ProfilePayloadType { Created, Added, // also triggered when Created Synced, Edited, Removed, } #[derive(Debug, thiserror::Error)] pub enum EventError { #[error("Event state was not properly initialized")] NotInitialized, #[error("Non-existent loading bar of key: {0}")] NoLoadingBar(Uuid), #[cfg(feature = "tauri")] #[error("Tauri error: {0}")] TauriError(#[from] tauri::Error), }