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