391 lines
12 KiB
Rust
391 lines
12 KiB
Rust
/* Copyright (C) 2020-2021 Purism SPC
|
|
* SPDX-License-Identifier: GPL-3.0+
|
|
*/
|
|
|
|
/*! Loading layout files */
|
|
|
|
use std::env;
|
|
use std::fmt;
|
|
use std::path::PathBuf;
|
|
|
|
use super::{ Error, LoadError };
|
|
use super::parsing;
|
|
|
|
use crate::layout;
|
|
use crate::layout::ArrangementKind;
|
|
use crate::logging;
|
|
use crate::xdg;
|
|
use crate::imservice::ContentPurpose;
|
|
|
|
|
|
const FALLBACK_LAYOUT_NAME: &str = "us";
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
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),
|
|
}
|
|
}
|
|
}
|
|
|
|
/* All functions in this family carry around ArrangementKind,
|
|
* because it's not guaranteed to be preserved,
|
|
* and the resulting layout needs to know which version was loaded.
|
|
* See `squeek_layout_get_kind`.
|
|
* Possible TODO: since this is used only in styling,
|
|
* and makes the below code nastier than needed, maybe it should go.
|
|
*/
|
|
|
|
/// Returns ordered names treating `name` as the base name,
|
|
/// ignoring any `+` inside.
|
|
fn _get_arrangement_names(name: &str, arrangement: ArrangementKind)
|
|
-> Vec<(ArrangementKind, String)>
|
|
{
|
|
let name_with_arrangement = match arrangement {
|
|
ArrangementKind::Base => name.into(),
|
|
ArrangementKind::Wide => format!("{}_wide", name),
|
|
};
|
|
|
|
let mut ret = Vec::new();
|
|
if name_with_arrangement != name {
|
|
ret.push((arrangement, name_with_arrangement));
|
|
}
|
|
ret.push((ArrangementKind::Base, name.into()));
|
|
ret
|
|
}
|
|
|
|
/// Returns names accounting for any `+` in the `name`,
|
|
/// including the fallback to the default layout.
|
|
fn get_preferred_names(name: &str, kind: ArrangementKind)
|
|
-> Vec<(ArrangementKind, String)>
|
|
{
|
|
let mut ret = _get_arrangement_names(name, kind);
|
|
|
|
let base_name_preferences = {
|
|
let mut parts = name.splitn(2, '+');
|
|
match parts.next() {
|
|
Some(base) => {
|
|
// The name is already equal to base, so nothing to add
|
|
if base == name {
|
|
vec![]
|
|
} else {
|
|
_get_arrangement_names(base, kind)
|
|
}
|
|
},
|
|
// The layout's base name starts with a "+". Weird but OK.
|
|
None => {
|
|
log_print!(logging::Level::Surprise, "Base layout name is empty: {}", name);
|
|
vec![]
|
|
}
|
|
}
|
|
};
|
|
|
|
ret.extend(base_name_preferences.into_iter());
|
|
let fallback_names = _get_arrangement_names(FALLBACK_LAYOUT_NAME, kind);
|
|
ret.extend(fallback_names.into_iter());
|
|
ret
|
|
}
|
|
|
|
/// Includes the subdirectory before the forward slash.
|
|
type LayoutPath = String;
|
|
|
|
// This is only used inside iter_fallbacks_with_meta.
|
|
// Placed at the top scope
|
|
// because `use LayoutPurpose::*;`
|
|
// complains about "not in scope" otherwise.
|
|
// This seems to be a Rust 2015 edition problem.
|
|
/// Helper for determining where to look up the layout.
|
|
enum LayoutPurpose<'a> {
|
|
Default,
|
|
Special(&'a str),
|
|
}
|
|
|
|
/// Returns the directory string
|
|
/// where the layout should be looked up, including the slash.
|
|
fn get_directory_string(
|
|
content_purpose: ContentPurpose,
|
|
overlay: Option<&str>) -> String
|
|
{
|
|
use self::LayoutPurpose::*;
|
|
|
|
let layout_purpose = match overlay {
|
|
None => match content_purpose {
|
|
ContentPurpose::Email => Special("email"),
|
|
ContentPurpose::Digits => Special("number"),
|
|
ContentPurpose::Number => Special("number"),
|
|
ContentPurpose::Phone => Special("number"),
|
|
ContentPurpose::Pin => Special("pin"),
|
|
ContentPurpose::Terminal => Special("terminal"),
|
|
ContentPurpose::Url => Special("url"),
|
|
_ => Default,
|
|
},
|
|
Some(overlay) => Special(overlay),
|
|
};
|
|
|
|
// For intuitiveness,
|
|
// default purpose layouts are stored in the root directory,
|
|
// as they correspond to typical text
|
|
// and are seen the most often.
|
|
match layout_purpose {
|
|
Default => "".into(),
|
|
Special(purpose) => format!("{}/", purpose),
|
|
}
|
|
}
|
|
|
|
/// Returns an iterator over all fallback paths.
|
|
fn to_layout_paths(
|
|
name_fallbacks: Vec<(ArrangementKind, String)>,
|
|
content_purpose: ContentPurpose,
|
|
overlay: Option<&str>,
|
|
) -> impl Iterator<Item=(ArrangementKind, LayoutPath)> {
|
|
let prepend_directory = get_directory_string(content_purpose, overlay);
|
|
|
|
name_fallbacks.into_iter()
|
|
.map(move |(arrangement, name)|
|
|
(arrangement, format!("{}{}", prepend_directory, name))
|
|
)
|
|
}
|
|
|
|
type LayoutSource = (ArrangementKind, DataSource);
|
|
|
|
fn to_layout_sources(
|
|
layout_paths: impl Iterator<Item=(ArrangementKind, LayoutPath)>,
|
|
filesystem_path: Option<PathBuf>,
|
|
) -> impl Iterator<Item=LayoutSource> {
|
|
layout_paths.flat_map(move |(arrangement, layout_path)| {
|
|
let mut sources = Vec::new();
|
|
if let Some(path) = &filesystem_path {
|
|
sources.push((
|
|
arrangement,
|
|
DataSource::File(
|
|
path.join(&layout_path)
|
|
.with_extension("yaml")
|
|
)
|
|
));
|
|
};
|
|
sources.push((arrangement, DataSource::Resource(layout_path.clone())));
|
|
sources.into_iter()
|
|
})
|
|
}
|
|
|
|
/// Returns possible sources, with first as the most preferred one.
|
|
/// Trying order: native lang of the right kind, native base,
|
|
/// fallback lang of the right kind, fallback base
|
|
fn iter_layout_sources(
|
|
name: &str,
|
|
arrangement: ArrangementKind,
|
|
purpose: ContentPurpose,
|
|
ui_overlay: Option<&str>,
|
|
layout_storage: Option<PathBuf>,
|
|
) -> impl Iterator<Item=LayoutSource> {
|
|
let names = get_preferred_names(name, arrangement);
|
|
let paths = to_layout_paths(names, purpose, ui_overlay);
|
|
to_layout_sources(paths, layout_storage)
|
|
}
|
|
|
|
fn load_layout_data(source: DataSource)
|
|
-> Result<crate::layout::LayoutParseData, LoadError>
|
|
{
|
|
let handler = logging::Print {};
|
|
match source {
|
|
DataSource::File(path) => {
|
|
parsing::Layout::from_file(path.clone())
|
|
.map_err(LoadError::BadData)
|
|
.and_then(|layout|
|
|
layout.build(handler).0.map_err(LoadError::BadKeyMap)
|
|
)
|
|
},
|
|
DataSource::Resource(name) => {
|
|
parsing::Layout::from_resource(&name)
|
|
.and_then(|layout|
|
|
layout.build(handler).0.map_err(LoadError::BadKeyMap)
|
|
)
|
|
},
|
|
}
|
|
}
|
|
|
|
fn load_layout_data_with_fallback(
|
|
name: &str,
|
|
kind: ArrangementKind,
|
|
purpose: ContentPurpose,
|
|
overlay: Option<&str>,
|
|
) -> (ArrangementKind, layout::LayoutParseData) {
|
|
|
|
// Build the path to the right keyboard layout subdirectory
|
|
let path = env::var_os("SQUEEKBOARD_KEYBOARDSDIR")
|
|
.map(PathBuf::from)
|
|
.or_else(|| xdg::data_path("squeekboard/keyboards"));
|
|
|
|
for (kind, source) in iter_layout_sources(&name, kind, purpose, overlay, path) {
|
|
let layout = load_layout_data(source.clone());
|
|
match layout {
|
|
Err(e) => match (e, source) {
|
|
(
|
|
LoadError::BadData(Error::Missing(e)),
|
|
DataSource::File(file)
|
|
) => log_print!(
|
|
logging::Level::Debug,
|
|
"Tried file {:?}, but it's missing: {}",
|
|
file, e
|
|
),
|
|
(e, source) => log_print!(
|
|
logging::Level::Warning,
|
|
"Failed to load layout from {}: {}, skipping",
|
|
source, e
|
|
),
|
|
},
|
|
Ok(layout) => {
|
|
log_print!(logging::Level::Info, "Loaded layout {}", source);
|
|
return (kind, layout);
|
|
}
|
|
}
|
|
}
|
|
|
|
panic!("No useful layout found!");
|
|
}
|
|
|
|
pub fn load_layout(
|
|
name: &String,
|
|
kind: ArrangementKind,
|
|
variant: ContentPurpose,
|
|
overlay: &Option<String>,
|
|
) -> layout::Layout {
|
|
let overlay = overlay.as_ref().map(String::as_str);
|
|
let (found_kind, layout)
|
|
= load_layout_data_with_fallback(name, kind, variant, overlay);
|
|
layout::Layout::new(layout, found_kind, variant)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use crate::logging::ProblemPanic;
|
|
|
|
#[test]
|
|
fn parsing_fallback() {
|
|
assert!(parsing::Layout::from_resource(FALLBACK_LAYOUT_NAME)
|
|
.map(|layout| layout.build(ProblemPanic).0.unwrap())
|
|
.is_ok()
|
|
);
|
|
}
|
|
|
|
/// First fallback should be to builtin, not to FALLBACK_LAYOUT_NAME
|
|
#[test]
|
|
fn test_fallback_basic_builtin() {
|
|
let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, None, None);
|
|
|
|
assert_eq!(
|
|
sources.collect::<Vec<_>>(),
|
|
vec!(
|
|
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
|
(
|
|
ArrangementKind::Base,
|
|
DataSource::Resource(FALLBACK_LAYOUT_NAME.into())
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
/// Prefer loading from file system before builtin.
|
|
#[test]
|
|
fn test_preferences_order_path() {
|
|
let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, None, Some(".".into()));
|
|
|
|
assert_eq!(
|
|
sources.collect::<Vec<_>>(),
|
|
vec!(
|
|
(ArrangementKind::Base, DataSource::File("./nb.yaml".into())),
|
|
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
|
(
|
|
ArrangementKind::Base,
|
|
DataSource::File("./us.yaml".into())
|
|
),
|
|
(
|
|
ArrangementKind::Base,
|
|
DataSource::Resource("us".into())
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
/// If layout contains a "+", it should reach for what's in front of it too.
|
|
#[test]
|
|
fn test_preferences_order_base() {
|
|
let sources = iter_layout_sources("nb+aliens", ArrangementKind::Base, ContentPurpose::Normal, None, None);
|
|
|
|
assert_eq!(
|
|
sources.collect::<Vec<_>>(),
|
|
vec!(
|
|
(ArrangementKind::Base, DataSource::Resource("nb+aliens".into())),
|
|
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
|
(
|
|
ArrangementKind::Base,
|
|
DataSource::Resource(FALLBACK_LAYOUT_NAME.into())
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_preferences_order_arrangement() {
|
|
let sources = iter_layout_sources("nb", ArrangementKind::Wide, ContentPurpose::Normal, None, None);
|
|
|
|
assert_eq!(
|
|
sources.collect::<Vec<_>>(),
|
|
vec!(
|
|
(ArrangementKind::Wide, DataSource::Resource("nb_wide".into())),
|
|
(ArrangementKind::Base, DataSource::Resource("nb".into())),
|
|
(
|
|
ArrangementKind::Wide,
|
|
DataSource::Resource("us_wide".into())
|
|
),
|
|
(
|
|
ArrangementKind::Base,
|
|
DataSource::Resource("us".into())
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_preferences_order_overlay() {
|
|
let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, Some("terminal"), None);
|
|
|
|
assert_eq!(
|
|
sources.collect::<Vec<_>>(),
|
|
vec!(
|
|
(ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
|
|
(
|
|
ArrangementKind::Base,
|
|
DataSource::Resource("terminal/us".into())
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_preferences_order_hint() {
|
|
let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Terminal, None, None);
|
|
|
|
assert_eq!(
|
|
sources.collect::<Vec<_>>(),
|
|
vec!(
|
|
(ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
|
|
(
|
|
ArrangementKind::Base,
|
|
DataSource::Resource("terminal/us".into())
|
|
),
|
|
)
|
|
);
|
|
}
|
|
}
|