ck3_history_extractor/
args.rs1use clap_derive::Parser;
2use derive_more::Display;
3use dialoguer::{Completion, Input, MultiSelect, Select};
4
5use std::{
6 error,
7 fmt::Debug,
8 fs,
9 path::{Path, PathBuf},
10};
11
12use super::steam::{get_game_path, get_library_path, get_mod_paths, SteamError, CK3_PATH};
13
14const CK3_EXTENSION: &str = "ck3";
15
16const LANGUAGES: [&'static str; 7] = [
18 "english",
19 "french",
20 "german",
21 "korean",
22 "russian",
23 "simp_chinese",
24 "spanish",
25];
26
27struct SaveFileNameCompletion {
29 save_files: Vec<String>,
30}
31
32impl Default for SaveFileNameCompletion {
33 fn default() -> Self {
34 let mut res = Vec::new();
35 let path = Path::new(".");
36 if path.is_dir() {
37 for entry in fs::read_dir(path).expect("Directory not found") {
38 let entry = entry.expect("Unable to read entry").path();
39 if entry.is_file() {
40 if let Some(ext) = entry.extension() {
41 if ext == CK3_EXTENSION {
42 res.push(entry.to_string_lossy().into_owned());
43 }
44 }
45 }
46 }
47 }
48 SaveFileNameCompletion { save_files: res }
49 }
50}
51
52impl Completion for SaveFileNameCompletion {
53 fn get(&self, input: &str) -> Option<String> {
54 self.save_files.iter().find(|x| x.contains(input)).cloned()
55 }
56}
57
58#[derive(Debug, Display)]
59enum InvalidPath {
60 #[display("invalid path (does not exist)")]
61 InvalidPath,
62 #[display("not a file")]
63 NotAFile,
64 #[display("not a directory")]
65 NotADir,
66}
67
68impl error::Error for InvalidPath {
69 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
70 None
71 }
72}
73
74fn validate_file_path(input: &String) -> Result<(), InvalidPath> {
76 if input.is_empty() {
77 return Ok(());
78 }
79 let p = Path::new(input);
80 if p.exists() {
81 if p.is_file() {
82 return Ok(());
83 } else {
84 return Err(InvalidPath::NotAFile);
85 }
86 } else {
87 return Err(InvalidPath::InvalidPath);
88 }
89}
90
91fn validate_dir_path(input: &String) -> Result<(), InvalidPath> {
93 if input.is_empty() {
94 return Ok(());
95 }
96 let p = Path::new(input);
97 if p.exists() {
98 if p.is_dir() {
99 return Ok(());
100 } else {
101 return Err(InvalidPath::NotADir);
102 }
103 } else {
104 return Err(InvalidPath::InvalidPath);
105 }
106}
107
108fn parse_lang_arg(input: &str) -> Result<&'static str, &'static str> {
110 LANGUAGES
111 .iter()
112 .find(|x| **x == input)
113 .map_or(Err("Invalid language"), |e| Ok(*e))
114}
115
116fn parse_path_arg(input: &str) -> Result<PathBuf, &'static str> {
118 let p = PathBuf::from(input);
119 if p.exists() {
120 Ok(p)
121 } else {
122 Err("Invalid path")
123 }
124}
125
126#[derive(Parser)]
128pub struct Args {
129 #[arg(value_parser = parse_path_arg)]
130 pub filename: PathBuf,
132 #[arg(short, long, default_value_t = 3)]
133 pub depth: usize,
135 #[arg(short, long, default_value_t = LANGUAGES[0], value_parser = parse_lang_arg)]
136 pub language: &'static str,
138 #[arg(short, long, default_value = None, value_parser = parse_path_arg)]
139 pub game_path: Option<PathBuf>,
141 #[arg(short, long, value_parser = parse_path_arg)]
142 pub include: Vec<PathBuf>,
144 #[arg(short, long, default_value = ".", value_parser = parse_path_arg)]
145 pub output: PathBuf,
147 #[arg(long, default_value = None,)]
148 pub dump: Option<PathBuf>,
150 #[arg(long,default_value = None,)]
151 pub dump_data: Option<PathBuf>,
153 #[arg(long, default_value_t = false)]
154 pub no_vis: bool,
156 #[arg(short, long, default_value_t = false)]
157 pub no_interaction: bool,
159 #[arg(short, long, default_value_t = false)]
160 pub use_internal: bool,
162}
163
164impl Args {
165 pub fn get_from_user() -> Self {
167 println!("Welcome to CK3 save parser!");
168 println!("Tab autocompletes the query, arrows cycle through possible options, space toggles selection and enter confirms the selection.");
169 let completion = SaveFileNameCompletion::default();
171 let filename = PathBuf::from(
172 Input::<String>::new()
173 .with_prompt("Enter the save file path")
174 .validate_with(validate_file_path)
175 .with_initial_text(completion.save_files.get(0).unwrap_or(&"".to_string()))
176 .completion_with(&completion)
177 .interact_text()
178 .unwrap(),
179 );
180 let ck3_path;
181 let mut mod_paths = Vec::new();
182 match get_library_path() {
183 Ok(p) => {
184 ck3_path = get_game_path(&p).unwrap_or_else(|e| {
185 eprintln!("Error trying to find your CK3 installation: {}", e);
186 CK3_PATH.into()
187 });
188 get_mod_paths(&p, &mut mod_paths).unwrap_or_else(|e| {
189 eprintln!("Error trying to find your CK3 mods: {}", e);
190 });
191 }
192 Err(e) => {
193 ck3_path = CK3_PATH.into();
194 if !matches!(e, SteamError::SteamDirNotFound | SteamError::CK3Missing) {
195 eprintln!("Error trying to find your CK3 installation: {}", e);
196 }
197 }
198 };
199 let game_path = Input::<String>::new()
200 .with_prompt("Enter the game path [empty for None]")
201 .allow_empty(true)
202 .validate_with(validate_dir_path)
203 .with_initial_text(ck3_path.to_string_lossy())
204 .interact_text()
205 .map_or(None, |x| {
206 if x.is_empty() {
207 None
208 } else {
209 Some(PathBuf::from(x))
210 }
211 });
212 let depth = Input::<usize>::new()
213 .with_prompt("Enter the rendering depth")
214 .default(3)
215 .interact()
216 .unwrap();
217 let include_paths = if mod_paths.len() > 0 {
218 let mod_selection = MultiSelect::new()
219 .with_prompt("Select the mods to include")
220 .items(&mod_paths)
221 .interact()
222 .unwrap();
223 mod_selection
224 .iter()
225 .map(|i| mod_paths[*i].as_ref().clone())
226 .collect::<Vec<_>>()
227 } else {
228 Vec::new()
229 };
230 let mut language = LANGUAGES[0];
231 if game_path.is_some() || !include_paths.is_empty() {
232 let language_selection = Select::new()
233 .with_prompt("Choose the localization language")
234 .items(&LANGUAGES)
235 .default(0)
236 .interact()
237 .unwrap();
238 if language_selection != 0 {
239 language = LANGUAGES[language_selection];
240 }
241 }
242 let output_path = Input::<String>::new()
243 .with_prompt("Enter the output path [empty for cwd]")
244 .allow_empty(true)
245 .validate_with(validate_dir_path)
246 .interact()
247 .map(|x| {
248 if x.is_empty() {
249 PathBuf::from(".")
250 } else {
251 PathBuf::from(x)
252 }
253 })
254 .unwrap();
255 Args {
256 filename,
257 depth,
258 language,
259 game_path,
260 include: include_paths,
261 output: output_path,
262 dump: None,
263 dump_data: None,
264 no_vis: false,
265 no_interaction: false,
266 use_internal: false,
267 }
268 }
269}