/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 | | } |