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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/layout.rs | 171 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 136 +++++++++++++++++++++++++++++++ src/ui.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/x11.rs | 92 +++++++++++++++++++++ 5 files changed, 873 insertions(+) create mode 100644 src/keymap.rs create mode 100644 src/layout.rs create mode 100644 src/main.rs create mode 100644 src/ui.rs create mode 100644 src/x11.rs (limited to 'src') 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, + }) +} diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..4dad3fb --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,171 @@ +#[derive(Debug, Clone, Copy)] +pub struct KeyPos { + pub row: usize, + pub col: f32, + pub width: f32, +} + +fn ansi_row0(name: &str) -> Option { + let keys: &[(&str, f32, f32)] = &[ + ("ESC", 0.0, 1.0), + ("TLDE", 1.0, 1.0), + ("AE01", 2.0, 1.0), + ("AE02", 3.0, 1.0), + ("AE03", 4.0, 1.0), + ("AE04", 5.0, 1.0), + ("AE05", 6.0, 1.0), + ("AE06", 7.0, 1.0), + ("AE07", 8.0, 1.0), + ("AE08", 9.0, 1.0), + ("AE09", 10.0, 1.0), + ("AE10", 11.0, 1.0), + ("AE11", 12.0, 1.0), + ("AE12", 13.0, 1.0), + ("BKSP", 14.0, 1.5), + ]; + keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 0, col: *c, width: *w }) +} + +fn ansi_row1(name: &str) -> Option { + let keys: &[(&str, f32, f32)] = &[ + ("TAB", 0.0, 1.5), + ("AD01", 1.5, 1.0), + ("AD02", 2.5, 1.0), + ("AD03", 3.5, 1.0), + ("AD04", 4.5, 1.0), + ("AD05", 5.5, 1.0), + ("AD06", 6.5, 1.0), + ("AD07", 7.5, 1.0), + ("AD08", 8.5, 1.0), + ("AD09", 9.5, 1.0), + ("AD10", 10.5, 1.0), + ("AD11", 11.5, 1.0), + ("AD12", 12.5, 1.0), + ("BKSL", 13.5, 1.5), + ]; + keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 1, col: *c, width: *w }) +} + +fn ansi_row2(name: &str) -> Option { + let keys: &[(&str, f32, f32)] = &[ + ("CAPS", 0.0, 1.75), + ("AC01", 1.75, 1.0), + ("AC02", 2.75, 1.0), + ("AC03", 3.75, 1.0), + ("AC04", 4.75, 1.0), + ("AC05", 5.75, 1.0), + ("AC06", 6.75, 1.0), + ("AC07", 7.75, 1.0), + ("AC08", 8.75, 1.0), + ("AC09", 9.75, 1.0), + ("AC10", 10.75, 1.0), + ("AC11", 11.75, 1.0), + ("RET", 12.75, 2.25), + ]; + keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 2, col: *c, width: *w }) +} + +fn ansi_row3(name: &str) -> Option { + let keys: &[(&str, f32, f32)] = &[ + ("LFSH", 0.0, 2.25), + ("AB01", 2.25, 1.0), + ("AB02", 3.25, 1.0), + ("AB03", 4.25, 1.0), + ("AB04", 5.25, 1.0), + ("AB05", 6.25, 1.0), + ("AB06", 7.25, 1.0), + ("AB07", 8.25, 1.0), + ("AB08", 9.25, 1.0), + ("AB09", 10.25, 1.0), + ("AB10", 11.25, 1.0), + ("RTSH", 12.25, 2.75), + ]; + keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 3, col: *c, width: *w }) +} + +fn ansi_row4(name: &str) -> Option { + let keys: &[(&str, f32, f32)] = &[ + ("LCTL", 0.0, 1.25), + ("LWIN", 1.25, 1.25), + ("LALT", 2.50, 1.25), + ("SPCE", 3.75, 6.25), + ("RALT", 10.0, 1.25), + ("RWIN", 11.25, 1.25), + ("MENU", 12.50, 1.25), + ("RCTL", 13.75, 1.25), + ]; + keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 4, col: *c, width: *w }) +} + +fn iso_row3(name: &str, _has_lsgt: bool) -> Option { + let keys: &[(&str, f32, f32)] = &[ + ("LFSH", 0.0, 1.25), + ("LSGT", 1.25, 1.0), + ("AB01", 2.25, 1.0), + ("AB02", 3.25, 1.0), + ("AB03", 4.25, 1.0), + ("AB04", 5.25, 1.0), + ("AB05", 6.25, 1.0), + ("AB06", 7.25, 1.0), + ("AB07", 8.25, 1.0), + ("AB08", 9.25, 1.0), + ("AB09", 10.25, 1.0), + ("AB10", 11.25, 1.0), + ("RTSH", 12.25, 2.75), + ]; + keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 3, col: *c, width: *w }) +} + +fn iso_row2(name: &str) -> Option { + let keys: &[(&str, f32, f32)] = &[ + ("CAPS", 0.0, 1.75), + ("AC01", 1.75, 1.0), + ("AC02", 2.75, 1.0), + ("AC03", 3.75, 1.0), + ("AC04", 4.75, 1.0), + ("AC05", 5.75, 1.0), + ("AC06", 6.75, 1.0), + ("AC07", 7.75, 1.0), + ("AC08", 8.75, 1.0), + ("AC09", 9.75, 1.0), + ("AC10", 10.75, 1.0), + ("AC11", 11.75, 1.0), + ("RET", 12.75, 2.25), + ]; + keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 2, col: *c, width: *w }) +} + +pub fn resolve_position(name: &str, is_iso: bool) -> KeyPos { + if is_iso { + if let Some(pos) = iso_row3(name, true).or_else(|| ansi_row0(name)) + .or_else(|| ansi_row1(name)) + .or_else(|| iso_row2(name)) + .or_else(|| ansi_row4(name)) + { + return pos; + } + } else { + if let Some(pos) = ansi_row0(name) + .or_else(|| ansi_row1(name)) + .or_else(|| ansi_row2(name)) + .or_else(|| ansi_row3(name)) + .or_else(|| ansi_row4(name)) + { + return pos; + } + } + + let extra = DEFAULT_POS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + KeyPos { + row: 5, + col: (extra % 16) as f32, + width: 1.0, + } +} + +use std::sync::atomic::AtomicU32; +static DEFAULT_POS: AtomicU32 = AtomicU32::new(0); + +pub fn reset_default_pos() { + DEFAULT_POS.store(0, std::sync::atomic::Ordering::Relaxed); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7a814c1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,136 @@ +mod x11; +mod keymap; +mod layout; +mod ui; + +use eframe::egui; + +struct XkbPreviewApp { + key_data: Option, + x11_conn: Option, + selected_level: usize, + active_level: usize, + active_group: usize, + error: Option, +} + +impl Default for XkbPreviewApp { + fn default() -> Self { + Self { + key_data: None, + x11_conn: None, + selected_level: 0, + active_level: 0, + active_group: 0, + error: None, + } + } +} + +impl eframe::App for XkbPreviewApp { + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + if self.key_data.is_none() && self.error.is_none() { + match load_keyboard() { + Ok(data) => self.key_data = Some(data), + Err(e) => self.error = Some(e), + } + } + + if self.x11_conn.is_none() { + self.x11_conn = x11::X11Connection::open(); + } + + if let Some(ref conn) = self.x11_conn { + let mask = conn.modifier_mask(); + self.active_level = keymap::level_from_x11_modmask(mask); + self.active_group = conn.active_group(); + } + + ui::render( + ui, + &self.key_data, + &mut self.selected_level, + self.active_level, + self.active_group, + &self.error, + ); + } +} + +fn load_keyboard() -> Result { + let names = x11::get_xkb_rule_names().unwrap_or_default(); + keymap::compile_keymap(&names) +} + +fn main() -> eframe::Result { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([980.0, 560.0]), + ..Default::default() + }; + eframe::run_native( + "xkbpreview", + options, + Box::new(|cc| { + setup_fonts(&cc.egui_ctx); + Ok(Box::new(XkbPreviewApp::default())) + }), + ) +} + +fn setup_fonts(ctx: &egui::Context) { + let mut fonts = egui::FontDefinitions::default(); + + let font_candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Regular.ttf", + "/usr/share/fonts/dejavu/DejaVuSans-Regular.ttf", + "/usr/share/fonts/TTF/DejaVuSans.ttf", + "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", + "/usr/share/fonts/noto/NotoSans-Regular.ttf", + "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf", + ]; + + let mut loaded = false; + for path in &font_candidates { + if let Ok(data) = std::fs::read(path) { + fonts + .font_data + .insert("fallback_font".into(), egui::FontData::from_owned(data).into()); + fonts + .families + .entry(egui::FontFamily::Proportional) + .or_default() + .push("fallback_font".into()); + loaded = true; + break; + } + } + + if !loaded { + if let Ok(home) = std::env::var("HOME") { + let user_candidates = [ + format!("{}/.fonts/DejaVuSans.ttf", home), + format!("{}/.local/share/fonts/DejaVuSans.ttf", home), + format!("{}/.fonts/NotoSans-Regular.ttf", home), + format!("{}/.local/share/fonts/NotoSans-Regular.ttf", home), + ]; + for path in &user_candidates { + if let Ok(data) = std::fs::read(path) { + fonts + .font_data + .insert("fallback_font".into(), egui::FontData::from_owned(data).into()); + fonts + .families + .entry(egui::FontFamily::Proportional) + .or_default() + .push("fallback_font".into()); + break; + } + } + } + } + + ctx.set_fonts(fonts); +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..b9abeff --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,221 @@ +use crate::keymap; +use eframe::egui::{self, Color32, CornerRadius, Stroke, Vec2}; + +const LEVEL_NAMES: &[&str; 4] = &[ + "Level 1 (base)", + "Level 2 (Shift)", + "Level 3 (AltGr)", + "Level 4 (Shift+AltGr)", +]; + +const KEY_BG: Color32 = Color32::from_rgb(0x33, 0x33, 0x33); +const KEY_BG_MOD_SHIFT: Color32 = Color32::from_rgb(0x2a, 0x5a, 0x2a); +const KEY_BG_MOD_L3: Color32 = Color32::from_rgb(0x4a, 0x2a, 0x5a); +const KEY_BG_MOD_OTHER: Color32 = Color32::from_rgb(0x3a, 0x3a, 0x3a); +const KEY_TEXT: Color32 = Color32::from_rgb(0xcc, 0xcc, 0xcc); +const KEY_TEXT_DIM: Color32 = Color32::from_rgb(0x55, 0x55, 0x55); +const KEY_TEXT_ACTIVE: Color32 = Color32::from_rgb(0xff, 0xff, 0xff); +const KEY_TEXT_MOD: Color32 = Color32::from_rgb(0xff, 0xff, 0xcc); +const KEY_TEXT_DEAD: Color32 = Color32::from_rgb(0xff, 0xaa, 0x44); + +const KEY_SPACING: f32 = 4.0; +const KEY_UNIT: f32 = 52.0; +const KEY_HEIGHT: f32 = 48.0; + +fn key_bg(kind: &keymap::KeyKind) -> Color32 { + match kind { + keymap::KeyKind::Shift => KEY_BG_MOD_SHIFT, + keymap::KeyKind::Level3 => KEY_BG_MOD_L3, + keymap::KeyKind::Lock | keymap::KeyKind::Control | keymap::KeyKind::Alt | keymap::KeyKind::Super => { + KEY_BG_MOD_OTHER + } + keymap::KeyKind::Normal => KEY_BG, + } +} + +fn text_color_for_level(level: usize, active_level: usize, kind: &keymap::KeyKind) -> Color32 { + if level == active_level { + match kind { + keymap::KeyKind::Shift | keymap::KeyKind::Level3 => KEY_TEXT_MOD, + _ => KEY_TEXT_ACTIVE, + } + } else { + KEY_TEXT_DIM + } +} + +pub fn render( + ui: &mut egui::Ui, + key_data: &Option, + selected_level: &mut usize, + active_level: usize, + active_group: usize, + error: &Option, +) { + egui::Panel::top("toolbar").show_inside(ui, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("xkbpreview").strong()); + + if let Some(data) = key_data { + let g = active_group.min(data.layouts.len().saturating_sub(1)); + ui.label(format!(" layout: {}", data.layouts[g])); + if data.is_iso { + ui.label(" [ISO]"); + } else { + ui.label(" [ANSI]"); + } + } + + ui.label(format!(" Lv:{}", active_level)); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label("Highlight:"); + for (i, name) in LEVEL_NAMES.iter().enumerate() { + let mark = if i == active_level { "[x]" } else { "[ ]" }; + if ui.selectable_label(*selected_level == i, format!("{} {}", mark, name)).clicked() { + *selected_level = i; + } + } + }); + }); + }); + + if let Some(err) = error { + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.colored_label(Color32::RED, format!("Error: {}", err)); + ui.label("Make sure you're running under X11 and the XKB configuration is available."); + }); + return; + } + + let data = match key_data { + Some(d) => d, + None => { + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.label("Loading keyboard layout..."); + }); + return; + } + }; + + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.vertical_centered(|ui| { + let rows = group_by_row(&data.keys); + let max_row = rows.iter().map(|(r, _)| *r).max().unwrap_or(0); + let total_w = 15.5 * (KEY_UNIT + KEY_SPACING); + + let (rect, _) = ui.allocate_exact_size( + Vec2::new(total_w, (max_row + 1) as f32 * (KEY_HEIGHT + KEY_SPACING)), + egui::Sense::hover(), + ); + + let painter = ui.painter_at(rect); + let origin = rect.min; + + for (row_idx, entries) in &rows { + for entry in entries { + let p = &entry.pos; + let x = p.col * (KEY_UNIT + KEY_SPACING); + let y = *row_idx as f32 * (KEY_HEIGHT + KEY_SPACING); + let w = p.width * KEY_UNIT + (p.width - 1.0) * KEY_SPACING; + + let key_rect = egui::Rect::from_min_size( + origin + Vec2::new(x, y), + Vec2::new(w, KEY_HEIGHT), + ); + + let bg = key_bg(&entry.kind); + painter.rect_filled(key_rect, CornerRadius::same(3), bg); + painter.rect_stroke(key_rect, CornerRadius::same(3), Stroke::new(1.0, Color32::from_rgb(0x55, 0x55, 0x55)), egui::StrokeKind::Middle); + + let inset = 3.0; + let cw = key_rect.width() / 2.0; + + let centers: [egui::Pos2; 4] = [ + key_rect.center(), + egui::Pos2::new(key_rect.left() + cw / 2.0, key_rect.top() + inset + 4.0), + egui::Pos2::new(key_rect.right() - cw / 2.0, key_rect.top() + inset + 4.0), + egui::Pos2::new(key_rect.left() + cw / 2.0, key_rect.bottom() - inset - 4.0), + ]; + + let level_order = [0usize, 1, 2, 3]; + let font_sizes = [15.0, 10.0, 10.0, 9.0]; + let aligns = [ + egui::Align2::CENTER_CENTER, + egui::Align2::LEFT_TOP, + egui::Align2::RIGHT_TOP, + egui::Align2::LEFT_BOTTOM, + ]; + + let g = active_group.min(3); + for &li in &level_order { + let label = &entry.labels[g][li]; + if label.is_empty() { + continue; + } + let color = if entry.dead[g][li] { + KEY_TEXT_DEAD + } else if li == 0 { + if 0 == active_level { + text_color_for_level(li, active_level, &entry.kind) + } else { + KEY_TEXT + } + } else { + text_color_for_level(li, active_level, &entry.kind) + }; + let font = if label.len() > 2 { + egui::FontId::proportional(font_sizes[li] - 1.0) + } else { + egui::FontId::proportional(font_sizes[li]) + }; + painter.text(centers[li], aligns[li], label, font, color); + } + } + } + + let legend_y = (max_row + 1) as f32 * (KEY_HEIGHT + KEY_SPACING) + 16.0; + let legend_items = [ + (KEY_BG_MOD_SHIFT, "Shift (triggers L2)"), + (KEY_BG_MOD_L3, "AltGr (triggers L3)"), + (KEY_BG_MOD_OTHER, "Other modifiers"), + (KEY_TEXT_DEAD, "Dead key accent"), + ]; + for (i, (color, text)) in legend_items.iter().enumerate() { + let ly = legend_y + i as f32 * 20.0; + let r = egui::Rect::from_min_size( + origin + Vec2::new(0.0, ly), + Vec2::new(14.0, 14.0), + ); + painter.rect_filled(r, CornerRadius::same(2), *color); + painter.text( + egui::Pos2::new(r.right() + 6.0, r.center().y), + egui::Align2::LEFT_CENTER, + *text, + egui::FontId::proportional(12.0), + KEY_TEXT, + ); + } + }); + }); +} + +fn group_by_row<'a>(keys: &'a [keymap::KeyEntry]) -> Vec<(usize, Vec<&'a keymap::KeyEntry>)> { + let mut rows: Vec<(usize, Vec<&'a keymap::KeyEntry>)> = Vec::new(); + for key in keys { + let row = key.pos.row; + if row > 4 { + continue; + } + if let Some(entry) = rows.iter_mut().find(|(r, _)| *r == row) { + entry.1.push(key); + } else { + rows.push((row, vec![key])); + } + } + rows.sort_by_key(|(r, _)| *r); + for (_, entries) in &mut rows { + entries.sort_by(|a, b| a.pos.col.partial_cmp(&b.pos.col).unwrap()); + } + rows +} diff --git a/src/x11.rs b/src/x11.rs new file mode 100644 index 0000000..27052b9 --- /dev/null +++ b/src/x11.rs @@ -0,0 +1,92 @@ +use x11rb::connection::{Connection, RequestConnection}; +use x11rb::protocol::xkb::{self, ConnectionExt as XkbConnectionExt}; +use x11rb::protocol::xproto::{AtomEnum, ConnectionExt}; +use x11rb::rust_connection::RustConnection; + +#[derive(Debug, Default)] +pub struct RuleNames { + pub rules: String, + pub model: String, + pub layout: String, + pub variant: String, + pub options: String, +} + +pub struct X11Connection { + pub conn: RustConnection, + pub root: x11rb::protocol::xproto::Window, + xkb_ok: bool, +} + +impl X11Connection { + pub fn open() -> Option { + let (conn, screen_num) = x11rb::connect(None).ok()?; + let root = conn.setup().roots[screen_num].root; + let xkb_ok = conn + .extension_information(xkb::X11_EXTENSION_NAME) + .ok() + .flatten() + .is_some(); + if xkb_ok { + let _ = conn.xkb_use_extension(1, 0).ok(); + } + Some(Self { conn, root, xkb_ok }) + } + + pub fn modifier_mask(&self) -> u16 { + self.conn + .query_pointer(self.root) + .ok() + .and_then(|c| c.reply().ok()) + .map(|r| r.mask.into()) + .unwrap_or(0) + } + + pub fn active_group(&self) -> usize { + if !self.xkb_ok { + return 0; + } + self.conn + .xkb_get_state(0x0100) + .ok() + .and_then(|c| c.reply().ok()) + .map(|r| u8::from(r.group) as usize) + .unwrap_or(0) + } +} + +pub fn get_xkb_rule_names() -> Option { + let (conn, screen_num) = x11rb::connect(None).ok()?; + let screen = &conn.setup().roots[screen_num]; + + let atom = conn + .intern_atom(false, b"_XKB_RULES_NAMES") + .ok()? + .reply() + .ok()? + .atom; + + let prop = conn + .get_property(false, screen.root, atom, AtomEnum::ANY, 0, 1024) + .ok()? + .reply() + .ok()?; + + if prop.value.is_empty() || prop.format == 0 { + return None; + } + + let parts: Vec<&str> = prop + .value + .split(|&b| b == 0) + .filter_map(|s| std::str::from_utf8(s).ok()) + .collect(); + + Some(RuleNames { + rules: parts.first().map(|s| s.to_string()).unwrap_or_default(), + model: parts.get(1).map(|s| s.to_string()).unwrap_or_default(), + layout: parts.get(2).map(|s| s.to_string()).unwrap_or_default(), + variant: parts.get(3).map(|s| s.to_string()).unwrap_or_default(), + options: parts.get(4).map(|s| s.to_string()).unwrap_or_default(), + }) +} -- cgit v1.2.3