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}