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                if !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
297        // Get the user's language tag.
298        // Windows version.
299        #[cfg(target_family = "windows")]
300        if let Some(tpnotelang) = tpnotelang {
301            lang = tpnotelang;
302        } else {
303            let mut buf = [0u16; LOCALE_NAME_MAX_LENGTH as usize];
304            let len = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32) };
305            if len > 0 {
306                lang = String::from_utf16_lossy(&buf[..((len - 1) as usize)]);
307            }
308        };
309
310        // Store result.
311        self.lang = lang;
312
313        // Store `force_lang` argument.
314        self.force_lang = match force_lang {
315            Some("") => self.lang.clone(),
316            Some(lang) => lang.to_owned(),
317            None => String::new(),
318        };
319    }
320
321    /// Copies the settings form
322    /// `LIB_CFG.schemes[settings.scheme].tmpl.filter.get_lang` into
323    /// `SETTINGS.get_lang_filter`. Then append the user's
324    /// default language subtag to
325    /// `SETTINGS.get_lang_filter.language_candidates`.
326    /// Errors are stored in the `SETTINGS.filter.get_lang.mode` `Mode::Error(e)` variant.
327    #[cfg(feature = "lang-detection")]
328    fn update_get_lang_filter(&mut self) {
329        use crate::config::Mode;
330
331        {
332            let lib_cfg = LIB_CFG.read_recursive();
333            let current_scheme = &lib_cfg.scheme[self.current_scheme];
334
335            // Start form config.
336            self.get_lang_filter = current_scheme.tmpl.filter.get_lang.clone();
337        } // Release lock.
338
339        // Check if disabled in config file. Early return.
340        if matches!(self.get_lang_filter.mode, Mode::Disabled) {
341            return;
342        }
343
344        // Read ISO codes from config object.
345        let iso_codes = &mut self.get_lang_filter.language_candidates;
346
347        // Check if all languages are selected, then we can return early.
348        if iso_codes.is_empty() {
349            return;
350        }
351
352        // Add the user's language subtag as reported from the OS.
353        // Silently ignore if anything goes wrong here.
354        if !self.lang.is_empty() {
355            if let Some((lang_subtag, _)) = self.lang.split_once('-') {
356                if let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
357                    if !iso_codes.contains(&iso_code) {
358                        iso_codes.push(iso_code);
359                    }
360                }
361            }
362        }
363
364        // Check if there are at least 2 languages in the list.
365        if iso_codes.len() <= 1 {
366            self.get_lang_filter.mode = Mode::Error(LibCfgError::NotEnoughLanguageCodes {
367                language_code: iso_codes[0].to_string(),
368            })
369        }
370    }
371
372    #[cfg(not(feature = "lang-detection"))]
373    /// Disable filter.
374    fn update_get_lang_filter(&mut self) {
375        self.get_lang_filter.mode = Mode::Disabled;
376    }
377
378    /// Read keys and values from
379    /// `LIB_CFG.schemes[self.current_scheme].tmpl.filter_btmap_lang` in the
380    /// `BTreeMap`. Add the user's default language and region.
381    fn update_map_lang_filter_btmap(&mut self) {
382        let mut btm = BTreeMap::new();
383        let lib_cfg = LIB_CFG.read_recursive();
384        for l in &lib_cfg.scheme[self.current_scheme].tmpl.filter.map_lang {
385            if l.len() >= 2 {
386                btm.insert(l[0].to_string(), l[1].to_string());
387            };
388        }
389        // Insert the user's default language and region in the Map.
390        if !self.lang.is_empty() {
391            if let Some((lang_subtag, _)) = self.lang.split_once('-') {
392                // Do not overwrite existing languages.
393                if !lang_subtag.is_empty() && !btm.contains_key(lang_subtag) {
394                    btm.insert(lang_subtag.to_string(), self.lang.to_string());
395                }
396            };
397        }
398
399        // Store result.
400        self.map_lang_filter_btmap = Some(btm);
401    }
402
403    /// Reads the environment variable `LANG_DETECTION`. If not empty,
404    /// parse the content and overwrite the `self.get_lang_filter` and the
405    /// `self.map_lang_filter` variables.
406    /// Finally, if `force_lang` is true, then it sets
407    /// `self.get_lang_filter.mode` Mode::Disabled.
408    #[cfg(feature = "lang-detection")]
409    fn update_env_lang_detection(&mut self) {
410        use crate::config::Mode;
411
412        if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
413            if env_var.is_empty() {
414                // Early return.
415                self.get_lang_filter.mode = Mode::Disabled;
416                self.map_lang_filter_btmap = None;
417                log::debug!(
418                    "Empty env. var. `{}` disables the `lang-detection` feature.",
419                    ENV_VAR_TPNOTE_LANG_DETECTION
420                );
421                return;
422            }
423
424            // Read and convert ISO codes from config object.
425            let mut hm: BTreeMap<String, String> = BTreeMap::new();
426            let mut all_languages_selected = false;
427            let iso_codes = env_var
428                .split(',')
429                .map(|t| {
430                    let t = t.trim();
431                    if let Some((lang_subtag, _)) = t.split_once('-') {
432                        // Do not overwrite existing languages.
433                        if !lang_subtag.is_empty() && !hm.contains_key(lang_subtag) {
434                            hm.insert(lang_subtag.to_string(), t.to_string());
435                        };
436                        lang_subtag
437                    } else {
438                        t
439                    }
440                })
441                // Check if this is the pseudo tag `TMPL_GET_LANG_filter_ALL `.
442                .filter(|&l| {
443                    if l == ENV_VAR_TPNOTE_LANG_PLUS_ALL {
444                        all_languages_selected = true;
445                        // Skip this string.
446                        false
447                    } else {
448                        // Continue.
449                        true
450                    }
451                })
452                .map(|l| {
453                    IsoCode639_1::from_str(l.trim()).map_err(|_| {
454                        // The error path.
455                        // Produce list of all available languages.
456                        let mut all_langs = lingua::Language::all()
457                            .iter()
458                            .map(|l| {
459                                let mut s = l.iso_code_639_1().to_string();
460                                s.push_str(", ");
461                                s
462                            })
463                            .collect::<Vec<String>>();
464                        all_langs.sort();
465                        let mut all_langs = all_langs.into_iter().collect::<String>();
466                        all_langs.truncate(all_langs.len() - ", ".len());
467                        // Insert data into error object.
468                        LibCfgError::ParseLanguageCode {
469                            language_code: l.into(),
470                            all_langs,
471                        }
472                    })
473                })
474                .collect::<Result<Vec<IsoCode639_1>, LibCfgError>>();
475
476            match iso_codes {
477                // The happy path.
478                Ok(mut iso_codes) => {
479                    // Add the user's language subtag as reported from the OS.
480                    // Continue the happy path.
481                    if !self.lang.is_empty() {
482                        if let Some(lang_subtag) = self.lang.split('-').next() {
483                            if let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
484                                if !iso_codes.contains(&iso_code) {
485                                    iso_codes.push(iso_code);
486                                }
487                                // Check if there is a remainder (region code).
488                                if lang_subtag != self.lang && !hm.contains_key(lang_subtag) {
489                                    hm.insert(lang_subtag.to_string(), self.lang.to_string());
490                                }
491                            }
492                        }
493                    }
494
495                    // Store result.
496                    if all_languages_selected {
497                        self.get_lang_filter.language_candidates = vec![];
498                        if matches!(self.get_lang_filter.mode, Mode::Disabled) {
499                            self.get_lang_filter.mode = Mode::Multilingual;
500                        }
501                    } else {
502                        match iso_codes.len() {
503                            0 => self.get_lang_filter.mode = Mode::Disabled,
504                            1 => {
505                                self.get_lang_filter.mode =
506                                    Mode::Error(LibCfgError::NotEnoughLanguageCodes {
507                                        language_code: iso_codes[0].to_string(),
508                                    })
509                            }
510                            _ => {
511                                self.get_lang_filter.language_candidates = iso_codes;
512                                if matches!(self.get_lang_filter.mode, Mode::Disabled) {
513                                    self.get_lang_filter.mode = Mode::Multilingual;
514                                }
515                            }
516                        }
517                    }
518                    self.map_lang_filter_btmap = Some(hm);
519                }
520                // The error path.
521                Err(e) =>
522                // Store error.
523                {
524                    self.get_lang_filter.mode = Mode::Error(e);
525                }
526            }
527        }
528    }
529
530    /// Ignore the environment variable `LANG_DETECTION`.
531    #[cfg(not(feature = "lang-detection"))]
532    fn update_env_lang_detection(&mut self) {
533        if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
534            if !env_var.is_empty() {
535                self.get_lang_filter.mode = Mode::Disabled;
536                self.map_lang_filter_btmap = None;
537                log::debug!(
538                    "Ignoring the env. var. `{}`. The `lang-detection` feature \
539                 is not included in this build.",
540                    ENV_VAR_TPNOTE_LANG_DETECTION
541                );
542            }
543        }
544    }
545}
546#[cfg(test)]
547mod tests {
548    use super::*;
549    /// Attention: as these test-functions run in parallel, make sure that
550    /// each environment variable appears in one function only!
551
552    #[test]
553    fn test_update_author_setting() {
554        let mut settings = Settings::default();
555        unsafe {
556            env::set_var(ENV_VAR_LOGNAME, "testauthor");
557        }
558        settings.update_author();
559        assert_eq!(settings.author, "testauthor");
560    }
561
562    #[test]
563    fn test_update_extension_default_setting() {
564        let mut settings = Settings::default();
565        unsafe {
566            env::set_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT, "markdown");
567        }
568        settings.update_extension_default();
569        assert_eq!(settings.extension_default, "markdown");
570
571        let mut settings = Settings::default();
572        unsafe {
573            std::env::remove_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT);
574        }
575        settings.update_extension_default();
576        assert_eq!(settings.extension_default, "md");
577    }
578
579    #[test]
580    #[cfg(not(target_family = "windows"))]
581    fn test_update_lang_setting() {
582        // Test 1
583        let mut settings = Settings::default();
584        unsafe {
585            env::remove_var(ENV_VAR_TPNOTE_LANG);
586            env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
587        }
588        settings.update_lang(None);
589        assert_eq!(settings.lang, "en-GB");
590
591        // Test empty input.
592        let mut settings = Settings::default();
593        unsafe {
594            env::remove_var(ENV_VAR_TPNOTE_LANG);
595            env::set_var(ENV_VAR_LANG, "");
596        }
597        settings.update_lang(None);
598        assert_eq!(settings.lang, "");
599
600        // Test precedence of `TPNOTE_LANG`.
601        let mut settings = Settings::default();
602        unsafe {
603            env::set_var(ENV_VAR_TPNOTE_LANG, "it-IT");
604            env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
605        }
606        settings.update_lang(None);
607        assert_eq!(settings.lang, "it-IT");
608    }
609
610    #[test]
611    #[cfg(feature = "lang-detection")]
612    fn test_update_get_lang_filter_setting() {
613        // Test 1.
614        let mut settings = Settings {
615            lang: "en-GB".to_string(),
616            ..Default::default()
617        };
618        settings.update_get_lang_filter();
619
620        let output_get_lang_filter = settings
621            .get_lang_filter
622            .language_candidates
623            .iter()
624            .map(|l| {
625                let mut l = l.to_string();
626                l.push(' ');
627                l
628            })
629            .collect::<String>();
630        assert_eq!(output_get_lang_filter, "en fr de ");
631
632        //
633        // Test 2.
634        let mut settings = Settings {
635            lang: "it-IT".to_string(),
636            ..Default::default()
637        };
638        settings.update_get_lang_filter();
639
640        let output_get_lang_filter = settings
641            .get_lang_filter
642            .language_candidates
643            .iter()
644            .map(|l| {
645                let mut l = l.to_string();
646                l.push(' ');
647                l
648            })
649            .collect::<String>();
650        assert_eq!(output_get_lang_filter, "en fr de it ");
651    }
652
653    #[test]
654    fn test_update_map_lang_filter_hmap_setting() {
655        // Test 1.
656        let mut settings = Settings {
657            lang: "it-IT".to_string(),
658            ..Default::default()
659        };
660        settings.update_map_lang_filter_btmap();
661
662        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
663
664        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
665        assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
666        assert_eq!(output_map_lang_filter.get("it").unwrap(), "it-IT");
667
668        //
669        // Test short `settings.lang`.
670        let mut settings = Settings {
671            lang: "it".to_string(),
672            ..Default::default()
673        };
674        settings.update_map_lang_filter_btmap();
675
676        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
677
678        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
679        assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
680        assert_eq!(output_map_lang_filter.get("it"), None);
681    }
682
683    #[test]
684    #[cfg(feature = "lang-detection")]
685    fn test_update_env_lang_detection() {
686        // Test 1.
687        // Test short `settings.lang`.
688        let mut settings = Settings {
689            lang: "en-GB".to_string(),
690            ..Default::default()
691        };
692        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
693        settings.update_env_lang_detection();
694
695        let output_get_lang_filter = settings
696            .get_lang_filter
697            .language_candidates
698            .iter()
699            .map(|l| {
700                let mut l = l.to_string();
701                l.push(' ');
702                l
703            })
704            .collect::<String>();
705        assert_eq!(output_get_lang_filter, "fr de hu en ");
706
707        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
708        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
709        assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
710        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
711
712        //
713        // Test 2.
714        let mut settings = Settings {
715            lang: "en-GB".to_string(),
716            ..Default::default()
717        };
718        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en-US") };
719        settings.update_env_lang_detection();
720
721        let output_get_lang_filter = settings
722            .get_lang_filter
723            .language_candidates
724            .iter()
725            .map(|l| {
726                let mut l = l.to_string();
727                l.push(' ');
728                l
729            })
730            .collect::<String>();
731        assert_eq!(output_get_lang_filter, "de de en ");
732
733        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
734        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
735        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
736
737        //
738        // Test 3.
739        let mut settings = Settings {
740            lang: "en-GB".to_string(),
741            ..Default::default()
742        };
743        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, en-US") };
744        settings.update_env_lang_detection();
745
746        assert!(settings.get_lang_filter.language_candidates.is_empty());
747
748        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
749        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
750        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
751
752        //
753        // Test 4.
754        let mut settings = Settings {
755            lang: "en-GB".to_string(),
756            ..Default::default()
757        };
758        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en") };
759        settings.update_env_lang_detection();
760
761        let output_get_lang_filter = settings
762            .get_lang_filter
763            .language_candidates
764            .iter()
765            .map(|l| {
766                let mut l = l.to_string();
767                l.push(' ');
768                l
769            })
770            .collect::<String>();
771        assert_eq!(output_get_lang_filter, "de de en ");
772
773        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
774        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
775        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
776
777        //
778        // Test 5.
779        let mut settings = Settings {
780            lang: "en-GB".to_string(),
781            ..Default::default()
782        };
783        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, de-AT, en") };
784        settings.update_env_lang_detection();
785
786        assert!(settings.get_lang_filter.language_candidates.is_empty());
787
788        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
789        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
790        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
791
792        // Test `force_lang`.
793        let mut settings = Settings {
794            lang: "en-GB".to_string(),
795            ..Default::default()
796        };
797        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
798        settings.update_env_lang_detection();
799
800        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
801        assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
802        assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
803        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
804
805        //
806        // Test empty env. var.
807        let mut settings = Settings {
808            lang: "".to_string(),
809            ..Default::default()
810        };
811        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "") };
812        settings.update_env_lang_detection();
813
814        assert_eq!(settings.get_lang_filter.mode, Mode::Disabled);
815        assert!(settings.map_lang_filter_btmap.is_none());
816
817        //
818        // Test faulty `settings.lang`.
819        let mut settings = Settings {
820            lang: "xy-XY".to_string(),
821            ..Default::default()
822        };
823        unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "en-GB, fr") };
824        settings.update_env_lang_detection();
825
826        let output_get_lang_filter = settings
827            .get_lang_filter
828            .language_candidates
829            .iter()
830            .map(|l| {
831                let mut l = l.to_string();
832                l.push(' ');
833                l
834            })
835            .collect::<String>();
836        assert_eq!(output_get_lang_filter, "en fr ");
837
838        let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
839        assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
840
841        //
842        // Test faulty entry in list.
843        let mut settings = Settings {
844            lang: "en-GB".to_string(),
845            ..Default::default()
846        };
847        unsafe {
848            env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, xy-XY");
849        }
850        settings.update_env_lang_detection();
851
852        assert!(matches!(settings.get_lang_filter.mode, Mode::Error(..)));
853        assert!(settings.map_lang_filter_btmap.is_none());
854        //
855        // Test empty list.
856        let mut settings = Settings {
857            lang: "en-GB".to_string(),
858            ..Default::default()
859        };
860        unsafe {
861            env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "");
862        }
863        settings.update_env_lang_detection();
864
865        assert!(matches!(settings.get_lang_filter.mode, Mode::Disabled));
866        assert!(settings.map_lang_filter_btmap.is_none());
867    }
868}