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