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