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