parent
55335c0fdc
commit
e6c9fe834c
7 changed files with 134 additions and 28 deletions
|
@ -9,6 +9,7 @@ use tokio::fs;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
use crate::state::{ModInfo, PackageInfo, State};
|
use crate::state::{ModInfo, PackageInfo, State};
|
||||||
|
use crate::util::config::Config;
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> {
|
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> {
|
||||||
|
@ -103,3 +104,23 @@ pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state))]
|
||||||
|
pub(crate) async fn save_settings(state: State) -> Result<()> {
|
||||||
|
// TODO: Avoid allocations, especially once the config grows, by
|
||||||
|
// creating a separate struct with only borrowed data to serialize from.
|
||||||
|
let cfg = Config {
|
||||||
|
path: state.config_path.as_ref().clone(),
|
||||||
|
game_dir: Some(state.game_dir.as_ref().clone()),
|
||||||
|
data_dir: Some(state.data_dir.as_ref().clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("Saving settings to '{}'", state.config_path.display());
|
||||||
|
tracing::debug!(?cfg);
|
||||||
|
|
||||||
|
let data = serde_sjson::to_string(&cfg).wrap_err("failed to serialize config")?;
|
||||||
|
|
||||||
|
fs::write(&cfg.path, &data)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("failed to write config to '{}'", cfg.path.display()))
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use tokio::sync::RwLock;
|
||||||
use crate::controller::app::*;
|
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_SAVE_SETTINGS;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY,
|
ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY,
|
||||||
ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG,
|
ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG,
|
||||||
|
@ -81,6 +82,17 @@ async fn handle_action(
|
||||||
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
|
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
|
||||||
.expect("failed to send command");
|
.expect("failed to send command");
|
||||||
}),
|
}),
|
||||||
|
AsyncAction::SaveSettings(state) => tokio::spawn(async move {
|
||||||
|
if let Err(err) = save_settings(state).await {
|
||||||
|
tracing::error!("Failed to save settings: {:?}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
event_sink
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.submit_command(ACTION_FINISH_SAVE_SETTINGS, (), Target::Auto)
|
||||||
|
.expect("failed to send command");
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,10 +82,18 @@ pub(crate) struct State {
|
||||||
pub selected_mod_index: Option<usize>,
|
pub selected_mod_index: Option<usize>,
|
||||||
pub is_deployment_in_progress: bool,
|
pub is_deployment_in_progress: bool,
|
||||||
pub is_reset_in_progress: bool,
|
pub is_reset_in_progress: bool,
|
||||||
|
pub is_save_in_progress: bool,
|
||||||
|
pub is_next_save_pending: bool,
|
||||||
pub game_dir: Arc<PathBuf>,
|
pub game_dir: Arc<PathBuf>,
|
||||||
pub data_dir: Arc<PathBuf>,
|
pub data_dir: Arc<PathBuf>,
|
||||||
pub ctx: Arc<sdk::Context>,
|
|
||||||
pub log: Arc<String>,
|
pub log: Arc<String>,
|
||||||
|
|
||||||
|
#[lens(ignore)]
|
||||||
|
#[data(ignore)]
|
||||||
|
pub config_path: Arc<PathBuf>,
|
||||||
|
#[lens(ignore)]
|
||||||
|
#[data(ignore)]
|
||||||
|
pub ctx: Arc<sdk::Context>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
@ -102,8 +110,11 @@ impl State {
|
||||||
selected_mod_index: None,
|
selected_mod_index: None,
|
||||||
is_deployment_in_progress: false,
|
is_deployment_in_progress: false,
|
||||||
is_reset_in_progress: false,
|
is_reset_in_progress: false,
|
||||||
game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()),
|
is_save_in_progress: false,
|
||||||
data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()),
|
is_next_save_pending: false,
|
||||||
|
config_path: Arc::new(config.path),
|
||||||
|
game_dir: Arc::new(config.game_dir.unwrap_or_default()),
|
||||||
|
data_dir: Arc::new(config.data_dir.unwrap_or_default()),
|
||||||
log: Arc::new(String::new()),
|
log: Arc::new(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,11 +28,17 @@ pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<ModInfo>> =
|
||||||
|
|
||||||
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log");
|
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = 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) enum AsyncAction {
|
pub(crate) enum AsyncAction {
|
||||||
DeployMods(State),
|
DeployMods(State),
|
||||||
ResetDeployment(State),
|
ResetDeployment(State),
|
||||||
AddMod((State, FileInfo)),
|
AddMod((State, FileInfo)),
|
||||||
DeleteMod((State, ModInfo)),
|
DeleteMod((State, ModInfo)),
|
||||||
|
SaveSettings(State),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Delegate {
|
pub(crate) struct Delegate {
|
||||||
|
@ -49,12 +55,16 @@ impl AppDelegate<State> for Delegate {
|
||||||
#[tracing::instrument(name = "Delegate", skip_all)]
|
#[tracing::instrument(name = "Delegate", skip_all)]
|
||||||
fn command(
|
fn command(
|
||||||
&mut self,
|
&mut self,
|
||||||
_ctx: &mut DelegateCtx,
|
ctx: &mut DelegateCtx,
|
||||||
_target: Target,
|
_target: Target,
|
||||||
cmd: &Command,
|
cmd: &Command,
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
_env: &Env,
|
_env: &Env,
|
||||||
) -> Handled {
|
) -> Handled {
|
||||||
|
if cfg!(debug_assertions) && !cmd.is(ACTION_LOG) {
|
||||||
|
tracing::trace!(?cmd);
|
||||||
|
}
|
||||||
|
|
||||||
match cmd {
|
match cmd {
|
||||||
cmd if cmd.is(ACTION_START_DEPLOY) => {
|
cmd if cmd.is(ACTION_START_DEPLOY) => {
|
||||||
if self
|
if self
|
||||||
|
@ -152,6 +162,8 @@ impl AppDelegate<State> for Delegate {
|
||||||
};
|
};
|
||||||
|
|
||||||
state.mods.remove(index);
|
state.mods.remove(index);
|
||||||
|
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
|
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
cmd if cmd.is(ACTION_ADD_MOD) => {
|
cmd if cmd.is(ACTION_ADD_MOD) => {
|
||||||
|
@ -173,6 +185,7 @@ impl AppDelegate<State> for Delegate {
|
||||||
.expect("command type matched but didn't contain the expected value");
|
.expect("command type matched but didn't contain the expected value");
|
||||||
if let Some(info) = info.take() {
|
if let Some(info) = info.take() {
|
||||||
state.add_mod(info);
|
state.add_mod(info);
|
||||||
|
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
}
|
}
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
|
@ -185,6 +198,31 @@ impl AppDelegate<State> for Delegate {
|
||||||
}
|
}
|
||||||
Handled::Yes
|
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()))
|
||||||
|
.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) => {
|
||||||
|
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 => {
|
cmd => {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
tracing::warn!("Unknown command: {:?}", cmd);
|
tracing::warn!("Unknown command: {:?}", cmd);
|
||||||
|
|
|
@ -2,6 +2,8 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use druid::text::Formatter;
|
use druid::text::Formatter;
|
||||||
|
use druid::widget::{TextBoxEvent, ValidationDelegate};
|
||||||
|
use druid::EventCtx;
|
||||||
|
|
||||||
pub(crate) struct PathBufFormatter;
|
pub(crate) struct PathBufFormatter;
|
||||||
|
|
||||||
|
@ -29,3 +31,19 @@ impl Formatter<Arc<PathBuf>> for PathBufFormatter {
|
||||||
Ok(Arc::new(p))
|
Ok(Arc::new(p))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TextBoxOnChanged<F: Fn(&mut EventCtx, &str)>(F);
|
||||||
|
|
||||||
|
impl<F: Fn(&mut EventCtx, &str)> TextBoxOnChanged<F> {
|
||||||
|
pub fn new(f: F) -> Self {
|
||||||
|
Self(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Fn(&mut EventCtx, &str)> ValidationDelegate for TextBoxOnChanged<F> {
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: TextBoxEvent, current_text: &str) {
|
||||||
|
if let TextBoxEvent::Complete = event {
|
||||||
|
(self.0)(ctx, current_text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,10 @@ use druid::{
|
||||||
TextAlignment, Widget, WidgetExt, WindowDesc,
|
TextAlignment, Widget, WidgetExt, WindowDesc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT};
|
use crate::state::{
|
||||||
|
ModInfo, PathBufFormatter, State, TextBoxOnChanged, View, ACTION_START_RESET_DEPLOYMENT,
|
||||||
|
ACTION_START_SAVE_SETTINGS,
|
||||||
|
};
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
|
ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
|
||||||
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY,
|
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY,
|
||||||
|
@ -246,6 +249,9 @@ fn build_view_settings() -> impl Widget<State> {
|
||||||
.with_flex_child(
|
.with_flex_child(
|
||||||
TextBox::new()
|
TextBox::new()
|
||||||
.with_formatter(PathBufFormatter::new())
|
.with_formatter(PathBufFormatter::new())
|
||||||
|
.delegate(TextBoxOnChanged::new(|ctx, _| {
|
||||||
|
ctx.submit_command(ACTION_START_SAVE_SETTINGS)
|
||||||
|
}))
|
||||||
.expand_width()
|
.expand_width()
|
||||||
.lens(State::data_dir),
|
.lens(State::data_dir),
|
||||||
1.,
|
1.,
|
||||||
|
@ -260,6 +266,9 @@ fn build_view_settings() -> impl Widget<State> {
|
||||||
.with_flex_child(
|
.with_flex_child(
|
||||||
TextBox::new()
|
TextBox::new()
|
||||||
.with_formatter(PathBufFormatter::new())
|
.with_formatter(PathBufFormatter::new())
|
||||||
|
.delegate(TextBoxOnChanged::new(|ctx, _| {
|
||||||
|
ctx.submit_command(ACTION_START_SAVE_SETTINGS)
|
||||||
|
}))
|
||||||
.expand_width()
|
.expand_width()
|
||||||
.lens(State::game_dir),
|
.lens(State::game_dir),
|
||||||
1.,
|
1.,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{parser::ValueSource, ArgMatches};
|
use clap::{parser::ValueSource, ArgMatches};
|
||||||
use color_eyre::{eyre::Context, Result};
|
use color_eyre::{eyre::Context, Result};
|
||||||
|
@ -8,18 +8,10 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
data_dir: Option<PathBuf>,
|
#[serde(skip)]
|
||||||
game_dir: Option<PathBuf>,
|
pub path: PathBuf,
|
||||||
}
|
pub data_dir: Option<PathBuf>,
|
||||||
|
pub game_dir: Option<PathBuf>,
|
||||||
impl Config {
|
|
||||||
pub fn game_dir(&self) -> Option<&PathBuf> {
|
|
||||||
self.game_dir.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn data_dir(&self) -> Option<&PathBuf> {
|
|
||||||
self.data_dir.as_ref()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(arget_os = "windows"))]
|
#[cfg(not(arget_os = "windows"))]
|
||||||
|
@ -60,22 +52,26 @@ pub fn get_default_data_dir() -> PathBuf {
|
||||||
PathBuf::from(data_dir).join("dtmm")
|
PathBuf::from(data_dir).join("dtmm")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_config<P: AsRef<Path>>(
|
#[tracing::instrument(skip(matches),fields(path = ?matches.get_one::<PathBuf>("config")))]
|
||||||
default_config_path: P,
|
pub(crate) fn read_config<P>(default: P, matches: &ArgMatches) -> Result<Config>
|
||||||
matches: &ArgMatches,
|
where
|
||||||
) -> Result<Config> {
|
P: Into<PathBuf> + std::fmt::Debug,
|
||||||
|
{
|
||||||
let path = matches
|
let path = matches
|
||||||
.get_one::<PathBuf>("config")
|
.get_one::<PathBuf>("config")
|
||||||
.expect("argument missing despite default");
|
.expect("argument missing despite default");
|
||||||
let default_config_path = default_config_path.as_ref();
|
let default_path = default.into();
|
||||||
|
|
||||||
match fs::read(path) {
|
match fs::read(path) {
|
||||||
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())
|
||||||
})?;
|
})?;
|
||||||
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();
|
||||||
|
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 matches.value_source("config") != Some(ValueSource::DefaultValue) {
|
||||||
|
@ -84,7 +80,7 @@ pub(crate) fn read_config<P: AsRef<Path>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let parent = default_config_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).wrap_err_with(|| {
|
||||||
|
@ -93,6 +89,7 @@ pub(crate) fn read_config<P: AsRef<Path>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
path: default_path,
|
||||||
data_dir: Some(get_default_data_dir()),
|
data_dir: Some(get_default_data_dir()),
|
||||||
game_dir: None,
|
game_dir: None,
|
||||||
};
|
};
|
||||||
|
@ -100,10 +97,10 @@ pub(crate) fn read_config<P: AsRef<Path>>(
|
||||||
{
|
{
|
||||||
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(default_config_path, data).wrap_err_with(|| {
|
fs::write(&config.path, data).wrap_err_with(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write default config to {}",
|
"failed to write default config to {}",
|
||||||
default_config_path.display()
|
config.path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue