use std::collections::HashMap;
use std::path::PathBuf;
use serde::Deserialize;
use crate::Keysym;
pub const BASE_LAYER: &str = "base";
pub const SHIFT_LAYER: &str = "shift";
pub const TERMINAL_BAR: &str = "bar-terminal";
const FALLBACK_LAYOUT: &str = include_str!( "../../keyboards/en.json" );
const FALLBACK_TERMINAL_BAR: &str = include_str!( "../../keyboards/bar-terminal.json" );
#[ derive( Clone, Debug, PartialEq, Eq, Deserialize ) ]
#[ serde( rename_all = "lowercase" ) ]
pub enum Action
{
Shift,
Backspace,
Enter,
Space,
Layer,
Layout,
Hide,
}
#[ derive( Clone, Copy, Debug, PartialEq, Eq, Deserialize ) ]
#[ serde( rename_all = "lowercase" ) ]
pub enum Modifier
{
Ctrl,
Alt,
}
#[ derive( Clone, Debug, Deserialize ) ]
#[ serde( untagged ) ]
enum KeySpec
{
Text( String ),
Full
{
#[ serde( default ) ]
text: Option<String>,
#[ serde( default ) ]
action: Option<Action>,
#[ serde( default ) ]
key: Option<String>,
#[ serde( default ) ]
modifier: Option<Modifier>,
#[ serde( default ) ]
layer: Option<String>,
#[ serde( default ) ]
label: Option<String>,
#[ serde( default = "one" ) ]
width: f32,
},
}
fn one() -> f32 { 1.0 }
#[ derive( Clone, Debug, Deserialize ) ]
struct LayoutFile
{
name: String,
#[ serde( default ) ]
label: Option<String>,
layers: HashMap<String, Vec<Vec<KeySpec>>>,
#[ serde( default ) ]
variants: HashMap<String, Vec<String>>,
}
#[ derive( Clone, Debug, PartialEq ) ]
pub struct Key
{
pub text: Option<String>,
pub action: Option<Action>,
pub keysym: Option<Keysym>,
pub modifier: Option<Modifier>,
pub layer: Option<String>,
pub label: String,
pub width: f32,
}
#[ derive( Clone, Debug ) ]
pub struct Layout
{
pub name: String,
pub label: String,
layers: HashMap<String, Vec<Vec<Key>>>,
variants: HashMap<String, Vec<String>>,
}
impl Layout
{
pub fn from_json( json: &str ) -> Result<Self, String>
{
let file: LayoutFile = serde_json::from_str( json ).map_err( |e| e.to_string() )?;
if !file.layers.contains_key( BASE_LAYER ) { return Err( format!( "layout {} has no `{BASE_LAYER}` layer", file.name ) ); }
let mut layers: HashMap<String, Vec<Vec<Key>>> = file.layers.iter()
.map( |( id, rows )| ( id.clone(), rows.iter().map( |row| row.iter().map( |k| Key::from_spec( k, id ) ).collect() ).collect() ) )
.collect();
if !layers.contains_key( SHIFT_LAYER )
{
let shifted = layers[ BASE_LAYER ].iter()
.map( |row| row.iter().map( Key::uppercased ).collect() )
.collect();
layers.insert( SHIFT_LAYER.to_string(), shifted );
}
let label = file.label.unwrap_or_else( || file.name.to_uppercase() );
Ok( Self { name: file.name, label, layers, variants: file.variants } )
}
pub fn rows( &self, layer: &str ) -> &[ Vec<Key> ]
{
self.layers.get( layer ).or_else( || self.layers.get( BASE_LAYER ) ).map( Vec::as_slice ).unwrap_or( &[] )
}
pub fn has_layer( &self, layer: &str ) -> bool
{
self.layers.contains_key( layer )
}
pub fn variants( &self, text: &str ) -> Vec<String>
{
if let Some( v ) = self.variants.get( text ) { return v.clone(); }
let lower = text.to_lowercase();
if lower != text
{
if let Some( v ) = self.variants.get( &lower ) { return v.iter().map( |s| s.to_uppercase() ).collect(); }
}
Vec::new()
}
}
impl Key
{
fn from_spec( spec: &KeySpec, layer: &str ) -> Self
{
match spec
{
KeySpec::Text( text ) => Self { text: Some( text.clone() ), action: None, keysym: None, modifier: None, layer: None, label: text.clone(), width: 1.0 },
KeySpec::Full { text, action, key, modifier, layer: target, label, width } =>
{
let keysym = key.as_deref().and_then( named_keysym );
let label = label.clone()
.or_else( || text.clone() )
.or_else( || key.clone().filter( |_| keysym.is_some() ) )
.or_else( || modifier.map( |m| match m { Modifier::Ctrl => "Ctrl", Modifier::Alt => "Alt" }.to_string() ) )
.unwrap_or_else( || default_label( action.as_ref(), target.as_deref(), layer ) );
Self { text: text.clone(), action: action.clone(), keysym, modifier: *modifier, layer: target.clone(), label, width: *width }
}
}
}
fn uppercased( &self ) -> Self
{
match &self.text
{
Some( t ) if self.action.is_none() =>
{
let up = t.to_uppercase();
Self { text: Some( up.clone() ), label: up, ..self.clone() }
}
_ => self.clone(),
}
}
}
fn default_label( action: Option<&Action>, target: Option<&str>, layer: &str ) -> String
{
match action
{
Some( Action::Shift ) => if layer == SHIFT_LAYER { "⇪" } else { "⇧" }.to_string(),
Some( Action::Backspace ) => "⌫".to_string(),
Some( Action::Enter ) => "↵".to_string(),
Some( Action::Space ) => String::new(),
Some( Action::Layer ) => target.map( |l| if l == BASE_LAYER { "ABC".to_string() } else { l.to_uppercase() } ).unwrap_or_default(),
Some( Action::Layout ) => String::new(),
Some( Action::Hide ) => "⌄".to_string(),
None => String::new(),
}
}
fn named_keysym( name: &str ) -> Option<Keysym>
{
Some( match name
{
"Escape" => Keysym::Escape,
"Tab" => Keysym::Tab,
"Left" => Keysym::Left,
"Right" => Keysym::Right,
"Up" => Keysym::Up,
"Down" => Keysym::Down,
"Home" => Keysym::Home,
"End" => Keysym::End,
"Page_Up" => Keysym::Page_Up,
"Page_Down" => Keysym::Page_Down,
"Delete" => Keysym::Delete,
"Insert" => Keysym::Insert,
"F1" => Keysym::F1, "F2" => Keysym::F2, "F3" => Keysym::F3, "F4" => Keysym::F4,
"F5" => Keysym::F5, "F6" => Keysym::F6, "F7" => Keysym::F7, "F8" => Keysym::F8,
"F9" => Keysym::F9, "F10" => Keysym::F10, "F11" => Keysym::F11, "F12" => Keysym::F12,
_ => return None,
} )
}
pub fn search_paths() -> Vec<PathBuf>
{
let mut out = Vec::new();
if let Some( dev ) = std::env::var_os( "LTK_KEYBOARDS_DIR" ) { out.push( PathBuf::from( dev ) ); }
if let Some( data ) = crate::dirs::data_home() { out.push( data.join( "ltk" ).join( "keyboards" ) ); }
if let Some( bundle ) = crate::resources::resource_dir() { out.push( bundle.join( "keyboards" ) ); }
out.push( PathBuf::from( "/usr/share/ltk/keyboards" ) );
out
}
pub fn load( name: &str ) -> Option<Layout>
{
search_paths().into_iter()
.map( |dir| dir.join( format!( "{name}.json" ) ) )
.find_map( |path| crate::vfs::read_to_string( &path ).ok() )
.and_then( |json| Layout::from_json( &json ).map_err( |e| eprintln!( "[ltk] keyboard layout {name}: {e}" ) ).ok() )
}
pub fn available() -> Vec<String>
{
let mut names: Vec<String> = search_paths().into_iter()
.filter_map( |dir| crate::vfs::read_dir( dir ).ok() )
.flatten()
.filter_map( |path| path.file_stem().and_then( |s| s.to_str() ).map( str::to_string ).filter( |_| path.extension().is_some_and( |e| e == "json" ) ) )
.filter( |name| !name.starts_with( "bar-" ) )
.collect();
names.sort();
names.dedup();
names
}
pub fn fallback() -> Layout
{
Layout::from_json( FALLBACK_LAYOUT ).expect( "the built-in layout parses" )
}
pub fn terminal_bar() -> Layout
{
load( TERMINAL_BAR ).unwrap_or_else( || Layout::from_json( FALLBACK_TERMINAL_BAR ).expect( "the built-in terminal bar parses" ) )
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn built_in_layout_parses_with_a_derived_shift_layer()
{
let layout = fallback();
assert_eq!( layout.name, "en" );
let base = layout.rows( BASE_LAYER );
let shift = layout.rows( SHIFT_LAYER );
assert_eq!( base.len(), shift.len() );
assert_eq!( base[ 0 ][ 0 ].text.as_deref(), Some( "q" ) );
assert_eq!( shift[ 0 ][ 0 ].text.as_deref(), Some( "Q" ) );
assert!( layout.has_layer( "symbols" ) );
}
#[ test ]
fn key_objects_take_action_width_and_default_labels()
{
let layout = Layout::from_json( r#"{ "name": "t", "layers": { "base": [ [ "a", { "action": "backspace", "width": 1.5 }, { "action": "layer", "layer": "symbols", "label": "?123" } ] ], "symbols": [ [ "1" ] ] } }"# ).unwrap();
let row = &layout.rows( BASE_LAYER )[ 0 ];
assert_eq!( row[ 1 ].action, Some( Action::Backspace ) );
assert_eq!( row[ 1 ].width, 1.5 );
assert_eq!( row[ 1 ].label, "⌫" );
assert_eq!( row[ 2 ].label, "?123" );
assert_eq!( row[ 2 ].layer.as_deref(), Some( "symbols" ) );
assert_eq!( layout.label, "T" );
}
#[ test ]
fn variants_follow_case()
{
let layout = Layout::from_json( r#"{ "name": "t", "layers": { "base": [ [ "a" ] ] }, "variants": { "a": [ "á", "à" ] } }"# ).unwrap();
assert_eq!( layout.variants( "a" ), [ "á", "à" ] );
assert_eq!( layout.variants( "A" ), [ "Á", "À" ] );
assert!( layout.variants( "b" ).is_empty() );
}
#[ test ]
fn named_keys_and_modifiers_parse_with_their_labels()
{
let layout = Layout::from_json( r#"{ "name": "t", "layers": { "base": [ [ { "key": "Escape", "label": "Esc" }, { "key": "Left" }, { "modifier": "ctrl" }, { "key": "NoSuchKey" } ] ] } }"# ).unwrap();
let row = &layout.rows( BASE_LAYER )[ 0 ];
assert_eq!( row[ 0 ].keysym, Some( Keysym::Escape ) );
assert_eq!( row[ 0 ].label, "Esc" );
assert_eq!( row[ 1 ].keysym, Some( Keysym::Left ) );
assert_eq!( row[ 1 ].label, "Left" );
assert_eq!( row[ 2 ].modifier, Some( Modifier::Ctrl ) );
assert_eq!( row[ 2 ].label, "Ctrl" );
assert_eq!( row[ 3 ].keysym, None );
}
#[ test ]
fn bars_are_not_listed_as_layouts()
{
let _globals = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
let dir = std::env::temp_dir().join( format!( "ltk-osk-test-{}", std::process::id() ) );
std::fs::create_dir_all( &dir ).unwrap();
std::fs::write( dir.join( "xx.json" ), "{}" ).unwrap();
std::fs::write( dir.join( "bar-xx.json" ), "{}" ).unwrap();
std::env::set_var( "LTK_KEYBOARDS_DIR", &dir );
let names = available();
std::env::remove_var( "LTK_KEYBOARDS_DIR" );
let _ = std::fs::remove_dir_all( &dir );
assert!( names.contains( &"xx".to_string() ) );
assert!( !names.iter().any( |n| n.starts_with( "bar-" ) ) );
}
#[ test ]
fn the_built_in_terminal_bar_parses()
{
let bar = Layout::from_json( FALLBACK_TERMINAL_BAR ).unwrap();
let row = &bar.rows( BASE_LAYER )[ 0 ];
assert!( row.iter().any( |k| k.keysym == Some( Keysym::Escape ) ) );
assert!( row.iter().any( |k| k.modifier == Some( Modifier::Ctrl ) ) );
assert!( row.iter().any( |k| k.text.as_deref() == Some( "|" ) ) );
assert!( bar.variants( "/" ).iter().any( |v| v == "\\" ) );
assert!( bar.variants( "-" ).iter().any( |v| v == "~" ) );
}
#[ test ]
fn shipped_layers_are_no_wider_than_their_base()
{
let units = |row: &Vec<Key>| row.iter().map( |k| k.width ).sum::<f32>();
for json in [ include_str!( "../../keyboards/en.json" ), include_str!( "../../keyboards/es.json" ) ]
{
let layout = Layout::from_json( json ).unwrap();
let base = layout.rows( BASE_LAYER ).iter().map( units ).fold( 0.0_f32, f32::max );
for ( name, rows ) in &layout.layers
{
for row in rows
{
assert!( units( row ) <= base, "{}: layer {name} has a row of {} units, the base {base}", layout.name, units( row ) );
}
}
}
}
#[ test ]
fn a_layout_without_base_is_rejected()
{
assert!( Layout::from_json( r#"{ "name": "t", "layers": { "symbols": [ [ "1" ] ] } }"# ).is_err() );
}
}