Skip to main content

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