cadmus_core/
context.rs

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