cadmus_core/
context.rs

1use crate::battery::Battery;
2use crate::db::Database;
3use crate::device::CURRENT_DEVICE;
4use crate::dictionary::{load_dictionary_from_file, Dictionary};
5use crate::font::Fonts;
6use crate::framebuffer::{Display, Framebuffer};
7use crate::frontlight::Frontlight;
8use crate::geom::Rectangle;
9use crate::helpers::{load_json, IsHidden};
10use crate::library::Library;
11use crate::lightsensor::LightSensor;
12use crate::rtc::Rtc;
13use crate::settings::Settings;
14use crate::view::keyboard::Layout;
15use crate::view::ViewId;
16use chrono::Local;
17use fxhash::FxHashMap;
18use globset::Glob;
19use rand_core::SeedableRng;
20use rand_xoshiro::Xoroshiro128Plus;
21use std::collections::{BTreeMap, VecDeque};
22#[cfg(test)]
23use std::env;
24use std::path::Path;
25use tracing::error;
26
27use walkdir::WalkDir;
28
29const KEYBOARD_LAYOUTS_DIRNAME: &str = "keyboard-layouts";
30const DICTIONARIES_DIRNAME: &str = "dictionaries";
31const INPUT_HISTORY_SIZE: usize = 32;
32
33pub struct Context {
34    pub fb: Box<dyn Framebuffer>,
35    pub rtc: Option<Rtc>,
36    pub display: Display,
37    pub settings: Settings,
38    pub library: Library,
39    pub database: Database,
40    pub fonts: Fonts,
41    pub dictionaries: BTreeMap<String, Dictionary>,
42    pub keyboard_layouts: BTreeMap<String, Layout>,
43    pub input_history: FxHashMap<ViewId, VecDeque<String>>,
44    pub frontlight: Box<dyn Frontlight>,
45    pub battery: Box<dyn Battery>,
46    pub lightsensor: Box<dyn LightSensor>,
47    pub notification_index: u8,
48    pub kb_rect: Rectangle,
49    pub rng: Xoroshiro128Plus,
50    pub plugged: bool,
51    pub covered: bool,
52    pub shared: bool,
53    pub online: bool,
54}
55
56impl Context {
57    pub fn new(
58        fb: Box<dyn Framebuffer>,
59        rtc: Option<Rtc>,
60        library: Library,
61        database: Database,
62        settings: Settings,
63        fonts: Fonts,
64        battery: Box<dyn Battery>,
65        frontlight: Box<dyn Frontlight>,
66        lightsensor: Box<dyn LightSensor>,
67    ) -> Context {
68        let dims = fb.dims();
69        let rotation = CURRENT_DEVICE.transformed_rotation(fb.rotation());
70        let rng = Xoroshiro128Plus::seed_from_u64(Local::now().timestamp_subsec_nanos() as u64);
71        Context {
72            fb,
73            rtc,
74            display: Display { dims, rotation },
75            library,
76            database,
77            settings,
78            fonts,
79            dictionaries: BTreeMap::new(),
80            keyboard_layouts: BTreeMap::new(),
81            input_history: FxHashMap::default(),
82            battery,
83            frontlight,
84            lightsensor,
85            notification_index: 0,
86            kb_rect: Rectangle::default(),
87            rng,
88            plugged: false,
89            covered: false,
90            shared: false,
91            online: false,
92        }
93    }
94
95    pub fn batch_import(&mut self) {
96        self.library.import(&self.settings.import);
97        let selected_library = self.settings.selected_library;
98        for (index, library_settings) in self.settings.libraries.iter().enumerate() {
99            if index == selected_library {
100                continue;
101            }
102            if let Ok(mut library) = Library::new(
103                &library_settings.path,
104                &self.database,
105                &library_settings.name,
106            )
107            .map_err(|e| error!("{:#?}", e))
108            {
109                library.import(&self.settings.import);
110                library.flush();
111            }
112        }
113    }
114
115    pub fn load_keyboard_layouts(&mut self) {
116        let glob = Glob::new("**/*.json").unwrap().compile_matcher();
117
118        #[cfg(test)]
119        let path = Path::new(
120            &env::var("TEST_ROOT_DIR")
121                .expect("TEST_ROOT_DIR must be set for test using keyboard layouts"),
122        )
123        .join(KEYBOARD_LAYOUTS_DIRNAME);
124
125        #[cfg(not(test))]
126        let path = Path::new(KEYBOARD_LAYOUTS_DIRNAME);
127
128        for entry in WalkDir::new(path)
129            .min_depth(1)
130            .into_iter()
131            .filter_entry(|e| !e.is_hidden())
132        {
133            if entry.is_err() {
134                continue;
135            }
136            let entry = entry.unwrap();
137            let path = entry.path();
138            if !glob.is_match(path) {
139                continue;
140            }
141            if let Ok(layout) = load_json::<Layout, _>(path)
142                .map_err(|e| error!("Can't load {}: {:#?}.", path.display(), e))
143            {
144                self.keyboard_layouts.insert(layout.name.clone(), layout);
145            }
146        }
147    }
148
149    pub fn load_dictionaries(&mut self) {
150        let glob = Glob::new("**/*.index").unwrap().compile_matcher();
151
152        #[cfg(test)]
153        let path = Path::new(
154            env::var("TEST_ROOT_DIR")
155                .expect("Please set TEST_ROOT_DIR for tests that need dictionaries")
156                .as_str(),
157        )
158        .join(DICTIONARIES_DIRNAME);
159
160        #[cfg(not(test))]
161        let path = Path::new(DICTIONARIES_DIRNAME);
162
163        for entry in WalkDir::new(path)
164            .min_depth(1)
165            .into_iter()
166            .filter_entry(|e| !e.is_hidden())
167        {
168            if entry.is_err() {
169                continue;
170            }
171            let entry = entry.unwrap();
172            if !glob.is_match(entry.path()) {
173                continue;
174            }
175            let index_path = entry.path().to_path_buf();
176            let mut content_path = index_path.clone();
177            content_path.set_extension("dict.dz");
178            if !content_path.exists() {
179                content_path.set_extension("");
180            }
181            if let Ok(mut dict) = load_dictionary_from_file(&content_path, &index_path) {
182                let name = dict.short_name().ok().unwrap_or_else(|| {
183                    index_path
184                        .file_stem()
185                        .map(|s| s.to_string_lossy().into_owned())
186                        .unwrap_or_default()
187                });
188                self.dictionaries.insert(name, dict);
189            }
190        }
191    }
192
193    pub fn record_input(&mut self, text: &str, id: ViewId) {
194        if text.is_empty() {
195            return;
196        }
197
198        let history = self.input_history.entry(id).or_insert_with(VecDeque::new);
199
200        if history.front().map(String::as_str) != Some(text) {
201            history.push_front(text.to_string());
202        }
203
204        if history.len() > INPUT_HISTORY_SIZE {
205            history.pop_back();
206        }
207    }
208
209    pub fn set_frontlight(&mut self, enable: bool) {
210        self.settings.frontlight = enable;
211
212        if enable {
213            let levels = self.settings.frontlight_levels;
214            self.frontlight.set_warmth(levels.warmth);
215            self.frontlight.set_intensity(levels.intensity);
216        } else {
217            self.settings.frontlight_levels = self.frontlight.levels();
218            self.frontlight.set_intensity(0.0);
219            self.frontlight.set_warmth(0.0);
220        }
221    }
222}
223
224#[cfg(test)]
225pub mod test_helpers {
226    use super::*;
227    use crate::battery::FakeBattery;
228    use crate::db::Database;
229    use crate::framebuffer::Pixmap;
230    use crate::frontlight::LightLevels;
231
232    pub fn create_test_context() -> Context {
233        let database = Database::new(":memory:").expect("failed to create in-memory database");
234        database.migrate().expect("failed to run migrations");
235        Context::new(
236            Box::new(Pixmap::new(600, 800, 1)),
237            None,
238            Library::new(Path::new("/tmp"), &database, "test").unwrap(),
239            database,
240            Settings::default(),
241            Fonts::load_from(
242                Path::new(
243                    &env::var("TEST_ROOT_DIR").expect("TEST_ROOT_DIR must be set for this test."),
244                )
245                .to_path_buf(),
246            )
247            .expect("Failed to load fonts"),
248            Box::new(FakeBattery::new()),
249            Box::new(LightLevels::default()),
250            Box::new(0u16),
251        )
252    }
253}