ck3_history_extractor/
args.rs

1use 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
16/// The languages supported by the game.
17const LANGUAGES: [&'static str; 7] = [
18    "english",
19    "french",
20    "german",
21    "korean",
22    "russian",
23    "simp_chinese",
24    "spanish",
25];
26
27/// A [Completion] struct for save file names, that also acts as a list of save files in the current directory.
28struct 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
74/// A function to validate the file path input.
75fn 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
91/// A function to validate the path input.
92fn 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
108/// A function to parse the language argument.
109fn 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
116/// A function to parse the path argument.
117fn 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/// The arguments to the program.
127#[derive(Parser)]
128pub struct Args {
129    #[arg(value_parser = parse_path_arg)]
130    /// The path to the save file.
131    pub filename: PathBuf,
132    #[arg(short, long, default_value_t = 3)]
133    /// The depth to render the player's history.
134    pub depth: usize,
135    #[arg(short, long, default_value_t = LANGUAGES[0], value_parser = parse_lang_arg)]
136    /// The language to use for localization.
137    pub language: &'static str,
138    #[arg(short, long, default_value = None, value_parser = parse_path_arg)]
139    /// The path to the game files.
140    pub game_path: Option<PathBuf>,
141    #[arg(short, long, value_parser = parse_path_arg)]
142    /// The paths to include in the rendering.
143    pub include: Vec<PathBuf>,
144    #[arg(short, long, default_value = ".", value_parser = parse_path_arg)]
145    /// The output path for the rendered files.
146    pub output: PathBuf,
147    #[arg(long, default_value = None,)]
148    /// A flag that tells the program to dump the game state to a json file.
149    pub dump: Option<PathBuf>,
150    #[arg(long,default_value = None,)]
151    /// A path to a file to dump the game data to.
152    pub dump_data: Option<PathBuf>,
153    #[arg(long, default_value_t = false)]
154    /// A flag that tells the program not to render any images.
155    pub no_vis: bool,
156    #[arg(short, long, default_value_t = false)]
157    /// A flag that tells the program not to interact with the user.
158    pub no_interaction: bool,
159    #[arg(short, long, default_value_t = false)]
160    /// A flag that tells the program to use the internal templates instead of the templates in the `templates` folder.
161    pub use_internal: bool,
162}
163
164impl Args {
165    /// Create the object based on user input.
166    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        //console interface only if we are in a terminal
170        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}