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, }) }