tpnote_lib/
settings.rs

1//! Configuration data that origins from environment variables.
2//! Unlike the configuration data in `LIB_CFG` which is sourced only once when
3//! Tp-Note is launched, the `SETTINGS` object may be sourced more often in
4//! order to follow changes in the related environment variables.
5
6use crate::config::{GetLang, Mode, LIB_CFG};
7use crate::error::LibCfgError;
8#[cfg(feature = "lang-detection")]
9use lingua;
10#[cfg(feature = "lang-detection")]
11use lingua::IsoCode639_1;
12use parking_lot::RwLock;
13use std::borrow::Cow;
14use std::collections::BTreeMap;
15use std::env;
16#[cfg(feature = "lang-detection")]
17use std::str::FromStr;
18#[cfg(target_family = "windows")]
19use windows_sys::Win32::Globalization::GetUserDefaultLocaleName;
20#[cfg(target_family = "windows")]
21use windows_sys::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH;
22
23/// The name of the environment variable which can be optionally set to
24/// overwrite the `scheme_default` configuration file setting.
25pub const ENV_VAR_TPNOTE_SCHEME: &str = "TPNOTE_SCHEME";
26
27/// The name of the environment variable which can be optionally set to
28/// overwrite the `filename.extension_default` configuration file setting.
29pub const ENV_VAR_TPNOTE_EXTENSION_DEFAULT: &str = "TPNOTE_EXTENSION_DEFAULT";
30
31/// Name of the environment variable, that can be optionally
32/// used to overwrite the user's default language setting, which is
33/// accessible as `{{ lang }}` template variable and used in various
34/// templates.
35pub const ENV_VAR_TPNOTE_LANG: &str = "TPNOTE_LANG";
36
37/// A pseudo language tag for the `get_lang_filter`. When placed in the
38/// `ENV_VAR_TPNOTE_LANG` list, all available languages are selected.
39pub const ENV_VAR_TPNOTE_LANG_PLUS_ALL: &str = "+all";
40
41/// Name of the environment variable, that can be optionally
42/// used to overwrite the user's `tmpl.filter.get_lang.language_candidates`
43/// and `tmpl.filter.map_lang` configuration file setting.
44pub const ENV_VAR_TPNOTE_LANG_DETECTION: &str = "TPNOTE_LANG_DETECTION";
45
46/// Name of the environment variable, that can be optionally
47/// used to overwrite the user's login name. The result is accessible as
48/// `{{ username }}` template variable and used in various templates.
49pub const ENV_VAR_TPNOTE_USER: &str = "TPNOTE_USER";
50
51/// Name of the `LOGNAME` environment variable.
52const ENV_VAR_LOGNAME: &str = "LOGNAME";
53
54/// Name of the `USERNAME` environment variable.
55const ENV_VAR_USERNAME: &str = "USERNAME";
56
57/// Name of the `USER` environment variable.
58const ENV_VAR_USER: &str = "USER";
59
60/// Name of the `LANG` environment variable.
61#[cfg(not(target_family = "windows"))]
62const ENV_VAR_LANG: &str = "LANG";
63
64/// Struct containing additional user configuration read from or depending
65/// on environment variables.
66#[derive(Debug)]
67#[allow(dead_code)]
68pub(crate) struct Settings {
69    /// This is the index as the schemes are listed in the config file.
70    pub current_scheme: usize,
71    /// This has the format of a login name.
72    pub author: String,
73    /// [RFC 5646, Tags for the Identification of Languages](http://www.rfc-editor.org/rfc/rfc5646.txt)
74    /// This will be injected as `lang` variable into content templates.
75    pub lang: String,
76    /// Extension without dot, e.g. `md`
77    pub extension_default: String,
78    /// See definition of type.
79    pub get_lang_filter: GetLang,
80    /// The keys and values from
81    /// `LIB_CFG.schemes[settings.current_scheme].tmpl.filter_btmap_lang` in the `BTreeMap`
82    /// with the user's default language and region added.
83    pub map_lang_filter_btmap: Option<BTreeMap<String, String>>,
84}
85
86const DEFAULT_SETTINGS: Settings = Settings {
87    current_scheme: 0,
88    author: String::new(),
89    lang: String::new(),
90    extension_default: String::new(),
91    get_lang_filter: GetLang {
92        mode: Mode::Disabled,
93        language_candidates: vec![],
94        relative_distance_min: 0.0,
95        consecutive_words_min: 0,
96        words_total_percentage_min: 0,
97    },
98    map_lang_filter_btmap: None,
99};
100
101impl Default for Settings {
102    #[cfg(not(any(test, doc)))]
103    /// Defaults to empty lists and values.
104    fn default() -> Self {
105        DEFAULT_SETTINGS
106    }
107
108    #[cfg(any(test, doc))]
109    /// Defaults to test values.
110    /// Do not use outside of tests.
111    fn default() -> Self {
112        let mut settings = DEFAULT_SETTINGS;
113        settings.author = String::from("testuser");
114        settings.lang = String::from("ab-AB");
115        settings.extension_default = String::from("md");
116        settings
117    }
118}
119
120/// Global mutable variable of type `Settings`.
121#[cfg(not(test))]
122pub(crate) static SETTINGS: RwLock<Settings> = RwLock::new(DEFAULT_SETTINGS);
123
124#[cfg(test)]
125/// Global default for `SETTINGS` in test environments.
126pub(crate) static SETTINGS: RwLock<Settings> = RwLock::new(DEFAULT_SETTINGS);
127
128/// Like `Settings::update`, with `scheme_source = SchemeSource::Force("default")`
129/// and `force_lang = None`.
130/// This is used in doctests only.
131pub fn set_test_default_settings() -> Result<(), LibCfgError> {
132    let mut settings = SETTINGS.write();
133    settings.update(SchemeSource::Force("default"), None)
134}
135
136/// How should `update_settings` collect the right scheme?
137#[derive(Debug, Clone)]
138pub(crate) enum SchemeSource<'a> {
139    /// Ignore `TPNOTE_SCHEME_NEW_DEFAULT`, take this.
140    Force(&'a str),
141    /// Take the value `lib_cfg.scheme_sync_default`.
142    SchemeSyncDefault,
143    /// Take `TPNOTE_SCHEME_NEW_DEFAULT` or -if not defined- take this.
144    SchemeNewDefault(&'a str),
145}
146
147impl Settings {
148    /// (Re)read environment variables and store them in the global `SETTINGS`
149    /// object. Some data originates from `LIB_CFG`.
150    /// First it sets `SETTINGS.current_scheme`:
151    /// 1. If `force_theme` is `Some(scheme)`, gets the index and stores result,
152    ///    or,
153    /// 2. if `force_theme` is `Some("")`, stores `lib_cfg.scheme_sync_default`,
154    ///    or,
155    /// 3. reads the environment variable `TPNOTE_SCHEME_NEW_DEFAULT`
156    ///    or, -if empty-
157    /// 4. copies `scheme_new_default` into `SETTINGS.current_scheme`.
158    ///
159    /// Then, it sets all other fields.
160    /// `force_lang=Some(_)` disables the `get_lang` filter by setting
161    /// `get_lang_filter.mode` to `Mode::Disabled`.
162    /// When `force_lang(l)` is `Some`, it sets `SETTINGS.lang` to `l`.
163    pub(crate) fn update(
164        &mut self,
165        scheme_source: SchemeSource,
166        force_lang: Option<&str>,
167    ) -> Result<(), LibCfgError> {
168        self.update_current_scheme(scheme_source)?;
169        self.update_author();
170        self.update_extension_default();
171        self.update_lang(force_lang);
172        self.update_get_lang_filter(force_lang.is_some());
173        self.update_map_lang_filter_btmap();
174        self.update_env_lang_detection(force_lang.is_some());
175
176        log::trace!(
177            "`SETTINGS` updated (reading config + env. vars.):\n{:#?}",
178            self
179        );
180
181        if let Mode::Error(e) = &self.get_lang_filter.mode {
182            Err(e.clone())
183        } else {
184            Ok(())
185        }
186    }
187
188    /// Sets `SETTINGS.current_scheme`:
189    fn update_current_scheme(&mut self, scheme_source: SchemeSource) -> Result<(), LibCfgError> {
190        let lib_cfg = LIB_CFG.read_recursive();
191
192        let scheme = match scheme_source {
193            SchemeSource::Force(s) => Cow::Borrowed(s),
194            SchemeSource::SchemeSyncDefault => Cow::Borrowed(&*lib_cfg.scheme_sync_default),
195            SchemeSource::SchemeNewDefault(s) => match env::var(ENV_VAR_TPNOTE_SCHEME) {
196                Ok(ed_env) if !ed_env.is_empty() => Cow::Owned(ed_env),
197                Err(_) | Ok(_) => Cow::Borrowed(s),
198            },
199        };
200        self.current_scheme = lib_cfg.scheme_idx(scheme.as_ref())?;
201        Ok(())
202    }
203
204    /// Set `SETTINGS.author` to content of the first not empty environment
205    /// variable: `TPNOTE_USER`, `LOGNAME` or `USER`.
206    fn update_author(&mut self) {
207        let author = env::var(ENV_VAR_TPNOTE_USER).unwrap_or_else(|_| {
208            env::var(ENV_VAR_LOGNAME).unwrap_or_else(|_| {
209                env::var(ENV_VAR_USERNAME)
210                    .unwrap_or_else(|_| env::var(ENV_VAR_USER).unwrap_or_default())
211            })
212        });
213
214        // Store result.
215        self.author = author;
216    }
217
218    /// Read the environment variable `TPNOTE_EXTENSION_DEFAULT` or -if empty-
219    /// the configuration file variable `filename.extension_default` into
220    /// `SETTINGS.extension_default`.
221    fn update_extension_default(&mut self) {
222        // Get the environment variable if it exists.
223        let ext = match env::var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT) {
224            Ok(ed_env) if !ed_env.is_empty() => ed_env,
225            Err(_) | Ok(_) => {
226                let lib_cfg = LIB_CFG.read_recursive();
227                lib_cfg.scheme[self.current_scheme]
228                    .filename
229                    .extension_default
230                    .to_string()
231            }
232        };
233        self.extension_default = ext;
234    }
235
236    /// If `lang=None` read the environment variable `TPNOTE_LANG` or
237    /// -if not defined- `LANG` into `SETTINGS.lang`.
238    /// If `force_lang=Some(l)`, copy `l` in `settings.lang`.
239    fn update_lang(&mut self, force_lang: Option<&str>) {
240        // Overwrite environment setting.
241        if let Some(l) = force_lang {
242            if !l.is_empty() {
243                self.lang = l.to_string();
244                return;
245            }
246        }
247
248        // Get the user's language tag.
249        // [RFC 5646, Tags for the Identification of Languages](http://www.rfc-editor.org/rfc/rfc5646.txt)
250        let mut lang = String::new();
251        // Get the environment variable if it exists.
252        let tpnotelang = env::var(ENV_VAR_TPNOTE_LANG).ok();
253        // Unix/MacOS version.
254        #[cfg(not(target_family = "windows"))]
255        if let Some(tpnotelang) = tpnotelang {
256            lang = tpnotelang;
257        } else {
258            // [Linux: Define Locale and Language Settings -
259            // ShellHacks](https://www.shellhacks.com/linux-define-locale-language-settings/)
260            if let Ok(lang_env) = env::var(ENV_VAR_LANG) {
261                if !lang_env.is_empty() {
262                    // [ISO 639](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code.
263                    let mut language = "";
264                    // [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes) country code.
265                    let mut territory = "";
266                    if let Some((l, lang_env)) = lang_env.split_once('_') {
267                        language = l;
268                        if let Some((t, _codeset)) = lang_env.split_once('.') {
269                            territory = t;
270                        }
271                    }
272                    lang = language.to_string();
273                    lang.push('-');
274                    lang.push_str(territory);
275                }
276            }
277        }
278
279        // Get the user's language tag.
280        // Windows version.
281        #[cfg(target_family = "windows")]
282        if let Some(tpnotelang) = tpnotelang {
283            lang = tpnotelang;
284        } else {
285            let mut buf = [0u16; LOCALE_NAME_MAX_LENGTH as usize];
286            let len = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32) };
287            if len > 0 {
288                lang = String::from_utf16_lossy(&buf[..((len - 1) as usize)]);
289            }
290        };
291
292        // Store result.
293        self.lang = lang;
294    }
295
296    /// Copies the settings form
297    /// `LIB_CFG.schemes[settings.scheme].tmpl.filter.get_lang` into
298    /// `SETTINGS.get_lang_filter`. Then append the user's
299    /// default language subtag to
300    /// `SETTINGS.get_lang_filter.language_candidates`.
301    /// Errors are stored in the `SETTINGS.filter.get_lang.mode` `Mode::Error(e)` variant.
302    /// If `force_lang` is `true`, set
303    /// `SETTINGS.get_lang_filter.mode = Mode::Disabled`.
304    #[cfg(feature = "lang-detection")]
305    fn update_get_lang_filter(&mut self, force_lang: bool) {
306        use crate::config::Mode;
307
308        {
309            let lib_cfg = LIB_CFG.read_recursive();
310            let current_scheme = &lib_cfg.scheme[self.current_scheme];
311
312            // Start form config.
313            self.get_lang_filter = current_scheme.tmpl.filter.get_lang.clone();
314        } // Release lock.
315
316        // `force_lang` disables the filter.
317        if force_lang {
318            self.get_lang_filter.mode = Mode::Disabled;
319            return;
320        }
321
322        // Check if disabled in config file. Early return.
323        if matches!(self.get_lang_filter.mode, Mode::Disabled) {
324            return;
325        }
326
327        // Read ISO codes from config object.
328        let iso_codes = &mut self.get_lang_filter.language_candidates;
329
330        // Check if all languages are selected, then we can return early.
331        if iso_codes.is_empty() {
332            return;
333        }
334
335        // Add the user's language subtag as reported from the OS.
336        // Silently ignore if anything goes wrong here.
337        if !self.lang.is_empty() {
338            if let Some((lang_subtag, _)) = self.lang.split_once('-') {
339                if let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
340                    if !iso_codes.contains(&iso_code) {
341                        iso_codes.push(iso_code);
342                    }
343                }
344            }
345        }
346
347        // Check if there are at least 2 languages in the list.
348        if iso_codes.len() <= 1 {
349            self.get_lang_filter.mode = Mode::Error(LibCfgError::NotEnoughLanguageCodes {
350                language_code: iso_codes[0].to_string(),
351            })
352        }
353    }
354
355    #[cfg(not(feature = "lang-detection"))]
356    /// Disable filter.
357    fn update_get_lang_filter(&mut self, _force_lang: bool) {
358        self.get_lang_filter.mode = Mode::Disabled;
359    }
360
361    /// Read keys and values from
362    /// `LIB_CFG.schemes[self.current_scheme].tmpl.filter_btmap_lang` in the
363    /// `BTreeMap`. Add the user's default language and region.
364    fn update_map_lang_filter_btmap(&mut self) {
365        let mut btm = BTreeMap::new();
366        let lib_cfg = LIB_CFG.read_recursive();
367        for l in &lib_cfg.scheme[self.current_scheme].tmpl.filter.map_lang {
368            if l.len() >= 2 {
369                btm.insert(l[0].to_string(), l[1].to_string());
370            };
371        }
372        // Insert the user's default language and region in the Map.
373        if !self.lang.is_empty() {
374            if let Some((lang_subtag, _)) = self.lang.split_once('-') {
375                // Do not overwrite existing languages.
376                if !lang_subtag.is_empty() && !btm.contains_key(lang_subtag) {
377                    btm.insert(lang_subtag.to_string(), self.lang.to_string());
378                }
379            };
380        }
381
382        // Store result.
383        self.map_lang_filter_btmap = Some(btm);
384    }
385
386    /// Reads the environment variable `LANG_DETECTION`. If not empty,
387    /// parse the content and overwrite the `self.get_lang_filter` and the
388    /// `self.map_lang_filter` variables.
389    /// Finally, if `force_lang` is true, then it sets
390    /// `self.get_lang_filter.mode` Mode::Disabled.
391    #[cfg(feature = "lang-detection")]
392    fn update_env_lang_detection(&mut self, force_lang: bool) {
393        use crate::config::Mode;
394
395        if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
396            if env_var.is_empty() {
397                // Early return.
398                self.get_lang_filter.mode = Mode::Disabled;
399                self.map_lang_filter_btmap = None;
400                log::debug!(
401                    "Empty env. var. `{}` disables the `lang-detection` feature.",
402                    ENV_VAR_TPNOTE_LANG_DETECTION
403                );
404                return;
405            }
406
407            // Read and convert ISO codes from config object.
408            let mut hm: BTreeMap<String, String> = BTreeMap::new();
409            let mut all_languages_selected = false;
410            let iso_codes = env_var
411                .split(',')
412                .map(|t| {
413                    let t = t.trim();
414                    if let Some((lang_subtag, _)) = t.split_once('-') {
415                        // Do not overwrite existing languages.
416                        if !lang_subtag.is_empty() && !hm.contains_key(lang_subtag) {
417                            hm.insert(lang_subtag.to_string(), t.to_string());
418                        };
419                        lang_subtag
420                    } else {
421                        t
422                    }
423                })
424                // Check if this is the pseudo tag `TMPL_GET_LANG_filter_ALL `.
425                .filter(|&l| {
426                    if l == ENV_VAR_TPNOTE_LANG_PLUS_ALL {
427                        all_languages_selected = true;
428                        // Skip this string.
429                        false
430                    } else {
431                        // Continue.
432                        true
433                    }
434                })
435                .map(|l| {
436                    IsoCode639_1::from_str(l.trim()).map_err(|_| {
437                        // The error path.
438                        // Produce list of all available languages.
439                        let mut all_langs = lingua::Language::all()
440                            .iter()
441                            .map(|l| {
442                                let mut s = l.iso_code_639_1().to_string();
443                                s.push_str(", ");
444                                s
445                            })
446                            .collect::<Vec<String>>();
447                        all_langs.sort();
448                        let mut all_langs = all_langs.into_iter().collect::<String>();
449                        all_langs.truncate(all_langs.len() - ", ".len());
450                        // Insert data into error object.
451                        LibCfgError::ParseLanguageCode {
452                            language_code: l.into(),
453                            all_langs,
454                        }
455                    })
456                })
457                .collect::<Result<Vec<IsoCode639_1>, LibCfgError>>();
458
459            match iso_codes {
460                // The happy path.
461                Ok(mut iso_codes) => {
462                    // Add the user's language subtag as reported from the OS.
463                    // Continue the happy path.
464                    if !self.lang.is_empty() {
465                        if let Some(lang_subtag) = self.lang.split('-').next() {
466                            if let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
467                                if !iso_codes.contains(&iso_code) {
468                                    iso_codes.push(iso_code);
469                                }
470                                // Check if there is a remainder (region code).
471                                if lang_subtag != self.lang && !hm.contains_key(lang_subtag) {
472                                    hm.insert(lang_subtag.to_string(), self.lang.to_string());
473                                }
474                            }
475                        }
476                    }
477
478                    // Store result.
479                    if all_languages_selected {
480                        self.get_lang_filter.language_candidates = vec![];
481                        if matches!(self.get_lang_filter.mode, Mode::Disabled) {
482                            self.get_lang_filter.mode = Mode::Multilingual;
483                        }
484                    } else {
485                        match iso_codes.len() {
486                            0 => self.get_lang_filter.mode = Mode::Disabled,
487                            1 => {
488                                self.get_lang_filter.mode =
489                                    Mode::Error(LibCfgError::NotEnoughLanguageCodes {
490                                        language_code: iso_codes[0].to_string(),
491                                    })
492                            }
493                            _ => {
494                                self.get_lang_filter.language_candidates = iso_codes;
495                                if matches!(self.get_lang_filter.mode, Mode::Disabled) {
496                                    self.get_lang_filter.mode = Mode::Multilingual;
497                                }
498                            }
499                        }
500                    }
501                    self.map_lang_filter_btmap = Some(hm);
502                }
503                // The error path.
504                Err(e) =>
505                // Store error.
506                {
507                    self.get_lang_filter.mode = Mode::Error(e);
508                }
509            }
510
511            // Even is `force_lang` is set and the environment variable is not
512            // in use, we always parse it (see code above) to identify errors.
513            if force_lang {
514                self.get_lang_filter.mode = Mode::Disabled;
515            }
516        }
517    }
518
519    /// Ignore the environment variable `LANG_DETECTION`.
520    #[cfg(not(feature = "lang-detection"))]
521    fn update_env_lang_detection(&mut self, _force_lang: bool) {
522        if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
523            if !env_var.is_empty() {
524                self.get_lang_filter.mode = Mode::Disabled;
525                self.map_lang_filter_btmap = None;
526                log::debug!(
527                    "Ignoring the env. var. `{}`. The `lang-detection` feature \
528                 is not included in this build.",
529                    ENV_VAR_TPNOTE_LANG_DETECTION
530                );
531            }
532        }
533    }
534}
535#[cfg(test)]
536mod tests {
537    use super::*;
538    /// Attention: as these test-functions run in parallel, make sure that
539    /// each environment variable appears in one function only!
540
541    #[test]
542    fn test_update_author_setting() {
543        let mut settings = Settings::default();
544        unsafe {
545            env::set_var(ENV_VAR_LOGNAME, "testauthor");
546        }
547        settings.update_author();
548        assert_eq!(settings.author, "testauthor");
549    }
550
551    #[test]
552    fn test_update_extension_default_setting() {
553        let mut settings = Settings::default();
554        unsafe {
555            env::set_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT, "markdown");
556        }
557        settings.update_extension_default();
558        assert_eq!(settings.extension_default, "markdown");
559
560        let mut settings = Settings::default();
561        unsafe {
562            std::env::remove_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT);
563        }
564        settings.update_extension_default();
565        assert_eq!(settings.extension_default, "md");
566    }
567
568    #[test]
569    #[cfg(not(target_family = "windows"))]
570    fn test_update_lang_setting() {
571        // Test 1
572        let mut settings = Settings::default();
573        unsafe {
574            env::remove_var(ENV_VAR_TPNOTE_LANG);
575            env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
576        }
577        settings.update_lang(None);
578        assert_eq!(settings.lang, "en-GB");
579
580        // Test empty input.
581        let mut settings = Settings::default();
582        unsafe {
583            env::remove_var(ENV_VAR_TPNOTE_LANG);
584            env::set_var(ENV_VAR_LANG, "");
585        }
586        settings.update_lang(None);
587        assert_eq!(settings.lang, "");
588
589        // Test precedence of `TPNOTE_LANG`.
590        let mut settings = Settings::default();
591        unsafe {
592            env::set_var(ENV_VAR_TPNOTE_LANG, "it-IT");
593            env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
594        }
595        settings.update_lang(None);
596        assert_eq!(settings.lang, "it-IT");
597    }
598
599    #[test]
600    #[cfg(feature = "lang-detection")]
601    fn test_update_get_lang_filter_setting() {
602        // Test 1.
603        let mut settings = Settings {
604            lang: "en-GB".to_string(),
605            ..Default::default()
606        };
607        settings.update_get_lang_filter(false);
608
609        let output_get_lang_filter = settings
610            .get_lang_filter
611            .language_candidates
612            .iter()
613            .map(|l| {
614                let mut l = l.to_string();
615                l.push(' ');
616                l
617            })
618            .collect::<String>();
619        assert_eq!(output_get_lang_filter, "en fr de ");
620
621        //
622        // Test 2.
623        let mut settings = Settings {
624            lang: "it-IT".to_string(),
625            ..Default::default()
626        };
627        settings.update_get_lang_filter(false);
628
629        let output_get_lang_filter = settings
630            .get_lang_filter
631            .language_candidates
632            .iter()
633            .map(|l| {
634                let mut l = l.to_string();
635                l.push(' ');
636                l
637            })
638            .collect::<String>();
639        assert_eq!(output_get_lang_filter, "en fr de it ");
640    }
641
642    #[test]
643    fn test_update_map_lang_filter_hmap_setting() {
644        // Test 1.
645        let mut settings = Settings {
646            lang: "it-IT".to_string(),
647            ..Default::default()
648        };
649        settings.update_map_lang_filter_btmap();
650
651        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
652
653        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
654        assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
655        assert_eq!(output_map_lang_filter.get("it").unwrap(), "it-IT");
656
657        //
658        // Test short `settings.lang`.
659        let mut settings = Settings {
660            lang: "it".to_string(),
661            ..Default::default()
662        };
663        settings.update_map_lang_filter_btmap();
664
665        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
666
667        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
668        assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
669        assert_eq!(output_map_lang_filter.get("it"), None);
670    }
671
672    #[test]
673    #[cfg(feature = "lang-detection")]
674    fn test_update_env_lang_detection() {
675        // Test 1.
676        // Test short `settings.lang`.
677        let mut settings = Settings {
678            lang: "en-GB".to_string(),
679            ..Default::default()
680        };
681        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
682        settings.update_env_lang_detection(false);
683
684        let output_get_lang_filter = settings
685            .get_lang_filter
686            .language_candidates
687            .iter()
688            .map(|l| {
689                let mut l = l.to_string();
690                l.push(' ');
691                l
692            })
693            .collect::<String>();
694        assert_eq!(output_get_lang_filter, "fr de hu en ");
695
696        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
697        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
698        assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
699        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
700
701        //
702        // Test 2.
703        let mut settings = Settings {
704            lang: "en-GB".to_string(),
705            ..Default::default()
706        };
707        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en-US") };
708        settings.update_env_lang_detection(false);
709
710        let output_get_lang_filter = settings
711            .get_lang_filter
712            .language_candidates
713            .iter()
714            .map(|l| {
715                let mut l = l.to_string();
716                l.push(' ');
717                l
718            })
719            .collect::<String>();
720        assert_eq!(output_get_lang_filter, "de de en ");
721
722        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
723        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
724        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
725
726        //
727        // Test 3.
728        let mut settings = Settings {
729            lang: "en-GB".to_string(),
730            ..Default::default()
731        };
732        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, en-US") };
733        settings.update_env_lang_detection(false);
734
735        assert!(settings.get_lang_filter.language_candidates.is_empty());
736
737        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
738        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
739        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
740
741        //
742        // Test 4.
743        let mut settings = Settings {
744            lang: "en-GB".to_string(),
745            ..Default::default()
746        };
747        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en") };
748        settings.update_env_lang_detection(false);
749
750        let output_get_lang_filter = settings
751            .get_lang_filter
752            .language_candidates
753            .iter()
754            .map(|l| {
755                let mut l = l.to_string();
756                l.push(' ');
757                l
758            })
759            .collect::<String>();
760        assert_eq!(output_get_lang_filter, "de de en ");
761
762        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
763        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
764        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
765
766        //
767        // Test 5.
768        let mut settings = Settings {
769            lang: "en-GB".to_string(),
770            ..Default::default()
771        };
772        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, de-AT, en") };
773        settings.update_env_lang_detection(false);
774
775        assert!(settings.get_lang_filter.language_candidates.is_empty());
776
777        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
778        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
779        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
780
781        // Test `force_lang`.
782        let mut settings = Settings {
783            lang: "en-GB".to_string(),
784            ..Default::default()
785        };
786        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
787        settings.update_env_lang_detection(true);
788
789        // `force_lang` must disables the `get_lang` filter.
790        assert_eq!(settings.get_lang_filter.mode, Mode::Disabled);
791
792        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
793        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
794        assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
795        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
796
797        //
798        // Test empty env. var.
799        let mut settings = Settings {
800            lang: "".to_string(),
801            ..Default::default()
802        };
803        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "") };
804        settings.update_env_lang_detection(false);
805
806        assert_eq!(settings.get_lang_filter.mode, Mode::Disabled);
807        assert!(settings.map_lang_filter_btmap.is_none());
808
809        //
810        // Test faulty `settings.lang`.
811        let mut settings = Settings {
812            lang: "xy-XY".to_string(),
813            ..Default::default()
814        };
815        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "en-GB, fr") };
816        settings.update_env_lang_detection(false);
817
818        let output_get_lang_filter = settings
819            .get_lang_filter
820            .language_candidates
821            .iter()
822            .map(|l| {
823                let mut l = l.to_string();
824                l.push(' ');
825                l
826            })
827            .collect::<String>();
828        assert_eq!(output_get_lang_filter, "en fr ");
829
830        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
831        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
832
833        //
834        // Test faulty entry in list.
835        let mut settings = Settings {
836            lang: "en-GB".to_string(),
837            ..Default::default()
838        };
839        unsafe {
840            env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, xy-XY");
841        }
842        settings.update_env_lang_detection(false);
843
844        assert!(matches!(settings.get_lang_filter.mode, Mode::Error(..)));
845        assert!(settings.map_lang_filter_btmap.is_none());
846        //
847        // Test empty list.
848        let mut settings = Settings {
849            lang: "en-GB".to_string(),
850            ..Default::default()
851        };
852        unsafe {
853            env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "");
854        }
855        settings.update_env_lang_detection(false);
856
857        assert!(matches!(settings.get_lang_filter.mode, Mode::Disabled));
858        assert!(settings.map_lang_filter_btmap.is_none());
859    }
860}