Darktide Mod Manager #39
3 changed files with 113 additions and 16 deletions
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = "1.3.2"
|
bitflags = "1.3.2"
|
||||||
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "unicode"] }
|
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] }
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
confy = "0.5.1"
|
confy = "0.5.1"
|
||||||
druid = { git = "https://github.com/linebender/druid.git", features = ["im"] }
|
druid = { git = "https://github.com/linebender/druid.git", features = ["im"] }
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
#![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::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::command;
|
use clap::command;
|
||||||
|
use clap::parser::ValueSource;
|
||||||
|
use clap::value_parser;
|
||||||
use clap::Arg;
|
use clap::Arg;
|
||||||
|
use color_eyre::eyre::Context;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use druid::AppLauncher;
|
use druid::AppLauncher;
|
||||||
|
@ -12,6 +18,8 @@ use druid::ExtEventSink;
|
||||||
use druid::SingleUse;
|
use druid::SingleUse;
|
||||||
use druid::Target;
|
use druid::Target;
|
||||||
use engine::import_mod;
|
use engine::import_mod;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
use state::ACTION_FINISH_ADD_MOD;
|
use state::ACTION_FINISH_ADD_MOD;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
|
@ -30,6 +38,12 @@ mod state;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct Config {
|
||||||
|
data_dir: Option<PathBuf>,
|
||||||
|
game_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
fn work_thread(
|
fn work_thread(
|
||||||
event_sink: Arc<RwLock<ExtEventSink>>,
|
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||||
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
|
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
|
||||||
|
@ -76,11 +90,52 @@ fn work_thread(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(arget_os = "windows"))]
|
||||||
|
fn get_default_config_path() -> String {
|
||||||
|
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")
|
||||||
|
});
|
||||||
|
|
||||||
|
format!("{config_dir}/dtmm/dtmm.cfg")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_default_config_path() -> String {
|
||||||
|
let config_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
||||||
|
format!("{config_dir}\\dtmm\\dtmm.cfg")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(arget_os = "windows"))]
|
||||||
|
fn get_default_data_dir() -> String {
|
||||||
|
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")
|
||||||
|
});
|
||||||
|
|
||||||
|
format!("{data_dir}/dtmm")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_default_data_dir() -> String {
|
||||||
|
let data_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
||||||
|
format!("{data_dir}\\dtmm")
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
#[tokio::main]
|
fn main() -> Result<()> {
|
||||||
async fn main() -> Result<()> {
|
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
|
let default_config_path = get_default_config_path();
|
||||||
|
|
||||||
|
tracing::trace!(default_config_path);
|
||||||
|
|
||||||
let matches = command!()
|
let matches = command!()
|
||||||
.arg(Arg::new("oodle").long("oodle").help(
|
.arg(Arg::new("oodle").long("oodle").help(
|
||||||
"The oodle library to load. This may either be:\n\
|
"The oodle library to load. This may either be:\n\
|
||||||
|
@ -88,6 +143,14 @@ async fn main() -> Result<()> {
|
||||||
- A file path relative to the current working directory.\n\
|
- A file path relative to the current working directory.\n\
|
||||||
- An absolute file path.",
|
- An absolute file path.",
|
||||||
))
|
))
|
||||||
|
.arg(
|
||||||
|
Arg::new("config")
|
||||||
|
.long("config")
|
||||||
|
.short('c')
|
||||||
|
.help("Path to the config file")
|
||||||
|
.value_parser(value_parser!(PathBuf))
|
||||||
|
.default_value(&default_config_path),
|
||||||
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -121,7 +184,48 @@ async fn main() -> Result<()> {
|
||||||
oodle_sys::init(matches.get_one::<String>("oodle"));
|
oodle_sys::init(matches.get_one::<String>("oodle"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let initial_state = State::new();
|
let config: Config = {
|
||||||
|
let path = matches
|
||||||
|
.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 config = Config {
|
||||||
|
data_dir: Some(PathBuf::from(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}")
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err)
|
||||||
|
.wrap_err_with(|| format!("failed to read config file {}", path.display()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let initial_state = State::new(config);
|
||||||
|
|
||||||
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
|
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let delegate = Delegate::new(sender);
|
let delegate = Delegate::new(sender);
|
||||||
|
|
|
@ -9,6 +9,8 @@ use druid::{
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use crate::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");
|
||||||
pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector =
|
pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector =
|
||||||
|
@ -105,26 +107,17 @@ 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() -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
let ctx = sdk::Context::new();
|
let ctx = sdk::Context::new();
|
||||||
|
|
||||||
let (game_dir, data_dir) = if cfg!(debug_assertions) {
|
|
||||||
(
|
|
||||||
std::env::current_dir().expect("PWD is borked").join("data"),
|
|
||||||
PathBuf::from("/tmp/dtmm"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(PathBuf::new(), PathBuf::new())
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ctx: Arc::new(ctx),
|
ctx: Arc::new(ctx),
|
||||||
current_view: View::default(),
|
current_view: View::default(),
|
||||||
mods: Vector::new(),
|
mods: Vector::new(),
|
||||||
selected_mod_index: None,
|
selected_mod_index: None,
|
||||||
is_deployment_in_progress: false,
|
is_deployment_in_progress: false,
|
||||||
game_dir: Arc::new(game_dir),
|
game_dir: Arc::new(config.game_dir.unwrap_or_default()),
|
||||||
data_dir: Arc::new(data_dir),
|
data_dir: Arc::new(config.data_dir.unwrap_or_default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue