use std::collections::HashMap;
use tracing::debug;
use super::KeyMapping;
pub struct XkbKeymap {
map: HashMap<char, KeyMapping>,
}
impl XkbKeymap {
pub fn from_default_layout() -> anyhow::Result<Self> {
let context = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
let keymap = xkbcommon::xkb::Keymap::new_from_names(
&context,
"", "", "", "", None, xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
)
.ok_or_else(|| anyhow::anyhow!("failed to create XKB keymap from default layout"))?;
let map = build_reverse_map(&keymap);
debug!("built XKB reverse keymap with {} entries", map.len());
Ok(Self { map })
}
pub fn lookup(&self, ch: char) -> Option<&KeyMapping> {
self.map.get(&ch)
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
fn build_reverse_map(keymap: &xkbcommon::xkb::Keymap) -> HashMap<char, KeyMapping> {
let mut map = HashMap::new();
let min = keymap.min_keycode().raw();
let max = keymap.max_keycode().raw();
let num_layouts = keymap.num_layouts();
for raw_keycode in min..=max {
let keycode = xkbcommon::xkb::Keycode::new(raw_keycode);
for layout in 0..num_layouts {
let num_levels = keymap.num_levels_for_key(keycode, layout);
for level in 0..num_levels {
let syms = keymap.key_get_syms_by_level(keycode, layout, level);
for &sym in syms {
let unicode = xkbcommon::xkb::keysym_to_utf32(sym);
if unicode == 0 {
continue;
}
if let Some(ch) = char::from_u32(unicode) {
let evdev_keycode = raw_keycode.saturating_sub(8);
let shift = level >= 1;
let mapping = KeyMapping {
keycode: evdev_keycode as u16,
shift,
};
map.entry(ch).or_insert(mapping);
}
}
}
}
}
map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_default_keymap() {
let km = XkbKeymap::from_default_layout();
if let Ok(km) = km {
assert!(!km.is_empty(), "keymap should not be empty");
assert!(km.lookup('a').is_some(), "'a' should be in the keymap");
}
}
#[test]
fn shift_mapping_for_uppercase() {
let km = XkbKeymap::from_default_layout();
if let Ok(km) = km {
if let Some(mapping) = km.lookup('A') {
assert!(
mapping.shift,
"uppercase 'A' should require shift on standard layouts"
);
}
}
}
}