1use 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
23pub const ENV_VAR_TPNOTE_SCHEME: &str = "TPNOTE_SCHEME";
26
27pub const ENV_VAR_TPNOTE_EXTENSION_DEFAULT: &str = "TPNOTE_EXTENSION_DEFAULT";
30
31pub const ENV_VAR_TPNOTE_LANG: &str = "TPNOTE_LANG";
36
37pub const ENV_VAR_TPNOTE_LANG_PLUS_ALL: &str = "+all";
40
41pub const ENV_VAR_TPNOTE_LANG_DETECTION: &str = "TPNOTE_LANG_DETECTION";
45
46pub const ENV_VAR_TPNOTE_USER: &str = "TPNOTE_USER";
50
51const ENV_VAR_LOGNAME: &str = "LOGNAME";
53
54const ENV_VAR_USERNAME: &str = "USERNAME";
56
57const ENV_VAR_USER: &str = "USER";
59
60#[cfg(not(target_family = "windows"))]
62const ENV_VAR_LANG: &str = "LANG";
63
64#[derive(Debug)]
67#[allow(dead_code)]
68pub(crate) struct Settings {
69 pub current_scheme: usize,
71 pub author: String,
73 pub lang: String,
78 pub force_lang: String,
82 pub extension_default: String,
84 pub get_lang_filter: GetLang,
86 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 fn default() -> Self {
112 DEFAULT_SETTINGS
113 }
114
115 #[cfg(any(test, doc))]
116 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#[cfg(not(test))]
129pub(crate) static SETTINGS: RwLock<Settings> = RwLock::new(DEFAULT_SETTINGS);
130
131#[cfg(test)]
132pub(crate) static SETTINGS: RwLock<Settings> = RwLock::new(DEFAULT_SETTINGS);
134
135pub fn set_test_default_settings() -> Result<(), LibCfgError> {
139 let mut settings = SETTINGS.write();
140 settings.update(SchemeSource::Force("default"), None)
141}
142
143#[derive(Debug, Clone)]
145pub(crate) enum SchemeSource<'a> {
146 Force(&'a str),
148 SchemeSyncDefault,
150 SchemeNewDefault(&'a str),
152}
153
154impl Settings {
155 pub(crate) fn update(
174 &mut self,
175 scheme_source: SchemeSource,
176 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 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 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 self.author = author;
239 }
240
241 fn update_extension_default(&mut self) {
245 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 fn update_lang(&mut self, force_lang: Option<&str>) {
266 let mut lang = String::new();
269 let tpnotelang = env::var(ENV_VAR_TPNOTE_LANG).ok();
271 #[cfg(not(target_family = "windows"))]
273 if let Some(tpnotelang) = tpnotelang {
274 lang = tpnotelang;
275 } else {
276 if let Ok(lang_env) = env::var(ENV_VAR_LANG) {
279 if !lang_env.is_empty() {
280 let mut language = "";
282 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 #[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 self.lang = lang;
312
313 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 #[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 self.get_lang_filter = current_scheme.tmpl.filter.get_lang.clone();
337 } if matches!(self.get_lang_filter.mode, Mode::Disabled) {
341 return;
342 }
343
344 let iso_codes = &mut self.get_lang_filter.language_candidates;
346
347 if iso_codes.is_empty() {
349 return;
350 }
351
352 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 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 fn update_get_lang_filter(&mut self) {
375 self.get_lang_filter.mode = Mode::Disabled;
376 }
377
378 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 if !self.lang.is_empty() {
391 if let Some((lang_subtag, _)) = self.lang.split_once('-') {
392 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 self.map_lang_filter_btmap = Some(btm);
401 }
402
403 #[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 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 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 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 .filter(|&l| {
443 if l == ENV_VAR_TPNOTE_LANG_PLUS_ALL {
444 all_languages_selected = true;
445 false
447 } else {
448 true
450 }
451 })
452 .map(|l| {
453 IsoCode639_1::from_str(l.trim()).map_err(|_| {
454 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 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 Ok(mut iso_codes) => {
479 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 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 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 Err(e) =>
522 {
524 self.get_lang_filter.mode = Mode::Error(e);
525 }
526 }
527 }
528 }
529
530 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}