## Coverage Report

#### Created: 2026-05-25 08:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/runner/work/MathCAT/MathCAT/src/prefs.rs

    Line
    Count
    Source
    1
    //! Preferences come from either the user or are programmatically set by the AT.
    2
    //! Either source can set any preference, but users and AT typically set different preferences.
    3
    //!
    4
    //! User prefs are read in from a YAML file (prefs.yaml). They can be written by hand.
    5
    //! In the future, there will hopefully be a nice UI that writes out the YAML file.
    6
    //!
    7
    //! AT prefs are set via the API given in the [crate::interface] module.
    8
    //! These in turn call [`PreferenceManager::set_string_pref`] and [`PreferenceManager::set_api_float_pref`].
    9
    //! Ultimately, user and api prefs are stored in a hashmap.
    10
    //!
    11
    //! Preferences can be found in a few places:
    12
    //! 1. Language-independent prefs found in the Rules dir
    13
    //! 2. Language-specific prefs
    14
    //! 3. Language-region-specific prefs
    15
    //!
    16
    //! If there are multiple definitions, the later ones overwrite the former ones.
    17
    //! This means that region-specific variants will overwrite more general variants.
    18
    //!
    19
    //! Note: there are a number of public 'get_xxx' functions that really are meant to be public only to the [crate::speech] module as speech needs access
    20
    //! to the preferences to generate the speech.
    21
    #![allow(clippy::needless_return)]
    22
    use yaml_rust::{Yaml, YamlLoader};
    23
    use crate::pretty_print::yaml_to_string;
    24
    use crate::tts::TTS;
    25
    use std::cell::RefCell;
    26
    use std::rc::Rc;
    27
    use log::{debug, error, warn};
    28
    use std::path::{Path, PathBuf};
    29
    use std::sync::LazyLock;
    30
    use crate::speech::{as_str_checked, RulesFor, FileAndTime};
    31
    use std::collections::{HashMap, HashSet};
    32
    use phf::phf_set;
    33
    use crate::shim_filesystem::*;
    34
    use crate::errors::*;
    35
    36
    /// Use to indicate preference not found with Preference::to_string()
    37
    pub static NO_PREFERENCE: &str = "\u{FFFF}";
    38
    39
    3
    static DEFAULT_LANG: LazyLock<Yaml> = LazyLock::new(|| Yaml::String("en".to_string()));
    40
    41
    42
    // Preferences are recorded here
    43
    /// Preferences are stored in a HashMap. It maps the name of the pref (a String) to its value (stored as YAML string/float)
    44
    pub type PreferenceHashMap = HashMap<String, Yaml>;
    45
    #[derive(Debug, Clone, Default)]
    46
    pub struct Preferences {
    47
        prefs: PreferenceHashMap        // FIX: pub so can get at iterator, should add iterator to Preferences instead
    48
    }
    49
    50
    use std::fmt;
    51
    impl fmt::Display for Preferences {
    52
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    53
    0
            let mut pref_vec: Vec<(&String, &Yaml)> = self.prefs.iter().collect();
    54
    0
            pref_vec.sort();
    55
    0
            for (name, value) in pref_vec {
    56
    0
                writeln!(f, "    {}: {}", name, yaml_to_string(value, 0))?;
    57
            }
    58
    0
            return Ok(());
    59
    0
        }
    60
    }
    61
    62
    impl Preferences{
    63
        // default values needed in case nothing else gets set
    64
    4.14k
        fn user_defaults() -> Preferences {
    65
    4.14k
            let mut prefs = PreferenceHashMap::with_capacity(39);
    66
    4.14k
            prefs.insert("Language".to_string(), Yaml::String("en".to_string()));
    67
    4.14k
            prefs.insert("LanguageAuto".to_string(), Yaml::String("".to_string()));     // illegal value so change will be recognized
    68
    4.14k
            prefs.insert("SpeechStyle".to_string(), Yaml::String("ClearSpeak".to_string()));
    69
    4.14k
            prefs.insert("Verbosity".to_string(), Yaml::String("Medium".to_string()));
    70
    4.14k
            prefs.insert("SpeechOverrides_CapitalLetters".to_string(), Yaml::String("".to_string())); // important for testing
    71
    4.14k
            prefs.insert("Blind".to_string(), Yaml::Boolean(true));
    72
    4.14k
            prefs.insert("MathRate".to_string(), Yaml::Real("100.0".to_string()));
    73
    4.14k
            prefs.insert("PauseFactor".to_string(), Yaml::Real("100.0".to_string()));
    74
    4.14k
            prefs.insert("NavMode".to_string(), Yaml::String("Enhanced".to_string()));
    75
    4.14k
            prefs.insert("Overview".to_string(), Yaml::Boolean(false));
    76
    4.14k
            prefs.insert("ResetOverView".to_string(), Yaml::Boolean(true));
    77
    4.14k
            prefs.insert("NavVerbosity".to_string(), Yaml::String("Verbose".to_string()));
    78
    4.14k
            prefs.insert("AutoZoomOut".to_string(), Yaml::Boolean(true));
    79
    4.14k
            prefs.insert("BrailleCode".to_string(), Yaml::String("Nemeth".to_string()));
    80
    4.14k
            prefs.insert("BrailleNavHighlight".to_string(), Yaml::String("EndPoints".to_string()));
    81
    4.14k
            prefs.insert("UEB_START_MODE".to_string(), Yaml::String("Grade2".to_string()));
    82
    4.14k
            prefs.insert("DecimalSeparators".to_string(), Yaml::String(".".to_string()));
    83
    4.14k
            prefs.insert("BlockSeparators".to_string(), Yaml::String(", \u{00A0}\u{202F}".to_string()));
    84
    85
    4.14k
            return Preferences{ prefs };
    86
    4.14k
        }
    87
    88
        // default values needed in case nothing else gets set
    89
    4.14k
        fn api_defaults() -> Preferences {
    90
    4.14k
            let mut prefs = PreferenceHashMap::with_capacity(19);
    91
    4.14k
            prefs.insert("TTS".to_string(), Yaml::String("none".to_string()));
    92
    4.14k
            prefs.insert("Pitch".to_string(), Yaml::Real("0.0".to_string()));
    93
    4.14k
            prefs.insert("Rate".to_string(), Yaml::Real("180.0".to_string()));
    94
    4.14k
            prefs.insert("Volume".to_string(), Yaml::Real("100.0".to_string()));
    95
    4.14k
            prefs.insert("Voice".to_string(), Yaml::String("none".to_string()));
    96
    4.14k
            prefs.insert("Gender".to_string(), Yaml::String("none".to_string()));
    97
    4.14k
            prefs.insert("Bookmark".to_string(), Yaml::Boolean(false));
    98
    4.14k
            prefs.insert("CapitalLetters_UseWord".to_string(), Yaml::Boolean(true));
    99
    4.14k
            prefs.insert("CapitalLetters_Pitch".to_string(), Yaml::Real("0.0".to_string()));
    100
    4.14k
            prefs.insert("CapitalLetters_Beep".to_string(), Yaml::Boolean(false));
    101
    4.14k
            prefs.insert("IntentErrorRecovery".to_string(), Yaml::String("IgnoreIntent".to_string()));    // also Error
    102
    4.14k
            prefs.insert("CheckRuleFiles".to_string(), Yaml::String(
    103
    4.14k
                        (if cfg!(target_family = "wasm") {"None"0} else {"Prefs"}).to_string()));    // avoid checking for rule files being changed (40% speedup!) (All, Prefs, None)
    104
    4.14k
            return Preferences{ prefs };
    105
    4.14k
        }
    106
    107
    4.14k
        fn read_prefs_file(file: &Path, mut base_prefs: Preferences) -> Result<Preferences> {
    108
    4.14k
            let file_name = file.to_str().unwrap();
    109
            let docs;
    110
    4.14k
            match read_to_string_shim(file) {
    111
    0
                Err(e) => {
    112
    0
                    bail!("Couldn't read file {}\n{}", file_name, e);
    113
                },
    114
    4.14k
                Ok( file_contents) => {
    115
    4.14k
                    match YamlLoader::load_from_str(&file_contents) {
    116
    0
                        Err(e) => {
    117
    0
                            bail!("Yaml parse error ('{}') in preference file {}.", e, file_name);
    118
                        },
    119
    4.14k
                        Ok(d) => docs = d,
    120
                    }
    121
    122
                }
    123
            }
    124
    4.14k
            if docs.len() != 1 {
    125
    0
                bail!("MathCAT: error in prefs file '{}'.\nFound {} 'documents' -- should only be 1.", file_name, docs.len());
    126
    4.14k
            }
    127
    128
    4.14k
            let doc = &docs[0];
    129
    4.14k
            if cfg!(debug_assertions) {
    130
    4.14k
                verify_keys(doc, "Speech", file_name)?0;
    131
    4.14k
                verify_keys(doc, "Navigation", file_name)?0;
    132
    4.14k
                verify_keys(doc, "Braille", file_name)?0;
    133
    4.14k
                verify_keys(doc, "Other", file_name)?0;
    134
    0
            }
    135
    136
    4.14k
            let prefs = &mut base_prefs.prefs;
    137
    4.14k
            add_prefs(prefs, &doc["Speech"], "", file_name);
    138
    4.14k
            add_prefs(prefs, &doc["Navigation"], "", file_name);
    139
    4.14k
            add_prefs(prefs, &doc["Braille"], "", file_name);
    140
    4.14k
            add_prefs(prefs, &doc["Other"], "", file_name);
    141
    4.14k
            return Ok( Preferences{ prefs: prefs.to_owned() } );
    142
    143
    144
    145
    16.5k
            fn verify_keys(dict: &Yaml, key: &str, file_name: &str) -> Result<()> {
    146
    16.5k
                let prefs = &dict[key];
    147
    16.5k
                if prefs.is_badvalue() {
    148
    0
                    bail!("Yaml error in file {}.\nDidn't find '{}' key.", file_name, key);
    149
    16.5k
                }
    150
    16.5k
                if prefs.as_hash().is_none() {
    151
    0
                    bail!("Yaml error in file {}.\n'{}' key is not a dictionary. Value found is {}.",
    152
    0
                                file_name, key, yaml_to_string(dict, 1));
    153
    16.5k
                }
    154
    16.5k
                return Ok(());
    155
    16.5k
            }
    156
    157
    41.4k
            fn add_prefs(map: &mut PreferenceHashMap, new_prefs: &Yaml, name_prefix: &str, file_name: &str) {
    158
    41.4k
                if new_prefs.is_badvalue() || new_prefs.is_null() || new_prefs.as_hash().is_none() {
    159
    0
                    return;
    160
    41.4k
                }
    161
    41.4k
                let new_prefs = new_prefs.as_hash().unwrap();
    162
    302k
                for (yaml_name, yaml_value) in new_prefs41.4k {
    163
    302k
                    let name = as_str_checked(yaml_name);
    164
    302k
                    if let Err(e0) = name {
    165
    0
                        error!("{}", e.context(
    166
    0
                            format!("name '{}' is not a string in file {}", yaml_to_string(yaml_name, 0), file_name)));
    167
                    } else {
    168
    302k
                        match yaml_value {
    169
    24.8k
                            Yaml::Hash(_) => add_prefs(map, yaml_value, &(name.unwrap().to_string() + "_"), file_name),
    170
    0
                            Yaml::Array(_) => error!("name '{}' has illegal array value {} in file '{}'",
    171
    0
                                                     yaml_to_string(yaml_name, 0), yaml_to_string(yaml_value, 0), file_name),
    172
                            Yaml::String(_) | Yaml::Boolean(_) | Yaml::Integer(_) | Yaml::Real(_) => {
    173
    277k
                                let trimmed_name = name_prefix.to_string() + name.unwrap().trim();
    174
    277k
                                let mut yaml_value = yaml_value.to_owned();
    175
    277k
                                if let Some(value236k) = yaml_value.as_str() {
    176
    236k
                                    yaml_value = Yaml::String(value.to_string());
    177
    236k
                                }41.4k
    178
    277k
                                map.insert(trimmed_name, yaml_value);
    179
                            },
    180
    0
                            _ => error!("name '{}' has illegal {:#?} value {} in file '{}'",
    181
    0
                                        yaml_to_string(yaml_name, 0), yaml_value, yaml_to_string(yaml_value, 0), file_name),
    182
                        }
    183
                    }
    184
                }
    185
    41.4k
            }
    186
    4.14k
        }
    187
    188
        #[allow(dead_code)]     // used in testing
    189
    0
        fn set_string_value(&mut self, name: &str, value: &str) {
    190
    0
            self.prefs.insert(name.to_string(), Yaml::String(value.trim().to_string()));
    191
    0
        }
    192
    193
        #[allow(dead_code)]     // used in testing
    194
    0
        fn set_bool_value(&mut self, name: &str, value: bool) {
    195
    0
            self.prefs.insert(name.to_string(), Yaml::Boolean(value));
    196
    0
        }
    197
    }
    198
    199
    200
    thread_local!{
    201
        static DEFAULT_USER_PREFERENCES: Preferences = Preferences::user_defaults();
    202
        static DEFAULT_API_PREFERENCES: Preferences = Preferences::api_defaults();
    203
        static PREF_MANAGER: Rc<RefCell<PreferenceManager>> =
    204
                Rc::new( RefCell::new( PreferenceManager::default() ) );
    205
    206
    }
    207
    208
    /// PreferenceManager keeps track of user and api prefs along with current files
    209
    ///
    210
    /// If one of the `FileAndTime` files changes while the program is running, the values will auto-update
    211
    /// Among other things, that means that a UI that changes a user pref will be reflected the next time someone gets speech, braille, etc.
    212
    //
    213
    // Note: I experimented with PREF_MANAGER being a Result<PreferenceManager> in the case of no rule files,
    214
    //   but it ended up being a mess (lots of unwrapping). Having a field is much cleaner.
    215
    //   Also note that if 'error' is not an empty string, SpeechRules can't work so using those requires a check.
    216
    #[derive(Debug, Default)]
    217
    pub struct PreferenceManager {
    218
        rules_dir: PathBuf,                   // full path to rules dir
    219
        error: String,                        // empty/default string if fields are set, otherwise error message
    220
        user_prefs: Preferences,              // prefs that come from reading prefs.yaml (system and user locations)
    221
        api_prefs: Preferences,               // prefs set by API calls (along with some defaults not in the user settings such as "pitch")
    222
        sys_prefs_file: Option<FileAndTime>,  // the system prefs.yaml file
    223
        user_prefs_file: Option<FileAndTime>, // the user prefs.yaml file
    224
        intent: PathBuf,                      // the intent rule style file
    225
        speech: PathBuf,                      // the speech rule style file
    226
        overview: PathBuf,                    // the overview rule file
    227
        navigation: PathBuf,                  // the navigation rule file
    228
        speech_unicode: PathBuf,              // short unicode.yaml file
    229
        speech_unicode_full: PathBuf,         // full unicode.yaml file
    230
        speech_defs: PathBuf,                 // the definition.yaml file
    231
        braille: PathBuf,                     // the braille rule file
    232
        braille_unicode: PathBuf,             // short braille unicode file
    233
        braille_unicode_full: PathBuf,        // full braille unicode file
    234
        braille_defs: PathBuf,                // the definition.yaml file
    235
    }
    236
    237
    238
    impl fmt::Display for PreferenceManager {
    239
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    240
    0
            writeln!(f, "PreferenceManager:")?;
    241
    0
            if self.error.is_empty() {
    242
    0
                writeln!(f, "  not initialized!!! Error is {}", &self.error)?;
    243
            } else {
    244
    0
                writeln!(f, "  user prefs:\n{}", self.user_prefs)?;
    245
    0
                writeln!(f, "  api prefs:\n{}", self.api_prefs)?;
    246
    0
                writeln!(f, "  style files: {:?}", self.speech.as_path())?;
    247
    0
                writeln!(f, "  unicode files: {:?}", self.speech_unicode.as_path())?;
    248
    0
                writeln!(f, "  intent files: {:?}", self.intent.as_path())?;
    249
    0
                writeln!(f, "  speech definition files: {:?}", self.speech_defs)?;
    250
    0
                writeln!(f, "  braille definition files: {:?}", self.braille_defs)?;
    251
            }
    252
    0
            return Ok(());
    253
    0
        }
    254
    }
    255
    256
    impl PreferenceManager {
    257
        /// Initialize (the) PreferenceManager (a global var).
    258
        /// 'rules_dir' is the path to "Rules" unless the env var MathCATRulesDir is set
    259
        ///
    260
        /// If rules_dir is an empty PathBuf, the existing rules_dir is used (an error if it doesn't exist)
    261
    5.09k
        pub fn initialize(&mut self, rules_dir: PathBuf) -> Result<()> {
    262
            #[cfg(not(feature = "include-zip"))]
    263
    5.09k
            let rules_dir5.09k = match rules_dir.canonicalize() {
    264
    1
                Err(e) => bail!("set_rules_dir: could not canonicalize path {}: {}", rules_dir.display(), e),
    265
    5.09k
                Ok(rules_dir) =>  rules_dir,
    266
            };
    267
    268
    5.09k
            self.set_rules_dir(&rules_dir)?0;
    269
    5.09k
            self.set_preference_files()?0;
    270
    5.09k
            self.set_all_files(&rules_dir)?0;
    271
    5.09k
            return Ok( () );
    272
    273
    5.09k
        }
    274
    275
    87.7k
        pub fn get() -> Rc<RefCell<PreferenceManager>> {
    276
    87.7k
            return PREF_MANAGER.with( |pm| pm.clone() );
    277
    87.7k
        }
    278
    279
    0
        pub fn get_error(&self) -> &str {
    280
    0
            return &self.error;
    281
    0
        }
    282
    283
        /// Return a `PreferenceHashMap` that is the merger of the api prefs onto the user prefs.
    284
    22.7k
        pub fn merge_prefs(&self) -> PreferenceHashMap {
    285
    22.7k
            let mut merged_prefs = self.user_prefs.prefs.clone();
    286
    22.7k
            merged_prefs.extend(self.api_prefs.prefs.clone());
    287
    22.7k
            return merged_prefs;
    288
    22.7k
        }
    289
    290
        /// Set the rules dir and return failure if it is a bad directory (non-existent, can't find all files, ...)
    291
    5.09k
        fn set_rules_dir(&mut self, rules_dir: &Path) -> Result<()> {
    292
            // Fix: should make sure all files exists -- fail if not true
    293
    5.09k
            if !is_dir_shim(rules_dir) {
    294
    0
                bail!("Unable to find MathCAT Rules directory '{}'", rules_dir.to_string_lossy())
    295
    5.09k
            }
    296
    5.09k
            self.rules_dir = rules_dir.to_path_buf();
    297
    5.09k
            return Ok( () );
    298
    5.09k
        }
    299
    300
        /// Set the rules dir and return failure if it is a bad directory (non-existent, can't find all files, ...)
    301
    1
        pub fn get_rules_dir(&self) -> PathBuf {
    302
            // Fix: should make sure rules_dir is set -- fail if not true
    303
    1
            return self.rules_dir.clone();
    304
    1
        }
    305
    306
        /// Read the preferences from the files (if not up to date) and set the preferences and preference files
    307
        /// Returns failure if the files don't exist or have errors
    308
    20.4k
        pub fn set_preference_files(&mut self) -> Result<()> {
    309
            // first, read in the preferences -- need to determine which files to read next
    310
            // the prefs files are in the rules dir and the user dir; differs from other files
    311
    20.4k
            if self.api_prefs.prefs.is_empty() {
    312
    4.14k
                self.api_prefs = Preferences{ prefs: DEFAULT_API_PREFERENCES.with(|defaults| defaults.prefs.clone()) };
    313
    16.2k
            }
    314
    315
    20.4k
            let should_update_system_prefs = self.sys_prefs_file.is_none() || !16.2kself.sys_prefs_file16.2k.as_ref().unwrap().is_up_to_date();
    316
    20.4k
            let should_update_user_prefs = self.user_prefs_file.is_none() || !16.2kself.user_prefs_file16.2k.as_ref().unwrap().is_up_to_date();
    317
    20.4k
            if !(should_update_system_prefs || should_update_user_prefs16.2k) {
    318
    16.2k
                return Ok( () );            // no need to do anything else
    319
    4.14k
            }
    320
    321
    4.14k
            let mut prefs = Preferences::default();
    322
    323
    4.14k
            let mut system_prefs_file = self.rules_dir.to_path_buf();
    324
    4.14k
            system_prefs_file.push("prefs.yaml");
    325
    4.14k
            if is_file_shim(&system_prefs_file) {
    326
    4.14k
                let defaults = DEFAULT_USER_PREFERENCES.with(|defaults| defaults.clone());
    327
    4.14k
                prefs = Preferences::read_prefs_file(&system_prefs_file, defaults)?0;
    328
    4.14k
                self.sys_prefs_file = Some( FileAndTime::new_with_time(system_prefs_file.clone()) );
    329
            } else {
    330
    0
                error!("MathCAT couldn't open file system preference file '{}'.\nUsing fallback defaults which may be inappropriate.",
    331
    0
                            system_prefs_file.to_str().unwrap());
    332
            };
    333
    334
    4.14k
            let mut user_prefs_file = dirs::config_dir();
    335
    4.14k
            if let Some(mut user_prefs_file_path_buf) = user_prefs_file {
    336
    4.14k
                user_prefs_file_path_buf.push("MathCAT/prefs.yaml");
    337
    4.14k
                if is_file_shim(&user_prefs_file_path_buf) {
    338
    0
                    prefs = Preferences::read_prefs_file(&user_prefs_file_path_buf, prefs)?;
    339
    4.14k
                }
    340
                // set the time otherwise keeps needing to do updates
    341
    4.14k
                self.user_prefs_file = Some( FileAndTime::new_with_time(user_prefs_file_path_buf.clone()) );
    342
    4.14k
                user_prefs_file = Some(user_prefs_file_path_buf);
    343
    0
            }
    344
    345
    4.14k
            if prefs.prefs.is_empty() {
    346
    0
                let user_prefs_file_name = match user_prefs_file {
    347
    0
                    None => "No user config directory".to_string(),
    348
    0
                    Some(file) => file.to_string_lossy().to_string(),
    349
                };
    350
    0
                bail!("Didn't find preferences in rule directory ('{}') or user directory ('{}')", &system_prefs_file.to_string_lossy(), user_prefs_file_name);
    351
    4.14k
            }
    352
    4.14k
            self.set_files_based_on_changes(&prefs)?0;
    353
    4.14k
            self.user_prefs = prefs;
    354
    355
            // set computed values for BLOCK_SEPARATORS and DECIMAL_SEPARATORS (a little messy about the language due immutable and mutable borrows)
    356
    4.14k
            let language = self.user_prefs.prefs.get("Language").unwrap_or(&DEFAULT_LANG).clone();
    357
    4.14k
            let language = language.as_str().unwrap();
    358
    4.14k
            self.set_separators(language)?0;
    359
    360
    4.14k
            return Ok( () );
    361
    20.4k
        }
    362
    363
    5.09k
        fn set_all_files(&mut self, rules_dir: &Path) -> Result<()> {
    364
            // try to find ./Rules/lang/style.yaml and ./Rules/lang/style.yaml
    365
            // we go through a series of fallbacks -- we try to maintain the language if possible
    366
    367
    5.09k
            let language = self.pref_to_string("Language");
    368
    5.09k
            let language = if language.as_str() == "Auto" {"en"4.14k} else {language.as_str()949};       // avoid 'temp value dropped while borrowed' error
    369
    5.09k
            let language_dir = rules_dir.to_path_buf().join("Languages");
    370
    5.09k
            self.set_speech_files(&language_dir, language, None)?0;  // also sets style file
    371
    372
    5.09k
            let braille_code = self.pref_to_string("BrailleCode");
    373
    5.09k
            let braille_dir = rules_dir.to_path_buf().join("Braille");
    374
    5.09k
            self.set_braille_files(&braille_dir, &braille_code)?0;
    375
    5.09k
            return Ok(());
    376
    5.09k
        }
    377
    378
    9.20k
        fn set_speech_files(&mut self, language_dir: &Path, language: &str, new_speech_style: Option<&str>) -> Result<()> {
    379
    9.20k
            PreferenceManager::unzip_files(language_dir, language, Some("en"))?0;
    380
    9.20k
            self.intent = PreferenceManager::find_file(language_dir, language, Some("en"), "intent.yaml")?0;
    381
    9.20k
            self.overview = PreferenceManager::find_file(language_dir, language, Some("en"), "overview.yaml")?0;
    382
    9.20k
            self.navigation = PreferenceManager::find_file(language_dir, language, Some("en"), "navigate.yaml")?0;
    383
    384
    9.20k
            self.speech_unicode = PreferenceManager::find_file(language_dir, language, Some("en"), "unicode.yaml")?0;
    385
    9.20k
            self.speech_unicode_full = PreferenceManager::find_file(language_dir, language, Some("en"), "unicode-full.yaml")?0;
    386
    387
    9.20k
            self.speech_defs = PreferenceManager::find_file(language_dir, language, Some("en"), "definitions.yaml")?0;
    388
    389
    9.20k
            match new_speech_style {
    390
    0
                Some(style_name) => self.set_style_file(language_dir, language, style_name)?,
    391
                // use the old style name if one isn't given
    392
    9.20k
                None => self.set_style_file(language_dir, language, &self.pref_to_string("SpeechStyle"))?0,
    393
            }
    394
    9.20k
            return Ok( () );
    395
    9.20k
        }
    396
    397
    10.7k
        fn set_style_file(&mut self, language_dir: &Path, language: &str, style_file_name: &str) -> Result<()> {
    398
    10.7k
            let style_file_name = style_file_name.to_string() + "_Rules.yaml";
    399
    10.7k
            self.speech = PreferenceManager::find_file(language_dir, language, Some("en"), &style_file_name)?0;
    400
            // debug!("set_style_file: language_dir: {}, language: {}, style_file_name: {}, self.speech: {}",
    401
            //        language_dir.display(), language, style_file_name, self.speech.display());
    402
    10.7k
            return Ok( () );
    403
    10.7k
        }
    404
    405
    5.69k
        fn set_braille_files(&mut self, braille_rules_dir: &Path, braille_code_name: &str) -> Result<()> {
    406
            // Fix: Currently the braille code and the directory it lives in have to have the same name
    407
    5.69k
            PreferenceManager::unzip_files(braille_rules_dir, braille_code_name, Some("UEB"))?0;
    408
    409
    5.69k
            let braille_file = braille_code_name.to_string() + "_Rules.yaml";
    410
    411
    5.69k
            self.braille = PreferenceManager::find_file(braille_rules_dir, braille_code_name, Some("UEB"), &(braille_file))?0;
    412
    413
    5.69k
            self.braille_unicode = PreferenceManager::find_file(braille_rules_dir, braille_code_name, Some("UEB"), "unicode.yaml")?0;
    414
    5.69k
            self.braille_unicode_full = PreferenceManager::find_file(braille_rules_dir, braille_code_name, Some("UEB"), "unicode-full.yaml")?0;
    415
    416
    5.69k
            self.braille_defs = PreferenceManager::find_file(braille_rules_dir, braille_code_name, Some("UEB"), "definitions.yaml")?0;
    417
    5.69k
            return Ok( () );
    418
    5.69k
        }
    419
    420
        /// If some preferences have changed, we may need to recompute other ones
    421
        /// The key prefs are Language, SpeechStyle, and BrailleCode, along with DecimalSeparator
    422
    4.14k
        fn set_files_based_on_changes(&mut self, new_prefs: &Preferences) -> Result<()> {
    423
    4.14k
            let old_language = self.user_prefs.prefs.get("Language");       // not set if first time
    424
    4.14k
            if old_language.is_none() {
    425
    4.14k
                return Ok( () );            // if "Language" isn't set yet, nothing else is either -- first time through, so no updating needed.
    426
    0
            }
    427
    428
    0
            let old_language = old_language.unwrap();
    429
    0
            let new_language = new_prefs.prefs.get("Language").unwrap();
    430
    0
            debug!("set_files_based_on_changes: old_language={old_language:?}, new_language={new_language:?}");
    431
    0
            if old_language != new_language {
    432
    0
                let language_dir = self.rules_dir.to_path_buf().join("Languages");
    433
    0
                self.set_speech_files(&language_dir, new_language.as_str().unwrap(), None)?;  // also sets style file
    434
            } else {
    435
    0
                let old_speech_style = self.user_prefs.prefs.get("SpeechStyle").unwrap();
    436
    0
                let new_speech_style = new_prefs.prefs.get("SpeechStyle").unwrap();
    437
    0
                let language_dir = self.rules_dir.to_path_buf().join("Languages");
    438
    0
                if old_speech_style != new_speech_style {
    439
    0
                    self.set_speech_files(&language_dir, new_language.as_str().unwrap(), new_speech_style.as_str())?;
    440
    0
                }
    441
            }
    442
    443
    0
            let old_braille_code = self.user_prefs.prefs.get("BrailleCode").unwrap();
    444
    0
            let new_braille_code = new_prefs.prefs.get("BrailleCode").unwrap();
    445
    0
            if old_braille_code != new_braille_code {
    446
    0
                let braille_code_dir = self.rules_dir.to_path_buf().join("Braille");
    447
    0
                self.set_braille_files(&braille_code_dir, new_braille_code.as_str().unwrap())?;  // also sets style file
    448
    0
            }
    449
    450
    0
            return Ok( () );
    451
    4.14k
        }
    452
    453
        /// Unzip the files if needed
    454
        /// Returns true if it unzipped them
    455
    41.3k
        pub fn unzip_files(path: &Path, lang: &str, default_lang: Option<&str>) -> Result<bool> {
    456
            thread_local!{
    457
                /// when a language/braille code dir is unzipped, it is recorded here
    458
                static UNZIPPED_FILES: RefCell<HashSet<String>> = RefCell::new( HashSet::with_capacity(31));
    459
            }
    460
            // ignore regional subdirs
    461
    41.3k
            let dir = PreferenceManager::get_language_dir(path, lang, default_lang)?0;
    462
    41.3k
            let language = if dir.ends_with(lang) {lang39.5k} else {dir.file_name().unwrap()1.76k.to_str().unwrap()};
    463
    41.3k
            let zip_file_name = language.to_string() + ".zip";
    464
    41.3k
            let zip_file_path = dir.join(&zip_file_name);
    465
    41.3k
            let zip_file_string = zip_file_path.to_string_lossy().to_string();
    466
            // debug!("unzip_files: dir: {}, zip_file_name: {}, zip_file_path: {}", dir.display(), zip_file_name, zip_file_string);
    467
    41.3k
            if UNZIPPED_FILES.with( |unzipped_files| unzipped_files.borrow().contains(&zip_file_string)) {
    468
    28.6k
                return Ok(false);
    469
    12.6k
            }
    470
    471
    12.6k
            let result11.2k = match zip_extract_shim(&dir, &zip_file_name) {
    472
    1.44k
                Err(e) => {
    473
    1.44k
                    if lang.contains('-') {
    474
                        // try again in parent dir of regional language
    475
    0
                        let language = lang.split_once('-').unwrap_or((lang, "")).0; // get the parent language
    476
                        // debug!("unzip_files: trying again in parent language: {}", language);
    477
    0
                        PreferenceManager::unzip_files(path, language, default_lang)
    478
    0
                                                    .with_context(|| format!("Couldn't open zip file {zip_file_string} in parent {language}: {e}."))?
    479
                    } else {
    480
                        // maybe just regional dialects
    481
    1.44k
                        let mut regional_dirs = Vec::new();
    482
    1.44k
                        find_all_dirs_shim(&dir, &mut regional_dirs);
    483
    1.44k
                        for dir in regional_dirs {
    484
                            // debug!("unzip_files: trying again in subdir: {}", dir.display());
    485
    1.44k
                            let language = format!("{}-{}", lang, dir.file_name().unwrap().to_str().unwrap());
    486
    1.44k
                            if let Ok(result) =PreferenceManager::unzip_files(path, &language, default_lang) {
    487
    1.44k
                                return Ok(result);
    488
    0
                            }
    489
                        }
    490
    0
                        bail!("Couldn't open zip file {}: {}.", zip_file_string, e)
    491
                    }
    492
                },
    493
    11.2k
                Ok(result) => {
    494
    11.2k
                    result
    495
                },
    496
            };
    497
    498
    11.2k
            UNZIPPED_FILES.with( |unzipped_files| unzipped_files.borrow_mut().insert(zip_file_string.clone()) );
    499
            // debug!("  unzip_files: unzipped {} files from {}", result, &zip_file_string);
    500
            // UNZIPPED_FILES.with( |unzipped_files| {
    501
            //     debug!("unzip_files: unzipped_files: {:?}", unzipped_files.borrow());
    502
            // });
    503
    504
    11.2k
            return Ok(result);
    505
    41.3k
        }
    506
    507
        /// Set BlockSeparators and DecimalSeparators
    508
        /// FIX: changing these values could change the parse, so we really should reparse the original expr, but that doesn't exist anymore (store the original string???)
    509
        ///
    510
        /// Note: DecimalSeparator is user-facing (can be Auto), DecimalSeparators is code-facing (always a char)
    511
    8.25k
        fn set_separators(&mut self, language_country: &str) -> Result<()> {
    512
            // This list was generated from https://en.wikipedia.org/wiki/Decimal_separator#Countries_using_decimal_point
    513
            // The countries were then mapped to language(s) using https://en.wikipedia.org/wiki/List_of_official_languages_by_country_and_territory
    514
            // When a language was used in other countries that used a "," separator, the language+country is listed
    515
            //   Sometimes there are multiple languages used in a country -- they are all listed, sometimes with a country code
    516
            // The country code isn't used when the language is used in smaller countries (i.e, when "." is more likely correct)
    517
            //   This decision is sometimes a bit arbitrary
    518
            //   For example, Swahili (sw) is used in: Democratic Republic of the Congo, Kenya, Rwanda, Tanzania, and Uganda.
    519
            //   Of these, Kenya, Tanzania, and Uganda are listed as using "." and I include Swahili in the list below.
    520
            static USE_DECIMAL_SEPARATOR: phf::Set<&str> = phf_set! {
    521
                "en", "bn", "km", "el-cy", "tr-cy", "zh", "es-do", "ar", "es-sv", "es-gt", "es-hn", "hi", "as", "gu", "kn", "ks",
    522
                "ml", "mr", "ne", "or", "pa", "sa", "sd", "ta", "te", "ur", "he", "ja", "sw", "ko", "de-li", "ms", "dv", "mt", "es-mx", "my",
    523
                "af-na", "es-ni", "es-pa", "fil", "ms-sg", "si", "th",
    524
                "es-419", // latin america
    525
            };
    526
    527
    8.25k
            let decimal_separator = self.pref_to_string("DecimalSeparator");
    528
    8.25k
            if !["Auto", ",", "."].contains(&decimal_separator.as_str()) {
    529
    2
                return Ok( () );
    530
    8.25k
            }
    531
    532
    8.25k
            if language_country == "Auto" && decimal_separator == "Auto"4.14k {
    533
    4.14k
                return Ok( () );        // "Auto" doesn't tell us anything -- we will get called again when Language is set
    534
    4.10k
            }
    535
    536
    4.10k
            let language_country = language_country.to_ascii_lowercase();
    537
    4.10k
            let language_country = &language_country;
    538
    4.10k
            let mut lang_country_split = language_country.split('-');
    539
    4.10k
            let language = lang_country_split.next().unwrap_or("");
    540
    4.10k
            let country = lang_country_split.next().unwrap_or("");
    541
    4.10k
            let mut use_period = decimal_separator == ".";
    542
    4.10k
            if decimal_separator == "Auto" {
    543
                // if we don't have a match for the lang-country, then just try lang
    544
    4.10k
                use_period = USE_DECIMAL_SEPARATOR.contains(language_country) || USE_DECIMAL_SEPARATOR2.34k.contains2.34k(language2.34k);
    545
    2
            }
    546
            // debug!("set_separators: use_period: {}", use_period);
    547
    4.10k
            self.user_prefs.prefs.insert("DecimalSeparators".to_string(), Yaml::String((if use_period {"."2.00k} else {","2.10k}).to_string()));
    548
    4.10k
            let mut block_separators =  (if use_period {", \u{00A0}\u{202F}"2.00k} else {". \u{00A0}\u{202F}"2.10k}).to_string();
    549
    4.10k
            if country == "ch" || country == "li" { // Switzerland and Liechtenstein also use ` as a block separator, at least in some cases
    550
    0
                block_separators.push('\'');
    551
    4.10k
            }
    552
    4.10k
            self.user_prefs.prefs.insert("BlockSeparators".to_string(), Yaml::String(block_separators));
    553
    4.10k
            return Ok( () );
    554
    8.25k
        }
    555
    556
    557
        /// Find a file matching `file_name` by starting in the regional directory and looking to the language.
    558
        /// If that fails, fall back to looking for the default repeating the same process -- something needs to be found or MathCAT crashes
    559
    88.7k
        fn find_file(rules_dir: &Path, lang: &str, default_lang: Option<&str>, file_name: &str) -> Result<PathBuf> {
    560
            // rules_dir: is the root of the search
    561
            //   to that we add the language dir(s)
    562
            //   if file_name doesn't exist in the language dir(s), we try to find it in the default dir
    563
            //   the exception to this is if it ends with _Rules.yaml, we look for other _Rules.yaml files
    564
            // returns the location of the file_name found
    565
    566
            // start by trying to find a dir that exists
    567
    88.7k
            let lang_dir = PreferenceManager::get_language_dir(rules_dir, lang, default_lang)?0;
    568
            // now find the file name in the dirs
    569
            // we start with the deepest dir and walk back to towards Rules
    570
    88.7k
            let mut alternative_style_file = None;      // back up in case we don't find the target style in lang_dir
    571
    88.7k
            let looking_for_style_file = file_name.ends_with("_Rules.yaml");
    572
    108k
            for os_path in lang_dir.ancestors()88.7k {   // ancestor returns self and ancestors
    573
    108k
                let path = PathBuf::from(os_path).join(file_name);
    574
                // debug!("find_file: checking file: {}", path.to_string_lossy());
    575
    108k
                if is_file_shim(&path) {
    576
                    // we make an exception for definitions.yaml -- there a language specific checks for Hundreds, etc
    577
    88.4k
                    if !(file_name == "definitions.yaml" && os_path14.9k.ends_with14.9k("Rules")) {
    578
                        // debug!("find_file -- found={}", path.to_string_lossy());
    579
    88.4k
                        return Ok(path);
    580
    2
                    }
    581
    19.7k
                };
    582
    19.7k
                if looking_for_style_file && alternative_style_file991.is_none991() &&
    583
    257
                   let Ok(alt_file_path249) = find_any_style_file(os_path) {
    584
    249
                        // debug!("find_file: found alternative style file '{}'", alt_file_path.display());
    585
    249
                        alternative_style_file = Some(alt_file_path);
    586
    19.5k
                    }
    587
    19.7k
                if os_path.ends_with("Rules") {
    588
                    // at root of Rules directory
    589
    256
                    break;
    590
    19.4k
                }
    591
            }
    592
    593
    594
    256
            if let Some(result248) = alternative_style_file {
    595
                // debug!("find_file: found alternative_style_file '{}'", result.to_string_lossy());
    596
    248
                return Ok(result);     // found an alternative style file in the same lang dir
    597
    8
            }
    598
    599
            // try a subdir (regional dialect) of the language dir
    600
    8
            let mut regional_dirs = Vec::new();
    601
    8
            find_all_dirs_shim(&lang_dir, &mut regional_dirs);
    602
    8
            for dir in regional_dirs {
    603
                // debug!("find_file: trying again in subdir: {}", dir.display());
    604
                // debug!(" ... files found = {:?}", find_files_in_dir_that_ends_with_shim(&dir, file_name));
    605
    8
                if find_files_in_dir_that_ends_with_shim(&dir, ".yaml").contains(&file_name.to_string()) {
    606
    0
                    let path = dir.join(file_name);
    607
    0
                    if is_file_shim(&path) {
    608
    0
                        return Ok(path);
    609
    0
                    }
    610
    8
                }
    611
            }
    612
    613
    8
            if let Some(default_lang) = default_lang {
    614
                // try again with the default language (we're likely in trouble)
    615
    8
                return PreferenceManager::find_file(rules_dir, default_lang, None, file_name);
    616
    0
            }
    617
    618
            // We are done for -- MathCAT can't do anything without the required files!
    619
    0
            bail!("Wasn't able to find/read MathCAT required file in directory: {}\n\
    620
                   Initially looked in there for language specific directory: {}\n\
    621
                   Looking for file: {}",
    622
    0
                rules_dir.to_str().unwrap(), lang, file_name);
    623
    624
    625
            /// try to find a xxx_Rules.yaml file -- returns an error if none is found ()
    626
    257
            fn find_any_style_file(path: &Path) -> Result<PathBuf> {
    627
                // try to find a xxx_Rules.yaml file
    628
                // we find the first file because this is the deepest (most language specific) speech rule file
    629
    257
                let rule_files = find_files_in_dir_that_ends_with_shim(path, "_Rules.yaml");
    630
    257
                if rule_files.is_empty() {
    631
    8
                    bail!{"didn't find file"};
    632
                } else {
    633
    249
                    return Ok( path.join(rule_files[0].clone()) );
    634
                }
    635
    257
            }
    636
    88.7k
        }
    637
    638
    130k
        fn get_language_dir(rules_dir: &Path, lang: &str, default_lang: Option<&str>) -> Result<PathBuf> {
    639
            // return 'Rules/Language/fr', 'Rules/Language/en/gb', etc, if they exist.
    640
            // fall back to main language, and then to default_dir if language dir doesn't exist
    641
    130k
            let mut full_path = rules_dir.to_path_buf();
    642
    130k
            full_path.push(lang.replace('-', std::path::MAIN_SEPARATOR_STR));
    643
    130k
            for parent in full_path.ancestors()130k {
    644
    130k
                if parent == rules_dir {
    645
    0
                    break;
    646
    130k
                } else if is_dir_shim(parent) {
    647
    130k
                    return Ok(parent.to_path_buf());
    648
    24
                }
    649
            }
    650
    651
            // didn't find the language -- try again with the default language
    652
    0
            match default_lang {
    653
    0
                Some(default_lang) => {
    654
    0
                    warn!("Couldn't find rules for language {lang}, ");
    655
    0
                    return PreferenceManager::get_language_dir(rules_dir, default_lang, None);
    656
                },
    657
                None => {
    658
                    // We are done for -- MathCAT can't do anything without the required files!
    659
    0
                    bail!("Wasn't able to find/read directory for language {}\n
    660
                            Wasn't able to find/read MathCAT default language directory: {}",
    661
    0
                            lang, rules_dir.join(default_lang.unwrap_or("")).as_os_str().to_str().unwrap());
    662
                }
    663
            }
    664
    130k
        }
    665
    666
    667
        /// Return the speech rule style file locations.
    668
    15.3k
        pub fn get_rule_file(&self, name: &RulesFor) -> &Path {
    669
    15.3k
            if !self.error.is_empty() {
    670
    0
                panic!("Internal error: get_rule_file called on invalid PreferenceManager -- error message\n{}", &self.error);
    671
    15.3k
            };
    672
    673
    15.3k
            let files = match name {
    674
    3.88k
                RulesFor::Intent => &self.intent,
    675
    9.03k
                RulesFor::Speech => &self.speech,
    676
    14
                RulesFor::OverView => &self.overview,
    677
    549
                RulesFor::Navigation => &self.navigation,
    678
    1.83k
                RulesFor::Braille => &self.braille,
    679
            };
    680
    15.3k
            return files.as_path();
    681
    15.3k
        }
    682
    683
        /// Return the unicode.yaml file locations.
    684
    19.0k
        pub fn get_speech_unicode_file(&self) ->(&Path, &Path) {
    685
    19.0k
            if !self.error.is_empty() {
    686
    0
                panic!("Internal error: get_speech_unicode_file called on invalid PreferenceManager -- error message\n{}", &self.error);
    687
    19.0k
            };
    688
    19.0k
            return (self.speech_unicode.as_path(), self.speech_unicode_full.as_path());
    689
    19.0k
        }
    690
    691
        /// Return the unicode.yaml file locations.
    692
    3.92k
        pub fn get_braille_unicode_file(&self) -> (&Path, &Path) {
    693
    3.92k
            if !self.error.is_empty() {
    694
    0
                panic!("Internal error: get_braille_unicode_file called on invalid PreferenceManager -- error message\n{}", &self.error);
    695
    3.92k
            };
    696
    697
    3.92k
            return (self.braille_unicode.as_path(), self.braille_unicode_full.as_path());
    698
    3.92k
        }
    699
    700
        /// Return the definitions.yaml file locations.
    701
    15.3k
        pub fn get_definitions_file(&self, use_speech_defs: bool) -> &Path {
    702
    15.3k
            if !self.error.is_empty() {
    703
    0
                panic!("Internal error: get_definitions_file called on invalid PreferenceManager -- error message\n{}", &self.error);
    704
    15.3k
            };
    705
    706
    15.3k
            let defs_file = if use_speech_defs {&self.speech_defs13.4k} else {&self.braille_defs1.82k};
    707
    15.3k
            return defs_file;
    708
    15.3k
        }
    709
    710
        /// Return the TTS engine currently in use.
    711
    85.4k
        pub fn get_tts(&self) -> TTS {
    712
    85.4k
            if !self.error.is_empty() {
    713
    0
                panic!("Internal error: get_tts called on invalid PreferenceManager -- error message\n{}", &self.error);
    714
    85.4k
            };
    715
    716
    85.4k
            return match self.pref_to_string("TTS").as_str().to_ascii_lowercase().as_str() {
    717
    85.4k
                "none" => TTS::None,
    718
    0
                "ssml" => TTS::SSML,
    719
    0
                "sapi5" => TTS::SAPI5,
    720
                _ => {
    721
    0
                    warn!("found unknown value for TTS: '{}'", self.pref_to_string("TTS").as_str());
    722
    0
                    TTS::None
    723
                }
    724
            }
    725
    85.4k
        }
    726
    727
        /// Set the string-valued preference.
    728
        ///
    729
        /// Note: changing the language, speech style, or braille code might fail if the files don't exist.
    730
        ///   If this happens, the preference is not set and an error is returned.
    731
        /// If "LanguageAuto" is set, we assume "Language" has already be checked to be "Auto"
    732
    16.2k
        pub fn set_string_pref(&mut self, key: &str, value: &str) -> Result<()> {
    733
    16.2k
            if !self.error.is_empty() {
    734
    0
                panic!("Internal error: set_string_pref called on invalid PreferenceManager -- error message\n{}", &self.error);
    735
    16.2k
            };
    736
    737
            // verify language, braille, and SpeechStyle because these are used as access into the file system
    738
            // should be an ascii string with only letters, dashes, and underscores
    739
    16.2k
            if matches!9.97k(key, "Language" | "BrailleCode"11.2k | "SpeechStyle"9.85k) &&
    740
    55.9k
               !value.chars()9.97k.all9.97k(|c| matches!(c, 'a'..='z'45.9k | 'A'..='Z'9.63k | '_' | '-')) {
    741
    3
                bail!("{} is an invalid value! Must contains only ascii letters, '_', or'-'", key);
    742
    16.2k
            }
    743
    744
            // don't do an update if the value hasn't changed
    745
    16.2k
            let mut is_user_pref = true;
    746
    16.2k
            if let Some(pref_value61) = self.api_prefs.prefs.get(key) {
    747
    61
                if pref_value.as_str().unwrap() != value {
    748
    59
                    is_user_pref = false;
    749
    59
                    self.reset_files_from_preference_change(key, value)?0;
    750
    2
                }
    751
    16.1k
            } else if let Some(pref_value) = self.user_prefs.prefs.get(key) {
    752
    16.1k
                if pref_value.as_str().unwrap() != value {
    753
    8.77k
                    self.reset_files_from_preference_change(key, value)?0;
    754
    7.40k
                }
    755
            } else {
    756
    0
                bail!("{} is an unknown MathCAT preference!", key);
    757
            }
    758
    759
            // debug!("Setting ({}) {} to '{}'", if is_user_pref {"user"} else {"sys"}, key, value);
    760
    16.2k
            if is_user_pref {
    761
                // a little messy about the DecimalSeparator due immutable and mutable borrows
    762
    16.1k
                let current_decimal_separator = self.user_prefs.prefs.get("DecimalSeparator").unwrap().clone();
    763
    16.1k
                let current_decimal_separator = current_decimal_separator.as_str().unwrap();
    764
    16.1k
                let is_decimal_separators_changed = key == "DecimalSeparator" && current_decimal_separator != value1.35k;
    765
    16.1k
                let is_language_changed = key == "Language" && self.user_prefs.prefs5.03k.get5.03k("Language").unwrap().as_str().unwrap() != value;
    766
    16.1k
                self.user_prefs.prefs.insert(key.to_string(), Yaml::String(value.to_string()));
    767
    16.1k
                if is_decimal_separators_changed || (current_decimal_separator == "Auto" && is_language_changed) {
    768
                    // a little messy about the language due immutable and mutable borrows)
    769
    4.08k
                    let language = self.user_prefs.prefs.get("Language").unwrap_or(&DEFAULT_LANG).clone();
    770
    4.08k
                    let language = language.as_str().unwrap();
    771
    4.08k
                    self.set_separators(language)?0;
    772
    12.0k
                }
    773
    59
            } else {
    774
    59
                self.api_prefs.prefs.insert(key.to_string(), Yaml::String(value.to_string()));
    775
    59
            }
    776
    16.2k
            return Ok( () );
    777
    16.2k
        }
    778
    779
    30.2k
        fn reset_files_from_preference_change(&mut self, changed_pref: &str, changed_value: &str) -> Result<()> {
    780
    30.2k
            if changed_pref == "Language" && changed_value == "Auto"4.10k {
    781
                // Language must have had a non-Auto value -- set LanguageAuto to old value so (probable) next change to LanguageAuto works well
    782
    0
                self.api_prefs.prefs.insert("LanguageAuto".to_string(),
    783
    0
                                    self.api_prefs.prefs.get("Language").unwrap_or(&DEFAULT_LANG).clone() );
    784
    0
                return Ok( () );
    785
    30.2k
            }
    786
    787
    30.2k
            let changed_pref = if changed_pref == "LanguageAuto" {"Language"0} else {changed_pref};
    788
    30.2k
            let language_dir = self.rules_dir.to_path_buf().join("Languages");
    789
    30.2k
            match changed_pref {
    790
    30.2k
                "Language" => {
    791
    4.10k
                    self.set_speech_files(&language_dir, changed_value, None)?0;
    792
    4.10k
                    crate::speech::invalidate_speech_language_caches();
    793
                },
    794
    26.1k
                "SpeechStyle" => {
    795
    1.51k
                    let language = self.pref_to_string("Language");
    796
    1.51k
                    let language = if language.as_str() == "Auto" {"en"62} else {language.as_str()1.45k};       // avoid 'temp value dropped while borrowed' error
    797
    1.51k
                    self.set_style_file(&language_dir, language, changed_value)?0;
    798
    1.51k
                    crate::speech::invalidate_speech_style_caches();
    799
                },
    800
    24.6k
                "BrailleCode" => {
    801
    601
                    let braille_dir = self.rules_dir.to_path_buf().join("Braille");
    802
    601
                    self.set_braille_files(&braille_dir, changed_value)?0;
    803
    601
                    crate::speech::invalidate_braille_caches();
    804
                },
    805
    24.0k
                _ => (),
    806
            }
    807
    30.2k
            return Ok( () );
    808
    30.2k
        }
    809
    810
        /// Set the number-valued preference.
    811
        /// All number-valued preferences are stored with type `f64`.
    812
    0
        pub fn set_api_float_pref(&mut self, key: &str, value: f64) {
    813
    0
            if !self.error.is_empty() {
    814
    0
                panic!("Internal error: set_api_float_pref called on invalid PreferenceManager -- error message\n{}", &self.error);
    815
    0
            };
    816
    817
    0
            self.api_prefs.prefs.insert(key.to_string(), Yaml::Real(value.to_string()));
    818
    0
        }
    819
    820
    1.50k
        pub fn set_api_boolean_pref(&mut self, key: &str, value: bool) {
    821
    1.50k
            if !self.error.is_empty() {
    822
    0
                panic!("Internal error: set_api_boolean_pref called on invalid PreferenceManager -- error message\n{}", &self.error);
    823
    1.50k
            };
    824
    825
    1.50k
            self.api_prefs.prefs.insert(key.to_string(), Yaml::Boolean(value));
    826
    1.50k
        }
    827
    828
        /// Return the current speech rate.
    829
    0
        pub fn get_rate(&self) -> f64 {
    830
    0
            if !self.error.is_empty() {
    831
    0
                panic!("Internal error: get_rate called on invalid PreferenceManager -- error message\n{}", &self.error);
    832
    0
            };
    833
    834
    0
            return match &self.pref_to_string("Rate").parse::<f64>() {
    835
    0
                Ok(val) => *val,
    836
                Err(_) => {
    837
    0
                    warn!("Rate ('{}') can't be converted to a floating point number", &self.pref_to_string("Rate"));
    838
    0
                    DEFAULT_API_PREFERENCES.with(|defaults| defaults.prefs["Rate"].as_f64().unwrap())
    839
                }
    840
            };
    841
    0
        }
    842
    843
    0
        pub fn get_api_prefs(&self) -> &Preferences {
    844
    0
            return &self.api_prefs;
    845
    0
        }
    846
    847
        /// returns value associated with 'name' or string NO_PREFERENCE
    848
        ///
    849
        /// Note: Option/Result not used because most of the time we know the preference exists, so no unwrapping is needed for 95% of calls
    850
    268k
        pub fn pref_to_string(&self, name: &str) -> String {
    851
    268k
            let mut value = self.api_prefs.prefs.get(name);
    852
    268k
            if value.is_none() {
    853
    122k
                value = self.user_prefs.prefs.get(name);
    854
    145k
            }
    855
    268k
            return match value {
    856
    11
                None => NO_PREFERENCE.to_string(),
    857
    268k
                Some(v) => match v {
    858
    236k
                    Yaml::String(s) => s.clone(),
    859
    27.3k
                    Yaml::Boolean(b)   => b.to_string(),
    860
    4.23k
                    Yaml::Integer(i)    => i.to_string(),
    861
    0
                    Yaml::Real(s) => s.clone(),
    862
    0
                    _  => NO_PREFERENCE.to_string(),       // shouldn't happen
    863
                }
    864
            }
    865
    268k
        }
    866
    867
        // occasionally useful to check a pref value when debugging
    868
        // fn get_pref(&self, pref_name: &str) -> String {
    869
        //     return yaml_to_string(self.user_prefs.prefs.get(pref_name).unwrap(), 1);
    870
        // }
    871
    872
        /// Warning!!! This is meant for testing only -- it overwrites any values from a user pref file and will be overwritten if the file is reread.
    873
        ///  set_preference() is the function that should be called.
    874
        /// This differs from set_preference in that the user preferences are changed, not the api ones
    875
    21.4k
        pub fn set_user_prefs(&mut self, key: &str, value: &str) -> Result<()> {
    876
    21.4k
            if !self.error.is_empty() {
    877
    0
                panic!("Internal error: set_user_prefs called on invalid PreferenceManager -- error message\n{}", &self.error);
    878
    21.4k
            };
    879
    880
    21.4k
            self.reset_files_from_preference_change(key, value)?0;
    881
    21.4k
            let is_decimal_separators_changed = key == "DecimalSeparator" && self.user_prefs.prefs3.46k.get3.46k("DecimalSeparator").unwrap().as_str().unwrap() != value;
    882
    21.4k
            let is_language_changed = key == "Language" && self.user_prefs.prefs15.get15("Language").unwrap().as_str().unwrap() != value;
    883
    21.4k
            self.user_prefs.prefs.insert(key.to_string(), Yaml::String(value.to_string()));
    884
    21.4k
            if is_decimal_separators_changed || is_language_changed21.4k {
    885
                // set computed values for BLOCK_SEPARATORS and DECIMAL_SEPARATORS (a little messy about the language due immutable and mutable borrows)
    886
    19
                let language = self.user_prefs.prefs.get("Language").unwrap_or(&DEFAULT_LANG).clone();
    887
    19
                let language = language.as_str().unwrap();
    888
    19
                self.set_separators(language)?0;
    889
    21.3k
            }
    890
    891
    21.4k
            return Ok(());
    892
    21.4k
        }
    893
    }
    894
    895
    896
    #[cfg(test)]
    897
    mod tests {
    898
        #[allow(unused_imports)]
    899
        use crate::init_logger;
    900
    901
        // For these tests, it is assumed that there are Rules subdirs zz and zz/aa dir; there is no zz/ab
    902
        // definitions.yaml is in Rules, zz, aa dirs
    903
        // unicode.yaml is in zz
    904
        // ClearSpeak_Rules.yaml is in zz
    905
        // These files are NOT in the zipped up version -- hence the config
    906
        use super::*;
    907
    908
        /// Version of abs_rules_dir_path that returns a PathBuf
    909
    12
        fn abs_rules_dir_path() -> PathBuf {
    910
    12
            return PathBuf::from(super::super::abs_rules_dir_path());
    911
    12
        }
    912
        /// Return a relative path to Rules dir (ie, .../Rules/zz... returns zz/...)
    913
        /// strip .../Rules from file path
    914
    33
        fn rel_path<'a>(rules_dir: &'a Path, path: &'a Path) -> &'a Path {
    915
    33
            let stripped_path = path.strip_prefix(rules_dir).unwrap();
    916
    33
            return stripped_path
    917
    33
        }
    918
    919
    2
        fn speech_rule_files_cache_is_empty() -> bool {
    920
    2
            crate::speech::SPEECH_RULES.with(|rules| rules.borrow().rule_files_cache_is_empty())
    921
    2
        }
    922
    923
    3
        fn speech_definitions_files_cache_is_empty() -> bool {
    924
    3
            crate::speech::SPEECH_RULES.with(|rules| rules.borrow().definitions_files_cache_is_empty())
    925
    3
        }
    926
    927
    2
        fn speech_definitions_files_cache_path() -> PathBuf {
    928
    2
            crate::speech::SPEECH_RULES.with(|rules| rules.borrow().definitions_files_cache_path())
    929
    2
        }
    930
    931
        #[test]
    932
    1
        fn separators() {
    933
    1
            PREF_MANAGER.with(|pref_manager| {
    934
    1
                let mut pref_manager = pref_manager.borrow_mut();
    935
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    936
    1
                pref_manager.set_user_prefs("Language", "en").unwrap();
    937
    1
                pref_manager.set_user_prefs("DecimalSeparator", "Auto").unwrap();
    938
    1
                assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ".");
    939
    1
                assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ", \u{00A0}\u{202F}");
    940
    941
    1
                pref_manager.set_user_prefs("Language", "sv").unwrap();
    942
    1
                assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ",");
    943
    1
                assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ". \u{00A0}\u{202F}");
    944
    945
                // test potentially ambiguous language (defaults to comma decimal separator)
    946
    1
                pref_manager.set_user_prefs("Language", "es").unwrap();
    947
    1
                assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ",");
    948
    1
                assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ". \u{00A0}\u{202F}");
    949
    950
                // test country override
    951
    1
                pref_manager.set_user_prefs("Language", "es-mx").unwrap();
    952
    1
                assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ".");
    953
    1
                assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ", \u{00A0}\u{202F}");
    954
    955
    1
                pref_manager.set_user_prefs("DecimalSeparator", ",").unwrap();
    956
    1
                assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ",");
    957
    1
                assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ". \u{00A0}\u{202F}");
    958
    959
    1
                pref_manager.set_user_prefs("DecimalSeparator", ".").unwrap();
    960
    1
                assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ".");
    961
    1
                assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ", \u{00A0}\u{202F}");
    962
    963
                // set to illegal value -- should leave values as before
    964
    1
                pref_manager.set_user_prefs("DecimalSeparator", ";").unwrap();
    965
    1
                assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ".");
    966
    1
                assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ", \u{00A0}\u{202F}");
    967
    968
                // manual
    969
    1
                pref_manager.set_user_prefs("DecimalSeparators", ",").unwrap();
    970
    1
                pref_manager.set_user_prefs("BlockSeparators", " ").unwrap();
    971
    1
                pref_manager.set_user_prefs("DecimalSeparator", "None").unwrap();
    972
    1
                assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ",");
    973
    1
                assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), " ");
    974
    1
            });
    975
    1
        }
    976
    977
        #[test]
    978
    1
        fn find_simple_style() {
    979
    1
            PREF_MANAGER.with(|pref_manager| {
    980
    1
                let mut pref_manager = pref_manager.borrow_mut();
    981
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    982
    1
                pref_manager.set_user_prefs("Language", "en").unwrap();
    983
    1
                pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
    984
    1
                assert_eq!(&pref_manager.pref_to_string("Language"), "en");
    985
    1
                assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "ClearSpeak");
    986
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/en/ClearSpeak_Rules.yaml"));
    987
    1
            });
    988
    1
        }
    989
    990
    cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] {
    991
        #[test]
    992
    1
        fn find_style_other_language() {
    993
            // zz dir should have both ClearSpeak and SimpleSpeak styles
    994
            // zz-aa dir should have only ClearSpeak style and unicode.yaml that includes the zz unicode but overrides "+"
    995
    1
            PREF_MANAGER.with(|pref_manager| {
    996
    1
                let mut pref_manager = pref_manager.borrow_mut();
    997
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    998
    1
                pref_manager.set_user_prefs("Language", "en").unwrap();
    999
    1
                pref_manager.set_user_prefs("SpeechStyle", "SimpleSpeak").unwrap();
    1000
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/en/SimpleSpeak_Rules.yaml"));
    1001
    1002
    1
                pref_manager.set_user_prefs("Language", "zz").unwrap();
    1003
    1
                assert_eq!(&pref_manager.pref_to_string("Language"), "zz");
    1004
    1
                assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "SimpleSpeak");
    1005
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/SimpleSpeak_Rules.yaml"));
    1006
    1007
                // make sure language stays the same
    1008
    1
                pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
    1009
    1
                assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "ClearSpeak");
    1010
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
    1011
    1012
                // make sure language stays the same
    1013
    1
                pref_manager.set_user_prefs("SpeechStyle", "SimpleSpeak").unwrap();
    1014
    1
                assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "SimpleSpeak");
    1015
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/SimpleSpeak_Rules.yaml"));
    1016
    1
            });
    1017
    1
        }
    1018
    1019
        #[test]
    1020
    1
        fn find_regional_overrides() {
    1021
            // zz dir should have both ClearSpeak and SimpleSpeak styles
    1022
            // zz-aa dir should have ClearSpeak style and unicode.yaml that includes the zz unicode but overrides "+"
    1023
    1
            PREF_MANAGER.with(|pref_manager| {
    1024
    1
                let mut pref_manager = pref_manager.borrow_mut();
    1025
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1026
    1
                pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
    1027
    1
                pref_manager.set_user_prefs("Language", "zz-aa").unwrap();
    1028
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/aa/ClearSpeak_Rules.yaml"));
    1029
    1030
    1
                pref_manager.set_user_prefs("SpeechStyle", "SimpleSpeak").unwrap();
    1031
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/SimpleSpeak_Rules.yaml"));
    1032
    1
            });
    1033
    1
        }
    1034
    1035
        #[test]
    1036
    1
        fn find_style_no_sublanguage() {
    1037
    1
            PREF_MANAGER.with(|pref_manager| {
    1038
    1
                let mut pref_manager = pref_manager.borrow_mut();
    1039
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1040
    1
                pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
    1041
    1
                pref_manager.set_user_prefs("Language", "zz-ab").unwrap();
    1042
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
    1043
    1
            });
    1044
    1
        }
    1045
    1046
        #[test]
    1047
    1
        fn found_all_files() {
    1048
    1
            PREF_MANAGER.with(|pref_manager| {
    1049
    1
                let mut pref_manager = pref_manager.borrow_mut();
    1050
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1051
    1
                pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
    1052
    1
                pref_manager.set_user_prefs("Language", "zz-aa").unwrap();
    1053
    1
                pref_manager.set_user_prefs("BrailleCode", "UEB").unwrap();
    1054
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.intent.as_path()), PathBuf::from("intent.yaml"));
    1055
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.overview.as_path()), PathBuf::from("Languages/zz/overview.yaml"));
    1056
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_defs.as_path()), PathBuf::from("Languages/zz/aa/definitions.yaml"));
    1057
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/aa/ClearSpeak_Rules.yaml"));
    1058
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_unicode.as_path()), PathBuf::from("Languages/zz/aa/unicode.yaml"));
    1059
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_unicode_full.as_path()), PathBuf::from("Languages/zz/unicode-full.yaml"));
    1060
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille.as_path()), PathBuf::from("Braille/UEB/UEB_Rules.yaml"));
    1061
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_unicode.as_path()), PathBuf::from("Braille/UEB/unicode.yaml"));
    1062
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_unicode_full.as_path()), PathBuf::from("Braille/UEB/unicode-full.yaml"));
    1063
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_defs.as_path()), PathBuf::from("Braille/UEB/definitions.yaml"));
    1064
    1065
    1
                pref_manager.set_user_prefs("Language", "zz-ab").unwrap();
    1066
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.intent.as_path()), PathBuf::from("intent.yaml"));
    1067
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.overview.as_path()), PathBuf::from("Languages/zz/overview.yaml"));
    1068
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_defs.as_path()), PathBuf::from("Languages/zz/definitions.yaml"));
    1069
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
    1070
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_unicode.as_path()), PathBuf::from("Languages/zz/unicode.yaml"));
    1071
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_unicode_full.as_path()), PathBuf::from("Languages/zz/unicode-full.yaml"));
    1072
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille.as_path()), PathBuf::from("Braille/UEB/UEB_Rules.yaml"));
    1073
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_unicode.as_path()), PathBuf::from("Braille/UEB/unicode.yaml"));
    1074
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_unicode_full.as_path()), PathBuf::from("Braille/UEB/unicode-full.yaml"));
    1075
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_defs.as_path()), PathBuf::from("Braille/UEB/definitions.yaml"));
    1076
    1
            })
    1077
    1
        }
    1078
    1079
        #[test]
    1080
    1
        fn test_prefs() {
    1081
    1
            PREF_MANAGER.with(|pref_manager| {
    1082
                // first test with internal settings
    1083
                {
    1084
    1
                    let mut pref_manager = pref_manager.borrow_mut();
    1085
    1
                    pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1086
    1087
    1
                    pref_manager.set_user_prefs("Language", "en").unwrap();
    1088
    1
                    pref_manager.set_user_prefs("ClearSpeak_AbsoluteValue", "Determinant").unwrap();
    1089
    1
                    pref_manager.set_user_prefs("ResetNavMode", "true").unwrap();
    1090
    1
                    pref_manager.set_user_prefs("BrailleCode", "Nemeth").unwrap();
    1091
    1
                    assert_eq!(pref_manager.pref_to_string("Language").as_str(), "en");
    1092
    1
                    assert_eq!(pref_manager.pref_to_string("SubjectArea").as_str(), "General");
    1093
    1
                    assert_eq!(pref_manager.pref_to_string("ClearSpeak_AbsoluteValue").as_str(), "Determinant");
    1094
    1
                    assert_eq!(pref_manager.pref_to_string("ResetNavMode").as_str(), "true");
    1095
    1
                    assert_eq!(pref_manager.pref_to_string("BrailleCode").as_str(), "Nemeth");
    1096
    1
                    assert_eq!(pref_manager.pref_to_string("X_Y_Z").as_str(), NO_PREFERENCE);
    1097
                }
    1098
    1099
                // now test with the interface
    1100
                {
    1101
                    use crate::interface::{set_preference, get_preference};
    1102
    1
                    set_preference("Language", "zz").unwrap();
    1103
    1
                    set_preference("ClearSpeak_AbsoluteValue", "Cardinality").unwrap();
    1104
    1
                    set_preference("Overview", "true").unwrap();
    1105
    1
                    set_preference("BrailleCode", "UEB").unwrap();
    1106
    1
                    assert_eq!(&get_preference("Language").unwrap(), "zz");
    1107
    1
                    assert_eq!(&get_preference("ClearSpeak_AbsoluteValue").unwrap(), "Cardinality");
    1108
    1
                    assert_eq!(&get_preference("Overview").unwrap(), "true");
    1109
    1
                    assert_eq!(&get_preference("BrailleCode").unwrap(), "UEB");
    1110
    1
                    assert!(&get_preference("X_Y_Z").is_err());
    1111
    1112
                }
    1113
    1
            });
    1114
    1
        }
    1115
    1116
        #[test]
    1117
    1
        fn test_language_change() {
    1118
            // set_preference borrows the pref manager, so the previous borrow's lifetime needs to be ended before using it
    1119
    1
            PREF_MANAGER.with(|pref_manager| {
    1120
    1
                let mut pref_manager = pref_manager.borrow_mut();
    1121
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1122
    1
            });
    1123
    1
            crate::interface::set_preference("Language", "en").unwrap();
    1124
    1
            crate::interface::set_preference("SpeechStyle", "ClearSpeak").unwrap();
    1125
    1
            PREF_MANAGER.with(|pref_manager| {
    1126
    1
                let pref_manager = pref_manager.borrow_mut();
    1127
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Speech)), PathBuf::from("Languages/en/ClearSpeak_Rules.yaml"));
    1128
    1
            });
    1129
    1130
    1
            crate::interface::set_preference("Language", "zz").unwrap();
    1131
    1
            PREF_MANAGER.with(|pref_manager| {
    1132
    1
                let pref_manager = pref_manager.borrow_mut();
    1133
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Speech)), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
    1134
    1
            });
    1135
    1
        }
    1136
    1137
        #[test]
    1138
    1
        fn test_speech_style_change() {
    1139
            use crate::speech::SPEECH_RULES;
    1140
    1141
    1
            PREF_MANAGER.with(|pref_manager| {
    1142
    1
                let mut pref_manager = pref_manager.borrow_mut();
    1143
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1144
    1
                pref_manager.set_user_prefs("Language", "en").unwrap();
    1145
    1
                pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
    1146
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Speech)), PathBuf::from("Languages/en/ClearSpeak_Rules.yaml"));
    1147
    1
            });
    1148
    1
            SPEECH_RULES.with(|rules| rules.borrow_mut().read_files().unwrap());
    1149
    1
            assert!(!speech_rule_files_cache_is_empty());
    1150
    1151
    1
            PREF_MANAGER.with(|pref_manager| {
    1152
    1
                pref_manager.borrow_mut().set_user_prefs("SpeechStyle", "SimpleSpeak").unwrap();
    1153
    1
                assert_eq!(rel_path(&pref_manager.borrow().rules_dir, pref_manager.borrow().get_rule_file(&RulesFor::Speech)), PathBuf::from("Languages/en/SimpleSpeak_Rules.yaml"));
    1154
    1
            });
    1155
    1
            assert!(speech_rule_files_cache_is_empty());
    1156
    1
        }
    1157
    1158
        #[test]
    1159
    1
        fn test_language_change_invalidates_definitions_caches() {
    1160
            use crate::speech::SPEECH_RULES;
    1161
    1162
    1
            PREF_MANAGER.with(|pref_manager| {
    1163
    1
                pref_manager.borrow_mut().initialize(abs_rules_dir_path()).unwrap();
    1164
    1
                pref_manager.borrow_mut().set_user_prefs("Language", "nb").unwrap();
    1165
    1
            });
    1166
    1
            SPEECH_RULES.with(|rules| rules.borrow_mut().read_files().unwrap());
    1167
    1
            let nb_defs_path = speech_definitions_files_cache_path();
    1168
    1
            assert!(!speech_definitions_files_cache_is_empty());
    1169
    1
            assert!(nb_defs_path.to_string_lossy().contains("nb"));
    1170
    1171
    1
            PREF_MANAGER.with(|pref_manager| {
    1172
    1
                pref_manager.borrow_mut().set_user_prefs("Language", "en").unwrap();
    1173
    1
            });
    1174
    1
            assert!(speech_definitions_files_cache_is_empty());
    1175
    1176
    1
            SPEECH_RULES.with(|rules| rules.borrow_mut().read_files().unwrap());
    1177
    1
            let en_defs_path = speech_definitions_files_cache_path();
    1178
    1
            assert!(!speech_definitions_files_cache_is_empty());
    1179
    1
            assert!(en_defs_path.to_string_lossy().contains("en"));
    1180
    1
            assert_ne!(nb_defs_path, en_defs_path);
    1181
    1
        }
    1182
    1183
        #[test]
    1184
    1
        fn test_some_changes() {
    1185
    1
            PREF_MANAGER.with(|pref_manager| {
    1186
    1
                let mut pref_manager = pref_manager.borrow_mut();
    1187
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1188
    1
                pref_manager.set_user_prefs("Verbosity", "Terse").unwrap();
    1189
    1190
    1
                assert_eq!(&pref_manager.pref_to_string("Verbosity"), "Terse");
    1191
    1192
    1
                pref_manager.set_user_prefs("BrailleCode", "UEB").unwrap();
    1193
    1
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Braille)), PathBuf::from("Braille/UEB/UEB_Rules.yaml"));
    1194
    1195
                // make sure they show up when building context for speech generation
    1196
    1
                let merged_prefs = pref_manager.merge_prefs();
    1197
    1
                assert_eq!(merged_prefs.get("Verbosity").unwrap().as_str().unwrap(), "Terse");
    1198
    1
            });
    1199
    1200
    1
            crate::interface::set_preference("NavVerbosity", "Terse").unwrap();
    1201
    1
            PREF_MANAGER.with(|pref_manager| {
    1202
    1
                let pref_manager = pref_manager.borrow_mut();
    1203
    1
                let merged_prefs = pref_manager.merge_prefs();
    1204
    1
                assert_eq!(merged_prefs.get("NavVerbosity").unwrap().as_str().unwrap(), "Terse");
    1205
    1
            });
    1206
    1
        }
    1207
    1208
        #[test]
    1209
    1
        fn test_illegal_pref_values() {
    1210
    1
            PREF_MANAGER.with(|pref_manager| {
    1211
    1
                let mut pref_manager = pref_manager.borrow_mut();
    1212
    1
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1213
    1
                assert!(pref_manager.set_string_pref("Language", "../../../my/path").is_err());
    1214
    1
                assert!(pref_manager.set_string_pref("BrailleCode", "C:\\my\\path").is_err());
    1215
    1
                assert!(pref_manager.set_string_pref("SpeechStyle", "/my/path").is_err());
    1216
    1
            });
    1217
    1
        }
    1218
    1219
        #[test]
    1220
        #[ignore]   // this is an ugly test for #262 -- it changes the prefs file and so is a bad thing in general
    1221
    0
        fn test_up_to_date() {
    1222
            use std::fs;
    1223
            use std::thread::sleep;
    1224
            use std::time::Duration;
    1225
            use crate::interface;
    1226
    0
            PREF_MANAGER.with(|pref_manager| {
    1227
    0
                let mut pref_manager = pref_manager.borrow_mut();
    1228
    0
                pref_manager.initialize(abs_rules_dir_path()).unwrap();
    1229
    0
                assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "ClearSpeak");
    1230
    0
                assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
    1231
    0
            });
    1232
    0
            interface::set_mathml("<math><mo>+</mo><mn>10</mn></math>").unwrap();
    1233
    0
            assert_eq!(interface::get_spoken_text().unwrap(), "ClearSpeak positive from zz 10");
    1234
    1235
    0
            let mut file_path = PathBuf::default();
    1236
    0
            let mut contents = vec![];
    1237
    0
            PREF_MANAGER.with(|pref_manager| {
    1238
    0
                let pref_manager = pref_manager.borrow();
    1239
    0
                if let Some(file_name) = pref_manager.user_prefs_file.as_ref().unwrap().debug_get_file() {
    1240
    0
                    file_path = PathBuf::from(file_name);
    1241
    0
                    contents = fs::read(&file_path).expect(&format!("Failed to write file {} during test", file_name));
    1242
    0
                    let changed_contents = String::from_utf8(contents.clone()).unwrap()
    1243
    0
                                    .replace("SpeechStyle: ClearSpeak", "SpeechStyle: SimpleSpeak");
    1244
    0
                    fs::write(&file_path, changed_contents).unwrap();
    1245
    0
                    sleep(Duration::from_millis(5));  // make sure the time changes enough to be recognized
    1246
    0
                }
    1247
    0
            });
    1248
    0
            assert_eq!(interface::get_spoken_text().unwrap(), "SimpleSpeak positive from zz 10");
    1249
    0
            fs::write(&file_path, contents).unwrap();
    1250
    1251
                    // assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "SimpleSpeak");
    1252
                    // assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/SimpleSpeak_Rules.yaml"));
    1253
    0
        }
    1254
    1255
    }}
    1256
    }