use crate::config::{GetLang, LIB_CFG, Mode};
use crate::error::LibCfgError;
#[cfg(feature = "lang-detection")]
use lingua;
#[cfg(feature = "lang-detection")]
use lingua::IsoCode639_1;
use parking_lot::RwLock;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::env;
#[cfg(feature = "lang-detection")]
use std::str::FromStr;
#[cfg(target_family = "windows")]
use windows_sys::Win32::Globalization::GetUserDefaultLocaleName;
#[cfg(target_family = "windows")]
use windows_sys::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH;
pub const ENV_VAR_TPNOTE_SCHEME: &str = "TPNOTE_SCHEME";
pub const ENV_VAR_TPNOTE_EXTENSION_DEFAULT: &str = "TPNOTE_EXTENSION_DEFAULT";
pub const ENV_VAR_TPNOTE_LANG: &str = "TPNOTE_LANG";
pub const ENV_VAR_TPNOTE_LANG_PLUS_ALL: &str = "+all";
pub const ENV_VAR_TPNOTE_LANG_DETECTION: &str = "TPNOTE_LANG_DETECTION";
pub const ENV_VAR_TPNOTE_USER: &str = "TPNOTE_USER";
const ENV_VAR_LOGNAME: &str = "LOGNAME";
const ENV_VAR_USERNAME: &str = "USERNAME";
const ENV_VAR_USER: &str = "USER";
#[cfg(not(target_family = "windows"))]
const ENV_VAR_LANG: &str = "LANG";
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct Settings {
pub current_scheme: usize,
pub author: String,
pub lang: String,
pub force_lang: String,
pub extension_default: String,
pub get_lang_filter: GetLang,
pub map_lang_filter_btmap: Option<BTreeMap<String, String>>,
}
const DEFAULT_SETTINGS: Settings = Settings {
current_scheme: 0,
author: String::new(),
lang: String::new(),
force_lang: String::new(),
extension_default: String::new(),
get_lang_filter: GetLang {
mode: Mode::Disabled,
language_candidates: vec![],
relative_distance_min: 0.0,
consecutive_words_min: 0,
words_total_percentage_min: 0,
},
map_lang_filter_btmap: None,
};
impl Default for Settings {
#[cfg(not(any(test, doc)))]
fn default() -> Self {
DEFAULT_SETTINGS
}
#[cfg(any(test, doc))]
fn default() -> Self {
let mut settings = DEFAULT_SETTINGS;
settings.author = String::from("testuser");
settings.lang = String::from("ab-AB");
settings.extension_default = String::from("md");
settings
}
}
#[cfg(not(test))]
pub(crate) static SETTINGS: RwLock<Settings> = RwLock::new(DEFAULT_SETTINGS);
#[cfg(test)]
pub(crate) static SETTINGS: RwLock<Settings> = RwLock::new(DEFAULT_SETTINGS);
pub fn set_test_default_settings() -> Result<(), LibCfgError> {
let mut settings = SETTINGS.write();
settings.update(SchemeSource::Force("default"), None)
}
#[derive(Debug, Clone)]
pub(crate) enum SchemeSource<'a> {
Force(&'a str),
SchemeSyncDefault,
SchemeNewDefault(&'a str),
}
impl Settings {
pub(crate) fn update(
&mut self,
scheme_source: SchemeSource,
force_lang: Option<&str>,
) -> Result<(), LibCfgError> {
self.update_current_scheme(scheme_source)?;
self.update_author();
self.update_extension_default();
self.update_lang(force_lang);
self.update_get_lang_filter();
self.update_map_lang_filter_btmap();
self.update_env_lang_detection();
log::trace!(
"`SETTINGS` updated (reading config + env. vars.):\n{:#?}",
self
);
if let Mode::Error(e) = &self.get_lang_filter.mode {
Err(e.clone())
} else {
Ok(())
}
}
pub(crate) fn update_current_scheme(
&mut self,
scheme_source: SchemeSource,
) -> Result<(), LibCfgError> {
let lib_cfg = LIB_CFG.read_recursive();
let scheme = match scheme_source {
SchemeSource::Force(s) => Cow::Borrowed(s),
SchemeSource::SchemeSyncDefault => Cow::Borrowed(&*lib_cfg.scheme_sync_default),
SchemeSource::SchemeNewDefault(s) => match env::var(ENV_VAR_TPNOTE_SCHEME) {
Ok(ed_env) if !ed_env.is_empty() => Cow::Owned(ed_env),
Err(_) | Ok(_) => Cow::Borrowed(s),
},
};
self.current_scheme = lib_cfg.scheme_idx(scheme.as_ref())?;
Ok(())
}
fn update_author(&mut self) {
let author = env::var(ENV_VAR_TPNOTE_USER).unwrap_or_else(|_| {
env::var(ENV_VAR_LOGNAME).unwrap_or_else(|_| {
env::var(ENV_VAR_USERNAME)
.unwrap_or_else(|_| env::var(ENV_VAR_USER).unwrap_or_default())
})
});
self.author = author;
}
fn update_extension_default(&mut self) {
let ext = match env::var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT) {
Ok(ed_env) if !ed_env.is_empty() => ed_env,
Err(_) | Ok(_) => {
let lib_cfg = LIB_CFG.read_recursive();
lib_cfg.scheme[self.current_scheme]
.filename
.extension_default
.to_string()
}
};
self.extension_default = ext;
}
fn update_lang(&mut self, force_lang: Option<&str>) {
let mut lang = String::new();
let tpnotelang = env::var(ENV_VAR_TPNOTE_LANG).ok();
#[cfg(not(target_family = "windows"))]
if let Some(tpnotelang) = tpnotelang {
lang = tpnotelang;
} else {
if let Ok(lang_env) = env::var(ENV_VAR_LANG) {
if !lang_env.is_empty() {
let mut language = "";
let mut territory = "";
if let Some((l, lang_env)) = lang_env.split_once('_') {
language = l;
if let Some((t, _codeset)) = lang_env.split_once('.') {
territory = t;
}
}
lang = language.to_string();
lang.push('-');
lang.push_str(territory);
}
}
}
#[cfg(target_family = "windows")]
if let Some(tpnotelang) = tpnotelang {
lang = tpnotelang;
} else {
let mut buf = [0u16; LOCALE_NAME_MAX_LENGTH as usize];
let len = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32) };
if len > 0 {
lang = String::from_utf16_lossy(&buf[..((len - 1) as usize)]);
}
};
self.lang = lang;
self.force_lang = match force_lang {
Some("") => self.lang.clone(),
Some(lang) => lang.to_owned(),
None => String::new(),
};
}
#[cfg(feature = "lang-detection")]
fn update_get_lang_filter(&mut self) {
use crate::config::Mode;
{
let lib_cfg = LIB_CFG.read_recursive();
let current_scheme = &lib_cfg.scheme[self.current_scheme];
self.get_lang_filter = current_scheme.tmpl.filter.get_lang.clone();
}
if matches!(self.get_lang_filter.mode, Mode::Disabled) {
return;
}
let iso_codes = &mut self.get_lang_filter.language_candidates;
if iso_codes.is_empty() {
return;
}
if !self.lang.is_empty() {
if let Some((lang_subtag, _)) = self.lang.split_once('-') {
if let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
if !iso_codes.contains(&iso_code) {
iso_codes.push(iso_code);
}
}
}
}
if iso_codes.len() <= 1 {
self.get_lang_filter.mode = Mode::Error(LibCfgError::NotEnoughLanguageCodes {
language_code: iso_codes[0].to_string(),
})
}
}
#[cfg(not(feature = "lang-detection"))]
fn update_get_lang_filter(&mut self) {
self.get_lang_filter.mode = Mode::Disabled;
}
fn update_map_lang_filter_btmap(&mut self) {
let mut btm = BTreeMap::new();
let lib_cfg = LIB_CFG.read_recursive();
for l in &lib_cfg.scheme[self.current_scheme].tmpl.filter.map_lang {
if l.len() >= 2 {
btm.insert(l[0].to_string(), l[1].to_string());
};
}
if !self.lang.is_empty() {
if let Some((lang_subtag, _)) = self.lang.split_once('-') {
if !lang_subtag.is_empty() && !btm.contains_key(lang_subtag) {
btm.insert(lang_subtag.to_string(), self.lang.to_string());
}
};
}
self.map_lang_filter_btmap = Some(btm);
}
#[cfg(feature = "lang-detection")]
fn update_env_lang_detection(&mut self) {
use crate::config::Mode;
if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
if env_var.is_empty() {
self.get_lang_filter.mode = Mode::Disabled;
self.map_lang_filter_btmap = None;
log::debug!(
"Empty env. var. `{}` disables the `lang-detection` feature.",
ENV_VAR_TPNOTE_LANG_DETECTION
);
return;
}
let mut hm: BTreeMap<String, String> = BTreeMap::new();
let mut all_languages_selected = false;
let iso_codes = env_var
.split(',')
.map(|t| {
let t = t.trim();
if let Some((lang_subtag, _)) = t.split_once('-') {
if !lang_subtag.is_empty() && !hm.contains_key(lang_subtag) {
hm.insert(lang_subtag.to_string(), t.to_string());
};
lang_subtag
} else {
t
}
})
.filter(|&l| {
if l == ENV_VAR_TPNOTE_LANG_PLUS_ALL {
all_languages_selected = true;
false
} else {
true
}
})
.map(|l| {
IsoCode639_1::from_str(l.trim()).map_err(|_| {
let mut all_langs = lingua::Language::all()
.iter()
.map(|l| {
let mut s = l.iso_code_639_1().to_string();
s.push_str(", ");
s
})
.collect::<Vec<String>>();
all_langs.sort();
let mut all_langs = all_langs.into_iter().collect::<String>();
all_langs.truncate(all_langs.len() - ", ".len());
LibCfgError::ParseLanguageCode {
language_code: l.into(),
all_langs,
}
})
})
.collect::<Result<Vec<IsoCode639_1>, LibCfgError>>();
match iso_codes {
Ok(mut iso_codes) => {
if !self.lang.is_empty() {
if let Some(lang_subtag) = self.lang.split('-').next() {
if let Ok(iso_code) = IsoCode639_1::from_str(lang_subtag) {
if !iso_codes.contains(&iso_code) {
iso_codes.push(iso_code);
}
if lang_subtag != self.lang && !hm.contains_key(lang_subtag) {
hm.insert(lang_subtag.to_string(), self.lang.to_string());
}
}
}
}
if all_languages_selected {
self.get_lang_filter.language_candidates = vec![];
if matches!(self.get_lang_filter.mode, Mode::Disabled) {
self.get_lang_filter.mode = Mode::Multilingual;
}
} else {
match iso_codes.len() {
0 => self.get_lang_filter.mode = Mode::Disabled,
1 => {
self.get_lang_filter.mode =
Mode::Error(LibCfgError::NotEnoughLanguageCodes {
language_code: iso_codes[0].to_string(),
})
}
_ => {
self.get_lang_filter.language_candidates = iso_codes;
if matches!(self.get_lang_filter.mode, Mode::Disabled) {
self.get_lang_filter.mode = Mode::Multilingual;
}
}
}
}
self.map_lang_filter_btmap = Some(hm);
}
Err(e) =>
{
self.get_lang_filter.mode = Mode::Error(e);
}
}
}
}
#[cfg(not(feature = "lang-detection"))]
fn update_env_lang_detection(&mut self) {
if let Ok(env_var) = env::var(ENV_VAR_TPNOTE_LANG_DETECTION) {
if !env_var.is_empty() {
self.get_lang_filter.mode = Mode::Disabled;
self.map_lang_filter_btmap = None;
log::debug!(
"Ignoring the env. var. `{}`. The `lang-detection` feature \
is not included in this build.",
ENV_VAR_TPNOTE_LANG_DETECTION
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update_author_setting() {
let mut settings = Settings::default();
unsafe {
env::set_var(ENV_VAR_LOGNAME, "testauthor");
}
settings.update_author();
assert_eq!(settings.author, "testauthor");
}
#[test]
fn test_update_extension_default_setting() {
let mut settings = Settings::default();
unsafe {
env::set_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT, "markdown");
}
settings.update_extension_default();
assert_eq!(settings.extension_default, "markdown");
let mut settings = Settings::default();
unsafe {
std::env::remove_var(ENV_VAR_TPNOTE_EXTENSION_DEFAULT);
}
settings.update_extension_default();
assert_eq!(settings.extension_default, "md");
}
#[test]
#[cfg(not(target_family = "windows"))]
fn test_update_lang_setting() {
let mut settings = Settings::default();
unsafe {
env::remove_var(ENV_VAR_TPNOTE_LANG);
env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
}
settings.update_lang(None);
assert_eq!(settings.lang, "en-GB");
let mut settings = Settings::default();
unsafe {
env::remove_var(ENV_VAR_TPNOTE_LANG);
env::set_var(ENV_VAR_LANG, "");
}
settings.update_lang(None);
assert_eq!(settings.lang, "");
let mut settings = Settings::default();
unsafe {
env::set_var(ENV_VAR_TPNOTE_LANG, "it-IT");
env::set_var(ENV_VAR_LANG, "en_GB.UTF-8");
}
settings.update_lang(None);
assert_eq!(settings.lang, "it-IT");
}
#[test]
#[cfg(feature = "lang-detection")]
fn test_update_get_lang_filter_setting() {
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
settings.update_get_lang_filter();
let output_get_lang_filter = settings
.get_lang_filter
.language_candidates
.iter()
.map(|l| {
let mut l = l.to_string();
l.push(' ');
l
})
.collect::<String>();
assert_eq!(output_get_lang_filter, "en fr de ");
let mut settings = Settings {
lang: "it-IT".to_string(),
..Default::default()
};
settings.update_get_lang_filter();
let output_get_lang_filter = settings
.get_lang_filter
.language_candidates
.iter()
.map(|l| {
let mut l = l.to_string();
l.push(' ');
l
})
.collect::<String>();
assert_eq!(output_get_lang_filter, "en fr de it ");
}
#[test]
fn test_update_map_lang_filter_hmap_setting() {
let mut settings = Settings {
lang: "it-IT".to_string(),
..Default::default()
};
settings.update_map_lang_filter_btmap();
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
assert_eq!(output_map_lang_filter.get("it").unwrap(), "it-IT");
let mut settings = Settings {
lang: "it".to_string(),
..Default::default()
};
settings.update_map_lang_filter_btmap();
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
assert_eq!(output_map_lang_filter.get("et").unwrap(), "et-ET");
assert_eq!(output_map_lang_filter.get("it"), None);
}
#[test]
#[cfg(feature = "lang-detection")]
fn test_update_env_lang_detection() {
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
settings.update_env_lang_detection();
let output_get_lang_filter = settings
.get_lang_filter
.language_candidates
.iter()
.map(|l| {
let mut l = l.to_string();
l.push(' ');
l
})
.collect::<String>();
assert_eq!(output_get_lang_filter, "fr de hu en ");
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en-US") };
settings.update_env_lang_detection();
let output_get_lang_filter = settings
.get_lang_filter
.language_candidates
.iter()
.map(|l| {
let mut l = l.to_string();
l.push(' ');
l
})
.collect::<String>();
assert_eq!(output_get_lang_filter, "de de en ");
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, en-US") };
settings.update_env_lang_detection();
assert!(settings.get_lang_filter.language_candidates.is_empty());
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-US");
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, de-AT, en") };
settings.update_env_lang_detection();
let output_get_lang_filter = settings
.get_lang_filter
.language_candidates
.iter()
.map(|l| {
let mut l = l.to_string();
l.push(' ');
l
})
.collect::<String>();
assert_eq!(output_get_lang_filter, "de de en ");
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, +all, de-AT, en") };
settings.update_env_lang_detection();
assert!(settings.get_lang_filter.language_candidates.is_empty());
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "fr-FR, de-DE, hu") };
settings.update_env_lang_detection();
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("de").unwrap(), "de-DE");
assert_eq!(output_map_lang_filter.get("fr").unwrap(), "fr-FR");
assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
let mut settings = Settings {
lang: "".to_string(),
..Default::default()
};
unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "") };
settings.update_env_lang_detection();
assert_eq!(settings.get_lang_filter.mode, Mode::Disabled);
assert!(settings.map_lang_filter_btmap.is_none());
let mut settings = Settings {
lang: "xy-XY".to_string(),
..Default::default()
};
unsafe { env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "en-GB, fr") };
settings.update_env_lang_detection();
let output_get_lang_filter = settings
.get_lang_filter
.language_candidates
.iter()
.map(|l| {
let mut l = l.to_string();
l.push(' ');
l
})
.collect::<String>();
assert_eq!(output_get_lang_filter, "en fr ");
let output_map_lang_filter = settings.map_lang_filter_btmap.unwrap();
assert_eq!(output_map_lang_filter.get("en").unwrap(), "en-GB");
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
unsafe {
env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "de-DE, xy-XY");
}
settings.update_env_lang_detection();
assert!(matches!(settings.get_lang_filter.mode, Mode::Error(..)));
assert!(settings.map_lang_filter_btmap.is_none());
let mut settings = Settings {
lang: "en-GB".to_string(),
..Default::default()
};
unsafe {
env::set_var(ENV_VAR_TPNOTE_LANG_DETECTION, "");
}
settings.update_env_lang_detection();
assert!(matches!(settings.get_lang_filter.mode, Mode::Disabled));
assert!(settings.map_lang_filter_btmap.is_none());
}
}