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 environment and configuration, take this.
140    Force(&'a str),
141    /// Take the value `lib_cfg.scheme_sync_default`.
142    SchemeSyncDefault,
143    /// Take env. var. `TPNOTE_SCHEME` 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 set `self.current_scheme`:
151    /// 1. If `SchemeSource::Force(Some(scheme))`, take `scheme`,
152    ///    or,
153    /// 2. if `SchemeSource::SchemeSyncDefault`, take `lib_cfg.scheme_sync_default`,
154    ///    or,
155    /// 3. if `SchemeSource::SchemeNewDefault(s)` take the environment variable
156    ///    `TPNOTE_SCHEME`,
157    ///    or, -if the above environment variable is not defined or empty-
158    /// 4. take `s`.
159    ///
160    /// Then, it set all other fields.
161    /// When `force_lang(l)` is `Some`, it sets `SETTINGS.lang` to `l`.
162    /// If `force_lang=None` or `force_lang=Some("")` read the environment
163    /// variable `TPNOTE_LANG` -or if not defined- `LANG` into `SETTINGS.lang`.
164    /// In addition, `force_lang=Some(_)` disables the `get_lang` filter by
165    /// setting `get_lang_filter.mode` to `Mode::Disabled`.
166    pub(crate) fn update(
167        &mut self,
168        scheme_source: SchemeSource,
169        force_lang: Option<&str>,
170    ) -> Result<(), LibCfgError> {
171        self.update_current_scheme(scheme_source)?;
172        self.update_author();
173        self.update_extension_default();
174        self.update_lang(force_lang);
175        self.update_get_lang_filter(force_lang.is_some());
176        self.update_map_lang_filter_btmap();
177        self.update_env_lang_detection(force_lang.is_some());
178
179        log::trace!(
180            "`SETTINGS` updated (reading config + env. vars.):\n{:#?}",
181            self
182        );
183
184        if let Mode::Error(e) = &self.get_lang_filter.mode {
185            Err(e.clone())
186        } else {
187            Ok(())
188        }
189    }
190
191    /// Set `SETTINGS.current_scheme`:
192    /// 1. If `SchemeSource::Force(Some(scheme))`, take `scheme`,
193    ///    or,
194    /// 2. if `SchemeSource::SchemeSyncDefault`, take `lib_cfg.scheme_sync_default`,
195    ///    or,
196    /// 3. if `SchemeSource::SchemeNewDefault(s)` take the environment variable
197    ///    `TPNOTE_SCHEME`,
198    ///    or, -if the above environment variable is not defined or empty-
199    /// 4. take `s`.
200    pub(crate) fn update_current_scheme(
201        &mut self,
202        scheme_source: SchemeSource,
203    ) -> Result<(), LibCfgError> {
204        let lib_cfg = LIB_CFG.read_recursive();
205
206        let scheme = match scheme_source {
207            SchemeSource::Force(s) => Cow::Borrowed(s),
208            SchemeSource::SchemeSyncDefault => Cow::Borrowed(&*lib_cfg.scheme_sync_default),
209            SchemeSource::SchemeNewDefault(s) => match env::var(ENV_VAR_TPNOTE_SCHEME) {
210                Ok(ed_env) if !ed_env.is_empty() => Cow::Owned(ed_env),
211                Err(_) | Ok(_) => Cow::Borrowed(s),
212            },
213        };
214        self.current_scheme = lib_cfg.scheme_idx(scheme.as_ref())?;
215        Ok(())
216    }
217
218    /// Set `SETTINGS.author` to content of the first not empty environment
219    /// variable: `TPNOTE_USER`, `LOGNAME` or `USER`.
220    fn update_author(&mut self) {
221        let author = env::var(ENV_VAR_TPNOTE_USER).unwrap_or_else(|_| {
222            env::var(ENV_VAR_LOGNAME).unwrap_or_else(|_| {
223                env::var(ENV_VAR_USERNAME)
224                    .unwrap_or_else(|_| env::var(ENV_VAR_USER).unwrap_or_default())
225            })
226        });
227
228        // Store result.
229        self.author = author;
230    }
231
232    /// Read the environment variable `TPNOTE_EXTENSION_DEFAULT` or -if empty-
233    /// the configuration file variable `filename.extension_default` into
234    /// `SETTINGS.extension_default`.
235    fn update_extension_default(&mut self) {
236        // Get the environment variable if it exists.
237        let ext = match env::var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT) {
238            Ok(ed_env) if !ed_env.is_empty() => ed_env,
239            Err(_) | Ok(_) => {
240                let lib_cfg = LIB_CFG.read_recursive();
241                lib_cfg.scheme[self.current_scheme]
242                    .filename
243                    .extension_default
244                    .to_string()
245            }
246        };
247        self.extension_default = ext;
248    }
249
250    /// If `force_lang=None` or `force_lang=Some("")` read the environment
251    /// -variable `TPNOTE_LANG` or if not defined- `LANG` into `SETTINGS.lang`.
252    /// If `force_lang=Some(l)`, copy `l` in `settings.lang`.
253    fn update_lang(&mut self, force_lang: Option<&str>) {
254        // Overwrite environment setting.
255        if let Some(l) = force_lang {
256            if !l.is_empty() {
257                self.lang = l.to_string();
258                return;
259            }
260        }
261
262        // Get the user's language tag.
263        // [RFC 5646, Tags for the Identification of Languages](http://www.rfc-editor.org/rfc/rfc5646.txt)
264        let mut lang = String::new();
265        // Get the environment variable if it exists.
266        let tpnotelang = env::var(ENV_VAR_TPNOTE_LANG).ok();
267        // Unix/MacOS version.
268        #[cfg(not(target_family = "windows"))]
269        if let Some(tpnotelang) = tpnotelang {
270            lang = tpnotelang;
271        } else {
272            // [Linux: Define Locale and Language Settings -
273            // ShellHacks](https://www.shellhacks.com/linux-define-locale-language-settings/)
274            if let Ok(lang_env) = env::var(ENV_VAR_LANG) {
275                if !lang_env.is_empty() {
276                    // [ISO 639](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code.
277                    let mut language = "";
278                    // [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes) country code.
279                    let mut territory = "";
280                    if let Some((l, lang_env)) = lang_env.split_once('_') {
281                        language = l;
282                        if let Some((t, _codeset)) = lang_env.split_once('.') {
283                            territory = t;
284                        }
285                    }
286                    lang = language.to_string();
287                    lang.push('-');
288                    lang.push_str(territory);
289                }
290            }
291        }
292
293        // Get the user's language tag.
294        // Windows version.
295        #[cfg(target_family = "windows")]
296        if let Some(tpnotelang) = tpnotelang {
297            lang = tpnotelang;
298        } else {
299            let mut buf = [0u16; LOCALE_NAME_MAX_LENGTH as usize];
300            let len = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32) };
301            if len > 0 {
302                lang = String::from_utf16_lossy(&buf[..((len - 1) as usize)]);
303            }
304        };
305
306        // Store result.
307        self.lang = lang;
308    }
309
310    /// Copies the settings form
311    /// `LIB_CFG.schemes[settings.scheme].tmpl.filter.get_lang` into
312    /// `SETTINGS.get_lang_filter`. Then append the user's
313    /// default language subtag to
314    /// `SETTINGS.get_lang_filter.language_candidates`.
315    /// Errors are stored in the `SETTINGS.filter.get_lang.mode` `Mode::Error(e)` variant.
316    /// If `force_lang` is `true`, set
317    /// `SETTINGS.get_lang_filter.mode = Mode::Disabled`.
318    #[cfg(feature = "lang-detection")]
319    fn update_get_lang_filter(&mut self, force_lang: bool) {
320        use crate::config::Mode;
321
322        {
323            let lib_cfg = LIB_CFG.read_recursive();
324            let current_scheme = &lib_cfg.scheme[self.current_scheme];
325
326            // Start form config.
327            self.get_lang_filter = current_scheme.tmpl.filter.get_lang.clone();
328        } // Release lock.
329
330        // `force_lang` disables the filter.
331        if force_lang {
332            self.get_lang_filter.mode = Mode::Disabled;
333            return;
334        }
335
336        // Check if disabled in config file. Early return.
337        if matches!(self.get_lang_filter.mode, Mode::Disabled) {
338            return;
339        }
340
341        // Read ISO codes from config object.
342        let iso_codes = &mut self.get_lang_filter.language_candidates;
343
344        // Check if all languages are selected, then we can return early.
345        if iso_codes.is_empty() {
346            return;
347        }
348
349        // Add the user's language subtag as reported from the OS.
350        // Silently ignore if anything goes wrong here.
351        if !self.lang.is_empty() {
352            if let Some((lang_subtag, _)) = self.lang.split_once('-') {
353                if let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
354                    if !iso_codes.contains(&iso_code) {
355                        iso_codes.push(iso_code);
356                    }
357                }
358            }
359        }
360
361        // Check if there are at least 2 languages in the list.
362        if iso_codes.len() <= 1 {
363            self.get_lang_filter.mode = Mode::Error(LibCfgError::NotEnoughLanguageCodes {
364                language_code: iso_codes[0].to_string(),
365            })
366        }
367    }
368
369    #[cfg(not(feature = "lang-detection"))]
370    /// Disable filter.
371    fn update_get_lang_filter(&mut self, _force_lang: bool) {
372        self.get_lang_filter.mode = Mode::Disabled;
373    }
374
375    /// Read keys and values from
376    /// `LIB_CFG.schemes[self.current_scheme].tmpl.filter_btmap_lang` in the
377    /// `BTreeMap`. Add the user's default language and region.
378    fn update_map_lang_filter_btmap(&mut self) {
379        let mut btm = BTreeMap::new();
380        let lib_cfg = LIB_CFG.read_recursive();
381        for l in &lib_cfg.scheme[self.current_scheme].tmpl.filter.map_lang {
382            if l.len() >= 2 {
383                btm.insert(l[0].to_string(), l[1].to_string());
384            };
385        }
386        // Insert the user's default language and region in the Map.
387        if !self.lang.is_empty() {
388            if let Some((lang_subtag, _)) = self.lang.split_once('-') {
389                // Do not overwrite existing languages.
390                if !lang_subtag.is_empty() && !btm.contains_key(lang_subtag) {
391                    btm.insert(lang_subtag.to_string(), self.lang.to_string());
392                }
393            };
394        }
395
396        // Store result.
397        self.map_lang_filter_btmap = Some(btm);
398    }
399
400    /// Reads the environment variable `LANG_DETECTION`. If not empty,
401    /// parse the content and overwrite the `self.get_lang_filter` and the
402    /// `self.map_lang_filter` variables.
403    /// Finally, if `force_lang` is true, then it sets
404    /// `self.get_lang_filter.mode` Mode::Disabled.
405    #[cfg(feature = "lang-detection")]
406    fn update_env_lang_detection(&mut self, force_lang: bool) {
407        use crate::config::Mode;
408
409        if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
410            if env_var.is_empty() {
411                // Early return.
412                self.get_lang_filter.mode = Mode::Disabled;
413                self.map_lang_filter_btmap = None;
414                log::debug!(
415                    "Empty env. var. `{}` disables the `lang-detection` feature.",
416                    ENV_VAR_TPNOTE_LANG_DETECTION
417                );
418                return;
419            }
420
421            // Read and convert ISO codes from config object.
422            let mut hm: BTreeMap<String, String> = BTreeMap::new();
423            let mut all_languages_selected = false;
424            let iso_codes = env_var
425                .split(',')
426                .map(|t| {
427                    let t = t.trim();
428                    if let Some((lang_subtag, _)) = t.split_once('-') {
429                        // Do not overwrite existing languages.
430                        if !lang_subtag.is_empty() && !hm.contains_key(lang_subtag) {
431                            hm.insert(lang_subtag.to_string(), t.to_string());
432                        };
433                        lang_subtag
434                    } else {
435                        t
436                    }
437                })
438                // Check if this is the pseudo tag `TMPL_GET_LANG_filter_ALL `.
439                .filter(|&l| {
440                    if l == ENV_VAR_TPNOTE_LANG_PLUS_ALL {
441                        all_languages_selected = true;
442                        // Skip this string.
443                        false
444                    } else {
445                        // Continue.
446                        true
447                    }
448                })
449                .map(|l| {
450                    IsoCode639_1::from_str(l.trim()).map_err(|_| {
451                        // The error path.
452                        // Produce list of all available languages.
453                        let mut all_langs = lingua::Language::all()
454                            .iter()
455                            .map(|l| {
456                                let mut s = l.iso_code_639_1().to_string();
457                                s.push_str(", ");
458                                s
459                            })
460                            .collect::<Vec<String>>();
461                        all_langs.sort();
462                        let mut all_langs = all_langs.into_iter().collect::<String>();
463                        all_langs.truncate(all_langs.len() - ", ".len());
464                        // Insert data into error object.
465                        LibCfgError::ParseLanguageCode {
466                            language_code: l.into(),
467                            all_langs,
468                        }
469                    })
470                })
471                .collect::<Result<Vec<IsoCode639_1>, LibCfgError>>();
472
473            match iso_codes {
474                // The happy path.
475                Ok(mut iso_codes) => {
476                    // Add the user's language subtag as reported from the OS.
477                    // Continue the happy path.
478                    if !self.lang.is_empty() {
479                        if let Some(lang_subtag) = self.lang.split('-').next() {
480                            if let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
481                                if !iso_codes.contains(&iso_code) {
482                                    iso_codes.push(iso_code);
483                                }
484                                // Check if there is a remainder (region code).
485                                if lang_subtag != self.lang && !hm.contains_key(lang_subtag) {
486                                    hm.insert(lang_subtag.to_string(), self.lang.to_string());
487                                }
488                            }
489                        }
490                    }
491
492                    // Store result.
493                    if all_languages_selected {
494                        self.get_lang_filter.language_candidates = vec![];
495                        if matches!(self.get_lang_filter.mode, Mode::Disabled) {
496                            self.get_lang_filter.mode = Mode::Multilingual;
497                        }
498                    } else {
499                        match iso_codes.len() {
500                            0 => self.get_lang_filter.mode = Mode::Disabled,
501                            1 => {
502                                self.get_lang_filter.mode =
503                                    Mode::Error(LibCfgError::NotEnoughLanguageCodes {
504                                        language_code: iso_codes[0].to_string(),
505                                    })
506                            }
507                            _ => {
508                                self.get_lang_filter.language_candidates = iso_codes;
509                                if matches!(self.get_lang_filter.mode, Mode::Disabled) {
510                                    self.get_lang_filter.mode = Mode::Multilingual;
511                                }
512                            }
513                        }
514                    }
515                    self.map_lang_filter_btmap = Some(hm);
516                }
517                // The error path.
518                Err(e) =>
519                // Store error.
520                {
521                    self.get_lang_filter.mode = Mode::Error(e);
522                }
523            }
524
525            // Even is `force_lang` is set and the environment variable is not
526            // in use, we always parse it (see code above) to identify errors.
527            if force_lang {
528                self.get_lang_filter.mode = Mode::Disabled;
529            }
530        }
531    }
532
533    /// Ignore the environment variable `LANG_DETECTION`.
534    #[cfg(not(feature = "lang-detection"))]
535    fn update_env_lang_detection(&mut self, _force_lang: bool) {
536        if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
537            if !env_var.is_empty() {
538                self.get_lang_filter.mode = Mode::Disabled;
539                self.map_lang_filter_btmap = None;
540                log::debug!(
541                    "Ignoring the env. var. `{}`. The `lang-detection` feature \
542                 is not included in this build.",
543                    ENV_VAR_TPNOTE_LANG_DETECTION
544                );
545            }
546        }
547    }
548}
549#[cfg(test)]
550mod tests {
551    use super::*;
552    /// Attention: as these test-functions run in parallel, make sure that
553    /// each environment variable appears in one function only!
554
555    #[test]
556    fn test_update_author_setting() {
557        let mut settings = Settings::default();
558        unsafe {
559            env::set_var(ENV_VAR_LOGNAME, "testauthor");
560        }
561        settings.update_author();
562        assert_eq!(settings.author, "testauthor");
563    }
564
565    #[test]
566    fn test_update_extension_default_setting() {
567        let mut settings = Settings::default();
568        unsafe {
569            env::set_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT, "markdown");
570        }
571        settings.update_extension_default();
572        assert_eq!(settings.extension_default, "markdown");
573
574        let mut settings = Settings::default();
575        unsafe {
576            std::env::remove_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT);
577        }
578        settings.update_extension_default();
579        assert_eq!(settings.extension_default, "md");
580    }
581
582    #[test]
583    #[cfg(not(target_family = "windows"))]
584    fn test_update_lang_setting() {
585        // Test 1
586        let mut settings = Settings::default();
587        unsafe {
588            env::remove_var(ENV_VAR_TPNOTE_LANG);
589            env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
590        }
591        settings.update_lang(None);
592        assert_eq!(settings.lang, "en-GB");
593
594        // Test empty input.
595        let mut settings = Settings::default();
596        unsafe {
597            env::remove_var(ENV_VAR_TPNOTE_LANG);
598            env::set_var(ENV_VAR_LANG, "");
599        }
600        settings.update_lang(None);
601        assert_eq!(settings.lang, "");
602
603        // Test precedence of `TPNOTE_LANG`.
604        let mut settings = Settings::default();
605        unsafe {
606            env::set_var(ENV_VAR_TPNOTE_LANG, "it-IT");
607            env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
608        }
609        settings.update_lang(None);
610        assert_eq!(settings.lang, "it-IT");
611    }
612
613    #[test]
614    #[cfg(feature = "lang-detection")]
615    fn test_update_get_lang_filter_setting() {
616        // Test 1.
617        let mut settings = Settings {
618            lang: "en-GB".to_string(),
619            ..Default::default()
620        };
621        settings.update_get_lang_filter(false);
622
623        let output_get_lang_filter = settings
624            .get_lang_filter
625            .language_candidates
626            .iter()
627            .map(|l| {
628                let mut l = l.to_string();
629                l.push(' ');
630                l
631            })
632            .collect::<String>();
633        assert_eq!(output_get_lang_filter, "en fr de ");
634
635        //
636        // Test 2.
637        let mut settings = Settings {
638            lang: "it-IT".to_string(),
639            ..Default::default()
640        };
641        settings.update_get_lang_filter(false);
642
643        let output_get_lang_filter = settings
644            .get_lang_filter
645            .language_candidates
646            .iter()
647            .map(|l| {
648                let mut l = l.to_string();
649                l.push(' ');
650                l
651            })
652            .collect::<String>();
653        assert_eq!(output_get_lang_filter, "en fr de it ");
654    }
655
656    #[test]
657    fn test_update_map_lang_filter_hmap_setting() {
658        // Test 1.
659        let mut settings = Settings {
660            lang: "it-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").unwrap(), "it-IT");
670
671        //
672        // Test short `settings.lang`.
673        let mut settings = Settings {
674            lang: "it".to_string(),
675            ..Default::default()
676        };
677        settings.update_map_lang_filter_btmap();
678
679        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
680
681        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
682        assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
683        assert_eq!(output_map_lang_filter.get("it"), None);
684    }
685
686    #[test]
687    #[cfg(feature = "lang-detection")]
688    fn test_update_env_lang_detection() {
689        // Test 1.
690        // Test short `settings.lang`.
691        let mut settings = Settings {
692            lang: "en-GB".to_string(),
693            ..Default::default()
694        };
695        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
696        settings.update_env_lang_detection(false);
697
698        let output_get_lang_filter = settings
699            .get_lang_filter
700            .language_candidates
701            .iter()
702            .map(|l| {
703                let mut l = l.to_string();
704                l.push(' ');
705                l
706            })
707            .collect::<String>();
708        assert_eq!(output_get_lang_filter, "fr de hu en ");
709
710        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
711        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
712        assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
713        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
714
715        //
716        // Test 2.
717        let mut settings = Settings {
718            lang: "en-GB".to_string(),
719            ..Default::default()
720        };
721        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en-US") };
722        settings.update_env_lang_detection(false);
723
724        let output_get_lang_filter = settings
725            .get_lang_filter
726            .language_candidates
727            .iter()
728            .map(|l| {
729                let mut l = l.to_string();
730                l.push(' ');
731                l
732            })
733            .collect::<String>();
734        assert_eq!(output_get_lang_filter, "de de en ");
735
736        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
737        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
738        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
739
740        //
741        // Test 3.
742        let mut settings = Settings {
743            lang: "en-GB".to_string(),
744            ..Default::default()
745        };
746        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, en-US") };
747        settings.update_env_lang_detection(false);
748
749        assert!(settings.get_lang_filter.language_candidates.is_empty());
750
751        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
752        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
753        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
754
755        //
756        // Test 4.
757        let mut settings = Settings {
758            lang: "en-GB".to_string(),
759            ..Default::default()
760        };
761        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en") };
762        settings.update_env_lang_detection(false);
763
764        let output_get_lang_filter = settings
765            .get_lang_filter
766            .language_candidates
767            .iter()
768            .map(|l| {
769                let mut l = l.to_string();
770                l.push(' ');
771                l
772            })
773            .collect::<String>();
774        assert_eq!(output_get_lang_filter, "de de en ");
775
776        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
777        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
778        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
779
780        //
781        // Test 5.
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, "de-DE, +all, de-AT, en") };
787        settings.update_env_lang_detection(false);
788
789        assert!(settings.get_lang_filter.language_candidates.is_empty());
790
791        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
792        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
793        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
794
795        // Test `force_lang`.
796        let mut settings = Settings {
797            lang: "en-GB".to_string(),
798            ..Default::default()
799        };
800        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
801        settings.update_env_lang_detection(true);
802
803        // `force_lang` must disables the `get_lang` filter.
804        assert_eq!(settings.get_lang_filter.mode, Mode::Disabled);
805
806        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
807        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
808        assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
809        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
810
811        //
812        // Test empty env. var.
813        let mut settings = Settings {
814            lang: "".to_string(),
815            ..Default::default()
816        };
817        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "") };
818        settings.update_env_lang_detection(false);
819
820        assert_eq!(settings.get_lang_filter.mode, Mode::Disabled);
821        assert!(settings.map_lang_filter_btmap.is_none());
822
823        //
824        // Test faulty `settings.lang`.
825        let mut settings = Settings {
826            lang: "xy-XY".to_string(),
827            ..Default::default()
828        };
829        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "en-GB, fr") };
830        settings.update_env_lang_detection(false);
831
832        let output_get_lang_filter = settings
833            .get_lang_filter
834            .language_candidates
835            .iter()
836            .map(|l| {
837                let mut l = l.to_string();
838                l.push(' ');
839                l
840            })
841            .collect::<String>();
842        assert_eq!(output_get_lang_filter, "en fr ");
843
844        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
845        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
846
847        //
848        // Test faulty entry in list.
849        let mut settings = Settings {
850            lang: "en-GB".to_string(),
851            ..Default::default()
852        };
853        unsafe {
854            env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, xy-XY");
855        }
856        settings.update_env_lang_detection(false);
857
858        assert!(matches!(settings.get_lang_filter.mode, Mode::Error(..)));
859        assert!(settings.map_lang_filter_btmap.is_none());
860        //
861        // Test empty list.
862        let mut settings = Settings {
863            lang: "en-GB".to_string(),
864            ..Default::default()
865        };
866        unsafe {
867            env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "");
868        }
869        settings.update_env_lang_detection(false);
870
871        assert!(matches!(settings.get_lang_filter.mode, Mode::Disabled));
872        assert!(settings.map_lang_filter_btmap.is_none());
873    }
874}