1use std::env::{split_paths, var_os};
2use std::fmt::Debug;
3use std::fs;
4use std::path::PathBuf;
5
6use clap::Subcommand;
7use clap::builder::ArgPredicate;
8use clap::{Parser, builder::BoolishValueParser, builder::OsStr};
9
10use crate::config::log_options::LogOptions;
11
12use super::colors::styles;
13
14pub const CARGO_PKG_NAME: &str = "mod_installer";
15
16pub const LONG: &str = r"
17
18 /\/\ ___ __| | (_)_ __ ___| |_ __ _| | | ___ _ __
19 / \ / _ \ / _` | | | '_ \/ __| __/ _` | | |/ _ \ '__|
20/ /\/\ \ (_) | (_| | | | | | \__ \ || (_| | | | __/ |
21\/ \/\___/ \__,_| |_|_| |_|___/\__\__,_|_|_|\___|_|
22";
23
24#[cfg(not(target_os = "windows"))]
25pub const WEIDU_FILE_NAME: &str = "weidu";
26#[cfg(target_os = "windows")]
27pub const WEIDU_FILE_NAME: &str = "weidu.exe";
28
29#[derive(Parser, Debug, PartialEq)]
31#[command(
32 name = CARGO_PKG_NAME,
33 version,
34 propagate_version = true,
35 styles = styles(),
36 about = format!("{}\n{}", LONG, env!("CARGO_PKG_DESCRIPTION"))
37)]
38pub struct Args {
39 #[command(subcommand)]
41 pub command: CommandType,
42}
43
44#[derive(Subcommand, Debug, PartialEq, Clone)]
46pub enum CommandType {
47 #[command()]
48 Normal(Normal),
49 #[command()]
50 Eet(Eet),
51 #[command(subcommand)]
52 Scan(Scan),
53}
54
55#[derive(Parser, Debug, PartialEq, Clone)]
57#[clap(short_flag = 'n')]
58pub struct Normal {
59 #[clap(env, long, short = 'f', value_parser = path_must_exist, required = true)]
61 pub log_file: PathBuf,
62
63 #[clap(env, short, long, value_parser = parse_absolute_path, required = true)]
65 pub game_directory: PathBuf,
66
67 #[clap(env, long, short = 'n', required = false)]
69 pub generate_directory: Option<PathBuf>,
70
71 #[clap(flatten)]
73 pub options: Options,
74}
75
76#[derive(Parser, Debug, PartialEq, Clone)]
78#[clap(short_flag = 'e')]
79pub struct Eet {
80 #[clap(env, short='1', long, value_parser = parse_absolute_path, required = true)]
82 pub bg1_game_directory: PathBuf,
83
84 #[clap(env, short='y', long, value_parser = path_must_exist, required = true)]
86 pub bg1_log_file: PathBuf,
87
88 #[clap(env, short='2', long, value_parser = parse_absolute_path, required = true)]
90 pub bg2_game_directory: PathBuf,
91
92 #[clap(env, short='z', long, value_parser = path_must_exist, required = true)]
94 pub bg2_log_file: PathBuf,
95
96 #[clap(env, short = 'p', long, value_parser = path_must_exist)]
98 pub new_pre_eet_dir: Option<PathBuf>,
99
100 #[clap(env, short = 'n', long, value_parser = path_must_exist)]
102 pub new_eet_dir: Option<PathBuf>,
103
104 #[clap(flatten)]
106 pub options: Options,
107}
108
109#[derive(Subcommand, Debug, PartialEq, Clone)]
111#[clap(short_flag = 's')]
112pub enum Scan {
113 #[command()]
114 Languages(ScanLangauges),
115 #[command()]
116 Components(ScanComponents),
117}
118
119#[derive(Parser, Debug, PartialEq, Clone)]
120#[clap(short_flag = 'l')]
121pub struct ScanLangauges {
122 #[clap(short, long, required = false, default_value = "")]
124 pub filter_by_selected_language: String,
125
126 #[clap(flatten)]
128 pub options: Options,
129}
130
131#[derive(Parser, Debug, PartialEq, Clone)]
132#[clap(short_flag = 'c')]
133pub struct ScanComponents {
134 #[clap(short, long, required = false, default_value = "")]
136 pub filter_by_selected_language: String,
137
138 #[clap(flatten)]
140 pub options: Options,
141}
142
143#[derive(Parser, Debug, PartialEq, Clone, Default)]
144pub struct Options {
145 #[clap(
147 env,
148 short,
149 long,
150 value_parser = parse_absolute_path,
151 default_value_os_t = find_weidu_bin(),
152 default_missing_value = find_weidu_bin_on_path(),
153 required = false
154 )]
155 pub weidu_binary: PathBuf,
156
157 #[clap(
159 env,
160 short,
161 long,
162 value_parser = path_exists_full,
163 use_value_delimiter = true,
164 value_delimiter = ',',
165 default_values_os_t = current_work_dir(),
166 default_missing_value = working_dir(),
167 required = false
168 )]
169 pub mod_directories: Vec<PathBuf>,
170
171 #[clap(short, long, default_value = "en_US")]
173 pub language: String,
174
175 #[clap(env, long, short, default_value = "5")]
177 pub depth: usize,
178
179 #[clap(
181 env,
182 short,
183 long,
184 num_args=0..=1,
185 action = clap::ArgAction::SetFalse,
186 default_value_t = true,
187 value_parser = BoolishValueParser::new(),
188 )]
189 pub skip_installed: bool,
190
191 #[clap(
193 env,
194 short,
195 long,
196 num_args=0..=1,
197 action = clap::ArgAction::SetTrue,
198 default_value_t = false,
199 value_parser = BoolishValueParser::new(),
200 conflicts_with = "never_abort",
201 )]
202 pub abort_on_warnings: bool,
203
204 #[clap(
206 env,
207 short = 'v',
208 long,
209 num_args=0..=1,
210 action = clap::ArgAction::SetTrue,
211 default_value_t = false,
212 value_parser = BoolishValueParser::new(),
213 conflicts_with = "abort_on_warnings",
214 )]
215 pub never_abort: bool,
216
217 #[clap(
219 env,
220 short,
221 long,
222 default_value = "3600",
223 default_value_if("batch_mode", ArgPredicate::IsPresent, "10800")
224 )]
225 pub timeout: usize,
226
227 #[clap(
229 env,
230 long,
231 short='u',
232 use_value_delimiter = true,
233 value_delimiter = ',',
234 default_value = "autolog,logapp,log-extern",
235 value_parser = LogOptions::value_parser,
236 required = false
237 )]
238 pub weidu_log_mode: Vec<LogOptions>,
239
240 #[clap(
242 env,
243 short = 'x',
244 long,
245 num_args=0..=1,
246 action = clap::ArgAction::SetTrue,
247 default_value_t = false,
248 value_parser = BoolishValueParser::new(),
249 )]
250 pub strict_matching: bool,
251
252 #[clap(
254 env,
255 long,
256 num_args=0..=1,
257 action = clap::ArgAction::SetFalse,
258 default_value_t = true,
259 value_parser = BoolishValueParser::new(),
260 )]
261 pub download: bool,
262
263 #[clap(
265 env,
266 short,
267 long,
268 num_args=0..=1,
269 action = clap::ArgAction::SetTrue,
270 default_value_t = false,
271 value_parser = BoolishValueParser::new(),
272 )]
273 pub overwrite: bool,
274
275 #[clap(
277 env,
278 short,
279 long,
280 num_args=0..=1,
281 action = clap::ArgAction::SetFalse,
282 default_value_t = true,
283 conflicts_with = "batch_mode",
284 value_parser = BoolishValueParser::new(),
285 )]
286 pub check_last_installed: bool,
287
288 #[clap(env, short = 'i', long, default_value_t = 500)]
290 pub tick: u64,
291
292 #[clap(env, short = '0', long, default_value_t = 10)]
294 pub lookback: usize,
295
296 #[clap(
298 env,
299 long,
300 num_args=0..=1,
301 action = clap::ArgAction::SetTrue,
302 default_value_t = false,
303 value_parser = BoolishValueParser::new(),
304 )]
305 pub casefold: bool,
306
307 #[clap(short = 'k', long, use_value_delimiter = true, value_delimiter = ',')]
309 pub generic_weidu_args: Vec<String>,
310
311 #[clap(
313 env,
314 long,
315 num_args=0..=1,
316 action = clap::ArgAction::SetTrue,
317 default_value_t = false,
318 conflicts_with = "check_last_installed",
319 value_parser = BoolishValueParser::new(),
320 )]
321 pub batch_mode: bool,
322}
323
324pub fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
325 let path = PathBuf::from(arg);
326 path.try_exists()?;
327 Ok(path)
328}
329
330fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
331 fs::canonicalize(path_must_exist(arg)?)
332}
333
334fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
335 let path = path_must_exist(arg).map_err(|err| err.to_string())?;
336 fs::canonicalize(path).map_err(|err| err.to_string())
337}
338
339fn find_weidu_bin() -> PathBuf {
340 if let Some(paths) = var_os("PATH") {
341 for path in split_paths(&paths) {
342 let full_path = path.join(WEIDU_FILE_NAME);
343 if full_path.is_file() && !full_path.is_dir() {
344 return full_path;
345 }
346 }
347 }
348 PathBuf::new()
349}
350
351fn find_weidu_bin_on_path() -> OsStr {
352 OsStr::from(find_weidu_bin().into_os_string())
353}
354
355fn current_work_dir() -> Vec<PathBuf> {
356 vec![std::env::current_dir().unwrap_or_default()]
357}
358
359fn working_dir() -> OsStr {
360 OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
361}
362
363#[cfg(test)]
364mod tests {
365
366 use super::*;
367 use pretty_assertions::assert_eq;
368 use std::{error::Error, path::PathBuf};
369
370 #[test]
371 fn test_bool_flags() -> Result<(), Box<dyn Error>> {
372 let workspace_root: PathBuf = std::env::current_dir()?;
373 let fake_game_dir: PathBuf = workspace_root.join("fixtures");
374 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
375 let fake_log_file = fake_game_dir.clone().join("weidu.log");
376 let fake_mod_dirs = fake_game_dir.clone().join("mods");
377 let tests = vec![
378 ("true", true),
379 ("t", true),
380 ("yes", true),
381 ("y", true),
382 ("1", true),
383 ("false", false),
384 ("f", false),
385 ("no", false),
386 ("n", false),
387 ("0", false),
388 ];
389 for (flag_value, expected_flag_value) in tests {
390 let expected = Args {
391 command: CommandType::Normal(Normal {
392 log_file: fake_log_file.clone(),
393 game_directory: fake_game_dir.clone(),
394 generate_directory: None,
395 options: Options {
396 weidu_binary: fake_weidu_bin.clone(),
397 mod_directories: vec![fake_mod_dirs.clone()],
398 language: "en_US".to_string(),
399 depth: 5,
400 skip_installed: expected_flag_value,
401 abort_on_warnings: expected_flag_value,
402 timeout: 3600,
403 weidu_log_mode: vec![
404 LogOptions::AutoLog,
405 LogOptions::LogAppend,
406 LogOptions::LogExternal,
407 ],
408 strict_matching: true,
409 download: true,
410 overwrite: false,
411 check_last_installed: true,
412 tick: 500,
413 lookback: 10,
414 casefold: false,
415 never_abort: false,
416 generic_weidu_args: vec![],
417 batch_mode: false,
418 },
419 }),
420 };
421 let test_arg_string = format!(
422 "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
423 flag_value,
424 flag_value,
425 fake_weidu_bin.to_str().unwrap_or_default(),
426 fake_mod_dirs.to_str().unwrap_or_default(),
427 fake_log_file.to_str().unwrap_or_default(),
428 fake_game_dir.to_str().unwrap_or_default(),
429 );
430 let result = Args::parse_from(test_arg_string.split(' '));
431 assert_eq!(
432 result, expected,
433 "Result {result:?} didn't match Expected {expected:?}",
434 );
435 }
436 Ok(())
437 }
438
439 #[test]
440 fn test_eet_flags() -> Result<(), Box<dyn Error>> {
441 let workspace_root: PathBuf = std::env::current_dir()?;
442 let fake_game_dir: PathBuf = workspace_root.join("fixtures");
443 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
444 let fake_log_file = fake_game_dir.clone().join("weidu.log");
445 let new_dir = PathBuf::new().join("test");
446 let expected_flag_value = true;
447
448 let expected = Args {
449 command: CommandType::Eet(Eet {
450 bg1_game_directory: fake_game_dir.clone(),
451 bg1_log_file: fake_log_file.clone(),
452 bg2_game_directory: fake_game_dir.clone(),
453 bg2_log_file: fake_log_file.clone(),
454 options: Options {
455 weidu_binary: fake_weidu_bin.clone(),
456 mod_directories: vec![std::env::current_dir().unwrap()],
457 language: "en_US".to_string(),
458 depth: 5,
459 skip_installed: expected_flag_value,
460 abort_on_warnings: !expected_flag_value,
461 timeout: 3600,
462 weidu_log_mode: vec![
463 LogOptions::AutoLog,
464 LogOptions::LogAppend,
465 LogOptions::LogExternal,
466 ],
467 strict_matching: !expected_flag_value,
468 download: expected_flag_value,
469 overwrite: !expected_flag_value,
470 check_last_installed: expected_flag_value,
471 tick: 500,
472 lookback: 10,
473 casefold: false,
474 never_abort: false,
475 generic_weidu_args: vec![],
476 batch_mode: false,
477 },
478 new_pre_eet_dir: None,
479 new_eet_dir: Some("test".into()),
480 }),
481 };
482 let test_arg_string = format!(
483 "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
484 fake_weidu_bin.to_str().unwrap_or_default(),
485 fake_game_dir.to_str().unwrap_or_default(),
486 fake_log_file.to_str().unwrap_or_default(),
487 fake_game_dir.to_str().unwrap_or_default(),
488 fake_log_file.to_str().unwrap_or_default(),
489 new_dir.to_str().unwrap_or_default(),
490 );
491 let result = Args::parse_from(test_arg_string.split(' '));
492 assert_eq!(
493 result, expected,
494 "Result {result:?} didn't match Expected {expected:?}",
495 );
496
497 Ok(())
498 }
499 #[test]
500 fn test_log_flags() -> Result<(), Box<dyn Error>> {
501 let workspace_root: PathBuf = std::env::current_dir()?;
502 let fake_game_dir: PathBuf = workspace_root.join("fixtures");
503 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
504 let fake_log_file = fake_game_dir.clone().join("weidu.log");
505 let fake_mod_dirs = fake_game_dir.clone().join("mods");
506 let log_file = tempfile::NamedTempFile::new()?;
507 let log_file_path = log_file.path().as_os_str().to_str().unwrap_or_default();
508 let expected = Args {
509 command: CommandType::Normal(Normal {
510 log_file: fake_log_file.clone(),
511 game_directory: fake_game_dir.clone(),
512 generate_directory: None,
513 options: Options {
514 weidu_binary: fake_weidu_bin.clone(),
515 mod_directories: vec![fake_mod_dirs.clone()],
516 language: "en_US".to_string(),
517 depth: 5,
518 skip_installed: true,
519 abort_on_warnings: false,
520 timeout: 3600,
521 weidu_log_mode: vec![
522 LogOptions::AutoLog,
523 LogOptions::LogAppend,
524 LogOptions::LogExternal,
525 #[cfg(not(target_os = "windows"))]
526 LogOptions::Log(log_file.path().canonicalize()?),
527 #[cfg(windows)]
528 LogOptions::Log(log_file.path()),
529 ],
530 strict_matching: false,
531 download: true,
532 overwrite: false,
533 check_last_installed: true,
534 tick: 500,
535 lookback: 10,
536 casefold: false,
537 never_abort: false,
538 generic_weidu_args: vec![],
539 batch_mode: false,
540 },
541 }),
542 };
543 let log_arg = format!("autolog,logapp,log-extern,log {log_file_path}");
544 let test_arg_string = vec![
545 "mod_installer",
546 "-n",
547 "-w",
548 fake_weidu_bin.to_str().unwrap_or_default(),
549 "-m",
550 fake_mod_dirs.to_str().unwrap_or_default(),
551 "--log-file",
552 fake_log_file.to_str().unwrap_or_default(),
553 "--game-directory",
554 fake_game_dir.to_str().unwrap_or_default(),
555 "--weidu-log-mode",
556 &log_arg,
557 ];
558 let result = Args::parse_from(test_arg_string);
559 assert_eq!(
560 result, expected,
561 "Result {result:?} didn't match Expected {expected:?}",
562 );
563 Ok(())
564 }
565}