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