parent
bb671c5fd2
commit
3895ab12d6
8 changed files with 447 additions and 310 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -2293,9 +2293,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.19"
|
version = "0.3.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2"
|
checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -2313,9 +2313,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.7"
|
version = "0.2.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c"
|
checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
80
crates/dtmm/src/log.rs
Normal file
80
crates/dtmm/src/log.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
use tracing_error::ErrorLayer;
|
||||||
|
use tracing_subscriber::filter::FilterFn;
|
||||||
|
use tracing_subscriber::fmt;
|
||||||
|
use tracing_subscriber::fmt::format::debug_fn;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
// I currently cannot find a way to add a parameter to `dtmt_shared::create_tracing_subscriber`
|
||||||
|
// that would allow me to pass an extra `Layer` to that function. So, for now,
|
||||||
|
// its code has to be duplicated here.
|
||||||
|
|
||||||
|
pub struct ChannelWriter {
|
||||||
|
tx: UnboundedSender<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelWriter {
|
||||||
|
pub fn new(tx: UnboundedSender<String>) -> Self {
|
||||||
|
Self { tx }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::io::Write for ChannelWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
let tx = self.tx.clone();
|
||||||
|
let string = String::from_utf8_lossy(buf).to_string();
|
||||||
|
|
||||||
|
// The `send` errors when the receiving end has closed.
|
||||||
|
// But there's not much we can do at that point, so we just ignore it.
|
||||||
|
let _ = tx.send(string);
|
||||||
|
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_tracing_subscriber(tx: UnboundedSender<String>) {
|
||||||
|
let env_layer =
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
|
||||||
|
|
||||||
|
let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) {
|
||||||
|
let fmt_layer = fmt::layer().pretty();
|
||||||
|
(Some(fmt_layer), None, None)
|
||||||
|
} else {
|
||||||
|
// Creates a layer that
|
||||||
|
// - only prints events that contain a message
|
||||||
|
// - does not print fields
|
||||||
|
// - does not print spans/targets
|
||||||
|
// - only prints time, not date
|
||||||
|
let fmt_layer = fmt::layer()
|
||||||
|
.event_format(dtmt_shared::Formatter)
|
||||||
|
.fmt_fields(debug_fn(dtmt_shared::format_field));
|
||||||
|
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
Some(fmt_layer),
|
||||||
|
Some(FilterFn::new(dtmt_shared::filter)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let channel_layer = fmt::layer()
|
||||||
|
// TODO: Re-enable and implement a formatter for the Druid widget
|
||||||
|
.with_ansi(false)
|
||||||
|
.event_format(dtmt_shared::Formatter)
|
||||||
|
.fmt_fields(debug_fn(dtmt_shared::format_field))
|
||||||
|
.with_writer(move || ChannelWriter::new(tx.clone()));
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(channel_layer)
|
||||||
|
.with(filter_layer)
|
||||||
|
.with(env_layer)
|
||||||
|
.with(dev_stdout_layer)
|
||||||
|
.with(prod_stdout_layer)
|
||||||
|
.with(ErrorLayer::new(fmt::format::Pretty::default()))
|
||||||
|
.init();
|
||||||
|
}
|
|
@ -1,170 +1,35 @@
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
#![feature(let_chains)]
|
#![feature(let_chains)]
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::path::PathBuf;
|
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::Context;
|
use color_eyre::eyre::Context;
|
||||||
use color_eyre::Report;
|
use color_eyre::{Report, Result};
|
||||||
use color_eyre::Result;
|
|
||||||
use druid::AppLauncher;
|
use druid::AppLauncher;
|
||||||
use druid::ExtEventSink;
|
|
||||||
use druid::SingleUse;
|
|
||||||
use druid::Target;
|
|
||||||
use engine::delete_mod;
|
|
||||||
use engine::import_mod;
|
|
||||||
use engine::reset_mod_deployment;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use state::ACTION_FINISH_ADD_MOD;
|
|
||||||
use state::ACTION_FINISH_DELETE_SELECTED_MOD;
|
|
||||||
use state::ACTION_FINISH_RESET_DEPLOYMENT;
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::engine::deploy_mods;
|
use crate::state::{Delegate, State};
|
||||||
use crate::state::{AsyncAction, Delegate, State, ACTION_FINISH_DEPLOY};
|
use crate::worker::work_thread;
|
||||||
|
|
||||||
mod controller;
|
mod controller;
|
||||||
mod engine;
|
mod engine;
|
||||||
|
mod log;
|
||||||
mod main_window;
|
mod main_window;
|
||||||
mod state;
|
mod state;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
mod util;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
mod worker;
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
struct Config {
|
|
||||||
data_dir: Option<PathBuf>,
|
|
||||||
game_dir: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn work_thread(
|
|
||||||
event_sink: Arc<RwLock<ExtEventSink>>,
|
|
||||||
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let rt = Runtime::new()?;
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
while let Some(action) = action_queue.write().await.recv().await {
|
|
||||||
let event_sink = event_sink.clone();
|
|
||||||
match action {
|
|
||||||
AsyncAction::DeployMods(state) => tokio::spawn(async move {
|
|
||||||
if let Err(err) = deploy_mods(state).await {
|
|
||||||
tracing::error!("Failed to deploy mods: {:?}", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
event_sink
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto)
|
|
||||||
.expect("failed to send command");
|
|
||||||
}),
|
|
||||||
AsyncAction::AddMod((state, info)) => tokio::spawn(async move {
|
|
||||||
match import_mod(state, info).await {
|
|
||||||
Ok(mod_info) => {
|
|
||||||
event_sink
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.submit_command(
|
|
||||||
ACTION_FINISH_ADD_MOD,
|
|
||||||
SingleUse::new(mod_info),
|
|
||||||
Target::Auto,
|
|
||||||
)
|
|
||||||
.expect("failed to send command");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Failed to import mod: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move {
|
|
||||||
if let Err(err) = delete_mod(state, &info).await {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to delete mod files. \
|
|
||||||
You might want to clean up the data directory manually. \
|
|
||||||
Reason: {:?}",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
event_sink
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.submit_command(
|
|
||||||
ACTION_FINISH_DELETE_SELECTED_MOD,
|
|
||||||
SingleUse::new(info),
|
|
||||||
Target::Auto,
|
|
||||||
)
|
|
||||||
.expect("failed to send command");
|
|
||||||
}),
|
|
||||||
AsyncAction::ResetDeployment(state) => tokio::spawn(async move {
|
|
||||||
if let Err(err) = reset_mod_deployment(state).await {
|
|
||||||
tracing::error!("Failed to reset mod deployment: {:?}", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
event_sink
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
|
|
||||||
.expect("failed to send command");
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(arget_os = "windows"))]
|
|
||||||
fn get_default_config_path() -> PathBuf {
|
|
||||||
let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| {
|
|
||||||
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
|
||||||
let user = std::env::var("USER").expect("user env variable not set");
|
|
||||||
format!("/home/{user}")
|
|
||||||
});
|
|
||||||
format!("{home}/.config")
|
|
||||||
});
|
|
||||||
|
|
||||||
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn get_default_config_path() -> PathBuf {
|
|
||||||
let config_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
|
||||||
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(arget_os = "windows"))]
|
|
||||||
fn get_default_data_dir() -> PathBuf {
|
|
||||||
let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| {
|
|
||||||
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
|
||||||
let user = std::env::var("USER").expect("user env variable not set");
|
|
||||||
format!("/home/{user}")
|
|
||||||
});
|
|
||||||
format!("{home}/.local/share")
|
|
||||||
});
|
|
||||||
|
|
||||||
PathBuf::from(data_dir).join("dtmm")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn get_default_data_dir() -> PathBuf {
|
|
||||||
let data_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
|
||||||
PathBuf::from(data_dir).join("dtmm")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
let default_config_path = get_default_config_path();
|
let default_config_path = util::get_default_config_path();
|
||||||
|
|
||||||
tracing::trace!(default_config_path = %default_config_path.display());
|
tracing::trace!(default_config_path = %default_config_path.display());
|
||||||
|
|
||||||
|
@ -185,78 +50,30 @@ fn main() -> Result<()> {
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
dtmt_shared::create_tracing_subscriber();
|
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
log::create_tracing_subscriber(log_tx);
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
oodle_sys::init(matches.get_one::<String>("oodle"));
|
oodle_sys::init(matches.get_one::<String>("oodle"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: Config = {
|
let config =
|
||||||
let path = matches
|
util::read_config(&default_config_path, &matches).wrap_err("failed to read config file")?;
|
||||||
.get_one::<PathBuf>("config")
|
|
||||||
.expect("argument missing despite default");
|
|
||||||
match fs::read(path) {
|
|
||||||
Ok(data) => {
|
|
||||||
let data = String::from_utf8(data).wrap_err_with(|| {
|
|
||||||
format!("config file {} contains invalid UTF-8", path.display())
|
|
||||||
})?;
|
|
||||||
serde_sjson::from_str(&data)
|
|
||||||
.wrap_err_with(|| format!("invalid config file {}", path.display()))?
|
|
||||||
}
|
|
||||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
|
||||||
if matches.value_source("config") != Some(ValueSource::DefaultValue) {
|
|
||||||
return Err(err).wrap_err_with(|| {
|
|
||||||
format!("failed to read config file {}", path.display())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let parent = default_config_path
|
|
||||||
.parent()
|
|
||||||
.expect("a file path always has a parent directory");
|
|
||||||
fs::create_dir_all(parent).wrap_err_with(|| {
|
|
||||||
format!("failed to create directories {}", parent.display())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
data_dir: Some(get_default_data_dir()),
|
|
||||||
game_dir: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let data = serde_sjson::to_string(&config)
|
|
||||||
.wrap_err("failed to serialize default config value")?;
|
|
||||||
fs::write(&default_config_path, data).wrap_err_with(|| {
|
|
||||||
format!(
|
|
||||||
"failed to write default config to {}",
|
|
||||||
default_config_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return Err(err)
|
|
||||||
.wrap_err_with(|| format!("failed to read config file {}", path.display()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let initial_state = State::new(config);
|
let initial_state = State::new(config);
|
||||||
|
|
||||||
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
|
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let delegate = Delegate::new(sender);
|
let delegate = Delegate::new(action_tx);
|
||||||
|
|
||||||
let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate);
|
let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate);
|
||||||
|
|
||||||
let event_sink = launcher.get_external_handle();
|
let event_sink = launcher.get_external_handle();
|
||||||
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 receiver = Arc::new(RwLock::new(receiver));
|
let action_rx = Arc::new(RwLock::new(action_rx));
|
||||||
|
let log_rx = Arc::new(RwLock::new(log_rx));
|
||||||
loop {
|
loop {
|
||||||
if let Err(err) = work_thread(event_sink.clone(), receiver.clone()) {
|
if let Err(err) = work_thread(event_sink.clone(), action_rx.clone(), log_rx.clone()) {
|
||||||
tracing::error!("Work thread failed, restarting: {:?}", err);
|
tracing::error!("Work thread failed, restarting: {:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use druid::im::Vector;
|
use druid::im::Vector;
|
||||||
use druid::widget::{
|
use druid::widget::{
|
||||||
Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split,
|
Align, Button, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe,
|
||||||
TextBox, ViewSwitcher,
|
Scroll, SizedBox, Split, TextBox, ViewSwitcher,
|
||||||
};
|
};
|
||||||
use druid::{
|
use druid::{
|
||||||
lens, FileDialogOptions, FileSpec, Insets, LensExt, SingleUse, Widget, WidgetExt, WindowDesc,
|
lens, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, LensExt, SingleUse,
|
||||||
|
Widget, WidgetExt, WindowDesc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT};
|
use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT};
|
||||||
|
@ -261,9 +262,25 @@ fn build_main() -> impl Widget<State> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_log_view() -> impl Widget<State> {
|
||||||
|
let font = FontDescriptor::new(FontFamily::MONOSPACE);
|
||||||
|
let label = Label::raw()
|
||||||
|
.with_font(font)
|
||||||
|
.with_line_break_mode(LineBreaking::WordWrap)
|
||||||
|
.lens(State::log);
|
||||||
|
|
||||||
|
SizedBox::new(label)
|
||||||
|
.expand_width()
|
||||||
|
.height(128.0)
|
||||||
|
.scroll()
|
||||||
|
.vertical()
|
||||||
|
}
|
||||||
|
|
||||||
fn build_window() -> impl Widget<State> {
|
fn build_window() -> impl Widget<State> {
|
||||||
|
// TODO: Add borders between the sections
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.must_fill_main_axis(true)
|
.must_fill_main_axis(true)
|
||||||
.with_child(build_top_bar())
|
.with_child(build_top_bar())
|
||||||
.with_flex_child(build_main(), 1.0)
|
.with_flex_child(build_main(), 1.0)
|
||||||
|
.with_child(build_log_view())
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use druid::{
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::util::Config;
|
||||||
|
|
||||||
pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod");
|
pub(crate) const ACTION_SELECT_MOD: Selector<usize> = 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_UP: Selector = Selector::new("dtmm.action.selected-mod-up");
|
||||||
|
@ -33,6 +33,8 @@ pub(crate) const ACTION_ADD_MOD: Selector<FileInfo> = Selector::new("dtmm.action
|
||||||
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<ModInfo>> =
|
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<ModInfo>> =
|
||||||
Selector::new("dtmm.action.finish-add-mod");
|
Selector::new("dtmm.action.finish-add-mod");
|
||||||
|
|
||||||
|
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log");
|
||||||
|
|
||||||
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
||||||
pub(crate) enum View {
|
pub(crate) enum View {
|
||||||
Mods,
|
Mods,
|
||||||
|
@ -154,6 +156,7 @@ pub(crate) struct State {
|
||||||
game_dir: Arc<PathBuf>,
|
game_dir: Arc<PathBuf>,
|
||||||
data_dir: Arc<PathBuf>,
|
data_dir: Arc<PathBuf>,
|
||||||
ctx: Arc<sdk::Context>,
|
ctx: Arc<sdk::Context>,
|
||||||
|
log: Arc<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
@ -170,8 +173,9 @@ 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.unwrap_or_default()),
|
game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()),
|
||||||
data_dir: Arc::new(config.data_dir.unwrap_or_default()),
|
data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()),
|
||||||
|
log: Arc::new(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,6 +234,11 @@ impl State {
|
||||||
pub(crate) fn get_ctx(&self) -> Arc<sdk::Context> {
|
pub(crate) fn get_ctx(&self) -> Arc<sdk::Context> {
|
||||||
self.ctx.clone()
|
self.ctx.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_log_line(&mut self, line: String) {
|
||||||
|
let log = Arc::make_mut(&mut self.log);
|
||||||
|
log.push_str(&line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct SelectedModLens;
|
pub(crate) struct SelectedModLens;
|
||||||
|
@ -454,6 +463,15 @@ impl AppDelegate<State> for Delegate {
|
||||||
}
|
}
|
||||||
Handled::Yes
|
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() {
|
||||||
|
state.add_log_line(line);
|
||||||
|
}
|
||||||
|
Handled::Yes
|
||||||
|
}
|
||||||
cmd => {
|
cmd => {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
tracing::warn!("Unknown command: {:?}", cmd);
|
tracing::warn!("Unknown command: {:?}", cmd);
|
||||||
|
|
117
crates/dtmm/src/util.rs
Normal file
117
crates/dtmm/src/util.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use clap::{parser::ValueSource, ArgMatches};
|
||||||
|
use color_eyre::{eyre::Context, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct Config {
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
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"))]
|
||||||
|
pub fn get_default_config_path() -> PathBuf {
|
||||||
|
let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
||||||
|
let user = std::env::var("USER").expect("user env variable not set");
|
||||||
|
format!("/home/{user}")
|
||||||
|
});
|
||||||
|
format!("{home}/.config")
|
||||||
|
});
|
||||||
|
|
||||||
|
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn get_default_config_path() -> PathBuf {
|
||||||
|
let config_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
||||||
|
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(arget_os = "windows"))]
|
||||||
|
pub fn get_default_data_dir() -> PathBuf {
|
||||||
|
let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
||||||
|
let user = std::env::var("USER").expect("user env variable not set");
|
||||||
|
format!("/home/{user}")
|
||||||
|
});
|
||||||
|
format!("{home}/.local/share")
|
||||||
|
});
|
||||||
|
|
||||||
|
PathBuf::from(data_dir).join("dtmm")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn get_default_data_dir() -> PathBuf {
|
||||||
|
let data_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
||||||
|
PathBuf::from(data_dir).join("dtmm")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_config<P: AsRef<Path>>(
|
||||||
|
default_config_path: P,
|
||||||
|
matches: &ArgMatches,
|
||||||
|
) -> Result<Config> {
|
||||||
|
let path = matches
|
||||||
|
.get_one::<PathBuf>("config")
|
||||||
|
.expect("argument missing despite default");
|
||||||
|
let default_config_path = default_config_path.as_ref();
|
||||||
|
|
||||||
|
match fs::read(path) {
|
||||||
|
Ok(data) => {
|
||||||
|
let data = String::from_utf8(data).wrap_err_with(|| {
|
||||||
|
format!("config file {} contains invalid UTF-8", path.display())
|
||||||
|
})?;
|
||||||
|
serde_sjson::from_str(&data)
|
||||||
|
.wrap_err_with(|| format!("invalid config file {}", path.display()))
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||||
|
if matches.value_source("config") != Some(ValueSource::DefaultValue) {
|
||||||
|
return Err(err)
|
||||||
|
.wrap_err_with(|| format!("failed to read config file {}", path.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let parent = default_config_path
|
||||||
|
.parent()
|
||||||
|
.expect("a file path always has a parent directory");
|
||||||
|
fs::create_dir_all(parent).wrap_err_with(|| {
|
||||||
|
format!("failed to create directories {}", parent.display())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
data_dir: Some(get_default_data_dir()),
|
||||||
|
game_dir: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let data = serde_sjson::to_string(&config)
|
||||||
|
.wrap_err("failed to serialize default config value")?;
|
||||||
|
fs::write(default_config_path, data).wrap_err_with(|| {
|
||||||
|
format!(
|
||||||
|
"failed to write default config to {}",
|
||||||
|
default_config_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
Err(err).wrap_err_with(|| format!("failed to read config file {}", path.display()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
crates/dtmm/src/worker.rs
Normal file
114
crates/dtmm/src/worker.rs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use druid::{ExtEventSink, SingleUse, Target};
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::engine::*;
|
||||||
|
use crate::state::*;
|
||||||
|
|
||||||
|
async fn handle_action(
|
||||||
|
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||||
|
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
|
||||||
|
) {
|
||||||
|
while let Some(action) = action_queue.write().await.recv().await {
|
||||||
|
let event_sink = event_sink.clone();
|
||||||
|
match action {
|
||||||
|
AsyncAction::DeployMods(state) => tokio::spawn(async move {
|
||||||
|
if let Err(err) = deploy_mods(state).await {
|
||||||
|
tracing::error!("Failed to deploy mods: {:?}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
event_sink
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto)
|
||||||
|
.expect("failed to send command");
|
||||||
|
}),
|
||||||
|
AsyncAction::AddMod((state, info)) => tokio::spawn(async move {
|
||||||
|
match import_mod(state, info).await {
|
||||||
|
Ok(mod_info) => {
|
||||||
|
event_sink
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.submit_command(
|
||||||
|
ACTION_FINISH_ADD_MOD,
|
||||||
|
SingleUse::new(mod_info),
|
||||||
|
Target::Auto,
|
||||||
|
)
|
||||||
|
.expect("failed to send command");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to import mod: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move {
|
||||||
|
if let Err(err) = delete_mod(state, &info).await {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to delete mod files. \
|
||||||
|
You might want to clean up the data directory manually. \
|
||||||
|
Reason: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
event_sink
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.submit_command(
|
||||||
|
ACTION_FINISH_DELETE_SELECTED_MOD,
|
||||||
|
SingleUse::new(info),
|
||||||
|
Target::Auto,
|
||||||
|
)
|
||||||
|
.expect("failed to send command");
|
||||||
|
}),
|
||||||
|
AsyncAction::ResetDeployment(state) => tokio::spawn(async move {
|
||||||
|
if let Err(err) = reset_mod_deployment(state).await {
|
||||||
|
tracing::error!("Failed to reset mod deployment: {:?}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
event_sink
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
|
||||||
|
.expect("failed to send command");
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_log(
|
||||||
|
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||||
|
log_queue: Arc<RwLock<UnboundedReceiver<String>>>,
|
||||||
|
) {
|
||||||
|
while let Some(line) = log_queue.write().await.recv().await {
|
||||||
|
let event_sink = event_sink.clone();
|
||||||
|
event_sink
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.submit_command(ACTION_LOG, SingleUse::new(line), Target::Auto)
|
||||||
|
.expect("failed to send command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn work_thread(
|
||||||
|
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||||
|
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
|
||||||
|
log_queue: Arc<RwLock<UnboundedReceiver<String>>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let rt = Runtime::new()?;
|
||||||
|
|
||||||
|
rt.block_on(async {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = handle_action(event_sink.clone(), action_queue.clone()) => {},
|
||||||
|
_ = handle_log(event_sink.clone(), log_queue.clone()) => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,8 +1,3 @@
|
||||||
// Rust Analyzer cannot properly determine that `cfg!(debug_assertions)` alone does not make code
|
|
||||||
// unused. These sections should be small enough that no truly dead code slips in.
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
mod prod {
|
|
||||||
use std::fmt::Result;
|
use std::fmt::Result;
|
||||||
|
|
||||||
use time::format_description::FormatItem;
|
use time::format_description::FormatItem;
|
||||||
|
@ -13,14 +8,15 @@ mod prod {
|
||||||
use tracing_error::ErrorLayer;
|
use tracing_error::ErrorLayer;
|
||||||
use tracing_subscriber::filter::FilterFn;
|
use tracing_subscriber::filter::FilterFn;
|
||||||
use tracing_subscriber::fmt::format::{debug_fn, Writer};
|
use tracing_subscriber::fmt::format::{debug_fn, Writer};
|
||||||
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
|
use tracing_subscriber::fmt::{self, FmtContext, FormatEvent, FormatFields};
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::registry::LookupSpan;
|
use tracing_subscriber::registry::LookupSpan;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]");
|
pub const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]");
|
||||||
|
|
||||||
fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result {
|
pub fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result {
|
||||||
if field.name() == "message" {
|
if field.name() == "message" {
|
||||||
write!(w, "{:?}", val)
|
write!(w, "{:?}", val)
|
||||||
} else {
|
} else {
|
||||||
|
@ -28,14 +24,14 @@ mod prod {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter(metadata: &Metadata<'_>) -> bool {
|
pub fn filter(metadata: &Metadata<'_>) -> bool {
|
||||||
metadata
|
metadata
|
||||||
.fields()
|
.fields()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|field| field.name() == "message")
|
.any(|field| field.name() == "message")
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Formatter;
|
pub struct Formatter;
|
||||||
|
|
||||||
impl<S, N> FormatEvent<S, N> for Formatter
|
impl<S, N> FormatEvent<S, N> for Formatter
|
||||||
where
|
where
|
||||||
|
@ -61,53 +57,31 @@ mod prod {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a subscriber that
|
|
||||||
/// - only prints events that contain a message
|
|
||||||
/// - does not print fields
|
|
||||||
/// - does not print spans/targets
|
|
||||||
/// - only prints time, not date
|
|
||||||
pub fn create_tracing_subscriber() {
|
pub fn create_tracing_subscriber() {
|
||||||
let filter_layer = EnvFilter::try_from_default_env()
|
let env_layer =
|
||||||
.unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
|
||||||
|
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) {
|
||||||
|
let fmt_layer = fmt::layer().pretty();
|
||||||
|
(Some(fmt_layer), None, None)
|
||||||
|
} else {
|
||||||
|
// Creates a layer that
|
||||||
|
// - only prints events that contain a message
|
||||||
|
// - does not print fields
|
||||||
|
// - does not print spans/targets
|
||||||
|
// - only prints time, not date
|
||||||
|
let fmt_layer = fmt::layer()
|
||||||
.event_format(Formatter)
|
.event_format(Formatter)
|
||||||
.fmt_fields(debug_fn(format_field));
|
.fmt_fields(debug_fn(format_field));
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
(None, Some(fmt_layer), Some(FilterFn::new(filter)))
|
||||||
.with(FilterFn::new(filter))
|
};
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.with(ErrorLayer::new(
|
|
||||||
tracing_subscriber::fmt::format::Pretty::default(),
|
|
||||||
))
|
|
||||||
.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
mod dev {
|
|
||||||
use tracing_error::ErrorLayer;
|
|
||||||
use tracing_subscriber::prelude::*;
|
|
||||||
use tracing_subscriber::EnvFilter;
|
|
||||||
|
|
||||||
pub fn create_tracing_subscriber() {
|
|
||||||
let filter_layer = EnvFilter::try_from_default_env()
|
|
||||||
.unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
|
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer().pretty();
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(filter_layer)
|
.with(filter_layer)
|
||||||
.with(fmt_layer)
|
.with(env_layer)
|
||||||
.with(ErrorLayer::new(
|
.with(dev_stdout_layer)
|
||||||
tracing_subscriber::fmt::format::Pretty::default(),
|
.with(prod_stdout_layer)
|
||||||
))
|
.with(ErrorLayer::new(fmt::format::Pretty::default()))
|
||||||
.init();
|
.init();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
pub use dev::create_tracing_subscriber;
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
pub use prod::create_tracing_subscriber;
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue