use std::path::PathBuf; use std::sync::Arc; use color_eyre::Report; use druid::im::Vector; use druid::{ AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target, WindowHandle, WindowId, }; use tokio::sync::mpsc::UnboundedSender; use crate::ui::window; use crate::util::ansi::ansi_to_rich_text; use crate::util::config::Config; use super::{ModInfo, State}; pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); pub(crate) const ACTION_START_DELETE_SELECTED_MOD: Selector>> = Selector::new("dtmm.action.srart-delete-selected-mod"); pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector>> = Selector::new("dtmm.action.finish-delete-selected-mod"); pub(crate) const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); pub(crate) const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); pub(crate) const ACTION_START_RESET_DEPLOYMENT: Selector = Selector::new("dtmm.action.start-reset-deployment"); pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector = Selector::new("dtmm.action.finish-reset-deployment"); pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); pub(crate) const ACTION_FINISH_ADD_MOD: Selector>> = Selector::new("dtmm.action.finish-add-mod"); pub(crate) const ACTION_LOG: Selector>> = Selector::new("dtmm.action.log"); pub(crate) const ACTION_START_SAVE_SETTINGS: Selector = Selector::new("dtmm.action.start-save-settings"); pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector = Selector::new("dtmm.action.finish-save-settings"); pub(crate) const ACTION_START_CHECK_UPDATE: Selector = Selector::new("dtmm.action.start-check-update"); pub(crate) const ACTION_FINISH_CHECK_UPDATE: Selector>> = Selector::new("dtmm.action.finish-check-update"); pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty"); pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector> = Selector::new("dtmm.action.show-error-dialog"); pub(crate) const ACTION_SET_WINDOW_HANDLE: Selector> = Selector::new("dtmm.action.set-window-handle"); pub(crate) type InitialLoadResult = (Config, Vector>); pub(crate) const ACTION_FINISH_LOAD_INITIAL: Selector>> = Selector::new("dtmm.action.finish-load-initial"); // A sub-selection of `State`'s fields that are required in `AsyncAction`s and that are // `Send + Sync` pub(crate) struct ActionState { pub mods: Vector>, pub game_dir: Arc, pub data_dir: Arc, pub mod_dir: Arc, pub config_path: Arc, pub ctx: Arc, pub nexus_api_key: Arc, pub is_io_enabled: bool, } impl From for ActionState { fn from(state: State) -> Self { Self { mods: state.mods, game_dir: state.game_dir, mod_dir: Arc::new(state.data_dir.join("mods")), data_dir: state.data_dir, config_path: state.config_path, ctx: state.ctx, nexus_api_key: state.nexus_api_key, is_io_enabled: state.is_io_enabled, } } } pub(crate) enum AsyncAction { DeployMods(ActionState), ResetDeployment(ActionState), AddMod(ActionState, FileInfo), DeleteMod(ActionState, Arc), SaveSettings(ActionState), CheckUpdates(ActionState), LoadInitial((PathBuf, bool)), Log((ActionState, Vec)), } impl std::fmt::Debug for AsyncAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AsyncAction::DeployMods(_) => write!(f, "AsyncAction::DeployMods(_state)"), AsyncAction::ResetDeployment(_) => write!(f, "AsyncAction::ResetDeployment(_state)"), AsyncAction::AddMod(_, info) => write!(f, "AsyncAction::AddMod(_state, {:?})", info), AsyncAction::DeleteMod(_, info) => { write!(f, "AsyncAction::DeleteMod(_state, {:?})", info) } AsyncAction::SaveSettings(_) => write!(f, "AsyncAction::SaveSettings(_state)"), AsyncAction::CheckUpdates(_) => write!(f, "AsyncAction::CheckUpdates(_state)"), AsyncAction::LoadInitial((path, is_default)) => write!( f, "AsyncAction::LoadInitial(({:?}, {:?}))", path, is_default ), AsyncAction::Log(_) => write!(f, "AsyncAction::Log(_)"), } } } pub(crate) struct Delegate { sender: UnboundedSender, } impl Delegate { pub fn new(sender: UnboundedSender) -> Self { Self { sender } } } impl AppDelegate for Delegate { #[tracing::instrument(name = "Delegate", skip_all)] fn command( &mut self, ctx: &mut DelegateCtx, _target: Target, cmd: &Command, state: &mut State, _env: &Env, ) -> Handled { if cfg!(debug_assertions) && !cmd.is(ACTION_LOG) { tracing::trace!(?cmd); } match cmd { cmd if cmd.is(ACTION_START_DEPLOY) => { if self .sender .send(AsyncAction::DeployMods(state.clone().into())) .is_ok() { state.is_deployment_in_progress = true; } else { tracing::error!("Failed to queue action to deploy mods"); } Handled::Yes } cmd if cmd.is(ACTION_FINISH_DEPLOY) => { state.is_deployment_in_progress = false; state.dirty = false; Handled::Yes } cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => { if self .sender .send(AsyncAction::ResetDeployment(state.clone().into())) .is_ok() { state.is_reset_in_progress = true; } else { tracing::error!("Failed to queue action to reset mod deployment"); } Handled::Yes } cmd if cmd.is(ACTION_FINISH_RESET_DEPLOYMENT) => { state.is_reset_in_progress = false; Handled::Yes } cmd if cmd.is(ACTION_SELECT_MOD) => { let index = cmd .get(ACTION_SELECT_MOD) .expect("command type matched but didn't contain the expected value"); state.select_mod(*index); // ctx.submit_command(ACTION_START_SAVE_SETTINGS); Handled::Yes } cmd if cmd.is(ACTION_SELECTED_MOD_UP) => { let Some(i) = state.selected_mod_index else { return Handled::No; }; let len = state.mods.len(); if len == 0 || i == 0 { return Handled::No; } state.mods.swap(i, i - 1); state.selected_mod_index = Some(i - 1); // ctx.submit_command(ACTION_START_SAVE_SETTINGS); Handled::Yes } cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => { let Some(i) = state.selected_mod_index else { return Handled::No; }; let len = state.mods.len(); if len == 0 || i == usize::MAX || i >= len - 1 { return Handled::No; } state.mods.swap(i, i + 1); state.selected_mod_index = Some(i + 1); // ctx.submit_command(ACTION_START_SAVE_SETTINGS); Handled::Yes } cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { let info = cmd .get(ACTION_START_DELETE_SELECTED_MOD) .and_then(SingleUse::take) .expect("command type matched but didn't contain the expected value"); if self .sender .send(AsyncAction::DeleteMod(state.clone().into(), info)) .is_err() { tracing::error!("Failed to queue action to deploy mods"); } Handled::Yes } cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => { let info = cmd .get(ACTION_FINISH_DELETE_SELECTED_MOD) .and_then(SingleUse::take) .expect("command type matched but didn't contain the expected value"); let found = state.mods.iter().enumerate().find(|(_, i)| i.id == info.id); let Some((index, _)) = found else { return Handled::No; }; state.mods.remove(index); Handled::Yes } cmd if cmd.is(ACTION_ADD_MOD) => { let info = cmd .get(ACTION_ADD_MOD) .expect("command type matched but didn't contain the expected value"); if self .sender .send(AsyncAction::AddMod(state.clone().into(), info.clone())) .is_err() { tracing::error!("Failed to queue action to add mod"); } Handled::Yes } cmd if cmd.is(ACTION_FINISH_ADD_MOD) => { let info = cmd .get(ACTION_FINISH_ADD_MOD) .expect("command type matched but didn't contain the expected value"); if let Some(info) = info.take() { state.add_mod(info); } Handled::Yes } cmd if cmd.is(ACTION_LOG) => { let line = cmd .get(ACTION_LOG) .expect("command type matched but didn't contain the expected value"); if let Some(line) = line.take() { { let line = String::from_utf8_lossy(&line); state.log.push_back(ansi_to_rich_text(line.trim())); } if self .sender .send(AsyncAction::Log((state.clone().into(), line))) .is_err() { tracing::error!("Failed to queue action to add mod"); } } Handled::Yes } cmd if cmd.is(ACTION_START_SAVE_SETTINGS) => { if state.is_save_in_progress { state.is_next_save_pending = true; } else if self .sender .send(AsyncAction::SaveSettings(state.clone().into())) .is_ok() { state.is_save_in_progress = true; } else { tracing::error!("Failed to queue action to save settings"); } Handled::Yes } cmd if cmd.is(ACTION_FINISH_SAVE_SETTINGS) => { tracing::trace!( in_progress = state.is_save_in_progress, next_pending = state.is_next_save_pending, "Finished saving settings", ); state.is_save_in_progress = false; if state.is_next_save_pending { state.is_next_save_pending = false; ctx.submit_command(ACTION_START_SAVE_SETTINGS); } Handled::Yes } cmd if cmd.is(ACTION_SET_DIRTY) => { state.dirty = true; Handled::Yes } cmd if cmd.is(ACTION_SHOW_ERROR_DIALOG) => { let err = cmd .get(ACTION_SHOW_ERROR_DIALOG) .and_then(SingleUse::take) .expect("command type matched but didn't contain the expected value"); let window = state .windows .get(&window::main::WINDOW_ID) .expect("root window does not exist"); let dialog = window::dialog::error::(err, window.clone()); ctx.new_window(dialog); Handled::Yes } cmd if cmd.is(ACTION_SET_WINDOW_HANDLE) => { let (id, handle) = cmd .get(ACTION_SET_WINDOW_HANDLE) .and_then(SingleUse::take) .expect("command type matched but didn't contain the expected value"); state.windows.insert(id, handle); Handled::Yes } cmd if cmd.is(ACTION_START_CHECK_UPDATE) => { if self .sender .send(AsyncAction::CheckUpdates(state.clone().into())) .is_ok() { state.is_update_in_progress = true; } else { tracing::error!("Failed to queue action to check updates"); } Handled::Yes } cmd if cmd.is(ACTION_FINISH_CHECK_UPDATE) => { let mut updates = cmd .get(ACTION_FINISH_CHECK_UPDATE) .and_then(SingleUse::take) .expect("command type matched but didn't contain the expected value"); if tracing::enabled!(tracing::Level::DEBUG) { let mods: Vec<_> = updates .iter() .map(|info| { format!( "{}: {} -> {:?}", info.name, info.version, info.nexus.as_ref().map(|n| &n.version) ) }) .collect(); tracing::info!("Mod updates:\n{}", mods.join("\n")); } for mod_info in state.mods.iter_mut() { if let Some(index) = updates.iter().position(|i2| i2.id == mod_info.id) { let update = updates.swap_remove(index); *mod_info = Arc::new(update); } } state.is_update_in_progress = false; Handled::Yes } cmd if cmd.is(ACTION_FINISH_LOAD_INITIAL) => { let data = cmd .get(ACTION_FINISH_LOAD_INITIAL) .and_then(SingleUse::take) .expect("command type matched but didn't contain the expected value"); if let Some((config, mods)) = data { state.mods = mods; state.config_path = Arc::new(config.path); state.data_dir = Arc::new(config.data_dir); state.game_dir = Arc::new(config.game_dir.unwrap_or_default()); state.is_io_enabled = config.unsafe_io; } state.loading = false; Handled::Yes } _ => Handled::No, } } fn window_added( &mut self, id: WindowId, handle: WindowHandle, data: &mut State, _: &Env, _: &mut DelegateCtx, ) { data.windows.insert(id, handle); } fn window_removed(&mut self, id: WindowId, data: &mut State, _: &Env, _: &mut DelegateCtx) { data.windows.remove(&id); } }