summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/keymap.rs253
-rw-r--r--src/layout.rs171
-rw-r--r--src/main.rs136
-rw-r--r--src/ui.rs221
-rw-r--r--src/x11.rs92
5 files changed, 873 insertions, 0 deletions
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 @@
1use crate::layout;
2use crate::x11;
3
4use xkbcommon::xkb;
5
6#[derive(Debug, Clone)]
7pub enum KeyKind {
8 Normal,
9 Shift,
10 Level3,
11 Lock,
12 Control,
13 Alt,
14 Super,
15}
16
17#[derive(Debug, Clone)]
18pub struct KeyEntry {
19 pub labels: [[String; 4]; 4],
20 pub kind: KeyKind,
21 pub dead: [[bool; 4]; 4],
22 pub pos: layout::KeyPos,
23}
24
25#[derive(Debug, Clone)]
26pub struct KeyData {
27 pub keys: Vec<KeyEntry>,
28 pub layouts: Vec<String>,
29 pub is_iso: bool,
30}
31
32fn dead_display(name: &str) -> &'static str {
33 match name.to_lowercase().as_str() {
34 "dead_grave" => "`",
35 "dead_acute" => "'",
36 "dead_circumflex" => "^",
37 "dead_tilde" => "~",
38 "dead_macron" => "-",
39 "dead_breve" => "u",
40 "dead_abovedot" => ".",
41 "dead_diaeresis" => "\"",
42 "dead_caron" => "v",
43 "dead_cedilla" => ",",
44 "dead_ogonek" => "~",
45 "dead_doubleacute" => "\"",
46 "dead_stroke" => "/",
47 "dead_hook" => "?",
48 "dead_horn" => "?",
49 "dead_psili" => "'",
50 "dead_dasia" => "'",
51 "dead_iota" => "i",
52 "dead_voiced_sound" => "\"",
53 "dead_semivoiced_sound" => "o",
54 _ => "?",
55 }
56}
57
58fn keysym_to_display(sym: xkb::Keysym) -> String {
59 let utf8 = xkb::keysym_to_utf8(sym);
60 let trimmed = utf8.trim().to_string();
61 if !trimmed.is_empty() {
62 let name = xkb::keysym_get_name(sym);
63 if name.to_lowercase().starts_with("dead_") {
64 return dead_display(&name).to_string();
65 }
66 return trimmed;
67 }
68 let name = xkb::keysym_get_name(sym);
69 if name.to_lowercase().starts_with("dead_") {
70 return dead_display(&name).to_string();
71 }
72 match sym {
73 xkb::Keysym::Shift_L | xkb::Keysym::Shift_R => "Shift".into(),
74 xkb::Keysym::ISO_Level3_Shift => "AltGr".into(),
75 xkb::Keysym::Control_L | xkb::Keysym::Control_R => "Ctrl".into(),
76 xkb::Keysym::Alt_L | xkb::Keysym::Alt_R => "Alt".into(),
77 xkb::Keysym::Super_L | xkb::Keysym::Super_R => "Super".into(),
78 xkb::Keysym::Menu => "Menu".into(),
79 xkb::Keysym::Caps_Lock => "Caps".into(),
80 xkb::Keysym::Tab => "Tab".into(),
81 xkb::Keysym::Return => "Enter".into(),
82 xkb::Keysym::Escape => "Esc".into(),
83 xkb::Keysym::BackSpace => "Bksp".into(),
84 xkb::Keysym::space => " ".into(),
85 xkb::Keysym::Delete => "Del".into(),
86 xkb::Keysym::Insert => "Ins".into(),
87 xkb::Keysym::Home => "Home".into(),
88 xkb::Keysym::End => "End".into(),
89 xkb::Keysym::Page_Up => "PgUp".into(),
90 xkb::Keysym::Page_Down => "PgDn".into(),
91 xkb::Keysym::Up => "^".into(),
92 xkb::Keysym::Down => "v".into(),
93 xkb::Keysym::Left => "<".into(),
94 xkb::Keysym::Right => ">".into(),
95 xkb::Keysym::KP_Enter => "Enter".into(),
96 xkb::Keysym::Num_Lock => "Num".into(),
97 xkb::Keysym::Scroll_Lock => "Scrl".into(),
98 xkb::Keysym::Pause => "Pause".into(),
99 xkb::Keysym::Print => "PrtSc".into(),
100 _ => name,
101 }
102}
103
104fn detect_key_kind(syms: &[xkb::Keysym]) -> KeyKind {
105 for &sym in syms {
106 match sym {
107 xkb::Keysym::Shift_L | xkb::Keysym::Shift_R => return KeyKind::Shift,
108 xkb::Keysym::ISO_Level3_Shift => return KeyKind::Level3,
109 xkb::Keysym::Caps_Lock => return KeyKind::Lock,
110 xkb::Keysym::Control_L | xkb::Keysym::Control_R => return KeyKind::Control,
111 xkb::Keysym::Alt_L | xkb::Keysym::Alt_R => return KeyKind::Alt,
112 xkb::Keysym::Super_L | xkb::Keysym::Super_R => return KeyKind::Super,
113 _ => {}
114 }
115 }
116 KeyKind::Normal
117}
118
119fn is_keysym_dead(sym: xkb::Keysym) -> bool {
120 xkb::keysym_get_name(sym).to_lowercase().starts_with("dead_")
121}
122
123pub fn level_from_x11_modmask(mask: u16) -> usize {
124 let shift = mask & 0x01 != 0;
125 let altgr = mask & 0x80 != 0;
126 match (shift, altgr) {
127 (false, false) => 0,
128 (true, false) => 1,
129 (false, true) => 2,
130 (true, true) => 3,
131 }
132}
133
134struct RawKey {
135 name: String,
136 labels: [[String; 4]; 4],
137 dead: [[bool; 4]; 4],
138 kind: KeyKind,
139}
140
141fn detect_is_iso(model: &str, keymap_has_lsgt: bool) -> bool {
142 if model.contains("105") || model.contains("102") || model.contains("98") {
143 return true;
144 }
145 if model.contains("101") || model.contains("104") || model.contains("106") {
146 return false;
147 }
148 keymap_has_lsgt
149}
150
151pub fn compile_keymap(names: &x11::RuleNames) -> Result<KeyData, String> {
152 let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
153
154 let options = Some(names.options.clone());
155
156 let keymap = xkb::Keymap::new_from_names(
157 &context,
158 &names.rules,
159 &names.model,
160 &names.layout,
161 &names.variant,
162 options,
163 xkb::KEYMAP_COMPILE_NO_FLAGS,
164 )
165 .ok_or_else(|| "failed to compile keymap".to_string())?;
166
167 let num_groups = keymap.num_layouts() as usize;
168
169 let layouts: Vec<String> = if names.layout.is_empty() || num_groups <= 1 {
170 vec![if names.layout.is_empty() {
171 "default".into()
172 } else {
173 names.layout.clone()
174 }]
175 } else {
176 let layout_parts: Vec<&str> = names.layout.split(',').collect();
177 let variant_parts: Vec<&str> = if names.variant.is_empty() {
178 vec![]
179 } else {
180 names.variant.split(',').collect()
181 };
182 layout_parts
183 .iter()
184 .enumerate()
185 .map(|(i, l)| {
186 let v = variant_parts.get(i).filter(|v| !v.is_empty());
187 match v {
188 Some(v) => format!("{}({})", l, v),
189 None => l.to_string(),
190 }
191 })
192 .collect()
193 };
194
195 layout::reset_default_pos();
196
197 let mut raw = Vec::new();
198 let mut has_lsgt = false;
199
200 keymap.key_for_each(|km, kc| {
201 let name = match km.key_get_name(kc) {
202 Some(n) => n.to_string(),
203 None => return,
204 };
205
206 if name == "LSGT" {
207 has_lsgt = true;
208 }
209
210 let mut labels: [[String; 4]; 4] = Default::default();
211 let mut dead = [[false; 4]; 4];
212 let mut all_syms = Vec::new();
213
214 for group in 0..num_groups.min(4) {
215 for level in 0..4_u32 {
216 let syms = km.key_get_syms_by_level(kc, group as u32, level);
217 let li = level as usize;
218 if syms.is_empty() {
219 labels[group][li] = String::new();
220 } else {
221 let display: Vec<String> =
222 syms.iter().map(|&s| keysym_to_display(s)).collect();
223 labels[group][li] = display.join(" ");
224 dead[group][li] = syms.iter().any(|&s| is_keysym_dead(s));
225 all_syms.extend_from_slice(syms);
226 }
227 }
228 }
229
230 let kind = detect_key_kind(&all_syms);
231
232 raw.push(RawKey { name, labels, dead, kind });
233 });
234
235 let is_iso = detect_is_iso(&names.model, has_lsgt);
236
237 let keys: Vec<KeyEntry> = raw
238 .into_iter()
239 .filter(|k| !(k.name == "LSGT" && !is_iso))
240 .map(|k| KeyEntry {
241 pos: layout::resolve_position(&k.name, is_iso),
242 labels: k.labels,
243 dead: k.dead,
244 kind: k.kind,
245 })
246 .collect();
247
248 Ok(KeyData {
249 keys,
250 layouts,
251 is_iso,
252 })
253}
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 @@
1#[derive(Debug, Clone, Copy)]
2pub struct KeyPos {
3 pub row: usize,
4 pub col: f32,
5 pub width: f32,
6}
7
8fn ansi_row0(name: &str) -> Option<KeyPos> {
9 let keys: &[(&str, f32, f32)] = &[
10 ("ESC", 0.0, 1.0),
11 ("TLDE", 1.0, 1.0),
12 ("AE01", 2.0, 1.0),
13 ("AE02", 3.0, 1.0),
14 ("AE03", 4.0, 1.0),
15 ("AE04", 5.0, 1.0),
16 ("AE05", 6.0, 1.0),
17 ("AE06", 7.0, 1.0),
18 ("AE07", 8.0, 1.0),
19 ("AE08", 9.0, 1.0),
20 ("AE09", 10.0, 1.0),
21 ("AE10", 11.0, 1.0),
22 ("AE11", 12.0, 1.0),
23 ("AE12", 13.0, 1.0),
24 ("BKSP", 14.0, 1.5),
25 ];
26 keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 0, col: *c, width: *w })
27}
28
29fn ansi_row1(name: &str) -> Option<KeyPos> {
30 let keys: &[(&str, f32, f32)] = &[
31 ("TAB", 0.0, 1.5),
32 ("AD01", 1.5, 1.0),
33 ("AD02", 2.5, 1.0),
34 ("AD03", 3.5, 1.0),
35 ("AD04", 4.5, 1.0),
36 ("AD05", 5.5, 1.0),
37 ("AD06", 6.5, 1.0),
38 ("AD07", 7.5, 1.0),
39 ("AD08", 8.5, 1.0),
40 ("AD09", 9.5, 1.0),
41 ("AD10", 10.5, 1.0),
42 ("AD11", 11.5, 1.0),
43 ("AD12", 12.5, 1.0),
44 ("BKSL", 13.5, 1.5),
45 ];
46 keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 1, col: *c, width: *w })
47}
48
49fn ansi_row2(name: &str) -> Option<KeyPos> {
50 let keys: &[(&str, f32, f32)] = &[
51 ("CAPS", 0.0, 1.75),
52 ("AC01", 1.75, 1.0),
53 ("AC02", 2.75, 1.0),
54 ("AC03", 3.75, 1.0),
55 ("AC04", 4.75, 1.0),
56 ("AC05", 5.75, 1.0),
57 ("AC06", 6.75, 1.0),
58 ("AC07", 7.75, 1.0),
59 ("AC08", 8.75, 1.0),
60 ("AC09", 9.75, 1.0),
61 ("AC10", 10.75, 1.0),
62 ("AC11", 11.75, 1.0),
63 ("RET", 12.75, 2.25),
64 ];
65 keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 2, col: *c, width: *w })
66}
67
68fn ansi_row3(name: &str) -> Option<KeyPos> {
69 let keys: &[(&str, f32, f32)] = &[
70 ("LFSH", 0.0, 2.25),
71 ("AB01", 2.25, 1.0),
72 ("AB02", 3.25, 1.0),
73 ("AB03", 4.25, 1.0),
74 ("AB04", 5.25, 1.0),
75 ("AB05", 6.25, 1.0),
76 ("AB06", 7.25, 1.0),
77 ("AB07", 8.25, 1.0),
78 ("AB08", 9.25, 1.0),
79 ("AB09", 10.25, 1.0),
80 ("AB10", 11.25, 1.0),
81 ("RTSH", 12.25, 2.75),
82 ];
83 keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 3, col: *c, width: *w })
84}
85
86fn ansi_row4(name: &str) -> Option<KeyPos> {
87 let keys: &[(&str, f32, f32)] = &[
88 ("LCTL", 0.0, 1.25),
89 ("LWIN", 1.25, 1.25),
90 ("LALT", 2.50, 1.25),
91 ("SPCE", 3.75, 6.25),
92 ("RALT", 10.0, 1.25),
93 ("RWIN", 11.25, 1.25),
94 ("MENU", 12.50, 1.25),
95 ("RCTL", 13.75, 1.25),
96 ];
97 keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 4, col: *c, width: *w })
98}
99
100fn iso_row3(name: &str, _has_lsgt: bool) -> Option<KeyPos> {
101 let keys: &[(&str, f32, f32)] = &[
102 ("LFSH", 0.0, 1.25),
103 ("LSGT", 1.25, 1.0),
104 ("AB01", 2.25, 1.0),
105 ("AB02", 3.25, 1.0),
106 ("AB03", 4.25, 1.0),
107 ("AB04", 5.25, 1.0),
108 ("AB05", 6.25, 1.0),
109 ("AB06", 7.25, 1.0),
110 ("AB07", 8.25, 1.0),
111 ("AB08", 9.25, 1.0),
112 ("AB09", 10.25, 1.0),
113 ("AB10", 11.25, 1.0),
114 ("RTSH", 12.25, 2.75),
115 ];
116 keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 3, col: *c, width: *w })
117}
118
119fn iso_row2(name: &str) -> Option<KeyPos> {
120 let keys: &[(&str, f32, f32)] = &[
121 ("CAPS", 0.0, 1.75),
122 ("AC01", 1.75, 1.0),
123 ("AC02", 2.75, 1.0),
124 ("AC03", 3.75, 1.0),
125 ("AC04", 4.75, 1.0),
126 ("AC05", 5.75, 1.0),
127 ("AC06", 6.75, 1.0),
128 ("AC07", 7.75, 1.0),
129 ("AC08", 8.75, 1.0),
130 ("AC09", 9.75, 1.0),
131 ("AC10", 10.75, 1.0),
132 ("AC11", 11.75, 1.0),
133 ("RET", 12.75, 2.25),
134 ];
135 keys.iter().find(|(n, _, _)| *n == name).map(|(_, c, w)| KeyPos { row: 2, col: *c, width: *w })
136}
137
138pub fn resolve_position(name: &str, is_iso: bool) -> KeyPos {
139 if is_iso {
140 if let Some(pos) = iso_row3(name, true).or_else(|| ansi_row0(name))
141 .or_else(|| ansi_row1(name))
142 .or_else(|| iso_row2(name))
143 .or_else(|| ansi_row4(name))
144 {
145 return pos;
146 }
147 } else {
148 if let Some(pos) = ansi_row0(name)
149 .or_else(|| ansi_row1(name))
150 .or_else(|| ansi_row2(name))
151 .or_else(|| ansi_row3(name))
152 .or_else(|| ansi_row4(name))
153 {
154 return pos;
155 }
156 }
157
158 let extra = DEFAULT_POS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
159 KeyPos {
160 row: 5,
161 col: (extra % 16) as f32,
162 width: 1.0,
163 }
164}
165
166use std::sync::atomic::AtomicU32;
167static DEFAULT_POS: AtomicU32 = AtomicU32::new(0);
168
169pub fn reset_default_pos() {
170 DEFAULT_POS.store(0, std::sync::atomic::Ordering::Relaxed);
171}
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 @@
1mod x11;
2mod keymap;
3mod layout;
4mod ui;
5
6use eframe::egui;
7
8struct XkbPreviewApp {
9 key_data: Option<keymap::KeyData>,
10 x11_conn: Option<x11::X11Connection>,
11 selected_level: usize,
12 active_level: usize,
13 active_group: usize,
14 error: Option<String>,
15}
16
17impl Default for XkbPreviewApp {
18 fn default() -> Self {
19 Self {
20 key_data: None,
21 x11_conn: None,
22 selected_level: 0,
23 active_level: 0,
24 active_group: 0,
25 error: None,
26 }
27 }
28}
29
30impl eframe::App for XkbPreviewApp {
31 fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
32 if self.key_data.is_none() && self.error.is_none() {
33 match load_keyboard() {
34 Ok(data) => self.key_data = Some(data),
35 Err(e) => self.error = Some(e),
36 }
37 }
38
39 if self.x11_conn.is_none() {
40 self.x11_conn = x11::X11Connection::open();
41 }
42
43 if let Some(ref conn) = self.x11_conn {
44 let mask = conn.modifier_mask();
45 self.active_level = keymap::level_from_x11_modmask(mask);
46 self.active_group = conn.active_group();
47 }
48
49 ui::render(
50 ui,
51 &self.key_data,
52 &mut self.selected_level,
53 self.active_level,
54 self.active_group,
55 &self.error,
56 );
57 }
58}
59
60fn load_keyboard() -> Result<keymap::KeyData, String> {
61 let names = x11::get_xkb_rule_names().unwrap_or_default();
62 keymap::compile_keymap(&names)
63}
64
65fn main() -> eframe::Result {
66 let options = eframe::NativeOptions {
67 viewport: egui::ViewportBuilder::default()
68 .with_inner_size([980.0, 560.0]),
69 ..Default::default()
70 };
71 eframe::run_native(
72 "xkbpreview",
73 options,
74 Box::new(|cc| {
75 setup_fonts(&cc.egui_ctx);
76 Ok(Box::new(XkbPreviewApp::default()))
77 }),
78 )
79}
80
81fn setup_fonts(ctx: &egui::Context) {
82 let mut fonts = egui::FontDefinitions::default();
83
84 let font_candidates = [
85 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
86 "/usr/share/fonts/dejavu/DejaVuSans.ttf",
87 "/usr/share/fonts/truetype/dejavu/DejaVuSans-Regular.ttf",
88 "/usr/share/fonts/dejavu/DejaVuSans-Regular.ttf",
89 "/usr/share/fonts/TTF/DejaVuSans.ttf",
90 "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
91 "/usr/share/fonts/noto/NotoSans-Regular.ttf",
92 "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
93 ];
94
95 let mut loaded = false;
96 for path in &font_candidates {
97 if let Ok(data) = std::fs::read(path) {
98 fonts
99 .font_data
100 .insert("fallback_font".into(), egui::FontData::from_owned(data).into());
101 fonts
102 .families
103 .entry(egui::FontFamily::Proportional)
104 .or_default()
105 .push("fallback_font".into());
106 loaded = true;
107 break;
108 }
109 }
110
111 if !loaded {
112 if let Ok(home) = std::env::var("HOME") {
113 let user_candidates = [
114 format!("{}/.fonts/DejaVuSans.ttf", home),
115 format!("{}/.local/share/fonts/DejaVuSans.ttf", home),
116 format!("{}/.fonts/NotoSans-Regular.ttf", home),
117 format!("{}/.local/share/fonts/NotoSans-Regular.ttf", home),
118 ];
119 for path in &user_candidates {
120 if let Ok(data) = std::fs::read(path) {
121 fonts
122 .font_data
123 .insert("fallback_font".into(), egui::FontData::from_owned(data).into());
124 fonts
125 .families
126 .entry(egui::FontFamily::Proportional)
127 .or_default()
128 .push("fallback_font".into());
129 break;
130 }
131 }
132 }
133 }
134
135 ctx.set_fonts(fonts);
136}
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 @@
1use crate::keymap;
2use eframe::egui::{self, Color32, CornerRadius, Stroke, Vec2};
3
4const LEVEL_NAMES: &[&str; 4] = &[
5 "Level 1 (base)",
6 "Level 2 (Shift)",
7 "Level 3 (AltGr)",
8 "Level 4 (Shift+AltGr)",
9];
10
11const KEY_BG: Color32 = Color32::from_rgb(0x33, 0x33, 0x33);
12const KEY_BG_MOD_SHIFT: Color32 = Color32::from_rgb(0x2a, 0x5a, 0x2a);
13const KEY_BG_MOD_L3: Color32 = Color32::from_rgb(0x4a, 0x2a, 0x5a);
14const KEY_BG_MOD_OTHER: Color32 = Color32::from_rgb(0x3a, 0x3a, 0x3a);
15const KEY_TEXT: Color32 = Color32::from_rgb(0xcc, 0xcc, 0xcc);
16const KEY_TEXT_DIM: Color32 = Color32::from_rgb(0x55, 0x55, 0x55);
17const KEY_TEXT_ACTIVE: Color32 = Color32::from_rgb(0xff, 0xff, 0xff);
18const KEY_TEXT_MOD: Color32 = Color32::from_rgb(0xff, 0xff, 0xcc);
19const KEY_TEXT_DEAD: Color32 = Color32::from_rgb(0xff, 0xaa, 0x44);
20
21const KEY_SPACING: f32 = 4.0;
22const KEY_UNIT: f32 = 52.0;
23const KEY_HEIGHT: f32 = 48.0;
24
25fn key_bg(kind: &keymap::KeyKind) -> Color32 {
26 match kind {
27 keymap::KeyKind::Shift => KEY_BG_MOD_SHIFT,
28 keymap::KeyKind::Level3 => KEY_BG_MOD_L3,
29 keymap::KeyKind::Lock | keymap::KeyKind::Control | keymap::KeyKind::Alt | keymap::KeyKind::Super => {
30 KEY_BG_MOD_OTHER
31 }
32 keymap::KeyKind::Normal => KEY_BG,
33 }
34}
35
36fn text_color_for_level(level: usize, active_level: usize, kind: &keymap::KeyKind) -> Color32 {
37 if level == active_level {
38 match kind {
39 keymap::KeyKind::Shift | keymap::KeyKind::Level3 => KEY_TEXT_MOD,
40 _ => KEY_TEXT_ACTIVE,
41 }
42 } else {
43 KEY_TEXT_DIM
44 }
45}
46
47pub fn render(
48 ui: &mut egui::Ui,
49 key_data: &Option<keymap::KeyData>,
50 selected_level: &mut usize,
51 active_level: usize,
52 active_group: usize,
53 error: &Option<String>,
54) {
55 egui::Panel::top("toolbar").show_inside(ui, |ui| {
56 ui.horizontal(|ui| {
57 ui.label(egui::RichText::new("xkbpreview").strong());
58
59 if let Some(data) = key_data {
60 let g = active_group.min(data.layouts.len().saturating_sub(1));
61 ui.label(format!(" layout: {}", data.layouts[g]));
62 if data.is_iso {
63 ui.label(" [ISO]");
64 } else {
65 ui.label(" [ANSI]");
66 }
67 }
68
69 ui.label(format!(" Lv:{}", active_level));
70
71 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
72 ui.label("Highlight:");
73 for (i, name) in LEVEL_NAMES.iter().enumerate() {
74 let mark = if i == active_level { "[x]" } else { "[ ]" };
75 if ui.selectable_label(*selected_level == i, format!("{} {}", mark, name)).clicked() {
76 *selected_level = i;
77 }
78 }
79 });
80 });
81 });
82
83 if let Some(err) = error {
84 egui::CentralPanel::default().show_inside(ui, |ui| {
85 ui.colored_label(Color32::RED, format!("Error: {}", err));
86 ui.label("Make sure you're running under X11 and the XKB configuration is available.");
87 });
88 return;
89 }
90
91 let data = match key_data {
92 Some(d) => d,
93 None => {
94 egui::CentralPanel::default().show_inside(ui, |ui| {
95 ui.label("Loading keyboard layout...");
96 });
97 return;
98 }
99 };
100
101 egui::CentralPanel::default().show_inside(ui, |ui| {
102 ui.vertical_centered(|ui| {
103 let rows = group_by_row(&data.keys);
104 let max_row = rows.iter().map(|(r, _)| *r).max().unwrap_or(0);
105 let total_w = 15.5 * (KEY_UNIT + KEY_SPACING);
106
107 let (rect, _) = ui.allocate_exact_size(
108 Vec2::new(total_w, (max_row + 1) as f32 * (KEY_HEIGHT + KEY_SPACING)),
109 egui::Sense::hover(),
110 );
111
112 let painter = ui.painter_at(rect);
113 let origin = rect.min;
114
115 for (row_idx, entries) in &rows {
116 for entry in entries {
117 let p = &entry.pos;
118 let x = p.col * (KEY_UNIT + KEY_SPACING);
119 let y = *row_idx as f32 * (KEY_HEIGHT + KEY_SPACING);
120 let w = p.width * KEY_UNIT + (p.width - 1.0) * KEY_SPACING;
121
122 let key_rect = egui::Rect::from_min_size(
123 origin + Vec2::new(x, y),
124 Vec2::new(w, KEY_HEIGHT),
125 );
126
127 let bg = key_bg(&entry.kind);
128 painter.rect_filled(key_rect, CornerRadius::same(3), bg);
129 painter.rect_stroke(key_rect, CornerRadius::same(3), Stroke::new(1.0, Color32::from_rgb(0x55, 0x55, 0x55)), egui::StrokeKind::Middle);
130
131 let inset = 3.0;
132 let cw = key_rect.width() / 2.0;
133
134 let centers: [egui::Pos2; 4] = [
135 key_rect.center(),
136 egui::Pos2::new(key_rect.left() + cw / 2.0, key_rect.top() + inset + 4.0),
137 egui::Pos2::new(key_rect.right() - cw / 2.0, key_rect.top() + inset + 4.0),
138 egui::Pos2::new(key_rect.left() + cw / 2.0, key_rect.bottom() - inset - 4.0),
139 ];
140
141 let level_order = [0usize, 1, 2, 3];
142 let font_sizes = [15.0, 10.0, 10.0, 9.0];
143 let aligns = [
144 egui::Align2::CENTER_CENTER,
145 egui::Align2::LEFT_TOP,
146 egui::Align2::RIGHT_TOP,
147 egui::Align2::LEFT_BOTTOM,
148 ];
149
150 let g = active_group.min(3);
151 for &li in &level_order {
152 let label = &entry.labels[g][li];
153 if label.is_empty() {
154 continue;
155 }
156 let color = if entry.dead[g][li] {
157 KEY_TEXT_DEAD
158 } else if li == 0 {
159 if 0 == active_level {
160 text_color_for_level(li, active_level, &entry.kind)
161 } else {
162 KEY_TEXT
163 }
164 } else {
165 text_color_for_level(li, active_level, &entry.kind)
166 };
167 let font = if label.len() > 2 {
168 egui::FontId::proportional(font_sizes[li] - 1.0)
169 } else {
170 egui::FontId::proportional(font_sizes[li])
171 };
172 painter.text(centers[li], aligns[li], label, font, color);
173 }
174 }
175 }
176
177 let legend_y = (max_row + 1) as f32 * (KEY_HEIGHT + KEY_SPACING) + 16.0;
178 let legend_items = [
179 (KEY_BG_MOD_SHIFT, "Shift (triggers L2)"),
180 (KEY_BG_MOD_L3, "AltGr (triggers L3)"),
181 (KEY_BG_MOD_OTHER, "Other modifiers"),
182 (KEY_TEXT_DEAD, "Dead key accent"),
183 ];
184 for (i, (color, text)) in legend_items.iter().enumerate() {
185 let ly = legend_y + i as f32 * 20.0;
186 let r = egui::Rect::from_min_size(
187 origin + Vec2::new(0.0, ly),
188 Vec2::new(14.0, 14.0),
189 );
190 painter.rect_filled(r, CornerRadius::same(2), *color);
191 painter.text(
192 egui::Pos2::new(r.right() + 6.0, r.center().y),
193 egui::Align2::LEFT_CENTER,
194 *text,
195 egui::FontId::proportional(12.0),
196 KEY_TEXT,
197 );
198 }
199 });
200 });
201}
202
203fn group_by_row<'a>(keys: &'a [keymap::KeyEntry]) -> Vec<(usize, Vec<&'a keymap::KeyEntry>)> {
204 let mut rows: Vec<(usize, Vec<&'a keymap::KeyEntry>)> = Vec::new();
205 for key in keys {
206 let row = key.pos.row;
207 if row > 4 {
208 continue;
209 }
210 if let Some(entry) = rows.iter_mut().find(|(r, _)| *r == row) {
211 entry.1.push(key);
212 } else {
213 rows.push((row, vec![key]));
214 }
215 }
216 rows.sort_by_key(|(r, _)| *r);
217 for (_, entries) in &mut rows {
218 entries.sort_by(|a, b| a.pos.col.partial_cmp(&b.pos.col).unwrap());
219 }
220 rows
221}
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 @@
1use x11rb::connection::{Connection, RequestConnection};
2use x11rb::protocol::xkb::{self, ConnectionExt as XkbConnectionExt};
3use x11rb::protocol::xproto::{AtomEnum, ConnectionExt};
4use x11rb::rust_connection::RustConnection;
5
6#[derive(Debug, Default)]
7pub struct RuleNames {
8 pub rules: String,
9 pub model: String,
10 pub layout: String,
11 pub variant: String,
12 pub options: String,
13}
14
15pub struct X11Connection {
16 pub conn: RustConnection,
17 pub root: x11rb::protocol::xproto::Window,
18 xkb_ok: bool,
19}
20
21impl X11Connection {
22 pub fn open() -> Option<Self> {
23 let (conn, screen_num) = x11rb::connect(None).ok()?;
24 let root = conn.setup().roots[screen_num].root;
25 let xkb_ok = conn
26 .extension_information(xkb::X11_EXTENSION_NAME)
27 .ok()
28 .flatten()
29 .is_some();
30 if xkb_ok {
31 let _ = conn.xkb_use_extension(1, 0).ok();
32 }
33 Some(Self { conn, root, xkb_ok })
34 }
35
36 pub fn modifier_mask(&self) -> u16 {
37 self.conn
38 .query_pointer(self.root)
39 .ok()
40 .and_then(|c| c.reply().ok())
41 .map(|r| r.mask.into())
42 .unwrap_or(0)
43 }
44
45 pub fn active_group(&self) -> usize {
46 if !self.xkb_ok {
47 return 0;
48 }
49 self.conn
50 .xkb_get_state(0x0100)
51 .ok()
52 .and_then(|c| c.reply().ok())
53 .map(|r| u8::from(r.group) as usize)
54 .unwrap_or(0)
55 }
56}
57
58pub fn get_xkb_rule_names() -> Option<RuleNames> {
59 let (conn, screen_num) = x11rb::connect(None).ok()?;
60 let screen = &conn.setup().roots[screen_num];
61
62 let atom = conn
63 .intern_atom(false, b"_XKB_RULES_NAMES")
64 .ok()?
65 .reply()
66 .ok()?
67 .atom;
68
69 let prop = conn
70 .get_property(false, screen.root, atom, AtomEnum::ANY, 0, 1024)
71 .ok()?
72 .reply()
73 .ok()?;
74
75 if prop.value.is_empty() || prop.format == 0 {
76 return None;
77 }
78
79 let parts: Vec<&str> = prop
80 .value
81 .split(|&b| b == 0)
82 .filter_map(|s| std::str::from_utf8(s).ok())
83 .collect();
84
85 Some(RuleNames {
86 rules: parts.first().map(|s| s.to_string()).unwrap_or_default(),
87 model: parts.get(1).map(|s| s.to_string()).unwrap_or_default(),
88 layout: parts.get(2).map(|s| s.to_string()).unwrap_or_default(),
89 variant: parts.get(3).map(|s| s.to_string()).unwrap_or_default(),
90 options: parts.get(4).map(|s| s.to_string()).unwrap_or_default(),
91 })
92}