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 #[clap(short = 'k', long, use_value_delimiter = true, value_delimiter = ',')]
304 pub generic_weidu_args: Vec<String>,
305}
306
307pub fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
308 let path = PathBuf::from(arg);
309 path.try_exists()?;
310 Ok(path)
311}
312
313fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
314 fs::canonicalize(path_must_exist(arg)?)
315}
316
317fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
318 let path = path_must_exist(arg).map_err(|err| err.to_string())?;
319 fs::canonicalize(path).map_err(|err| err.to_string())
320}
321
322fn find_weidu_bin() -> PathBuf {
323 if let Some(paths) = var_os("PATH") {
324 for path in split_paths(&paths) {
325 let full_path = path.join(WEIDU_FILE_NAME);
326 if full_path.is_file() && !full_path.is_dir() {
327 return full_path;
328 }
329 }
330 }
331 PathBuf::new()
332}
333
334fn find_weidu_bin_on_path() -> OsStr {
335 OsStr::from(find_weidu_bin().into_os_string())
336}
337
338fn current_work_dir() -> Vec<PathBuf> {
339 vec![std::env::current_dir().unwrap()]
340}
341
342fn working_dir() -> OsStr {
343 OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
344}
345
346#[cfg(test)]
347mod tests {
348
349 use super::*;
350 use pretty_assertions::assert_eq;
351 use std::{error::Error, path::PathBuf};
352
353 #[test]
354 fn test_bool_flags() -> Result<(), Box<dyn Error>> {
355 let workspace_root: PathBuf = std::env::current_dir()?;
356 let fake_game_dir: PathBuf = workspace_root
357 .parent()
358 .ok_or("Could not get workspace root")?
359 .join("fixtures");
360 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
361 let fake_log_file = fake_game_dir.clone().join("weidu.log");
362 let fake_mod_dirs = fake_game_dir.clone().join("mods");
363 let tests = vec![
364 ("true", true),
365 ("t", true),
366 ("yes", true),
367 ("y", true),
368 ("1", true),
369 ("false", false),
370 ("f", false),
371 ("no", false),
372 ("n", false),
373 ("0", false),
374 ];
375 for (flag_value, expected_flag_value) in tests {
376 let expected = Args {
377 command: CommandType::Normal(Normal {
378 log_file: fake_log_file.clone(),
379 game_directory: fake_game_dir.clone(),
380 generate_directory: None,
381 options: Options {
382 weidu_binary: fake_weidu_bin.clone(),
383 mod_directories: vec![fake_mod_dirs.clone()],
384 language: "en_US".to_string(),
385 depth: 5,
386 skip_installed: expected_flag_value,
387 abort_on_warnings: expected_flag_value,
388 timeout: 3600,
389 weidu_log_mode: vec![
390 LogOptions::AutoLog,
391 LogOptions::LogAppend,
392 LogOptions::LogExternal,
393 ],
394 strict_matching: true,
395 download: true,
396 overwrite: false,
397 check_last_installed: true,
398 tick: 500,
399 lookback: 10,
400 casefold: false,
401 never_abort: false,
402 generic_weidu_args: vec![],
403 },
404 }),
405 };
406 let test_arg_string = format!(
407 "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
408 flag_value,
409 flag_value,
410 fake_weidu_bin.to_str().unwrap_or_default(),
411 fake_mod_dirs.to_str().unwrap_or_default(),
412 fake_log_file.to_str().unwrap_or_default(),
413 fake_game_dir.to_str().unwrap_or_default(),
414 );
415 let result = Args::parse_from(test_arg_string.split(' '));
416 assert_eq!(
417 result, expected,
418 "Result {result:?} didn't match Expected {expected:?}",
419 );
420 }
421 Ok(())
422 }
423
424 #[test]
425 fn test_eet_flags() -> Result<(), Box<dyn Error>> {
426 let workspace_root: PathBuf = std::env::current_dir()?;
427 let fake_game_dir: PathBuf = workspace_root
428 .parent()
429 .ok_or("Could not get workspace root")?
430 .join("fixtures");
431 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
432 let fake_log_file = fake_game_dir.clone().join("weidu.log");
433 let new_dir = PathBuf::new().join("test");
434 let expected_flag_value = true;
435
436 let expected = Args {
437 command: CommandType::Eet(Eet {
438 bg1_game_directory: fake_game_dir.clone(),
439 bg1_log_file: fake_log_file.clone(),
440 bg2_game_directory: fake_game_dir.clone(),
441 bg2_log_file: fake_log_file.clone(),
442 options: Options {
443 weidu_binary: fake_weidu_bin.clone(),
444 mod_directories: vec![std::env::current_dir().unwrap()],
445 language: "en_US".to_string(),
446 depth: 5,
447 skip_installed: expected_flag_value,
448 abort_on_warnings: !expected_flag_value,
449 timeout: 3600,
450 weidu_log_mode: vec![
451 LogOptions::AutoLog,
452 LogOptions::LogAppend,
453 LogOptions::LogExternal,
454 ],
455 strict_matching: !expected_flag_value,
456 download: expected_flag_value,
457 overwrite: !expected_flag_value,
458 check_last_installed: expected_flag_value,
459 tick: 500,
460 lookback: 10,
461 casefold: false,
462 never_abort: false,
463 generic_weidu_args: vec![],
464 },
465 new_pre_eet_dir: None,
466 new_eet_dir: Some("test".into()),
467 }),
468 };
469 let test_arg_string = format!(
470 "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
471 fake_weidu_bin.to_str().unwrap_or_default(),
472 fake_game_dir.to_str().unwrap_or_default(),
473 fake_log_file.to_str().unwrap_or_default(),
474 fake_game_dir.to_str().unwrap_or_default(),
475 fake_log_file.to_str().unwrap_or_default(),
476 new_dir.to_str().unwrap_or_default(),
477 );
478 let result = Args::parse_from(test_arg_string.split(' '));
479 assert_eq!(
480 result, expected,
481 "Result {result:?} didn't match Expected {expected:?}",
482 );
483
484 Ok(())
485 }
486 #[test]
487 fn test_log_flags() -> Result<(), Box<dyn Error>> {
488 let workspace_root: PathBuf = std::env::current_dir()?;
489 let fake_game_dir: PathBuf = workspace_root
490 .parent()
491 .ok_or("Could not get workspace root")?
492 .join("fixtures");
493 let fake_weidu_bin = fake_game_dir.clone().join("weidu");
494 let fake_log_file = fake_game_dir.clone().join("weidu.log");
495 let fake_mod_dirs = fake_game_dir.clone().join("mods");
496 let log_file = tempfile::NamedTempFile::new()?;
497 let log_file_path = log_file.path().as_os_str().to_str().unwrap_or_default();
498 let expected = Args {
499 command: CommandType::Normal(Normal {
500 log_file: fake_log_file.clone(),
501 game_directory: fake_game_dir.clone(),
502 generate_directory: None,
503 options: Options {
504 weidu_binary: fake_weidu_bin.clone(),
505 mod_directories: vec![fake_mod_dirs.clone()],
506 language: "en_US".to_string(),
507 depth: 5,
508 skip_installed: true,
509 abort_on_warnings: false,
510 timeout: 3600,
511 weidu_log_mode: vec![
512 LogOptions::AutoLog,
513 LogOptions::LogAppend,
514 LogOptions::LogExternal,
515 LogOptions::Log(log_file_path.into()),
516 ],
517 strict_matching: false,
518 download: true,
519 overwrite: false,
520 check_last_installed: true,
521 tick: 500,
522 lookback: 10,
523 casefold: false,
524 never_abort: false,
525 generic_weidu_args: vec![],
526 },
527 }),
528 };
529 let log_arg = format!("autolog,logapp,log-extern,log {log_file_path}");
530 let test_arg_string = vec![
531 "mod_installer",
532 "-n",
533 "-w",
534 fake_weidu_bin.to_str().unwrap_or_default(),
535 "-m",
536 fake_mod_dirs.to_str().unwrap_or_default(),
537 "--log-file",
538 fake_log_file.to_str().unwrap_or_default(),
539 "--game-directory",
540 fake_game_dir.to_str().unwrap_or_default(),
541 "--weidu-log-mode",
542 &log_arg,
543 ];
544 let result = Args::parse_from(test_arg_string);
545 assert_eq!(
546 result, expected,
547 "Result {result:?} didn't match Expected {expected:?}",
548 );
549 Ok(())
550 }
551}