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 && !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 #[cfg(target_family = "windows")]
299 if let Some(tpnotelang) = tpnotelang {
300 lang = tpnotelang;
301 } else {
302 let mut buf = [0u16; LOCALE_NAME_MAX_LENGTH as usize];
303 let len = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32) };
304 if len > 0 {
305 lang = String::from_utf16_lossy(&buf[..((len - 1) as usize)]);
306 }
307 };
308
309 self.lang = lang;
311
312 self.force_lang = match force_lang {
314 Some("") => self.lang.clone(),
315 Some(lang) => lang.to_owned(),
316 None => String::new(),
317 };
318 }
319
320 #[cfg(feature = "lang-detection")]
327 fn update_get_lang_filter(&mut self) {
328 use crate::config::Mode;
329
330 {
331 let lib_cfg = LIB_CFG.read_recursive();
332 let current_scheme = &lib_cfg.scheme[self.current_scheme];
333
334 self.get_lang_filter = current_scheme.tmpl.filter.get_lang.clone();
336 } if matches!(self.get_lang_filter.mode, Mode::Disabled) {
340 return;
341 }
342
343 let iso_codes = &mut self.get_lang_filter.language_candidates;
345
346 if iso_codes.is_empty() {
348 return;
349 }
350
351 if !self.lang.is_empty()
354 && let Some((lang_subtag, _)) = self.lang.split_once('-')
355 && let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag)
356 && !iso_codes.contains(&iso_code) {
357 iso_codes.push(iso_code);
358 }
359
360 if iso_codes.len() <= 1 {
362 self.get_lang_filter.mode = Mode::Error(LibCfgError::NotEnoughLanguageCodes {
363 language_code: iso_codes[0].to_string(),
364 })
365 }
366 }
367
368 #[cfg(not(feature = "lang-detection"))]
369 fn update_get_lang_filter(&mut self) {
371 self.get_lang_filter.mode = Mode::Disabled;
372 }
373
374 fn update_map_lang_filter_btmap(&mut self) {
378 let mut btm = BTreeMap::new();
379 let lib_cfg = LIB_CFG.read_recursive();
380 for l in &lib_cfg.scheme[self.current_scheme].tmpl.filter.map_lang {
381 if l.len() >= 2 {
382 btm.insert(l[0].to_string(), l[1].to_string());
383 };
384 }
385 if !self.lang.is_empty()
387 && let Some((lang_subtag, _)) = self.lang.split_once('-') {
388 if !lang_subtag.is_empty() && !btm.contains_key(lang_subtag) {
390 btm.insert(lang_subtag.to_string(), self.lang.to_string());
391 }
392 };
393
394 self.map_lang_filter_btmap = Some(btm);
396 }
397
398 #[cfg(feature = "lang-detection")]
404 fn update_env_lang_detection(&mut self) {
405 use crate::config::Mode;
406
407 if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
408 if env_var.is_empty() {
409 self.get_lang_filter.mode = Mode::Disabled;
411 self.map_lang_filter_btmap = None;
412 log::debug!(
413 "Empty env. var. `{}` disables the `lang-detection` feature.",
414 ENV_VAR_TPNOTE_LANG_DETECTION
415 );
416 return;
417 }
418
419 let mut hm: BTreeMap<String, String> = BTreeMap::new();
421 let mut all_languages_selected = false;
422 let iso_codes = env_var
423 .split(',')
424 .map(|t| {
425 let t = t.trim();
426 if let Some((lang_subtag, _)) = t.split_once('-') {
427 if !lang_subtag.is_empty() && !hm.contains_key(lang_subtag) {
429 hm.insert(lang_subtag.to_string(), t.to_string());
430 };
431 lang_subtag
432 } else {
433 t
434 }
435 })
436 .filter(|&l| {
438 if l == ENV_VAR_TPNOTE_LANG_PLUS_ALL {
439 all_languages_selected = true;
440 false
442 } else {
443 true
445 }
446 })
447 .map(|l| {
448 IsoCode639_1::from_str(l.trim()).map_err(|_| {
449 let mut all_langs = lingua::Language::all()
452 .iter()
453 .map(|l| {
454 let mut s = l.iso_code_639_1().to_string();
455 s.push_str(", ");
456 s
457 })
458 .collect::<Vec<String>>();
459 all_langs.sort();
460 let mut all_langs = all_langs.into_iter().collect::<String>();
461 all_langs.truncate(all_langs.len() - ", ".len());
462 LibCfgError::ParseLanguageCode {
464 language_code: l.into(),
465 all_langs,
466 }
467 })
468 })
469 .collect::<Result<Vec<IsoCode639_1>, LibCfgError>>();
470
471 match iso_codes {
472 Ok(mut iso_codes) => {
474 if !self.lang.is_empty()
477 && let Some(lang_subtag) = self.lang.split('-').next()
478 && let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
479 if !iso_codes.contains(&iso_code) {
480 iso_codes.push(iso_code);
481 }
482 if lang_subtag != self.lang && !hm.contains_key(lang_subtag) {
484 hm.insert(lang_subtag.to_string(), self.lang.to_string());
485 }
486 }
487
488 if all_languages_selected {
490 self.get_lang_filter.language_candidates = vec![];
491 if matches!(self.get_lang_filter.mode, Mode::Disabled) {
492 self.get_lang_filter.mode = Mode::Multilingual;
493 }
494 } else {
495 match iso_codes.len() {
496 0 => self.get_lang_filter.mode = Mode::Disabled,
497 1 => {
498 self.get_lang_filter.mode =
499 Mode::Error(LibCfgError::NotEnoughLanguageCodes {
500 language_code: iso_codes[0].to_string(),
501 })
502 }
503 _ => {
504 self.get_lang_filter.language_candidates = iso_codes;
505 if matches!(self.get_lang_filter.mode, Mode::Disabled) {
506 self.get_lang_filter.mode = Mode::Multilingual;
507 }
508 }
509 }
510 }
511 self.map_lang_filter_btmap = Some(hm);
512 }
513 Err(e) =>
515 {
517 self.get_lang_filter.mode = Mode::Error(e);
518 }
519 }
520 }
521 }
522
523 #[cfg(not(feature = "lang-detection"))]
525 fn update_env_lang_detection(&mut self) {
526 if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
527 if !env_var.is_empty() {
528 self.get_lang_filter.mode = Mode::Disabled;
529 self.map_lang_filter_btmap = None;
530 log::debug!(
531 "Ignoring the env. var. `{}`. The `lang-detection` feature \
532 is not included in this build.",
533 ENV_VAR_TPNOTE_LANG_DETECTION
534 );
535 }
536 }
537 }
538}
539#[cfg(test)]
540mod tests {
541 use super::*;
542 #[test]
546 fn test_update_author_setting() {
547 let mut settings = Settings::default();
548 unsafe {
549 env::set_var(ENV_VAR_LOGNAME, "testauthor");
550 }
551 settings.update_author();
552 assert_eq!(settings.author, "testauthor");
553 }
554
555 #[test]
556 fn test_update_extension_default_setting() {
557 let mut settings = Settings::default();
558 unsafe {
559 env::set_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT, "markdown");
560 }
561 settings.update_extension_default();
562 assert_eq!(settings.extension_default, "markdown");
563
564 let mut settings = Settings::default();
565 unsafe {
566 std::env::remove_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT);
567 }
568 settings.update_extension_default();
569 assert_eq!(settings.extension_default, "md");
570 }
571
572 #[test]
573 #[cfg(not(target_family = "windows"))]
574 fn test_update_lang_setting() {
575 let mut settings = Settings::default();
577 unsafe {
578 env::remove_var(ENV_VAR_TPNOTE_LANG);
579 env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
580 }
581 settings.update_lang(None);
582 assert_eq!(settings.lang, "en-GB");
583
584 let mut settings = Settings::default();
586 unsafe {
587 env::remove_var(ENV_VAR_TPNOTE_LANG);
588 env::set_var(ENV_VAR_LANG, "");
589 }
590 settings.update_lang(None);
591 assert_eq!(settings.lang, "");
592
593 let mut settings = Settings::default();
595 unsafe {
596 env::set_var(ENV_VAR_TPNOTE_LANG, "it-IT");
597 env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
598 }
599 settings.update_lang(None);
600 assert_eq!(settings.lang, "it-IT");
601 }
602
603 #[test]
604 #[cfg(feature = "lang-detection")]
605 fn test_update_get_lang_filter_setting() {
606 let mut settings = Settings {
608 lang: "en-GB".to_string(),
609 ..Default::default()
610 };
611 settings.update_get_lang_filter();
612
613 let output_get_lang_filter = settings
614 .get_lang_filter
615 .language_candidates
616 .iter()
617 .map(|l| {
618 let mut l = l.to_string();
619 l.push(' ');
620 l
621 })
622 .collect::<String>();
623 assert_eq!(output_get_lang_filter, "");
624
625 let mut settings = Settings {
628 lang: "it-IT".to_string(),
629 ..Default::default()
630 };
631 settings.update_get_lang_filter();
632
633 let output_get_lang_filter = settings
634 .get_lang_filter
635 .language_candidates
636 .iter()
637 .map(|l| {
638 let mut l = l.to_string();
639 l.push(' ');
640 l
641 })
642 .collect::<String>();
643 assert_eq!(output_get_lang_filter, "");
644 }
645
646 #[test]
647 fn test_update_map_lang_filter_hmap_setting() {
648 let mut settings = Settings {
650 lang: "it-IT".to_string(),
651 ..Default::default()
652 };
653 settings.update_map_lang_filter_btmap();
654
655 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
656
657 assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
658 assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
659 assert_eq!(output_map_lang_filter.get("it").unwrap(), "it-IT");
660
661 let mut settings = Settings {
664 lang: "it".to_string(),
665 ..Default::default()
666 };
667 settings.update_map_lang_filter_btmap();
668
669 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
670
671 assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
672 assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
673 assert_eq!(output_map_lang_filter.get("it"), None);
674 }
675
676 #[test]
677 #[cfg(feature = "lang-detection")]
678 fn test_update_env_lang_detection() {
679 let mut settings = Settings {
682 lang: "en-GB".to_string(),
683 ..Default::default()
684 };
685 unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
686 settings.update_env_lang_detection();
687
688 let output_get_lang_filter = settings
689 .get_lang_filter
690 .language_candidates
691 .iter()
692 .map(|l| {
693 let mut l = l.to_string();
694 l.push(' ');
695 l
696 })
697 .collect::<String>();
698 assert_eq!(output_get_lang_filter, "fr de hu en ");
699
700 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
701 assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
702 assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
703 assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
704
705 let mut settings = Settings {
708 lang: "en-GB".to_string(),
709 ..Default::default()
710 };
711 unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en-US") };
712 settings.update_env_lang_detection();
713
714 let output_get_lang_filter = settings
715 .get_lang_filter
716 .language_candidates
717 .iter()
718 .map(|l| {
719 let mut l = l.to_string();
720 l.push(' ');
721 l
722 })
723 .collect::<String>();
724 assert_eq!(output_get_lang_filter, "de de en ");
725
726 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
727 assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
728 assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
729
730 let mut settings = Settings {
733 lang: "en-GB".to_string(),
734 ..Default::default()
735 };
736 unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, en-US") };
737 settings.update_env_lang_detection();
738
739 assert!(settings.get_lang_filter.language_candidates.is_empty());
740
741 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
742 assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
743 assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
744
745 let mut settings = Settings {
748 lang: "en-GB".to_string(),
749 ..Default::default()
750 };
751 unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en") };
752 settings.update_env_lang_detection();
753
754 let output_get_lang_filter = settings
755 .get_lang_filter
756 .language_candidates
757 .iter()
758 .map(|l| {
759 let mut l = l.to_string();
760 l.push(' ');
761 l
762 })
763 .collect::<String>();
764 assert_eq!(output_get_lang_filter, "de de en ");
765
766 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
767 assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
768 assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
769
770 let mut settings = Settings {
773 lang: "en-GB".to_string(),
774 ..Default::default()
775 };
776 unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, de-AT, en") };
777 settings.update_env_lang_detection();
778
779 assert!(settings.get_lang_filter.language_candidates.is_empty());
780
781 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
782 assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
783 assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
784
785 let mut settings = Settings {
787 lang: "en-GB".to_string(),
788 ..Default::default()
789 };
790 unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
791 settings.update_env_lang_detection();
792
793 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
794 assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
795 assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
796 assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
797
798 let mut settings = Settings {
801 lang: "".to_string(),
802 ..Default::default()
803 };
804 unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "") };
805 settings.update_env_lang_detection();
806
807 assert_eq!(settings.get_lang_filter.mode, Mode::Disabled);
808 assert!(settings.map_lang_filter_btmap.is_none());
809
810 let mut settings = Settings {
813 lang: "xy-XY".to_string(),
814 ..Default::default()
815 };
816 unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "en-GB, fr") };
817 settings.update_env_lang_detection();
818
819 let output_get_lang_filter = settings
820 .get_lang_filter
821 .language_candidates
822 .iter()
823 .map(|l| {
824 let mut l = l.to_string();
825 l.push(' ');
826 l
827 })
828 .collect::<String>();
829 assert_eq!(output_get_lang_filter, "en fr ");
830
831 let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
832 assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
833
834 let mut settings = Settings {
837 lang: "en-GB".to_string(),
838 ..Default::default()
839 };
840 unsafe {
841 env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, xy-XY");
842 }
843 settings.update_env_lang_detection();
844
845 assert!(matches!(settings.get_lang_filter.mode, Mode::Error(..)));
846 assert!(settings.map_lang_filter_btmap.is_none());
847 let mut settings = Settings {
850 lang: "en-GB".to_string(),
851 ..Default::default()
852 };
853 unsafe {
854 env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "");
855 }
856 settings.update_env_lang_detection();
857
858 assert!(matches!(settings.get_lang_filter.mode, Mode::Disabled));
859 assert!(settings.map_lang_filter_btmap.is_none());
860 }
861}