WIP WIP: keymap generation test passes meta: Update features and version WiP: cargo.lock WIP: don't crash WIP: no outlines parsing: New tests WIP: base level works WIP: remove old keyboard symbols correctly input WIP: lodaing files WIP: fallback works Valid fallback
484 lines
14 KiB
Rust
484 lines
14 KiB
Rust
/**! 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<Layout, LoadError> {
|
|
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<String, Vec<ButtonIds>>,
|
|
#[serde(default)]
|
|
buttons: HashMap<String, ButtonMeta>,
|
|
outlines: HashMap<String, Outline>
|
|
}
|
|
|
|
#[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<Action>,
|
|
/// The name of the outline. If not present, will be "default"
|
|
outline: Option<String>,
|
|
/// FIXME: start using it
|
|
keysym: Option<String>,
|
|
/// If not present, will be derived from the button ID
|
|
label: Option<String>,
|
|
/// Conflicts with label
|
|
icon: Option<String>,
|
|
}
|
|
|
|
#[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<Layout, Error> {
|
|
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::<String, Rc<RefCell<KeyState>>>::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<String, ButtonMeta>,
|
|
outlines: &HashMap<String, Outline>,
|
|
name: &str,
|
|
state: Rc<RefCell<KeyState>>,
|
|
) -> ::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()
|
|
);
|
|
}
|
|
}
|