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>,
46 pub battery: Box<dyn Battery>,
47 pub lightsensor: Box<dyn LightSensor>,
48 pub notification_index: u8,
49 pub kb_rect: Rectangle,
50 pub rng: Xoroshiro128Plus,
51 pub plugged: bool,
52 pub covered: bool,
53 pub shared: bool,
54 pub online: bool,
55}
56
57impl Context {
58 pub fn new(
59 fb: Box<dyn Framebuffer>,
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 let alarm_manager = match CURRENT_DEVICE.rtc() {
72 Ok(rtc) => Some(AlarmManager::new(rtc.clone())),
73 Err(e) => {
74 tracing::warn!(error = %e, "RTC init failed, alarm manager unavailable");
75 None
76 }
77 };
78 Context {
79 fb,
80 alarm_manager,
81 display: Display { dims, rotation },
82 library,
83 database,
84 settings,
85 fonts,
86 dictionaries: BTreeMap::new(),
87 keyboard_layouts: BTreeMap::new(),
88 input_history: FxHashMap::default(),
89 battery,
90 frontlight,
91 lightsensor,
92 notification_index: 0,
93 kb_rect: Rectangle::default(),
94 rng,
95 plugged: false,
96 covered: false,
97 shared: false,
98 online: false,
99 }
100 }
101
102 pub fn load_keyboard_layouts(&mut self) {
103 let glob = Glob::new("**/*.json").unwrap().compile_matcher();
104
105 #[cfg(test)]
106 let path = Path::new(
107 &env::var("TEST_ROOT_DIR")
108 .expect("TEST_ROOT_DIR must be set for test using keyboard layouts"),
109 )
110 .join(KEYBOARD_LAYOUTS_DIRNAME);
111
112 #[cfg(not(test))]
113 let path = CURRENT_DEVICE.install_path(KEYBOARD_LAYOUTS_DIRNAME);
114
115 for entry in WalkDir::new(path)
116 .min_depth(1)
117 .into_iter()
118 .filter_entry(|e| !e.is_hidden())
119 {
120 if entry.is_err() {
121 continue;
122 }
123 let entry = entry.unwrap();
124 let path = entry.path();
125 if !glob.is_match(path) {
126 continue;
127 }
128 if let Ok(layout) = load_json::<Layout, _>(path)
129 .map_err(|e| error!("Can't load {}: {:#?}.", path.display(), e))
130 {
131 self.keyboard_layouts.insert(layout.name.clone(), layout);
132 }
133 }
134 }
135
136 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
137 pub fn load_dictionaries(&mut self) {
138 self.dictionaries.clear();
139
140 let glob = Glob::new("**/*.index").unwrap().compile_matcher();
141
142 #[cfg(test)]
143 let path = Path::new(
144 env::var("TEST_ROOT_DIR")
145 .expect("Please set TEST_ROOT_DIR for tests that need dictionaries")
146 .as_str(),
147 )
148 .join(DICTIONARIES_DIRNAME);
149
150 #[cfg(not(test))]
151 let path = CURRENT_DEVICE.data_path(DICTIONARIES_DIRNAME);
152
153 for entry in WalkDir::new(path)
154 .min_depth(1)
155 .into_iter()
156 .filter_entry(|e| !e.is_hidden())
157 {
158 if entry.is_err() {
159 continue;
160 }
161 let entry = entry.unwrap();
162 if !glob.is_match(entry.path()) {
163 continue;
164 }
165 let index_path = entry.path().to_path_buf();
166 let mut content_path = index_path.clone();
167 content_path.set_extension("dict.dz");
168 if !content_path.exists() {
169 content_path.set_extension("");
170 }
171
172 let dict_result = match fingerprint_dict_pair(&index_path) {
173 Ok(fp) => load_dictionary_from_db(&content_path, &self.database, fp),
174 Err(e) => {
175 tracing::warn!(
176 path = %index_path.display(),
177 error = %e,
178 "failed to fingerprint index file, skipping dictionary"
179 );
180 continue;
181 }
182 };
183
184 if let Ok(mut dict) = dict_result {
185 let name = dict.short_name().ok().unwrap_or_else(|| {
186 index_path
187 .file_stem()
188 .map(|s| s.to_string_lossy().into_owned())
189 .unwrap_or_default()
190 });
191 self.dictionaries.insert(name, dict);
192 }
193 }
194 }
195
196 pub fn record_input(&mut self, text: &str, id: ViewId) {
197 if text.is_empty() {
198 return;
199 }
200
201 let history = self.input_history.entry(id).or_insert_with(VecDeque::new);
202
203 if history.front().map(String::as_str) != Some(text) {
204 history.push_front(text.to_string());
205 }
206
207 if history.len() > INPUT_HISTORY_SIZE {
208 history.pop_back();
209 }
210 }
211
212 pub fn set_frontlight(&mut self, enable: bool) {
213 self.settings.frontlight = enable;
214
215 if enable {
216 let levels = self.settings.frontlight_levels;
217 self.frontlight.set_warmth(levels.warmth);
218 self.frontlight.set_intensity(levels.intensity);
219 } else {
220 self.settings.frontlight_levels = self.frontlight.levels();
221 self.frontlight.set_intensity(0.0);
222 self.frontlight.set_warmth(0.0);
223 }
224 }
225}
226
227fn fingerprint_dict_pair(index_path: &Path) -> io::Result<Fp> {
233 index_path.fingerprint()
234}
235
236#[cfg(test)]
237pub mod test_helpers {
238 use super::*;
239 use crate::battery::FakeBattery;
240 use crate::db::Database;
241 use crate::framebuffer::Pixmap;
242 use crate::frontlight::LightLevels;
243
244 pub fn create_test_context() -> Context {
245 let database = Database::new(":memory:").expect("failed to create in-memory database");
246 database.migrate().expect("failed to run migrations");
247 Context::new(
248 Box::new(Pixmap::new(600, 800, 1)),
249 Library::new(Path::new("/tmp"), &database, "test").unwrap(),
250 database,
251 Settings::default(),
252 Fonts::load_from(
253 Path::new(
254 &env::var("TEST_ROOT_DIR").expect("TEST_ROOT_DIR must be set for this test."),
255 )
256 .to_path_buf(),
257 )
258 .expect("Failed to load fonts"),
259 Box::new(FakeBattery::new()),
260 Box::new(LightLevels::default()),
261 Box::new(0u16),
262 )
263 }
264}