Merge pull request 'feat(dtmm): Delay initial load' (#76) from issue/72 into master
Reviewed-on: #76
This commit is contained in:
commit
995e6bf92d
7 changed files with 183 additions and 113 deletions
|
@ -1,6 +1,6 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{Cursor, ErrorKind, Read};
|
use std::io::{Cursor, ErrorKind, Read};
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
|
@ -10,12 +10,12 @@ use druid::{FileInfo, ImageBuf};
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
use nexusmods::Api as NexusApi;
|
use nexusmods::Api as NexusApi;
|
||||||
use tokio::fs::{self, DirEntry};
|
use tokio::fs::{self, DirEntry};
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
use crate::state::{ActionState, ModInfo, ModOrder, NexusInfo, PackageInfo};
|
use crate::state::{ActionState, InitialLoadResult, ModInfo, ModOrder, NexusInfo, PackageInfo};
|
||||||
|
use crate::util;
|
||||||
use crate::util::config::{ConfigSerialize, LoadOrderEntry};
|
use crate::util::config::{ConfigSerialize, LoadOrderEntry};
|
||||||
|
|
||||||
use super::read_sjson_file;
|
use super::read_sjson_file;
|
||||||
|
@ -198,7 +198,7 @@ pub(crate) async fn save_settings(state: ActionState) -> Result<()> {
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| {
|
.wrap_err_with(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write config to '{}'",
|
"Failed to write config to '{}'",
|
||||||
state.config_path.display()
|
state.config_path.display()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -269,51 +269,47 @@ async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(mod_order))]
|
#[tracing::instrument(skip(mod_order))]
|
||||||
pub(crate) fn load_mods<'a, P, S>(mod_dir: P, mod_order: S) -> Result<Vector<Arc<ModInfo>>>
|
pub(crate) async fn load_mods<'a, P, S>(mod_dir: P, mod_order: S) -> Result<Vector<Arc<ModInfo>>>
|
||||||
where
|
where
|
||||||
S: Iterator<Item = &'a LoadOrderEntry>,
|
S: Iterator<Item = &'a LoadOrderEntry>,
|
||||||
P: AsRef<Path> + std::fmt::Debug,
|
P: AsRef<Path> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let rt = Runtime::new()?;
|
let mod_dir = mod_dir.as_ref();
|
||||||
|
let read_dir = match fs::read_dir(mod_dir).await {
|
||||||
rt.block_on(async move {
|
Ok(read_dir) => read_dir,
|
||||||
let mod_dir = mod_dir.as_ref();
|
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||||
let read_dir = match fs::read_dir(mod_dir).await {
|
return Ok(Vector::new());
|
||||||
Ok(read_dir) => read_dir,
|
|
||||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
|
||||||
return Ok(Vector::new());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return Err(err)
|
|
||||||
.wrap_err_with(|| format!("Failed to open directory '{}'", mod_dir.display()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let stream = ReadDirStream::new(read_dir)
|
|
||||||
.map(|res| res.wrap_err("Failed to read dir entry"))
|
|
||||||
.then(read_mod_dir_entry);
|
|
||||||
tokio::pin!(stream);
|
|
||||||
|
|
||||||
let mut mods: HashMap<String, ModInfo> = HashMap::new();
|
|
||||||
|
|
||||||
while let Some(res) = stream.next().await {
|
|
||||||
let info = res?;
|
|
||||||
mods.insert(info.id.clone(), info);
|
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err)
|
||||||
|
.wrap_err_with(|| format!("Failed to open directory '{}'", mod_dir.display()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mods = mod_order
|
let stream = ReadDirStream::new(read_dir)
|
||||||
.filter_map(|entry| {
|
.map(|res| res.wrap_err("Failed to read dir entry"))
|
||||||
if let Some(mut info) = mods.remove(&entry.id) {
|
.then(read_mod_dir_entry);
|
||||||
info.enabled = entry.enabled;
|
tokio::pin!(stream);
|
||||||
Some(Arc::new(info))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok::<_, color_eyre::Report>(mods)
|
let mut mods: HashMap<String, ModInfo> = HashMap::new();
|
||||||
})
|
|
||||||
|
while let Some(res) = stream.next().await {
|
||||||
|
let info = res?;
|
||||||
|
mods.insert(info.id.clone(), info);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mods = mod_order
|
||||||
|
.filter_map(|entry| {
|
||||||
|
if let Some(mut info) = mods.remove(&entry.id) {
|
||||||
|
info.enabled = entry.enabled;
|
||||||
|
Some(Arc::new(info))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn check_mod_order(state: &ActionState) -> Result<()> {
|
pub(crate) fn check_mod_order(state: &ActionState) -> Result<()> {
|
||||||
|
@ -422,3 +418,26 @@ pub(crate) async fn check_updates(state: ActionState) -> Result<Vec<ModInfo>> {
|
||||||
.collect();
|
.collect();
|
||||||
Ok(updates)
|
Ok(updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn load_initial(path: PathBuf, is_default: bool) -> Result<InitialLoadResult> {
|
||||||
|
let config = util::config::read_config(path, is_default)
|
||||||
|
.await
|
||||||
|
.wrap_err("Failed to read config file")?;
|
||||||
|
|
||||||
|
let game_info = tokio::task::spawn_blocking(dtmt_shared::collect_game_info)
|
||||||
|
.await
|
||||||
|
.wrap_err("Failed to collect Steam game info")?;
|
||||||
|
|
||||||
|
{
|
||||||
|
if config.game_dir.is_none() && game_info.is_none() {
|
||||||
|
tracing::error!("No Game Directory set. Head to the 'Settings' tab to set it manually",);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod_dir = config.data_dir.join("mods");
|
||||||
|
let mods = load_mods(mod_dir, config.mod_order.iter())
|
||||||
|
.await
|
||||||
|
.wrap_err("Failed to load mods")?;
|
||||||
|
|
||||||
|
Ok((config, mods))
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ use crate::controller::app::*;
|
||||||
use crate::controller::game::*;
|
use crate::controller::game::*;
|
||||||
use crate::state::AsyncAction;
|
use crate::state::AsyncAction;
|
||||||
use crate::state::ACTION_FINISH_CHECK_UPDATE;
|
use crate::state::ACTION_FINISH_CHECK_UPDATE;
|
||||||
|
use crate::state::ACTION_FINISH_LOAD_INITIAL;
|
||||||
use crate::state::ACTION_FINISH_SAVE_SETTINGS;
|
use crate::state::ACTION_FINISH_SAVE_SETTINGS;
|
||||||
use crate::state::ACTION_SHOW_ERROR_DIALOG;
|
use crate::state::ACTION_SHOW_ERROR_DIALOG;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
|
@ -144,6 +145,29 @@ async fn handle_action(
|
||||||
)
|
)
|
||||||
.expect("failed to send command");
|
.expect("failed to send command");
|
||||||
}),
|
}),
|
||||||
|
AsyncAction::LoadInitial((path, is_default)) => tokio::spawn(async move {
|
||||||
|
let data = match load_initial(path, is_default)
|
||||||
|
.await
|
||||||
|
.wrap_err("Failed to load initial application data")
|
||||||
|
{
|
||||||
|
Ok(data) => Some(data),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("{:?}", err);
|
||||||
|
send_error(event_sink.clone(), err).await;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
event_sink
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.submit_command(
|
||||||
|
ACTION_FINISH_LOAD_INITIAL,
|
||||||
|
SingleUse::new(data),
|
||||||
|
Target::Auto,
|
||||||
|
)
|
||||||
|
.expect("failed to send command");
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,19 +7,16 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::command;
|
use clap::command;
|
||||||
|
use clap::parser::ValueSource;
|
||||||
use clap::value_parser;
|
use clap::value_parser;
|
||||||
use clap::Arg;
|
use clap::Arg;
|
||||||
use color_eyre::eyre;
|
use color_eyre::eyre;
|
||||||
use color_eyre::eyre::Context;
|
|
||||||
use color_eyre::{Report, Result};
|
use color_eyre::{Report, Result};
|
||||||
use druid::AppLauncher;
|
use druid::AppLauncher;
|
||||||
use druid::SingleUse;
|
|
||||||
use druid::Target;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::controller::app::load_mods;
|
|
||||||
use crate::controller::worker::work_thread;
|
use crate::controller::worker::work_thread;
|
||||||
use crate::state::ACTION_SHOW_ERROR_DIALOG;
|
use crate::state::AsyncAction;
|
||||||
use crate::state::{Delegate, State};
|
use crate::state::{Delegate, State};
|
||||||
use crate::ui::theme;
|
use crate::ui::theme;
|
||||||
|
|
||||||
|
@ -64,41 +61,53 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let delegate = Delegate::new(action_tx);
|
|
||||||
|
// let config = util::config::read_config(&default_config_path, &matches)
|
||||||
|
// .wrap_err("Failed to read config file")?;
|
||||||
|
// let game_info = dtmt_shared::collect_game_info();
|
||||||
|
|
||||||
|
// tracing::debug!(?config, ?game_info);
|
||||||
|
|
||||||
|
// let game_dir = config.game_dir.or_else(|| game_info.map(|i| i.path));
|
||||||
|
// if game_dir.is_none() {
|
||||||
|
// let err =
|
||||||
|
// eyre::eyre!("No Game Directory set. Head to the 'Settings' tab to set it manually",);
|
||||||
|
// event_sink
|
||||||
|
// .submit_command(ACTION_SHOW_ERROR_DIALOG, SingleUse::new(err), Target::Auto)
|
||||||
|
// .expect("failed to send command");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let initial_state = {
|
||||||
|
// let mut state = State::new(
|
||||||
|
// config.path,
|
||||||
|
// game_dir.unwrap_or_default(),
|
||||||
|
// config.data_dir.unwrap_or_default(),
|
||||||
|
// config.nexus_api_key.unwrap_or_default(),
|
||||||
|
// );
|
||||||
|
// state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter())
|
||||||
|
// .wrap_err("Failed to load mods")?;
|
||||||
|
// state
|
||||||
|
// };
|
||||||
|
|
||||||
|
let config_path = matches
|
||||||
|
.get_one::<PathBuf>("config")
|
||||||
|
.cloned()
|
||||||
|
.expect("argument has default value");
|
||||||
|
let is_config_default = matches.value_source("config") != Some(ValueSource::DefaultValue);
|
||||||
|
if action_tx
|
||||||
|
.send(AsyncAction::LoadInitial((config_path, is_config_default)))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
let err = eyre::eyre!("Failed to send action");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
let launcher = AppLauncher::with_window(ui::window::main::new())
|
let launcher = AppLauncher::with_window(ui::window::main::new())
|
||||||
.delegate(delegate)
|
.delegate(Delegate::new(action_tx))
|
||||||
.configure_env(theme::set_theme_env);
|
.configure_env(theme::set_theme_env);
|
||||||
|
|
||||||
let event_sink = launcher.get_external_handle();
|
let event_sink = launcher.get_external_handle();
|
||||||
|
|
||||||
let config = util::config::read_config(&default_config_path, &matches)
|
|
||||||
.wrap_err("Failed to read config file")?;
|
|
||||||
let game_info = dtmt_shared::collect_game_info();
|
|
||||||
|
|
||||||
tracing::debug!(?config, ?game_info);
|
|
||||||
|
|
||||||
let game_dir = config.game_dir.or_else(|| game_info.map(|i| i.path));
|
|
||||||
if game_dir.is_none() {
|
|
||||||
let err =
|
|
||||||
eyre::eyre!("No Game Directory set. Head to the 'Settings' tab to set it manually",);
|
|
||||||
event_sink
|
|
||||||
.submit_command(ACTION_SHOW_ERROR_DIALOG, SingleUse::new(err), Target::Auto)
|
|
||||||
.expect("failed to send command");
|
|
||||||
}
|
|
||||||
|
|
||||||
let initial_state = {
|
|
||||||
let mut state = State::new(
|
|
||||||
config.path,
|
|
||||||
game_dir.unwrap_or_default(),
|
|
||||||
config.data_dir.unwrap_or_default(),
|
|
||||||
config.nexus_api_key.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter())
|
|
||||||
.wrap_err("Failed to load mods")?;
|
|
||||||
state
|
|
||||||
};
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let event_sink = Arc::new(RwLock::new(event_sink));
|
let event_sink = Arc::new(RwLock::new(event_sink));
|
||||||
let action_rx = Arc::new(RwLock::new(action_rx));
|
let action_rx = Arc::new(RwLock::new(action_rx));
|
||||||
|
@ -110,5 +119,5 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
launcher.launch(initial_state).map_err(Report::new)
|
launcher.launch(State::new()).map_err(Report::new)
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,6 +158,8 @@ pub(crate) struct State {
|
||||||
|
|
||||||
#[data(ignore)]
|
#[data(ignore)]
|
||||||
pub log: Arc<String>,
|
pub log: Arc<String>,
|
||||||
|
// True, when the initial loading of configuration and mods is still in progress
|
||||||
|
pub loading: bool,
|
||||||
|
|
||||||
#[lens(ignore)]
|
#[lens(ignore)]
|
||||||
#[data(ignore)]
|
#[data(ignore)]
|
||||||
|
@ -174,12 +176,7 @@ impl State {
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
pub const selected_mod: SelectedModLens = SelectedModLens;
|
pub const selected_mod: SelectedModLens = SelectedModLens;
|
||||||
|
|
||||||
pub fn new(
|
pub fn new() -> Self {
|
||||||
config_path: PathBuf,
|
|
||||||
game_dir: PathBuf,
|
|
||||||
data_dir: PathBuf,
|
|
||||||
nexus_api_key: String,
|
|
||||||
) -> Self {
|
|
||||||
let ctx = sdk::Context::new();
|
let ctx = sdk::Context::new();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
@ -193,12 +190,13 @@ impl State {
|
||||||
is_save_in_progress: false,
|
is_save_in_progress: false,
|
||||||
is_next_save_pending: false,
|
is_next_save_pending: false,
|
||||||
is_update_in_progress: false,
|
is_update_in_progress: false,
|
||||||
config_path: Arc::new(config_path),
|
config_path: Arc::new(PathBuf::new()),
|
||||||
game_dir: Arc::new(game_dir),
|
game_dir: Arc::new(PathBuf::new()),
|
||||||
data_dir: Arc::new(data_dir),
|
data_dir: Arc::new(PathBuf::new()),
|
||||||
nexus_api_key: Arc::new(nexus_api_key),
|
nexus_api_key: Arc::new(String::new()),
|
||||||
log: Arc::new(String::new()),
|
log: Arc::new(String::new()),
|
||||||
windows: HashMap::new(),
|
windows: HashMap::new(),
|
||||||
|
loading: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,10 +224,6 @@ impl State {
|
||||||
self.selected_mod_index.map(|i| i > 0).unwrap_or(false)
|
self.selected_mod_index.map(|i| i > 0).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_mod_dir(&self) -> PathBuf {
|
|
||||||
self.data_dir.join("mods")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_log_line(&mut self, line: String) {
|
pub(crate) fn add_log_line(&mut self, line: String) {
|
||||||
let log = Arc::make_mut(&mut self.log);
|
let log = Arc::make_mut(&mut self.log);
|
||||||
log.push_str(&line);
|
log.push_str(&line);
|
||||||
|
|
|
@ -7,7 +7,7 @@ use druid::{
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::ui::window;
|
use crate::{ui::window, util::config::Config};
|
||||||
|
|
||||||
use super::{ModInfo, State};
|
use super::{ModInfo, State};
|
||||||
|
|
||||||
|
@ -52,6 +52,10 @@ pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector<SingleUse<Report>> =
|
||||||
pub(crate) const ACTION_SET_WINDOW_HANDLE: Selector<SingleUse<(WindowId, WindowHandle)>> =
|
pub(crate) const ACTION_SET_WINDOW_HANDLE: Selector<SingleUse<(WindowId, WindowHandle)>> =
|
||||||
Selector::new("dtmm.action.set-window-handle");
|
Selector::new("dtmm.action.set-window-handle");
|
||||||
|
|
||||||
|
pub(crate) type InitialLoadResult = (Config, Vector<Arc<ModInfo>>);
|
||||||
|
pub(crate) const ACTION_FINISH_LOAD_INITIAL: Selector<SingleUse<Option<InitialLoadResult>>> =
|
||||||
|
Selector::new("dtmm.action.finish-load-initial");
|
||||||
|
|
||||||
// A sub-selection of `State`'s fields that are required in `AsyncAction`s and that are
|
// A sub-selection of `State`'s fields that are required in `AsyncAction`s and that are
|
||||||
// `Send + Sync`
|
// `Send + Sync`
|
||||||
pub(crate) struct ActionState {
|
pub(crate) struct ActionState {
|
||||||
|
@ -85,6 +89,7 @@ pub(crate) enum AsyncAction {
|
||||||
DeleteMod(ActionState, Arc<ModInfo>),
|
DeleteMod(ActionState, Arc<ModInfo>),
|
||||||
SaveSettings(ActionState),
|
SaveSettings(ActionState),
|
||||||
CheckUpdates(ActionState),
|
CheckUpdates(ActionState),
|
||||||
|
LoadInitial((PathBuf, bool)),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Delegate {
|
pub(crate) struct Delegate {
|
||||||
|
@ -354,6 +359,23 @@ impl AppDelegate<State> for Delegate {
|
||||||
state.is_update_in_progress = false;
|
state.is_update_in_progress = false;
|
||||||
Handled::Yes
|
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.loading = false;
|
||||||
|
|
||||||
|
Handled::Yes
|
||||||
|
}
|
||||||
_ => Handled::No,
|
_ => Handled::No,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,12 +78,15 @@ impl<W: Widget<State>> Controller<State, W> for DirtyStateController {
|
||||||
data: &State,
|
data: &State,
|
||||||
env: &Env,
|
env: &Env,
|
||||||
) {
|
) {
|
||||||
if compare_state_fields!(old_data, data, mods, game_dir, data_dir, nexus_api_key) {
|
// Only start tracking changes after the initial load has finished
|
||||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
if old_data.loading == data.loading {
|
||||||
}
|
if compare_state_fields!(old_data, data, mods, game_dir, data_dir, nexus_api_key) {
|
||||||
|
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
if compare_state_fields!(old_data, data, mods, game_dir) {
|
if compare_state_fields!(old_data, data, mods, game_dir) {
|
||||||
ctx.submit_command(ACTION_SET_DIRTY);
|
ctx.submit_command(ACTION_SET_DIRTY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
child.update(ctx, old_data, data, env)
|
child.update(ctx, old_data, data, env)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{fs, path::Path};
|
|
||||||
|
|
||||||
use clap::{parser::ValueSource, ArgMatches};
|
|
||||||
use color_eyre::{eyre::Context, Result};
|
use color_eyre::{eyre::Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
use crate::state::{ActionState, ModInfo};
|
use crate::state::{ActionState, ModInfo};
|
||||||
|
|
||||||
|
@ -58,7 +58,8 @@ pub(crate) struct LoadOrderEntry {
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub data_dir: Option<PathBuf>,
|
#[serde(default = "get_default_data_dir")]
|
||||||
|
pub data_dir: PathBuf,
|
||||||
pub game_dir: Option<PathBuf>,
|
pub game_dir: Option<PathBuf>,
|
||||||
pub nexus_api_key: Option<String>,
|
pub nexus_api_key: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -99,21 +100,19 @@ pub fn get_default_data_dir() -> PathBuf {
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn get_default_data_dir() -> PathBuf {
|
pub fn get_default_data_dir() -> PathBuf {
|
||||||
let data_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
let data_dir = std::env::var("LOCALAPPDATA").expect("appdata env var not set");
|
||||||
PathBuf::from(data_dir).join("dtmm")
|
PathBuf::from(data_dir).join("dtmm")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(matches),fields(path = ?matches.get_one::<PathBuf>("config")))]
|
#[tracing::instrument]
|
||||||
pub(crate) fn read_config<P>(default: P, matches: &ArgMatches) -> Result<Config>
|
pub(crate) async fn read_config<P>(path: P, is_default: bool) -> Result<Config>
|
||||||
where
|
where
|
||||||
P: Into<PathBuf> + std::fmt::Debug,
|
P: Into<PathBuf> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let path = matches
|
let path = path.into();
|
||||||
.get_one::<PathBuf>("config")
|
let default_path = get_default_config_path();
|
||||||
.expect("argument missing despite default");
|
|
||||||
let default_path = default.into();
|
|
||||||
|
|
||||||
match fs::read(path) {
|
match fs::read(&path).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let data = String::from_utf8(data).wrap_err_with(|| {
|
let data = String::from_utf8(data).wrap_err_with(|| {
|
||||||
format!("Config file '{}' contains invalid UTF-8", path.display())
|
format!("Config file '{}' contains invalid UTF-8", path.display())
|
||||||
|
@ -121,11 +120,11 @@ where
|
||||||
let mut cfg: Config = serde_sjson::from_str(&data)
|
let mut cfg: Config = serde_sjson::from_str(&data)
|
||||||
.wrap_err_with(|| format!("Invalid config file {}", path.display()))?;
|
.wrap_err_with(|| format!("Invalid config file {}", path.display()))?;
|
||||||
|
|
||||||
cfg.path = path.clone();
|
cfg.path = path;
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||||
if matches.value_source("config") != Some(ValueSource::DefaultValue) {
|
if !is_default {
|
||||||
return Err(err)
|
return Err(err)
|
||||||
.wrap_err_with(|| format!("Failed to read config file {}", path.display()))?;
|
.wrap_err_with(|| format!("Failed to read config file {}", path.display()))?;
|
||||||
}
|
}
|
||||||
|
@ -134,14 +133,14 @@ where
|
||||||
let parent = default_path
|
let parent = default_path
|
||||||
.parent()
|
.parent()
|
||||||
.expect("a file path always has a parent directory");
|
.expect("a file path always has a parent directory");
|
||||||
fs::create_dir_all(parent).wrap_err_with(|| {
|
fs::create_dir_all(parent).await.wrap_err_with(|| {
|
||||||
format!("Failed to create directories {}", parent.display())
|
format!("Failed to create directories {}", parent.display())
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
path: default_path,
|
path: default_path,
|
||||||
data_dir: Some(get_default_data_dir()),
|
data_dir: get_default_data_dir(),
|
||||||
game_dir: None,
|
game_dir: None,
|
||||||
nexus_api_key: None,
|
nexus_api_key: None,
|
||||||
mod_order: Vec::new(),
|
mod_order: Vec::new(),
|
||||||
|
@ -150,7 +149,7 @@ where
|
||||||
{
|
{
|
||||||
let data = serde_sjson::to_string(&config)
|
let data = serde_sjson::to_string(&config)
|
||||||
.wrap_err("Failed to serialize default config value")?;
|
.wrap_err("Failed to serialize default config value")?;
|
||||||
fs::write(&config.path, data).wrap_err_with(|| {
|
fs::write(&config.path, data).await.wrap_err_with(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write default config to {}",
|
"failed to write default config to {}",
|
||||||
config.path.display()
|
config.path.display()
|
||||||
|
|
Loading…
Add table
Reference in a new issue