From 247d2805db0c1b686ef4c6cc0f786ff416aeb7e3 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 11 Jul 2026 01:46:59 +0200 Subject: init --- src/keymap.rs | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 src/keymap.rs (limited to 'src/keymap.rs') diff --git a/src/keymap.rs b/src/keymap.rs new file mode 100644 index 0000000..d14f9ab --- /dev/null +++ b/src/keymap.rs @@ -0,0 +1,253 @@ +use crate::layout; +use crate::x11; + +use xkbcommon::xkb; + +#[derive(Debug, Clone)] +pub enum KeyKind { + Normal, + Shift, + Level3, + Lock, + Control, + Alt, + Super, +} + +#[derive(Debug, Clone)] +pub struct KeyEntry { + pub labels: [[String; 4]; 4], + pub kind: KeyKind, + pub dead: [[bool; 4]; 4], + pub pos: layout::KeyPos, +} + +#[derive(Debug, Clone)] +pub struct KeyData { + pub keys: Vec, + pub layouts: Vec, + pub is_iso: bool, +} + +fn dead_display(name: &str) -> &'static str { + match name.to_lowercase().as_str() { + "dead_grave" => "`", + "dead_acute" => "'", + "dead_circumflex" => "^", + "dead_tilde" => "~", + "dead_macron" => "-", + "dead_breve" => "u", + "dead_abovedot" => ".", + "dead_diaeresis" => "\"", + "dead_caron" => "v", + "dead_cedilla" => ",", + "dead_ogonek" => "~", + "dead_doubleacute" => "\"", + "dead_stroke" => "/", + "dead_hook" => "?", + "dead_horn" => "?", + "dead_psili" => "'", + "dead_dasia" => "'", + "dead_iota" => "i", + "dead_voiced_sound" => "\"", + "dead_semivoiced_sound" => "o", + _ => "?", + } +} + +fn keysym_to_display(sym: xkb::Keysym) -> String { + let utf8 = xkb::keysym_to_utf8(sym); + let trimmed = utf8.trim().to_string(); + if !trimmed.is_empty() { + let name = xkb::keysym_get_name(sym); + if name.to_lowercase().starts_with("dead_") { + return dead_display(&name).to_string(); + } + return trimmed; + } + let name = xkb::keysym_get_name(sym); + if name.to_lowercase().starts_with("dead_") { + return dead_display(&name).to_string(); + } + match sym { + xkb::Keysym::Shift_L | xkb::Keysym::Shift_R => "Shift".into(), + xkb::Keysym::ISO_Level3_Shift => "AltGr".into(), + xkb::Keysym::Control_L | xkb::Keysym::Control_R => "Ctrl".into(), + xkb::Keysym::Alt_L | xkb::Keysym::Alt_R => "Alt".into(), + xkb::Keysym::Super_L | xkb::Keysym::Super_R => "Super".into(), + xkb::Keysym::Menu => "Menu".into(), + xkb::Keysym::Caps_Lock => "Caps".into(), + xkb::Keysym::Tab => "Tab".into(), + xkb::Keysym::Return => "Enter".into(), + xkb::Keysym::Escape => "Esc".into(), + xkb::Keysym::BackSpace => "Bksp".into(), + xkb::Keysym::space => " ".into(), + xkb::Keysym::Delete => "Del".into(), + xkb::Keysym::Insert => "Ins".into(), + xkb::Keysym::Home => "Home".into(), + xkb::Keysym::End => "End".into(), + xkb::Keysym::Page_Up => "PgUp".into(), + xkb::Keysym::Page_Down => "PgDn".into(), + xkb::Keysym::Up => "^".into(), + xkb::Keysym::Down => "v".into(), + xkb::Keysym::Left => "<".into(), + xkb::Keysym::Right => ">".into(), + xkb::Keysym::KP_Enter => "Enter".into(), + xkb::Keysym::Num_Lock => "Num".into(), + xkb::Keysym::Scroll_Lock => "Scrl".into(), + xkb::Keysym::Pause => "Pause".into(), + xkb::Keysym::Print => "PrtSc".into(), + _ => name, + } +} + +fn detect_key_kind(syms: &[xkb::Keysym]) -> KeyKind { + for &sym in syms { + match sym { + xkb::Keysym::Shift_L | xkb::Keysym::Shift_R => return KeyKind::Shift, + xkb::Keysym::ISO_Level3_Shift => return KeyKind::Level3, + xkb::Keysym::Caps_Lock => return KeyKind::Lock, + xkb::Keysym::Control_L | xkb::Keysym::Control_R => return KeyKind::Control, + xkb::Keysym::Alt_L | xkb::Keysym::Alt_R => return KeyKind::Alt, + xkb::Keysym::Super_L | xkb::Keysym::Super_R => return KeyKind::Super, + _ => {} + } + } + KeyKind::Normal +} + +fn is_keysym_dead(sym: xkb::Keysym) -> bool { + xkb::keysym_get_name(sym).to_lowercase().starts_with("dead_") +} + +pub fn level_from_x11_modmask(mask: u16) -> usize { + let shift = mask & 0x01 != 0; + let altgr = mask & 0x80 != 0; + match (shift, altgr) { + (false, false) => 0, + (true, false) => 1, + (false, true) => 2, + (true, true) => 3, + } +} + +struct RawKey { + name: String, + labels: [[String; 4]; 4], + dead: [[bool; 4]; 4], + kind: KeyKind, +} + +fn detect_is_iso(model: &str, keymap_has_lsgt: bool) -> bool { + if model.contains("105") || model.contains("102") || model.contains("98") { + return true; + } + if model.contains("101") || model.contains("104") || model.contains("106") { + return false; + } + keymap_has_lsgt +} + +pub fn compile_keymap(names: &x11::RuleNames) -> Result { + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + + let options = Some(names.options.clone()); + + let keymap = xkb::Keymap::new_from_names( + &context, + &names.rules, + &names.model, + &names.layout, + &names.variant, + options, + xkb::KEYMAP_COMPILE_NO_FLAGS, + ) + .ok_or_else(|| "failed to compile keymap".to_string())?; + + let num_groups = keymap.num_layouts() as usize; + + let layouts: Vec = if names.layout.is_empty() || num_groups <= 1 { + vec![if names.layout.is_empty() { + "default".into() + } else { + names.layout.clone() + }] + } else { + let layout_parts: Vec<&str> = names.layout.split(',').collect(); + let variant_parts: Vec<&str> = if names.variant.is_empty() { + vec![] + } else { + names.variant.split(',').collect() + }; + layout_parts + .iter() + .enumerate() + .map(|(i, l)| { + let v = variant_parts.get(i).filter(|v| !v.is_empty()); + match v { + Some(v) => format!("{}({})", l, v), + None => l.to_string(), + } + }) + .collect() + }; + + layout::reset_default_pos(); + + let mut raw = Vec::new(); + let mut has_lsgt = false; + + keymap.key_for_each(|km, kc| { + let name = match km.key_get_name(kc) { + Some(n) => n.to_string(), + None => return, + }; + + if name == "LSGT" { + has_lsgt = true; + } + + let mut labels: [[String; 4]; 4] = Default::default(); + let mut dead = [[false; 4]; 4]; + let mut all_syms = Vec::new(); + + for group in 0..num_groups.min(4) { + for level in 0..4_u32 { + let syms = km.key_get_syms_by_level(kc, group as u32, level); + let li = level as usize; + if syms.is_empty() { + labels[group][li] = String::new(); + } else { + let display: Vec = + syms.iter().map(|&s| keysym_to_display(s)).collect(); + labels[group][li] = display.join(" "); + dead[group][li] = syms.iter().any(|&s| is_keysym_dead(s)); + all_syms.extend_from_slice(syms); + } + } + } + + let kind = detect_key_kind(&all_syms); + + raw.push(RawKey { name, labels, dead, kind }); + }); + + let is_iso = detect_is_iso(&names.model, has_lsgt); + + let keys: Vec = raw + .into_iter() + .filter(|k| !(k.name == "LSGT" && !is_iso)) + .map(|k| KeyEntry { + pos: layout::resolve_position(&k.name, is_iso), + labels: k.labels, + dead: k.dead, + kind: k.kind, + }) + .collect(); + + Ok(KeyData { + keys, + layouts, + is_iso, + }) +} -- cgit v1.2.3