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}