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.2k
self.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.2k
self.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
}