/**! The parsing of the data files for layouts */ use std::cell::RefCell; use std::collections::{ HashMap, HashSet }; use std::env; use std::ffi::CString; use std::fmt; use std::fs; use std::io; use std::path::PathBuf; use std::rc::Rc; use std::vec::Vec; use ::keyboard::{ KeyState, generate_keymap, generate_keycodes, FormattingError }; use ::resources; use ::util::c::as_str; use ::xdg; // traits, derives use std::io::BufReader; use std::iter::FromIterator; use serde::Deserialize; /// Gathers stuff defined in C or called by C pub mod c { use super::*; use std::os::raw::c_char; #[no_mangle] pub extern "C" fn squeek_load_layout(name: *const c_char) -> *mut ::layout::Layout { let name = as_str(&name) .expect("Bad layout name") .expect("Empty layout name"); let layout = load_layout_with_fallback(name); Box::into_raw(Box::new(layout)) } } const FALLBACK_LAYOUT_NAME: &str = "us"; #[derive(Debug)] enum LoadError { BadData(Error), MissingResource, BadResource(serde_yaml::Error), BadKeyMap(FormattingError), } impl fmt::Display for LoadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use self::LoadError::*; match self { BadData(e) => write!(f, "Bad data: {}", e), MissingResource => write!(f, "Missing resource"), BadResource(e) => write!(f, "Bad resource: {}", e), BadKeyMap(e) => write!(f, "Bad key map: {}", e), } } } fn load_layout_from_resource( name: &str ) -> Result { let data = resources::get_keyboard(name) .ok_or(LoadError::MissingResource)?; serde_yaml::from_str(data) .map_err(|e| LoadError::BadResource(e)) } enum DataSource { File(PathBuf), Resource(String), } impl fmt::Display for DataSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { DataSource::File(path) => write!(f, "Path: {:?}", path.display()), DataSource::Resource(name) => write!(f, "Resource: {}", name), } } } /// Tries to load the layout from the first place where it's present. /// If the layout exists, but is broken, fallback is activated. fn load_layout( name: &str ) -> (Result<::layout::Layout, LoadError>, DataSource) { let path = env::var_os("SQUEEKBOARD_KEYBOARDSDIR") .map(PathBuf::from) .or_else(|| xdg::data_path("squeekboard/keyboards")) .map(|path| path.join(name).with_extension("yaml")); let (layout, source) = match path { Some(path) => {( Layout::from_yaml_stream(path.clone()) .map_err(|e| LoadError::BadData(e)), DataSource::File(path) )}, None => {( load_layout_from_resource(name), DataSource::Resource(name.into()) )}, }; let layout = layout.and_then( |layout| layout.build().map_err(LoadError::BadKeyMap) ); (layout, source) } fn load_layout_with_fallback( name: &str ) -> ::layout::Layout { let (layout, source) = load_layout(name); let (layout, source) = match (layout, source) { (Err(e), source) => { eprintln!( "Failed to load layout from {}: {}, using fallback", source, e ); load_layout(FALLBACK_LAYOUT_NAME) }, res => res, }; let (layout, source) = match (layout, source) { (Err(e), source) => { eprintln!( "Failed to load fallback layout from {}: {}, using hardcoded", source, e ); ( load_layout_from_resource(FALLBACK_LAYOUT_NAME) .and_then( |layout| layout.build().map_err(LoadError::BadKeyMap) ), DataSource::Resource(FALLBACK_LAYOUT_NAME.into()), ) }, res => res, }; match (layout, source) { (Err(e), source) => { panic!( format!("Failed to load hardcoded layout from {}: {:?}", source, e ) ); }, (Ok(layout), source) => { eprintln!("Loaded layout from {}", source); layout } } } /// The root element describing an entire keyboard #[derive(Debug, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] struct Layout { bounds: Bounds, views: HashMap>, #[serde(default)] buttons: HashMap, outlines: HashMap } #[derive(Debug, Clone, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] struct Bounds { x: f64, y: f64, width: f64, height: f64, } /// Buttons are embedded in a single string type ButtonIds = String; #[derive(Debug, Default, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] struct ButtonMeta { /// Action other than keysym (conflicts with keysym) action: Option, /// The name of the outline. If not present, will be "default" outline: Option, /// FIXME: start using it keysym: Option, /// If not present, will be derived from the button ID label: Option, /// Conflicts with label icon: Option, } #[derive(Debug, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] enum Action { /// FIXME: start using it #[serde(rename="set_view")] SetView(String), #[serde(rename="show_prefs")] ShowPrefs, } #[derive(Debug, Clone, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] struct Outline { corner_radius: f64, bounds: Bounds, } #[derive(Debug)] enum Error { Yaml(serde_yaml::Error), Io(io::Error), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::Yaml(e) => write!(f, "YAML: {}", e), Error::Io(e) => write!(f, "IO: {}", e), } } } impl Layout { fn from_yaml_stream(path: PathBuf) -> Result { let infile = BufReader::new( fs::OpenOptions::new() .read(true) .open(&path) .map_err(Error::Io)? ); serde_yaml::from_reader(infile) .map_err(Error::Yaml) } fn build(self) -> Result<::layout::Layout, FormattingError> { let button_names = self.views.values() .flat_map(|rows| { rows.iter() .flat_map(|row| row.split_ascii_whitespace()) }); let button_names: HashSet<&str> = HashSet::from_iter(button_names); let keycodes = generate_keycodes( button_names.iter() .map(|name| *name) .filter(|name| { match self.buttons.get(*name) { // buttons with defined action can't emit keysyms // and so don't need keycodes Some(ButtonMeta { action: Some(_), .. }) => false, _ => true, } }) ); let button_states = button_names.iter().map(|name| {( String::from(*name), Rc::new(RefCell::new(KeyState { pressed: false, locked: false, keycode: keycodes.get(*name).map(|k| *k), symbol: ::symbol::Symbol { // FIXME: allow user to switch views action: ::symbol::Action::Submit { text: None, // TODO: derive keysym name & value from button name keys: vec!(), }, }, })) )}); let button_states = HashMap::>>::from_iter( button_states ); // TODO: generate from symbols let keymap_str = generate_keymap(&button_states)?; let views = HashMap::from_iter( self.views.iter().map(|(name, view)| {( String::from(name), Box::new(::layout::View { bounds: ::layout::c::Bounds { x: self.bounds.x, y: self.bounds.y, width: self.bounds.width, height: self.bounds.height, }, rows: view.iter().map(|row| { Box::new(::layout::Row { angle: 0, bounds: None, buttons: row.split_ascii_whitespace().map(|name| { Box::new(create_button( &self.buttons, &self.outlines, name, button_states.get(name.into()) .expect("Button state not created") .clone() )) }).collect(), }) }).collect(), }) )}) ); Ok(::layout::Layout { current_view: "base".into(), views: views, keymap_str: { CString::new(keymap_str) .expect("Invalid keymap string generated") }, }) } } /// TODO: Since this will receive user-provided data, /// all .expect() on them should be turned into soft fails fn create_button( button_info: &HashMap, outlines: &HashMap, name: &str, state: Rc>, ) -> ::layout::Button { let cname = CString::new(name.clone()) .expect("Bad name"); // don't remove, because multiple buttons with the same name are allowed let default_meta = ButtonMeta::default(); let button_meta = button_info.get(name) .unwrap_or(&default_meta); // TODO: move conversion to the C/Rust boundary let label = if let Some(label) = &button_meta.label { ::layout::Label::Text(CString::new(label.as_str()) .expect("Bad label")) } else if let Some(icon) = &button_meta.icon { ::layout::Label::IconName(CString::new(icon.as_str()) .expect("Bad icon")) } else { ::layout::Label::Text(cname.clone()) }; let outline_name = match &button_meta.outline { Some(outline) => { if outlines.contains_key(outline) { outline.clone() } else { eprintln!("Outline named {} does not exist! Using default for button {}", outline, name); "default".into() } } None => "default".into(), }; let outline = outlines.get(&outline_name) .map(|outline| (*outline).clone()) .unwrap_or_else(|| { eprintln!("No default outline defied Using 1x1!"); Outline { corner_radius: 0f64, bounds: Bounds { x: 0f64, y: 0f64, width: 1f64, height: 1f64 }, } }); ::layout::Button { name: cname, // TODO: do layout before creating buttons bounds: ::layout::c::Bounds { x: outline.bounds.x, y: outline.bounds.y, width: outline.bounds.width, height: outline.bounds.height, }, corner_radius: outline.corner_radius, label: label, state: state, } } #[cfg(test)] mod tests { use super::*; use std::error::Error as ErrorTrait; #[test] fn test_parse_path() { assert_eq!( Layout::from_yaml_stream( PathBuf::from("tests/layout.yaml") ).unwrap(), Layout { bounds: Bounds { x: 0f64, y: 0f64, width: 0f64, height: 0f64 }, views: hashmap!( "base".into() => vec!("test".into()), ), buttons: hashmap!{ "test".into() => ButtonMeta { icon: None, keysym: None, action: None, label: Some("test".into()), outline: None, } }, outlines: hashmap!{ "default".into() => Outline { corner_radius: 1f64, bounds: Bounds { x: 0f64, y: 0f64, width: 0f64, height: 0f64 }, } }, } ); } /// Check if the default protection works #[test] fn test_empty_views() { let out = Layout::from_yaml_stream(PathBuf::from("tests/layout2.yaml")); match out { Ok(_) => assert!(false, "Data mistakenly accepted"), Err(e) => { let mut handled = false; if let Error::Yaml(ye) = &e { handled = ye.description() == "missing field `views`"; }; if !handled { println!("Unexpected error {:?}", e); assert!(false) } } } } #[test] fn test_extra_field() { let out = Layout::from_yaml_stream(PathBuf::from("tests/layout3.yaml")); match out { Ok(_) => assert!(false, "Data mistakenly accepted"), Err(e) => { let mut handled = false; if let Error::Yaml(ye) = &e { handled = ye.description() .starts_with("unknown field `bad_field`"); }; if !handled { println!("Unexpected error {:?}", e); assert!(false) } } } } #[test] fn parsing_fallback() { assert!(load_layout_from_resource(FALLBACK_LAYOUT_NAME) .and_then(|layout| layout.build().map_err(LoadError::BadKeyMap)) .is_ok() ); } }