1 /* Copyright (C) 2020-2021 Purism SPC
2 * SPDX-License-Identifier: GPL-3.0+
3 */
4
5 /*! Loading layout files */
6
7 use std::env;
8 use std::fmt;
9 use std::path::PathBuf;
10 use std::convert::TryFrom;
11
12 use super::{ Error, LoadError };
13 use super::parsing;
14
15 use ::layout::ArrangementKind;
16 use ::logging;
17 use ::util::c::as_str;
18 use ::xdg;
19 use ::imservice::ContentPurpose;
20
21 // traits, derives
22 use ::logging::Warn;
23
24
25 /// Gathers stuff defined in C or called by C
26 pub mod c {
27 use super::*;
28 use std::os::raw::c_char;
29
30 #[no_mangle]
31 pub extern "C"
squeek_load_layout( name: *const c_char, type_: u32, variant: u32, overlay: *const c_char, ) -> *mut ::layout::Layout32 fn squeek_load_layout(
33 name: *const c_char, // name of the keyboard
34 type_: u32, // type like Wide
35 variant: u32, // purpose variant like numeric, terminal...
36 overlay: *const c_char, // the overlay (looking for "terminal")
37 ) -> *mut ::layout::Layout {
38 let type_ = match type_ {
39 0 => ArrangementKind::Base,
40 1 => ArrangementKind::Wide,
41 _ => panic!("Bad enum value"),
42 };
43
44 let name = as_str(&name)
45 .expect("Bad layout name")
46 .expect("Empty layout name");
47
48 let variant = ContentPurpose::try_from(variant)
49 .or_print(
50 logging::Problem::Warning,
51 "Received invalid purpose value",
52 )
53 .unwrap_or(ContentPurpose::Normal);
54
55 let overlay_str = as_str(&overlay)
56 .expect("Bad overlay name")
57 .expect("Empty overlay name");
58 let overlay_str = match overlay_str {
59 "" => None,
60 other => Some(other),
61 };
62
63 let (kind, layout) = load_layout_data_with_fallback(&name, type_, variant, overlay_str);
64 let layout = ::layout::Layout::new(layout, kind, variant);
65 Box::into_raw(Box::new(layout))
66 }
67 }
68
69 const FALLBACK_LAYOUT_NAME: &str = "us";
70
71
72 #[derive(Debug, Clone, PartialEq)]
73 enum DataSource {
74 File(PathBuf),
75 Resource(String),
76 }
77
78 impl fmt::Display for DataSource {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 DataSource::File(path) => write!(f, "Path: {:?}", path.display()),
82 DataSource::Resource(name) => write!(f, "Resource: {}", name),
83 }
84 }
85 }
86
87 /* All functions in this family carry around ArrangementKind,
88 * because it's not guaranteed to be preserved,
89 * and the resulting layout needs to know which version was loaded.
90 * See `squeek_layout_get_kind`.
91 * Possible TODO: since this is used only in styling,
92 * and makes the below code nastier than needed, maybe it should go.
93 */
94
95 /// Returns ordered names treating `name` as the base name,
96 /// ignoring any `+` inside.
_get_arrangement_names(name: &str, arrangement: ArrangementKind) -> Vec<(ArrangementKind, String)>97 fn _get_arrangement_names(name: &str, arrangement: ArrangementKind)
98 -> Vec<(ArrangementKind, String)>
99 {
100 let name_with_arrangement = match arrangement {
101 ArrangementKind::Base => name.into(),
102 ArrangementKind::Wide => format!("{}_wide", name),
103 };
104
105 let mut ret = Vec::new();
106 if name_with_arrangement != name {
107 ret.push((arrangement, name_with_arrangement));
108 }
109 ret.push((ArrangementKind::Base, name.into()));
110 ret
111 }
112
113 /// Returns names accounting for any `+` in the `name`,
114 /// including the fallback to the default layout.
get_preferred_names(name: &str, kind: ArrangementKind) -> Vec<(ArrangementKind, String)>115 fn get_preferred_names(name: &str, kind: ArrangementKind)
116 -> Vec<(ArrangementKind, String)>
117 {
118 let mut ret = _get_arrangement_names(name, kind);
119
120 let base_name_preferences = {
121 let mut parts = name.splitn(2, '+');
122 match parts.next() {
123 Some(base) => {
124 // The name is already equal to base, so nothing to add
125 if base == name {
126 vec![]
127 } else {
128 _get_arrangement_names(base, kind)
129 }
130 },
131 // The layout's base name starts with a "+". Weird but OK.
132 None => {
133 log_print!(logging::Level::Surprise, "Base layout name is empty: {}", name);
134 vec![]
135 }
136 }
137 };
138
139 ret.extend(base_name_preferences.into_iter());
140 let fallback_names = _get_arrangement_names(FALLBACK_LAYOUT_NAME, kind);
141 ret.extend(fallback_names.into_iter());
142 ret
143 }
144
145 /// Includes the subdirectory before the forward slash.
146 type LayoutPath = String;
147
148 // This is only used inside iter_fallbacks_with_meta.
149 // Placed at the top scope
150 // because `use LayoutPurpose::*;`
151 // complains about "not in scope" otherwise.
152 // This seems to be a Rust 2015 edition problem.
153 /// Helper for determining where to look up the layout.
154 enum LayoutPurpose<'a> {
155 Default,
156 Special(&'a str),
157 }
158
159 /// Returns the directory string
160 /// where the layout should be looked up, including the slash.
get_directory_string( content_purpose: ContentPurpose, overlay: Option<&str>) -> String161 fn get_directory_string(
162 content_purpose: ContentPurpose,
163 overlay: Option<&str>) -> String
164 {
165 use self::LayoutPurpose::*;
166
167 let layout_purpose = match overlay {
168 None => match content_purpose {
169 ContentPurpose::Email => Special("email"),
170 ContentPurpose::Digits => Special("number"),
171 ContentPurpose::Number => Special("number"),
172 ContentPurpose::Phone => Special("number"),
173 ContentPurpose::Pin => Special("pin"),
174 ContentPurpose::Terminal => Special("terminal"),
175 ContentPurpose::Url => Special("url"),
176 _ => Default,
177 },
178 Some(overlay) => Special(overlay),
179 };
180
181 // For intuitiveness,
182 // default purpose layouts are stored in the root directory,
183 // as they correspond to typical text
184 // and are seen the most often.
185 match layout_purpose {
186 Default => "".into(),
187 Special(purpose) => format!("{}/", purpose),
188 }
189 }
190
191 /// Returns an iterator over all fallback paths.
to_layout_paths( name_fallbacks: Vec<(ArrangementKind, String)>, content_purpose: ContentPurpose, overlay: Option<&str>, ) -> impl Iterator<Item=(ArrangementKind, LayoutPath)>192 fn to_layout_paths(
193 name_fallbacks: Vec<(ArrangementKind, String)>,
194 content_purpose: ContentPurpose,
195 overlay: Option<&str>,
196 ) -> impl Iterator<Item=(ArrangementKind, LayoutPath)> {
197 let prepend_directory = get_directory_string(content_purpose, overlay);
198
199 name_fallbacks.into_iter()
200 .map(move |(arrangement, name)|
201 (arrangement, format!("{}{}", prepend_directory, name))
202 )
203 }
204
205 type LayoutSource = (ArrangementKind, DataSource);
206
to_layout_sources( layout_paths: impl Iterator<Item=(ArrangementKind, LayoutPath)>, filesystem_path: Option<PathBuf>, ) -> impl Iterator<Item=LayoutSource>207 fn to_layout_sources(
208 layout_paths: impl Iterator<Item=(ArrangementKind, LayoutPath)>,
209 filesystem_path: Option<PathBuf>,
210 ) -> impl Iterator<Item=LayoutSource> {
211 layout_paths.flat_map(move |(arrangement, layout_path)| {
212 let mut sources = Vec::new();
213 if let Some(path) = &filesystem_path {
214 sources.push((
215 arrangement,
216 DataSource::File(
217 path.join(&layout_path)
218 .with_extension("yaml")
219 )
220 ));
221 };
222 sources.push((arrangement, DataSource::Resource(layout_path.clone())));
223 sources.into_iter()
224 })
225 }
226
227 /// Returns possible sources, with first as the most preferred one.
228 /// Trying order: native lang of the right kind, native base,
229 /// fallback lang of the right kind, fallback base
iter_layout_sources( name: &str, arrangement: ArrangementKind, purpose: ContentPurpose, ui_overlay: Option<&str>, layout_storage: Option<PathBuf>, ) -> impl Iterator<Item=LayoutSource>230 fn iter_layout_sources(
231 name: &str,
232 arrangement: ArrangementKind,
233 purpose: ContentPurpose,
234 ui_overlay: Option<&str>,
235 layout_storage: Option<PathBuf>,
236 ) -> impl Iterator<Item=LayoutSource> {
237 let names = get_preferred_names(name, arrangement);
238 let paths = to_layout_paths(names, purpose, ui_overlay);
239 to_layout_sources(paths, layout_storage)
240 }
241
load_layout_data(source: DataSource) -> Result<::layout::LayoutData, LoadError>242 fn load_layout_data(source: DataSource)
243 -> Result<::layout::LayoutData, LoadError>
244 {
245 let handler = logging::Print {};
246 match source {
247 DataSource::File(path) => {
248 parsing::Layout::from_file(path.clone())
249 .map_err(LoadError::BadData)
250 .and_then(|layout|
251 layout.build(handler).0.map_err(LoadError::BadKeyMap)
252 )
253 },
254 DataSource::Resource(name) => {
255 parsing::Layout::from_resource(&name)
256 .and_then(|layout|
257 layout.build(handler).0.map_err(LoadError::BadKeyMap)
258 )
259 },
260 }
261 }
262
load_layout_data_with_fallback( name: &str, kind: ArrangementKind, purpose: ContentPurpose, overlay: Option<&str>, ) -> (ArrangementKind, ::layout::LayoutData)263 fn load_layout_data_with_fallback(
264 name: &str,
265 kind: ArrangementKind,
266 purpose: ContentPurpose,
267 overlay: Option<&str>,
268 ) -> (ArrangementKind, ::layout::LayoutData) {
269
270 // Build the path to the right keyboard layout subdirectory
271 let path = env::var_os("SQUEEKBOARD_KEYBOARDSDIR")
272 .map(PathBuf::from)
273 .or_else(|| xdg::data_path("squeekboard/keyboards"));
274
275 for (kind, source) in iter_layout_sources(&name, kind, purpose, overlay, path) {
276 let layout = load_layout_data(source.clone());
277 match layout {
278 Err(e) => match (e, source) {
279 (
280 LoadError::BadData(Error::Missing(e)),
281 DataSource::File(file)
282 ) => log_print!(
283 logging::Level::Debug,
284 "Tried file {:?}, but it's missing: {}",
285 file, e
286 ),
287 (e, source) => log_print!(
288 logging::Level::Warning,
289 "Failed to load layout from {}: {}, skipping",
290 source, e
291 ),
292 },
293 Ok(layout) => {
294 log_print!(logging::Level::Info, "Loaded layout {}", source);
295 return (kind, layout);
296 }
297 }
298 }
299
300 panic!("No useful layout found!");
301 }
302
303
304 #[cfg(test)]
305 mod tests {
306 use super::*;
307
308 use ::logging::ProblemPanic;
309
310 #[test]
parsing_fallback()311 fn parsing_fallback() {
312 assert!(parsing::Layout::from_resource(FALLBACK_LAYOUT_NAME)
313 .map(|layout| layout.build(ProblemPanic).0.unwrap())
314 .is_ok()
315 );
316 }
317
318 /// First fallback should be to builtin, not to FALLBACK_LAYOUT_NAME
319 #[test]
test_fallback_basic_builtin()320 fn test_fallback_basic_builtin() {
321 let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, None, None);
322
323 assert_eq!(
324 sources.collect::<Vec<_>>(),
325 vec!(
326 (ArrangementKind::Base, DataSource::Resource("nb".into())),
327 (
328 ArrangementKind::Base,
329 DataSource::Resource(FALLBACK_LAYOUT_NAME.into())
330 ),
331 )
332 );
333 }
334
335 /// Prefer loading from file system before builtin.
336 #[test]
test_preferences_order_path()337 fn test_preferences_order_path() {
338 let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, None, Some(".".into()));
339
340 assert_eq!(
341 sources.collect::<Vec<_>>(),
342 vec!(
343 (ArrangementKind::Base, DataSource::File("./nb.yaml".into())),
344 (ArrangementKind::Base, DataSource::Resource("nb".into())),
345 (
346 ArrangementKind::Base,
347 DataSource::File("./us.yaml".into())
348 ),
349 (
350 ArrangementKind::Base,
351 DataSource::Resource("us".into())
352 ),
353 )
354 );
355 }
356
357 /// If layout contains a "+", it should reach for what's in front of it too.
358 #[test]
test_preferences_order_base()359 fn test_preferences_order_base() {
360 let sources = iter_layout_sources("nb+aliens", ArrangementKind::Base, ContentPurpose::Normal, None, None);
361
362 assert_eq!(
363 sources.collect::<Vec<_>>(),
364 vec!(
365 (ArrangementKind::Base, DataSource::Resource("nb+aliens".into())),
366 (ArrangementKind::Base, DataSource::Resource("nb".into())),
367 (
368 ArrangementKind::Base,
369 DataSource::Resource(FALLBACK_LAYOUT_NAME.into())
370 ),
371 )
372 );
373 }
374
375 #[test]
test_preferences_order_arrangement()376 fn test_preferences_order_arrangement() {
377 let sources = iter_layout_sources("nb", ArrangementKind::Wide, ContentPurpose::Normal, None, None);
378
379 assert_eq!(
380 sources.collect::<Vec<_>>(),
381 vec!(
382 (ArrangementKind::Wide, DataSource::Resource("nb_wide".into())),
383 (ArrangementKind::Base, DataSource::Resource("nb".into())),
384 (
385 ArrangementKind::Wide,
386 DataSource::Resource("us_wide".into())
387 ),
388 (
389 ArrangementKind::Base,
390 DataSource::Resource("us".into())
391 ),
392 )
393 );
394 }
395
396 #[test]
test_preferences_order_overlay()397 fn test_preferences_order_overlay() {
398 let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, Some("terminal"), None);
399
400 assert_eq!(
401 sources.collect::<Vec<_>>(),
402 vec!(
403 (ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
404 (
405 ArrangementKind::Base,
406 DataSource::Resource("terminal/us".into())
407 ),
408 )
409 );
410 }
411
412 #[test]
test_preferences_order_hint()413 fn test_preferences_order_hint() {
414 let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Terminal, None, None);
415
416 assert_eq!(
417 sources.collect::<Vec<_>>(),
418 vec!(
419 (ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
420 (
421 ArrangementKind::Base,
422 DataSource::Resource("terminal/us".into())
423 ),
424 )
425 );
426 }
427 }
428