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