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