ajour_core/config/
mod.rs

1use crate::catalog;
2use crate::error::FilesystemError;
3use crate::repository::CompressionFormat;
4use glob::MatchOptions;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fmt::{self, Display, Formatter};
8use std::fs::create_dir_all;
9use std::path::{Path, PathBuf};
10
11mod addons;
12mod wow;
13
14use crate::fs::PersistentData;
15
16pub use crate::config::addons::Addons;
17pub use crate::config::wow::{Flavor, Wow};
18
19/// Config struct.
20#[derive(Deserialize, Serialize, Debug, PartialEq, Default, Clone)]
21pub struct Config {
22    #[serde(default)]
23    pub wow: Wow,
24
25    #[serde(default)]
26    pub addons: Addons,
27
28    pub theme: Option<String>,
29
30    #[serde(default)]
31    pub column_config: ColumnConfig,
32
33    pub window_size: Option<(u32, u32)>,
34
35    pub scale: Option<f64>,
36
37    pub backup_directory: Option<PathBuf>,
38
39    #[serde(default)]
40    pub backup_addons: bool,
41
42    #[serde(default)]
43    pub backup_wtf: bool,
44
45    #[serde(default)]
46    pub hide_ignored_addons: bool,
47
48    #[serde(default)]
49    pub self_update_channel: SelfUpdateChannel,
50
51    #[serde(default)]
52    pub weak_auras_account: HashMap<Flavor, String>,
53
54    #[serde(default = "default_true")]
55    pub alternating_row_colors: bool,
56
57    #[serde(default)]
58    pub language: Language,
59
60    #[serde(default)]
61    pub catalog_source: Option<catalog::Source>,
62
63    #[serde(default)]
64    pub auto_update: bool,
65
66    #[serde(default)]
67    pub compression_format: CompressionFormat,
68}
69
70impl Config {
71    /// Returns a `PathBuf` to the flavor directory.
72    pub fn get_flavor_directory_for_flavor(&self, flavor: &Flavor, path: &Path) -> PathBuf {
73        path.join(&flavor.folder_name())
74    }
75
76    /// Returns a `Option<PathBuf>` to the root directory of the Flavor.
77    pub fn get_root_directory_for_flavor(&self, flavor: &Flavor) -> Option<PathBuf> {
78        if let Some(flavor_dir) = self.wow.directories.get(flavor) {
79            Some(flavor_dir.parent().unwrap().to_path_buf())
80        } else {
81            None
82        }
83    }
84
85    /// Returns a `Option<PathBuf>` to the directory containing the addons.
86    /// This will return `None` if no `wow_directory` is set in the config.
87    pub fn get_addon_directory_for_flavor(&self, flavor: &Flavor) -> Option<PathBuf> {
88        let dir = self.wow.directories.get(flavor);
89        match dir {
90            Some(dir) => {
91                // The path to the addons directory
92                let mut addon_dir = dir.join("Interface/AddOns");
93
94                // If path doesn't exist, it could have been modified by the user.
95                // Check for a case-insensitive version and use that instead.
96                if !addon_dir.exists() {
97                    let options = MatchOptions {
98                        case_sensitive: false,
99                        ..Default::default()
100                    };
101
102                    // For some reason the case insensitive pattern doesn't work
103                    // unless we add an actual pattern symbol, hence the `?`.
104                    let pattern = format!("{}/?nterface/?ddons", dir.display());
105
106                    for entry in glob::glob_with(&pattern, options).unwrap() {
107                        if let Ok(path) = entry {
108                            addon_dir = path;
109                        }
110                    }
111                }
112
113                // If flavor dir exists but not addon dir we try to create it.
114                // This state can happen if you do a fresh install of WoW and
115                // launch Ajour before you launch WoW.
116                if dir.exists() && !addon_dir.exists() {
117                    let _ = create_dir_all(&addon_dir);
118                }
119
120                Some(addon_dir)
121            }
122            None => None,
123        }
124    }
125
126    /// Returns a `Option<PathBuf>` to the directory which will hold the
127    /// temporary zip archives.
128    /// This will return `None` if flavor does not have a directory.
129    pub fn get_download_directory_for_flavor(&self, flavor: Flavor) -> Option<PathBuf> {
130        self.wow.directories.get(&flavor).cloned()
131    }
132
133    /// Returns a `Option<PathBuf>` to the WTF directory.
134    /// This will return `None` if no `wow_directory` is set in the config.
135    pub fn get_wtf_directory_for_flavor(&self, flavor: &Flavor) -> Option<PathBuf> {
136        let dir = self.wow.directories.get(flavor);
137        match dir {
138            Some(dir) => {
139                // The path to the WTF directory
140                let mut addon_dir = dir.join("WTF");
141
142                // If path doesn't exist, it could have been modified by the user.
143                // Check for a case-insensitive version and use that instead.
144                if !addon_dir.exists() {
145                    let options = MatchOptions {
146                        case_sensitive: false,
147                        ..Default::default()
148                    };
149
150                    // For some reason the case insensitive pattern doesn't work
151                    // unless we add an actual pattern symbol, hence the `?`.
152                    let pattern = format!("{}/?tf", dir.display());
153
154                    for entry in glob::glob_with(&pattern, options).unwrap() {
155                        if let Ok(path) = entry {
156                            addon_dir = path;
157                        }
158                    }
159                }
160
161                Some(addon_dir)
162            }
163            None => None,
164        }
165    }
166}
167
168impl PersistentData for Config {
169    fn relative_path() -> PathBuf {
170        PathBuf::from("ajour.yml")
171    }
172}
173
174#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
175pub enum ColumnConfig {
176    V1 {
177        local_version_width: u16,
178        remote_version_width: u16,
179        status_width: u16,
180    },
181    V2 {
182        columns: Vec<ColumnConfigV2>,
183    },
184    V3 {
185        my_addons_columns: Vec<ColumnConfigV2>,
186        catalog_columns: Vec<ColumnConfigV2>,
187        #[serde(default)]
188        aura_columns: Vec<ColumnConfigV2>,
189    },
190}
191
192#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
193pub struct ColumnConfigV2 {
194    pub key: String,
195    pub width: Option<u16>,
196    pub hidden: bool,
197}
198
199impl Default for ColumnConfig {
200    fn default() -> Self {
201        ColumnConfig::V1 {
202            local_version_width: 150,
203            remote_version_width: 150,
204            status_width: 85,
205        }
206    }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210pub enum SelfUpdateChannel {
211    Stable,
212    Beta,
213}
214
215impl SelfUpdateChannel {
216    pub const fn all() -> [Self; 2] {
217        [SelfUpdateChannel::Stable, SelfUpdateChannel::Beta]
218    }
219}
220
221impl Default for SelfUpdateChannel {
222    fn default() -> Self {
223        SelfUpdateChannel::Stable
224    }
225}
226
227impl Display for SelfUpdateChannel {
228    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
229        let s = match self {
230            SelfUpdateChannel::Stable => "Stable",
231            SelfUpdateChannel::Beta => "Beta",
232        };
233
234        write!(f, "{}", s)
235    }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord)]
239pub enum Language {
240    Czech,
241    Norwegian,
242    English,
243    Danish,
244    German,
245    French,
246    Hungarian,
247    Portuguese,
248    Russian,
249    Slovak,
250    Swedish,
251    Spanish,
252    Turkish,
253    Ukrainian,
254}
255
256impl std::fmt::Display for Language {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        write!(
259            f,
260            "{}",
261            match self {
262                Language::Czech => "Čeština",
263                Language::Danish => "Dansk",
264                Language::English => "English",
265                Language::French => "Français",
266                Language::German => "Deutsch",
267                Language::Hungarian => "Magyar",
268                Language::Norwegian => "Norsk Bokmål",
269                Language::Portuguese => "Português",
270                Language::Russian => "Pусский",
271                Language::Slovak => "Slovenčina",
272                Language::Spanish => "Español",
273                Language::Swedish => "Svenska",
274                Language::Turkish => "Türkçe",
275                Language::Ukrainian => "Yкраїнська",
276            }
277        )
278    }
279}
280
281impl Language {
282    // Alphabetically sorted based on their local name (@see `impl Display`).
283    pub const ALL: [Language; 14] = [
284        Language::Czech,
285        Language::Danish,
286        Language::German,
287        Language::English,
288        Language::Spanish,
289        Language::French,
290        Language::Hungarian,
291        Language::Norwegian,
292        Language::Portuguese,
293        Language::Russian,
294        Language::Slovak,
295        Language::Swedish,
296        Language::Turkish,
297        Language::Ukrainian,
298    ];
299
300    pub const fn language_code(self) -> &'static str {
301        match self {
302            Language::Czech => "cs_CZ",
303            Language::English => "en_US",
304            Language::Danish => "da_DK",
305            Language::German => "de_DE",
306            Language::French => "fr_FR",
307            Language::Russian => "ru_RU",
308            Language::Swedish => "se_SE",
309            Language::Spanish => "es_ES",
310            Language::Hungarian => "hu_HU",
311            Language::Norwegian => "nb_NO",
312            Language::Slovak => "sk_SK",
313            Language::Turkish => "tr_TR",
314            Language::Portuguese => "pt_PT",
315            Language::Ukrainian => "uk_UA",
316        }
317    }
318}
319
320impl Default for Language {
321    fn default() -> Language {
322        Language::English
323    }
324}
325
326/// Returns a Config.
327///
328/// This functions handles the initialization of a Config.
329pub async fn load_config() -> Result<Config, FilesystemError> {
330    log::debug!("loading config");
331
332    Ok(Config::load_or_default()?)
333}
334
335const fn default_true() -> bool {
336    true
337}