1 /* Copyright (C) 2020-2021 Purism SPC
2  * SPDX-License-Identifier: GPL-3.0+
3  */
4 
5 /*! Parsing of the data files containing layouts */
6 
7 use std::cell::RefCell;
8 use std::collections::{ HashMap, HashSet };
9 use std::ffi::CString;
10 use std::fs;
11 use std::path::PathBuf;
12 use std::rc::Rc;
13 use std::vec::Vec;
14 
15 use xkbcommon::xkb;
16 
17 use super::{ Error, LoadError };
18 
19 use ::action;
20 use ::keyboard::{
21     KeyState, PressType,
22     generate_keymaps, generate_keycodes, KeyCode, FormattingError
23 };
24 use ::layout;
25 use ::logging;
26 use ::util::hash_map_map;
27 use ::resources;
28 
29 // traits, derives
30 use serde::Deserialize;
31 use std::io::BufReader;
32 use std::iter::FromIterator;
33 use ::logging::Warn;
34 
35 // TODO: find a nice way to make sure non-positive sizes don't break layouts
36 
37 /// The root element describing an entire keyboard
38 #[derive(Debug, Deserialize, PartialEq)]
39 #[serde(deny_unknown_fields)]
40 pub struct Layout {
41     #[serde(default)]
42     margins: Margins,
43     views: HashMap<String, Vec<ButtonIds>>,
44     #[serde(default)]
45     buttons: HashMap<String, ButtonMeta>,
46     outlines: HashMap<String, Outline>
47 }
48 
49 #[derive(Debug, Clone, Deserialize, PartialEq, Default)]
50 #[serde(deny_unknown_fields)]
51 struct Margins {
52     top: f64,
53     bottom: f64,
54     side: f64,
55 }
56 
57 /// Buttons are embedded in a single string
58 type ButtonIds = String;
59 
60 /// All info about a single button
61 /// Buttons can have multiple instances though.
62 #[derive(Debug, Default, Deserialize, PartialEq)]
63 #[serde(deny_unknown_fields)]
64 struct ButtonMeta {
65     // TODO: structure (action, keysym, text, modifier) as an enum
66     // to detect conflicts and missing values at compile time
67     /// Special action to perform on activation.
68     /// Conflicts with keysym, text, modifier.
69     action: Option<Action>,
70     /// The name of the XKB keysym to emit on activation.
71     /// Conflicts with action, text, modifier.
72     keysym: Option<String>,
73     /// The text to submit on activation. Will be derived from ID if not present
74     /// Conflicts with action, keysym, modifier.
75     text: Option<String>,
76     /// The modifier to apply while the key is locked
77     /// Conflicts with action, keysym, text
78     modifier: Option<Modifier>,
79     /// If not present, will be derived from text or the button ID
80     label: Option<String>,
81     /// Conflicts with label
82     icon: Option<String>,
83     /// The name of the outline. If not present, will be "default"
84     outline: Option<String>,
85 }
86 
87 #[derive(Debug, Deserialize, PartialEq, Clone)]
88 #[serde(deny_unknown_fields)]
89 enum Action {
90     #[serde(rename="locking")]
91     Locking {
92         lock_view: String,
93         unlock_view: String,
94         pops: Option<bool>,
95         #[serde(default)]
96         looks_locked_from: Vec<String>,
97     },
98     #[serde(rename="set_view")]
99     SetView(String),
100     #[serde(rename="show_prefs")]
101     ShowPrefs,
102     /// Remove last character
103     #[serde(rename="erase")]
104     Erase,
105 }
106 
107 #[derive(Debug, Clone, PartialEq, Deserialize)]
108 #[serde(deny_unknown_fields)]
109 enum Modifier {
110     Control,
111     Shift,
112     Lock,
113     #[serde(alias="Mod1")]
114     Alt,
115     Mod2,
116     Mod3,
117     Mod4,
118     Mod5,
119 }
120 
121 #[derive(Debug, Clone, Deserialize, PartialEq)]
122 #[serde(deny_unknown_fields)]
123 struct Outline {
124     width: f64,
125     height: f64,
126 }
127 
add_offsets<'a, I: 'a, T, F: 'a>(iterator: I, get_size: F) -> impl Iterator<Item=(f64, T)> + 'a where I: Iterator<Item=T>, F: Fn(&T) -> f64,128 pub fn add_offsets<'a, I: 'a, T, F: 'a>(iterator: I, get_size: F)
129     -> impl Iterator<Item=(f64, T)> + 'a
130     where I: Iterator<Item=T>,
131         F: Fn(&T) -> f64,
132 {
133     let mut offset = 0.0;
134     iterator.map(move |item| {
135         let size = get_size(&item);
136         let value = (offset, item);
137         offset += size;
138         value
139     })
140 }
141 
142 impl Layout {
from_resource(name: &str) -> Result<Layout, LoadError>143     pub fn from_resource(name: &str) -> Result<Layout, LoadError> {
144         let data = resources::get_keyboard(name)
145                     .ok_or(LoadError::MissingResource)?;
146         serde_yaml::from_str(data)
147                     .map_err(LoadError::BadResource)
148     }
149 
from_file(path: PathBuf) -> Result<Layout, Error>150     pub fn from_file(path: PathBuf) -> Result<Layout, Error> {
151         let infile = BufReader::new(
152             fs::OpenOptions::new()
153                 .read(true)
154                 .open(&path)?
155         );
156         serde_yaml::from_reader(infile).map_err(Error::Yaml)
157     }
158 
build<H: logging::Handler>(self, mut warning_handler: H) -> (Result<::layout::LayoutData, FormattingError>, H)159     pub fn build<H: logging::Handler>(self, mut warning_handler: H)
160         -> (Result<::layout::LayoutData, FormattingError>, H)
161     {
162         let button_names = self.views.values()
163             .flat_map(|rows| {
164                 rows.iter()
165                     .flat_map(|row| row.split_ascii_whitespace())
166             });
167 
168         let button_names: HashSet<&str>
169             = HashSet::from_iter(button_names);
170 
171         let button_actions: Vec<(&str, ::action::Action)>
172             = button_names.iter().map(|name| {(
173                 *name,
174                 create_action(
175                     &self.buttons,
176                     name,
177                     self.views.keys().collect(),
178                     &mut warning_handler,
179                 )
180             )}).collect();
181 
182         let symbolmap: HashMap<String, KeyCode> = generate_keycodes(
183             extract_symbol_names(&button_actions)
184         );
185 
186         let button_states = HashMap::<String, KeyState>::from_iter(
187             button_actions.into_iter().map(|(name, action)| {
188                 let keycodes = match &action {
189                     ::action::Action::Submit { text: _, keys } => {
190                         keys.iter().map(|named_keysym| {
191                             symbolmap.get(named_keysym.0.as_str())
192                                 .expect(
193                                     format!(
194                                         "keysym {} in key {} missing from symbol map",
195                                         named_keysym.0,
196                                         name
197                                     ).as_str()
198                                 )
199                                 .clone()
200                         }).collect()
201                     },
202                     action::Action::Erase => vec![
203                         symbolmap.get("BackSpace")
204                             .expect(&format!("BackSpace missing from symbol map"))
205                             .clone(),
206                     ],
207                     _ => Vec::new(),
208                 };
209                 (
210                     name.into(),
211                     KeyState {
212                         pressed: PressType::Released,
213                         keycodes,
214                         action,
215                     }
216                 )
217             })
218         );
219 
220         let keymaps = match generate_keymaps(symbolmap) {
221             Err(e) => { return (Err(e), warning_handler) },
222             Ok(v) => v,
223         };
224 
225         let button_states_cache = hash_map_map(
226             button_states,
227             |name, state| {(
228                 name,
229                 Rc::new(RefCell::new(state))
230             )}
231         );
232 
233         let views: Vec<_> = self.views.iter()
234             .map(|(name, view)| {
235                 let rows = view.iter().map(|row| {
236                     let buttons = row.split_ascii_whitespace()
237                         .map(|name| {
238                             Box::new(create_button(
239                                 &self.buttons,
240                                 &self.outlines,
241                                 name,
242                                 button_states_cache.get(name.into())
243                                     .expect("Button state not created")
244                                     .clone(),
245                                 &mut warning_handler,
246                             ))
247                         });
248                     layout::Row::new(
249                         add_offsets(
250                             buttons,
251                             |button| button.size.width,
252                         ).collect()
253                     )
254                 });
255                 let rows = add_offsets(rows, |row| row.get_size().height)
256                     .collect();
257                 (
258                     name.clone(),
259                     layout::View::new(rows)
260                 )
261             }).collect();
262 
263         // Center views on the same point.
264         let views = {
265             let total_size = layout::View::calculate_super_size(
266                 views.iter().map(|(_name, view)| view).collect()
267             );
268 
269             HashMap::from_iter(views.into_iter().map(|(name, view)| (
270                 name,
271                 (
272                     layout::c::Point {
273                         x: (total_size.width - view.get_size().width) / 2.0,
274                         y: (total_size.height - view.get_size().height) / 2.0,
275                     },
276                     view,
277                 ),
278             )))
279         };
280 
281         (
282             Ok(::layout::LayoutData {
283                 views: views,
284                 keymaps: keymaps.into_iter().map(|keymap_str|
285                     CString::new(keymap_str)
286                         .expect("Invalid keymap string generated")
287                 ).collect(),
288                 // FIXME: use a dedicated field
289                 margins: layout::Margins {
290                     top: self.margins.top,
291                     left: self.margins.side,
292                     bottom: self.margins.bottom,
293                     right: self.margins.side,
294                 },
295             }),
296             warning_handler,
297         )
298     }
299 }
300 
create_action<H: logging::Handler>( button_info: &HashMap<String, ButtonMeta>, name: &str, view_names: Vec<&String>, warning_handler: &mut H, ) -> ::action::Action301 fn create_action<H: logging::Handler>(
302     button_info: &HashMap<String, ButtonMeta>,
303     name: &str,
304     view_names: Vec<&String>,
305     warning_handler: &mut H,
306 ) -> ::action::Action {
307     let default_meta = ButtonMeta::default();
308     let symbol_meta = button_info.get(name)
309         .unwrap_or(&default_meta);
310 
311     fn keysym_valid(name: &str) -> bool {
312         xkb::keysym_from_name(name, xkb::KEYSYM_NO_FLAGS) != xkb::KEY_NoSymbol
313     }
314 
315     enum SubmitData {
316         Action(Action),
317         Text(String),
318         Keysym(String),
319         Modifier(Modifier),
320     }
321 
322     let submission = match (
323         &symbol_meta.action,
324         &symbol_meta.keysym,
325         &symbol_meta.text,
326         &symbol_meta.modifier,
327     ) {
328         (Some(action), None, None, None) => SubmitData::Action(action.clone()),
329         (None, Some(keysym), None, None) => SubmitData::Keysym(keysym.clone()),
330         (None, None, Some(text), None) => SubmitData::Text(text.clone()),
331         (None, None, None, Some(modifier)) => {
332             SubmitData::Modifier(modifier.clone())
333         },
334         (None, None, None, None) => SubmitData::Text(name.into()),
335         _ => {
336             warning_handler.handle(
337                 logging::Level::Warning,
338                 &format!(
339                     "Button {} has more than one of (action, keysym, text, modifier)",
340                     name,
341                 ),
342             );
343             SubmitData::Text("".into())
344         },
345     };
346 
347     fn filter_view_name<H: logging::Handler>(
348         button_name: &str,
349         view_name: String,
350         view_names: &Vec<&String>,
351         warning_handler: &mut H,
352     ) -> String {
353         if view_names.contains(&&view_name) {
354             view_name
355         } else {
356             warning_handler.handle(
357                 logging::Level::Warning,
358                 &format!("Button {} switches to missing view {}",
359                     button_name,
360                     view_name,
361                 ),
362             );
363             "base".into()
364         }
365     }
366 
367     match submission {
368         SubmitData::Action(
369             Action::SetView(view_name)
370         ) => ::action::Action::SetView(
371             filter_view_name(
372                 name, view_name.clone(), &view_names,
373                 warning_handler,
374             )
375         ),
376         SubmitData::Action(Action::Locking {
377             lock_view, unlock_view,
378             pops,
379             looks_locked_from,
380         }) => ::action::Action::LockView {
381             lock: filter_view_name(
382                 name,
383                 lock_view.clone(),
384                 &view_names,
385                 warning_handler,
386             ),
387             unlock: filter_view_name(
388                 name,
389                 unlock_view.clone(),
390                 &view_names,
391                 warning_handler,
392             ),
393             latches: pops.unwrap_or(true),
394             looks_locked_from,
395         },
396         SubmitData::Action(
397             Action::ShowPrefs
398         ) => ::action::Action::ShowPreferences,
399         SubmitData::Action(Action::Erase) => action::Action::Erase,
400         SubmitData::Keysym(keysym) => ::action::Action::Submit {
401             text: None,
402             keys: vec!(::action::KeySym(
403                 match keysym_valid(keysym.as_str()) {
404                     true => keysym.clone(),
405                     false => {
406                         warning_handler.handle(
407                             logging::Level::Warning,
408                             &format!(
409                                 "Keysym name invalid: {}",
410                                 keysym,
411                             ),
412                         );
413                         "space".into() // placeholder
414                     },
415                 }
416             )),
417         },
418         SubmitData::Text(text) => ::action::Action::Submit {
419             text: CString::new(text.clone()).or_warn(
420                 warning_handler,
421                 logging::Problem::Warning,
422                 &format!("Text {} contains problems", text),
423             ),
424             keys: text.chars().map(|codepoint| {
425                 let codepoint_string = codepoint.to_string();
426                 ::action::KeySym(match keysym_valid(codepoint_string.as_str()) {
427                     true => codepoint_string,
428                     false => format!("U{:04X}", codepoint as u32),
429                 })
430             }).collect(),
431         },
432         SubmitData::Modifier(modifier) => match modifier {
433             Modifier::Control => action::Action::ApplyModifier(
434                 action::Modifier::Control,
435             ),
436             Modifier::Alt => action::Action::ApplyModifier(
437                 action::Modifier::Alt,
438             ),
439             Modifier::Mod4 => action::Action::ApplyModifier(
440                 action::Modifier::Mod4,
441             ),
442             unsupported_modifier => {
443                 warning_handler.handle(
444                     logging::Level::Bug,
445                     &format!(
446                         "Modifier {:?} unsupported", unsupported_modifier,
447                     ),
448                 );
449                 action::Action::Submit {
450                     text: None,
451                     keys: Vec::new(),
452                 }
453             },
454         },
455     }
456 }
457 
458 /// TODO: Since this will receive user-provided data,
459 /// all .expect() on them should be turned into soft fails
create_button<H: logging::Handler>( button_info: &HashMap<String, ButtonMeta>, outlines: &HashMap<String, Outline>, name: &str, state: Rc<RefCell<KeyState>>, warning_handler: &mut H, ) -> ::layout::Button460 fn create_button<H: logging::Handler>(
461     button_info: &HashMap<String, ButtonMeta>,
462     outlines: &HashMap<String, Outline>,
463     name: &str,
464     state: Rc<RefCell<KeyState>>,
465     warning_handler: &mut H,
466 ) -> ::layout::Button {
467     let cname = CString::new(name.clone())
468         .expect("Bad name");
469     // don't remove, because multiple buttons with the same name are allowed
470     let default_meta = ButtonMeta::default();
471     let button_meta = button_info.get(name)
472         .unwrap_or(&default_meta);
473 
474     // TODO: move conversion to the C/Rust boundary
475     let label = if let Some(label) = &button_meta.label {
476         ::layout::Label::Text(CString::new(label.as_str())
477             .expect("Bad label"))
478     } else if let Some(icon) = &button_meta.icon {
479         ::layout::Label::IconName(CString::new(icon.as_str())
480             .expect("Bad icon"))
481     } else if let Some(text) = &button_meta.text {
482         ::layout::Label::Text(
483             CString::new(text.as_str())
484                 .or_warn(
485                     warning_handler,
486                     logging::Problem::Warning,
487                     &format!("Text {} is invalid", text),
488                 ).unwrap_or_else(|| CString::new("").unwrap())
489         )
490     } else {
491         ::layout::Label::Text(cname.clone())
492     };
493 
494     let outline_name = match &button_meta.outline {
495         Some(outline) => {
496             if outlines.contains_key(outline) {
497                 outline.clone()
498             } else {
499                 warning_handler.handle(
500                     logging::Level::Warning,
501                     &format!("Outline named {} does not exist! Using default for button {}", outline, name)
502                 );
503                 "default".into()
504             }
505         }
506         None => "default".into(),
507     };
508 
509     let outline = outlines.get(&outline_name)
510         .map(|outline| (*outline).clone())
511         .or_warn(
512             warning_handler,
513             logging::Problem::Warning,
514             "No default outline defined! Using 1x1!",
515         ).unwrap_or(Outline { width: 1f64, height: 1f64 });
516 
517     layout::Button {
518         name: cname,
519         outline_name: CString::new(outline_name).expect("Bad outline"),
520         // TODO: do layout before creating buttons
521         size: layout::Size {
522             width: outline.width,
523             height: outline.height,
524         },
525         label: label,
526         state: state,
527     }
528 }
529 
extract_symbol_names<'a>(actions: &'a [(&str, action::Action)]) -> impl Iterator<Item=String> + 'a530 fn extract_symbol_names<'a>(actions: &'a [(&str, action::Action)])
531     -> impl Iterator<Item=String> + 'a
532 {
533     actions.iter()
534         .filter_map(|(_name, act)| {
535             match act {
536                 action::Action::Submit {
537                     text: _, keys,
538                 } => Some(keys.clone()),
539                 action::Action::Erase => Some(vec!(action::KeySym("BackSpace".into()))),
540                 _ => None,
541             }
542         })
543         .flatten()
544         .map(|named_keysym| named_keysym.0)
545 }
546 
547 
548 #[cfg(test)]
549 mod tests {
550     use super::*;
551 
552     use std::env;
553 
554     use ::logging::ProblemPanic;
555 
path_from_root(file: &'static str) -> PathBuf556     fn path_from_root(file: &'static str) -> PathBuf {
557         let source_dir = env::var("SOURCE_DIR")
558             .map(PathBuf::from)
559             .unwrap_or_else(|e| {
560                 if let env::VarError::NotPresent = e {
561                     let this_file = file!();
562                     PathBuf::from(this_file)
563                         .parent().unwrap()
564                         .parent().unwrap()
565                         .into()
566                 } else {
567                     panic!("{:?}", e);
568                 }
569             });
570         source_dir.join(file)
571     }
572 
573     #[test]
test_parse_path()574     fn test_parse_path() {
575         assert_eq!(
576             Layout::from_file(path_from_root("tests/layout.yaml")).unwrap(),
577             Layout {
578                 margins: Margins { top: 0f64, bottom: 0f64, side: 0f64 },
579                 views: hashmap!(
580                     "base".into() => vec!("test".into()),
581                 ),
582                 buttons: hashmap!{
583                     "test".into() => ButtonMeta {
584                         icon: None,
585                         keysym: None,
586                         action: None,
587                         text: None,
588                         modifier: None,
589                         label: Some("test".into()),
590                         outline: None,
591                     }
592                 },
593                 outlines: hashmap!{
594                     "default".into() => Outline { width: 0f64, height: 0f64 },
595                 },
596             }
597         );
598     }
599 
600     /// Check if the default protection works
601     #[test]
test_empty_views()602     fn test_empty_views() {
603         let out = Layout::from_file(path_from_root("tests/layout2.yaml"));
604         match out {
605             Ok(_) => assert!(false, "Data mistakenly accepted"),
606             Err(e) => {
607                 let mut handled = false;
608                 if let Error::Yaml(ye) = &e {
609                     handled = ye.to_string()
610                         .starts_with("missing field `views`");
611                 };
612                 if !handled {
613                     println!("Unexpected error {:?}", e);
614                     assert!(false)
615                 }
616             }
617         }
618     }
619 
620     #[test]
test_extra_field()621     fn test_extra_field() {
622         let out = Layout::from_file(path_from_root("tests/layout3.yaml"));
623         match out {
624             Ok(_) => assert!(false, "Data mistakenly accepted"),
625             Err(e) => {
626                 let mut handled = false;
627                 if let Error::Yaml(ye) = &e {
628                     handled = ye.to_string()
629                         .starts_with("unknown field `bad_field`");
630                 };
631                 if !handled {
632                     println!("Unexpected error {:?}", e);
633                     assert!(false)
634                 }
635             }
636         }
637     }
638 
639     #[test]
test_layout_punctuation()640     fn test_layout_punctuation() {
641         let out = Layout::from_file(path_from_root("tests/layout_key1.yaml"))
642             .unwrap()
643             .build(ProblemPanic).0
644             .unwrap();
645         assert_eq!(
646             out.views["base"].1
647                 .get_rows()[0].1
648                 .get_buttons()[0].1
649                 .label,
650             ::layout::Label::Text(CString::new("test").unwrap())
651         );
652     }
653 
654     #[test]
test_layout_unicode()655     fn test_layout_unicode() {
656         let out = Layout::from_file(path_from_root("tests/layout_key2.yaml"))
657             .unwrap()
658             .build(ProblemPanic).0
659             .unwrap();
660         assert_eq!(
661             out.views["base"].1
662                 .get_rows()[0].1
663                 .get_buttons()[0].1
664                 .label,
665             ::layout::Label::Text(CString::new("test").unwrap())
666         );
667     }
668 
669     /// Test multiple codepoints
670     #[test]
test_layout_unicode_multi()671     fn test_layout_unicode_multi() {
672         let out = Layout::from_file(path_from_root("tests/layout_key3.yaml"))
673             .unwrap()
674             .build(ProblemPanic).0
675             .unwrap();
676         assert_eq!(
677             out.views["base"].1
678                 .get_rows()[0].1
679                 .get_buttons()[0].1
680                 .state.borrow()
681                 .keycodes.len(),
682             2
683         );
684     }
685 
686     /// Test if erase yields a useable keycode
687     #[test]
test_layout_erase()688     fn test_layout_erase() {
689         let out = Layout::from_file(path_from_root("tests/layout_erase.yaml"))
690             .unwrap()
691             .build(ProblemPanic).0
692             .unwrap();
693         assert_eq!(
694             out.views["base"].1
695                 .get_rows()[0].1
696                 .get_buttons()[0].1
697                 .state.borrow()
698                 .keycodes.len(),
699             1
700         );
701     }
702 
703     #[test]
unicode_keysym()704     fn unicode_keysym() {
705         let keysym = xkb::keysym_from_name(
706             format!("U{:X}", "å".chars().next().unwrap() as u32).as_str(),
707             xkb::KEYSYM_NO_FLAGS,
708         );
709         let keysym = xkb::keysym_to_utf8(keysym);
710         assert_eq!(keysym, "å\0");
711     }
712 
713     #[test]
test_key_unicode()714     fn test_key_unicode() {
715         assert_eq!(
716             create_action(
717                 &hashmap!{
718                     ".".into() => ButtonMeta {
719                         icon: None,
720                         keysym: None,
721                         text: None,
722                         action: None,
723                         modifier: None,
724                         label: Some("test".into()),
725                         outline: None,
726                     }
727                 },
728                 ".",
729                 Vec::new(),
730                 &mut ProblemPanic,
731             ),
732             ::action::Action::Submit {
733                 text: Some(CString::new(".").unwrap()),
734                 keys: vec!(::action::KeySym("U002E".into())),
735             },
736         );
737     }
738 
739     #[test]
test_layout_margins()740     fn test_layout_margins() {
741         let out = Layout::from_file(path_from_root("tests/layout_margins.yaml"))
742             .unwrap()
743             .build(ProblemPanic).0
744             .unwrap();
745         assert_eq!(
746             out.margins,
747             layout::Margins {
748                 top: 1.0,
749                 bottom: 3.0,
750                 left: 2.0,
751                 right: 2.0,
752             }
753         );
754     }
755 
756     #[test]
test_extract_symbols()757     fn test_extract_symbols() {
758         let actions = [(
759             "ac",
760             action::Action::Submit {
761                 text: None,
762                 keys: vec![
763                     action::KeySym("a".into()),
764                     action::KeySym("c".into()),
765                 ],
766             },
767         )];
768         assert_eq!(
769             extract_symbol_names(&actions[..]).collect::<Vec<_>>(),
770             vec!["a", "c"],
771         );
772     }
773 
774     #[test]
test_extract_symbols_erase()775     fn test_extract_symbols_erase() {
776         let actions = [(
777             "Erase",
778             action::Action::Erase,
779         )];
780         assert_eq!(
781             extract_symbol_names(&actions[..]).collect::<Vec<_>>(),
782             vec!["BackSpace"],
783         );
784     }
785 
786 }
787