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