1use std::env::{split_paths, var_os};
2use std::fmt::Debug;
3use std::fs;
4use std::path::PathBuf;
5
6use clap::Subcommand;
7use clap::{Parser, builder::BoolishValueParser, builder::OsStr};
8
9use crate::log_options::LogOptions;
10
11use super::colors::styles;
12
13pub const CARGO_PKG_NAME: &str = "mod_installer";
14
15pub const LONG: &str = r"
16
17 /\/\ ___ __| | (_)_ __ ___| |_ __ _| | | ___ _ __
18 / \ / _ \ / _` | | | '_ \/ __| __/ _` | | |/ _ \ '__|
19/ /\/\ \ (_) | (_| | | | | | \__ \ || (_| | | | __/ |
20\/ \/\___/ \__,_| |_|_| |_|___/\__\__,_|_|_|\___|_|
21";
22
23#[cfg(not(target_os = "windows"))]
24pub const WEIDU_FILE_NAME: &str = "weidu";
25#[cfg(target_os = "windows")]
26pub const WEIDU_FILE_NAME: &str = "weidu.exe";
27
28#[derive(Parser, Debug, PartialEq)]
30#[command(
31 version,
32 propagate_version = true,
33 styles = styles(),
34 about = format!("{}\n{}", LONG, std::env::var("CARGO_PKG_DESCRIPTION").unwrap_or_default())
35)]
36pub struct Args {
37 #[command(subcommand)]
39 pub command: CommandType,
40}
41
42#[derive(Subcommand, Debug, PartialEq, Clone)]
44pub enum CommandType {
45 #[command()]
46 Normal(Normal),
47 #[command()]
48 Eet(Eet),
49 #[command(subcommand)]
50 Scan(Scan),
51}
52
53#[derive(Parser, Debug, PartialEq, Clone)]
55#[clap(short_flag = 'n')]
56pub struct Normal {
57 #[clap(env, long, short = 'f', value_parser = path_must_exist, required = true)]
59 pub log_file: PathBuf,
60
61 #[clap(env, short, long, value_parser = parse_absolute_path, required = true)]
63 pub game_directory: PathBuf,
64
65 #[clap(env, long, short = 'n', required = false)]
67 pub generate_directory: Option<PathBuf>,
68
69 #[clap(flatten)]
71 pub options: Options,
72}
73
74#[derive(Parser, Debug, PartialEq, Clone)]
76#[clap(short_flag = 'e')]
77pub struct Eet {
78 #[clap(env, short='1', long, value_parser = parse_absolute_path, required = true)]
80 pub bg1_game_directory: PathBuf,
81
82 #[clap(env, short='y', long, value_parser = path_must_exist, required = true)]
84 pub bg1_log_file: PathBuf,
85
86 #[clap(env, short='2', long, value_parser = parse_absolute_path, required = true)]
88 pub bg2_game_directory: PathBuf,
89
90 #[clap(env, short='z', long, value_parser = path_must_exist, required = true)]
92 pub bg2_log_file: PathBuf,
93
94 #[clap(env, short = 'p', long, value_parser = path_must_exist)]
96 pub new_pre_eet_dir: Option<PathBuf>,
97
98 #[clap(env, short = 'n', long, value_parser = path_must_exist)]
100 pub new_eet_dir: Option<PathBuf>,
101
102 #[clap(flatten)]
104 pub options: Options,
105}
106
107#[derive(Subcommand, Debug, PartialEq, Clone)]
109#[clap(short_flag = 's')]
110pub enum Scan {
111 #[command()]
112 Langauges(ScanLangauges),
113 #[command()]
114 Components(ScanComponents),
115}
116
117#[derive(Parser, Debug, PartialEq, Clone)]
118#[clap(short_flag = 'l')]
119pub struct ScanLangauges {
120 #[clap(short, long, required = false, default_value = "")]
122 pub filter_by_selected_language: String,
123
124 #[clap(flatten)]
126 pub options: Options,
127}
128
129#[derive(Parser, Debug, PartialEq, Clone)]
130#[clap(short_flag = 'c')]
131pub struct ScanComponents {
132 #[clap(env, short, long, value_parser = parse_absolute_path)]
134 pub game_directory: PathBuf,
135
136 #[clap(short, long, required = false, default_value = "")]
138 pub filter_by_selected_language: String,
139
140 #[clap(flatten)]
142 pub options: Options,
143}
144
145#[derive(Parser, Debug, PartialEq, Clone, Default)]
146pub struct Options {
147 #[clap(
149 env,
150 short,
151 long,
152 value_parser = parse_absolute_path,
153 default_value_os_t = find_weidu_bin(),
154 default_missing_value = find_weidu_bin_on_path(),
155 required = false
156 )]
157 pub weidu_binary: PathBuf,
158
159 #[clap(
161 env,
162 short,
163 long,
164 value_parser = path_exists_full,
165 use_value_delimiter = true,
166 value_delimiter = ',',
167 default_values_os_t = current_work_dir(),
168 default_missing_value = working_dir(),
169 required = false
170 )]
171 pub mod_directories: Vec<PathBuf>,
172
173 #[clap(short, long, default_value = "en_US")]
175 pub language: String,
176
177 #[clap(env, long, short, default_value = "5")]
179 pub depth: usize,
180
181 #[clap(
183 env,
184 short,
185 long,
186 num_args=0..=1,
187 action = clap::ArgAction::SetFalse,
188 default_value_t = true,
189 value_parser = BoolishValueParser::new(),
190 )]
191 pub skip_installed: bool,
192
193 #[clap(
195 env,
196 short,
197 long,
198 num_args=0..=1,
199 action = clap::ArgAction::SetTrue,
200 default_value_t = false,
201 value_parser = BoolishValueParser::new(),
202 conflicts_with = "never_abort",
203 )]
204 pub abort_on_warnings: bool,
205
206 #[clap(
208 env,
209 short = 'v',
210 long,
211 num_args=0..=1,
212 action = clap::ArgAction::SetTrue,
213 default_value_t = false,
214 value_parser = BoolishValueParser::new(),
215 conflicts_with = "abort_on_warnings",
216 )]
217 pub never_abort: bool,
218
219 #[clap(env, long, short, default_value = "3600")]
221 pub timeout: usize,
222
223 #[clap(
225 env,
226 long,
227 short='u',
228 use_value_delimiter = true,
229 value_delimiter = ',',
230 default_value = "autolog,logapp,log-extern",
231 value_parser = LogOptions::value_parser,
232 required = false
233 )]
234 pub weidu_log_mode: Vec<LogOptions>,
235
236 #[clap(
238 env,
239 short = 'x',
240 long,
241 num_args=0..=1,
242 action = clap::ArgAction::SetTrue,
243 default_value_t = false,
244 value_parser = BoolishValueParser::new(),
245 )]
246 pub strict_matching: bool,
247
248 #[clap(
250 env,
251 long,
252 num_args=0..=1,
253 action = clap::ArgAction::SetFalse,
254 default_value_t = true,
255 value_parser = BoolishValueParser::new(),
256 )]
257 pub download: bool,
258
259 #[clap(
261 env,
262 short,
263 long,
264 num_args=0..=1,
265 action = clap::ArgAction::SetTrue,
266 default_value_t = false,
267 value_parser = BoolishValueParser::new(),
268 )]
269 pub overwrite: bool,
270
271 #[clap(
273 env,
274 short,
275 long,
276 num_args=0..=1,
277 action = clap::ArgAction::SetFalse,
278 default_value_t = true,
279 value_parser = BoolishValueParser::new(),
280 )]
281 pub check_last_installed: bool,
282
283 #[clap(env, short = 'i', long, default_value_t = 500)]
285 pub tick: u64,
286
287 #[clap(env, short = '0', long, default_value_t = 10)]
289 pub lookback: usize,
290
291 #[clap(
293 env,
294 long,
295 num_args=0..=1,
296 action = clap::ArgAction::SetTrue,
297 default_value_t = false,
298 value_parser = BoolishValueParser::new(),
299 )]
300 pub casefold: bool,
301}
302
303pub fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
304 let path = PathBuf::from(arg);
305 path.try_exists()?;
306 Ok(path)
307}
308
309fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
310 fs::canonicalize(path_must_exist(arg)?)
311}
312
313fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
314 let path = path_must_exist(arg).map_err(|err| err.to_string())?;
315 fs::canonicalize(path).map_err(|err| err.to_string())
316}
317
318fn find_weidu_bin() -> PathBuf {
319 if let Some(paths) = var_os("PATH") {
320 for path in split_paths(&paths) {
321 let full_path = path.join(WEIDU_FILE_NAME);
322 if full_path.is_file() && !full_path.is_dir() {
323 return full_path;
324 }
325 }
326 }
327 PathBuf::new()
328}
329
330fn find_weidu_bin_on_path() -> OsStr {
331 OsStr::from(find_weidu_bin().into_os_string())
332}
333
334fn current_work_dir() -> Vec<PathBuf> {
335 vec![std::env::current_dir().unwrap()]
336}
337
338fn working_dir() -> OsStr {
339 OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
340}
341
342#[cfg(test)]
343mod tests {
344
345 use super::*;
346 use pretty_assertions::assert_eq;
347 use std::{error::Error, path::PathBuf};
348
349 #[test]
350 fn test_bool_flags() -> Result<(), Box<dyn Error>> {
351 let workspace_root: PathBuf = std::env::current_dir()?;
352 let fake_game_dir: PathBuf = workspace_root
353 .parent()
354 .ok_or("Could not get workspace root")?
355 .join("fixtures");
356 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
357 let fake_log_file = fake_game_dir.clone().join("weidu.log");
358 let fake_mod_dirs = fake_game_dir.clone().join("mods");
359 let tests = vec![
360 ("true", true),
361 ("t", true),
362 ("yes", true),
363 ("y", true),
364 ("1", true),
365 ("false", false),
366 ("f", false),
367 ("no", false),
368 ("n", false),
369 ("0", false),
370 ];
371 for (flag_value, expected_flag_value) in tests {
372 let expected = Args {
373 command: CommandType::Normal(Normal {
374 log_file: fake_log_file.clone(),
375 game_directory: fake_game_dir.clone(),
376 generate_directory: None,
377 options: Options {
378 weidu_binary: fake_weidu_bin.clone(),
379 mod_directories: vec![fake_mod_dirs.clone()],
380 language: "en_US".to_string(),
381 depth: 5,
382 skip_installed: expected_flag_value,
383 abort_on_warnings: expected_flag_value,
384 timeout: 3600,
385 weidu_log_mode: vec![
386 LogOptions::AutoLog,
387 LogOptions::LogAppend,
388 LogOptions::LogExternal,
389 ],
390 strict_matching: true,
391 download: true,
392 overwrite: false,
393 check_last_installed: true,
394 tick: 500,
395 lookback: 10,
396 casefold: false,
397 never_abort: false,
398 },
399 }),
400 };
401 let test_arg_string = format!(
402 "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
403 flag_value,
404 flag_value,
405 fake_weidu_bin.to_str().unwrap_or_default(),
406 fake_mod_dirs.to_str().unwrap_or_default(),
407 fake_log_file.to_str().unwrap_or_default(),
408 fake_game_dir.to_str().unwrap_or_default(),
409 );
410 let result = Args::parse_from(test_arg_string.split(' '));
411 assert_eq!(
412 result, expected,
413 "Result {result:?} didn't match Expected {expected:?}",
414 );
415 }
416 Ok(())
417 }
418
419 #[test]
420 fn test_eet_flags() -> Result<(), Box<dyn Error>> {
421 let workspace_root: PathBuf = std::env::current_dir()?;
422 let fake_game_dir: PathBuf = workspace_root
423 .parent()
424 .ok_or("Could not get workspace root")?
425 .join("fixtures");
426 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
427 let fake_log_file = fake_game_dir.clone().join("weidu.log");
428 let new_dir = PathBuf::new().join("test");
429 let expected_flag_value = true;
430
431 let expected = Args {
432 command: CommandType::Eet(Eet {
433 bg1_game_directory: fake_game_dir.clone(),
434 bg1_log_file: fake_log_file.clone(),
435 bg2_game_directory: fake_game_dir.clone(),
436 bg2_log_file: fake_log_file.clone(),
437 options: Options {
438 weidu_binary: fake_weidu_bin.clone(),
439 mod_directories: vec![std::env::current_dir().unwrap()],
440 language: "en_US".to_string(),
441 depth: 5,
442 skip_installed: expected_flag_value,
443 abort_on_warnings: !expected_flag_value,
444 timeout: 3600,
445 weidu_log_mode: vec![
446 LogOptions::AutoLog,
447 LogOptions::LogAppend,
448 LogOptions::LogExternal,
449 ],
450 strict_matching: !expected_flag_value,
451 download: expected_flag_value,
452 overwrite: !expected_flag_value,
453 check_last_installed: expected_flag_value,
454 tick: 500,
455 lookback: 10,
456 casefold: false,
457 never_abort: false,
458 },
459 new_pre_eet_dir: None,
460 new_eet_dir: Some("test".into()),
461 }),
462 };
463 let test_arg_string = format!(
464 "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
465 fake_weidu_bin.to_str().unwrap_or_default(),
466 fake_game_dir.to_str().unwrap_or_default(),
467 fake_log_file.to_str().unwrap_or_default(),
468 fake_game_dir.to_str().unwrap_or_default(),
469 fake_log_file.to_str().unwrap_or_default(),
470 new_dir.to_str().unwrap_or_default(),
471 );
472 let result = Args::parse_from(test_arg_string.split(' '));
473 assert_eq!(
474 result, expected,
475 "Result {result:?} didn't match Expected {expected:?}",
476 );
477
478 Ok(())
479 }
480}