From a2fe33b657abc0b48dcfd43df23d0b76f353585c Mon Sep 17 00:00:00 2001 From: Eivind Gamst Date: Wed, 29 Apr 2026 15:24:34 +0200 Subject: [PATCH 1/5] First itteration on new format --- Cargo.toml | 10 +- benches/bench_setup.rs | 19 +- examples/check_ron.rs | 6 + examples/find_invariant_keys.rs | 419 ++++++ examples/generate_ron.rs | 127 ++ scripts/generate_benchmark_table.js | 1 + src/composer.rs | 10 +- src/keysyms.rs | 13 + src/lib.rs | 1 + src/modifiers.rs | 4 +- src/ron_format.rs | 2060 +++++++++++++++++++++++++++ src/testing.rs | 16 + src/xkb/mod.rs | 179 ++- src/xkb/serialize.rs | 43 + tests/compose.rs | 29 +- tests/keymap.rs | 60 +- tests/level.rs | 5 +- xkb-core/src/keysym.rs | 3 +- xkb-core/src/keysym_utf.rs | 6 + xkb-core/src/lib.rs | 3 + xkb-core/src/state.rs | 18 +- xkb-core/src/text.rs | 23 +- xkb-core/src/xkbcomp/ast_build.rs | 8 +- xkb-core/src/xkbcomp/keywords.rs | 8 +- xkb-core/src/xkbcomp/prelude.rs | 76 +- xkb-core/src/xkbcomp/scanner.rs | 18 +- xkb-core/src/xkbcomp/vmod.rs | 4 +- 27 files changed, 3046 insertions(+), 123 deletions(-) create mode 100644 examples/check_ron.rs create mode 100644 examples/find_invariant_keys.rs create mode 100644 examples/generate_ron.rs create mode 100644 src/ron_format.rs diff --git a/Cargo.toml b/Cargo.toml index 822d0291..484eeaa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,9 @@ testing = ["xkb"] [dependencies] xkb-core = { version = "0.2.0", path = "xkb-core", optional = true } -compact_str = "0.8" +compact_str = { version = "0.8", features = ["serde"] } +serde = { version = "1", features = ["derive"] } +ron = "0.8" [dev-dependencies] wkb = { package = "wayland-keyboard", path = ".", features = ["testing"] } @@ -66,3 +68,9 @@ name = "bench_size_xkbcommon_dl" [[example]] name = "bench_size_xkbcommon_compat" + +[[example]] +name = "generate_ron" + +[[example]] +name = "find_invariant_keys" diff --git a/benches/bench_setup.rs b/benches/bench_setup.rs index 3ac432c6..c25e0cbd 100644 --- a/benches/bench_setup.rs +++ b/benches/bench_setup.rs @@ -187,9 +187,26 @@ fn bench_setup_with_compose(c: &mut Criterion) { group.finish(); } +fn bench_setup_from_ron(c: &mut Criterion) { + let mut group = c.benchmark_group("setup/from_ron"); + + // Pre-build a WKB and serialize to RON (this is the "compiled" artifact) + let wkb = wkb::WKB::new_from_names("", "", "us", "", None).unwrap(); + let ron_str = wkb.to_ron().expect("serialize to RON"); + + group.bench_function("wkb-from-ron", |b| { + b.iter(|| { + let wkb = wkb::WKB::from_ron(black_box(&ron_str)).expect("deserialize from RON"); + black_box(wkb); + }); + }); + + group.finish(); +} + criterion_group! { name = benches; config = cfg(); - targets = bench_setup_no_compose, bench_setup_with_compose, + targets = bench_setup_no_compose, bench_setup_with_compose, bench_setup_from_ron, } criterion_main!(benches); diff --git a/examples/check_ron.rs b/examples/check_ron.rs new file mode 100644 index 00000000..eb30d17c --- /dev/null +++ b/examples/check_ron.rs @@ -0,0 +1,6 @@ +fn main() { + let layout = std::env::args().nth(1).unwrap_or("be".into()); + let wkb = wkb::WKB::new_from_names("", "", &layout, "", None).unwrap(); + let ron = wkb.to_ron().unwrap(); + println!("{}", ron); +} diff --git a/examples/find_invariant_keys.rs b/examples/find_invariant_keys.rs new file mode 100644 index 00000000..ee78af85 --- /dev/null +++ b/examples/find_invariant_keys.rs @@ -0,0 +1,419 @@ +//! Find keys that always produce the same character across ALL XKB layouts. +//! These keys can be omitted from .ron files (assumed as defaults). +//! +//! Usage: cargo run --release --example find_invariant_keys + +use std::collections::{BTreeMap, HashMap}; +use std::fs; +use std::io::{BufRead, BufReader}; +use std::path::Path; + +fn parse_evdev_lst(path: &Path) -> (Vec, Vec<(String, String)>) { + let file = fs::File::open(path).expect("failed to open evdev.lst"); + let reader = BufReader::new(file); + let mut section = String::new(); + let mut layouts = Vec::new(); + let mut variants = Vec::new(); + + for line in reader.lines() { + let line = line.unwrap(); + if line.starts_with("! layout") { + section = "layout".into(); + continue; + } + if line.starts_with("! variant") { + section = "variant".into(); + continue; + } + if line.starts_with('!') { + section.clear(); + continue; + } + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if section == "layout" { + if let Some(name) = trimmed.split_whitespace().next() { + layouts.push(name.to_string()); + } + } else if section == "variant" { + let mut parts = trimmed.splitn(2, char::is_whitespace); + if let Some(variant_name) = parts.next() { + if let Some(rest) = parts.next() { + let rest = rest.trim(); + if let Some(layout) = rest.split(':').next() { + variants.push((variant_name.to_string(), layout.trim().to_string())); + } + } + } + } + } + (layouts, variants) +} + +fn main() { + let evdev_lst = Path::new("/usr/share/X11/xkb/rules/evdev.lst"); + let (layouts, variants) = parse_evdev_lst(evdev_lst); + + // For each (evdev, level), track: HashMap, count> + // Key: (evdev_code, level), Value: map of char -> how many layouts have it + let mut key_char_counts: HashMap<(u32, usize), HashMap, u32>> = HashMap::new(); + let mut key_sym_counts: HashMap<(u32, usize), HashMap> = HashMap::new(); + let mut total_layouts = 0u32; + + let mut entries: Vec<(&str, &str)> = layouts.iter().map(|l| (l.as_str(), "")).collect(); + for (v, p) in &variants { + entries.push((p.as_str(), v.as_str())); + } + + for (layout, variant) in &entries { + let wkb = match wkb::WKB::new_from_names("", "", layout, variant, None) { + Ok(w) => w, + Err(_) => continue, + }; + total_layouts += 1; + + // Only check layout 0 + for evdev in 0u32..701 { + for level in 0..8usize { + let ch = wkb.level_key_char(evdev, 0, level); + *key_char_counts + .entry((evdev, level)) + .or_default() + .entry(ch) + .or_default() += 1; + + let sym = wkb.level_keysym(evdev, 0, level); + *key_sym_counts + .entry((evdev, level)) + .or_default() + .entry(sym) + .or_default() += 1; + } + } + + if total_layouts % 50 == 0 { + eprint!("\r Compiled {} layouts...", total_layouts); + } + } + eprintln!("\r Compiled {} layouts total.", total_layouts); + + // Find keys where ALL layouts agree on the same char (level 0 only for simplicity first) + println!( + "\n=== Invariant keys (same char in ALL {} layouts) ===", + total_layouts + ); + println!("Format: evdev level -> char (or None) [keysym]"); + + let mut invariant_chars: BTreeMap<(u32, usize), (Option, u32)> = BTreeMap::new(); + + for (&(evdev, level), char_map) in &key_char_counts { + // Get the count of None entries + let none_count = char_map.get(&None).copied().unwrap_or(0); + let some_entries: Vec<_> = char_map.iter().filter(|(&k, _)| k.is_some()).collect(); + + // If there's exactly one non-None char value, and it appears in >=99% of + // layouts that DO produce a char for this key (ignore None layouts) + if some_entries.len() == 1 { + let (&ch, &count) = some_entries[0]; + let total_with_char = total_layouts - none_count; + if total_with_char > 0 && count as f64 / total_with_char as f64 >= 0.99 { + let sym_map = &key_sym_counts[&(evdev, level)]; + // For keysyms, also ignore 0 entries (no keysym) + let non_zero_syms: Vec<_> = sym_map.iter().filter(|(&k, _)| k != 0).collect(); + let sym = if non_zero_syms.len() == 1 { + *non_zero_syms[0].0 + } else { + 0 + }; + invariant_chars.insert((evdev, level), (ch, sym)); + } + } + } + + // Group by evdev for readability + let mut by_evdev: BTreeMap, u32)>> = BTreeMap::new(); + for (&(evdev, level), &(ch, sym)) in &invariant_chars { + by_evdev.entry(evdev).or_default().push((level, ch, sym)); + } + + // Print keys that are invariant at level 0 with Some(char) + println!("\n--- Level 0 invariant chars ---"); + for (&evdev, levels) in &by_evdev { + for &(level, ch, sym) in levels { + if level == 0 { + let ch_str = match ch { + Some(c) if c.is_control() => format!("U+{:04X}", c as u32), + Some(c) => format!("'{}'", c), + None => "None".into(), + }; + let sym_name = wkb::keysyms::keysym_get_name(sym).unwrap_or("?"); + println!(" evdev {:>3} -> {} [{}]", evdev, ch_str, sym_name); + } + } + } + + // Print keys invariant at ALL levels (same across all layouts for every level) + println!("\n--- Keys invariant at ALL levels (0-7) ---"); + for (&evdev, levels) in &by_evdev { + if levels.len() == 8 { + // Check if all levels have the same char as level 0 + let level0_ch = levels.iter().find(|(l, _, _)| *l == 0).map(|(_, c, _)| *c); + let all_same = levels.iter().all(|(_, c, _)| Some(*c) == level0_ch); + let ch = level0_ch.unwrap_or(None); + let ch_str = match ch { + Some(c) if c.is_control() => format!("U+{:04X}", c as u32), + Some(c) => format!("'{}'", c), + None => "None".into(), + }; + if all_same { + println!(" evdev {:>3} -> {} (all levels identical)", evdev, ch_str); + } else { + let parts: Vec = levels + .iter() + .map(|(l, c, _)| { + let cs = match c { + Some(ch) if ch.is_control() => format!("U+{:04X}", *ch as u32), + Some(ch) => format!("'{}'", ch), + None => "None".into(), + }; + format!("L{}={}", l, cs) + }) + .collect(); + println!(" evdev {:>3} -> {}", evdev, parts.join(" ")); + } + } + } + + // Count how many keys are NOT invariant at level 0 (these are the layout-specific ones) + let mut non_invariant_l0 = 0; + let mut invariant_l0_some = 0; + let mut invariant_l0_none = 0; + for evdev in 0u32..701 { + match invariant_chars.get(&(evdev, 0)) { + Some((Some(_), _)) => invariant_l0_some += 1, + Some((None, _)) => invariant_l0_none += 1, + None => non_invariant_l0 += 1, + } + } + println!("\n--- Summary (level 0) ---"); + println!(" Invariant with char: {}", invariant_l0_some); + println!(" Invariant None: {}", invariant_l0_none); + println!(" Layout-specific: {}", non_invariant_l0); + + // Also find keys that are almost-invariant (same in >95% of layouts) + println!("\n--- Almost-invariant level 0 (>99% same) ---"); + let threshold = (total_layouts as f64 * 0.99) as u32; + for evdev in 0u32..701 { + if invariant_chars.contains_key(&(evdev, 0)) { + continue; + } + if let Some(char_map) = key_char_counts.get(&(evdev, 0)) { + if let Some((&ch, &count)) = char_map.iter().max_by_key(|(_, c)| **c) { + if count >= threshold { + let ch_str = match ch { + Some(c) if c.is_control() => format!("U+{:04X}", c as u32), + Some(c) => format!("'{}'", c), + None => "None".into(), + }; + let exceptions = total_layouts - count; + println!( + " evdev {:>3} -> {} ({} exceptions out of {})", + evdev, ch_str, exceptions, total_layouts + ); + } + } + } + } + + // Output Rust table for DEFAULT_KEYSYMS: keys always None in state_keymap with invariant keysyms + println!("\n=== Rust DEFAULT_KEYSYMS table (evdev, level0_keysym) ==="); + println!("/// Keys that always produce None in state_keymap across all layouts."); + println!("/// Their keysyms are identical in all layouts at level 0."); + println!("/// On deserialize, these are repopulated into keysym_map at all levels."); + println!("static DEFAULT_KEYSYMS: &[(u32, u32)] = &["); + for evdev in 0u32..701 { + // Check: invariant None at all levels in state_keymap + let all_none = (0..8).all(|level| { + invariant_chars + .get(&(evdev, level)) + .map(|(ch, _)| ch.is_none()) + .unwrap_or(false) + }); + if !all_none { + continue; + } + + // Get the invariant keysym at level 0 + if let Some(sym_map) = key_sym_counts.get(&(evdev, 0)) { + let non_zero: Vec<_> = sym_map.iter().filter(|(&k, _)| k != 0).collect(); + if non_zero.len() == 1 { + let (&sym, _) = non_zero[0]; + let name = wkb::keysyms::keysym_get_name(sym).unwrap_or("?"); + println!(" ({}, 0x{:04x}), // {}", evdev, sym, name); + } + } + } + println!("];"); + + // Output INVARIANT_CHARS table: keys that produce chars and are invariant (or almost) across layouts + // Include keys at >=99% threshold + let threshold99 = (total_layouts as f64 * 0.99) as u32; + println!("\n=== Rust INVARIANT_CHARS table ==="); + println!("/// Keys invariant (or almost-invariant >99%) across all layouts."); + println!("/// Format: (evdev, [(level, char)]) — omit levels where char is None."); + println!("/// During RON serialization, these are omitted. During deserialization, they're pre-populated."); + println!("pub(crate) static INVARIANT_CHARS: &[(u32, &[(u8, char)])] = &["); + + for evdev in 0u32..701 { + // Check if this evdev is invariant or almost-invariant at level 0 + let is_invariant_l0 = invariant_chars + .get(&(evdev, 0)) + .map(|(ch, _)| ch.is_some()) + .unwrap_or(false); + let is_almost_l0 = if !is_invariant_l0 { + key_char_counts + .get(&(evdev, 0)) + .map(|m| { + m.iter() + .filter(|(ch, _)| ch.is_some()) + .max_by_key(|(_, c)| **c) + .map(|(_, &c)| c >= threshold99) + .unwrap_or(false) + }) + .unwrap_or(false) + } else { + false + }; + + if !is_invariant_l0 && !is_almost_l0 { + continue; + } + + // Get the majority char at each level + let mut level_chars: Vec<(u8, char)> = Vec::new(); + for level in 0..8usize { + if let Some(char_map) = key_char_counts.get(&(evdev, level)) { + // Find majority char (most common across layouts) + if let Some((&ch, &count)) = char_map + .iter() + .filter(|(ch, _)| ch.is_some()) + .max_by_key(|(_, c)| **c) + { + if count >= threshold99 { + if let Some(c) = ch { + level_chars.push((level as u8, c)); + } + } + } + } + } + + if level_chars.is_empty() { + continue; + } + + let parts: Vec = level_chars + .iter() + .map(|(l, c)| { + if c.is_control() || *c == '\'' || *c == '\\' { + format!("({}, '\\u{{{:04X}}}')", l, *c as u32) + } else { + format!("({}, '{}')", l, c) + } + }) + .collect(); + println!(" ({}, &[{}]),", evdev, parts.join(", ")); + } + println!("];"); + + // Also output INVARIANT_KEYSYMS for keys with chars (keysym at each level) + println!("\n=== Rust INVARIANT_KEYSYMS_WITH_CHARS table ==="); + println!("/// Keysyms for invariant char-producing keys, per level."); + println!("pub(crate) static INVARIANT_KEYSYMS_WITH_CHARS: &[(u32, &[(u8, u32)])] = &["); + for evdev in 0u32..701 { + let is_invariant_l0 = invariant_chars + .get(&(evdev, 0)) + .map(|(ch, _)| ch.is_some()) + .unwrap_or(false); + let is_almost_l0 = if !is_invariant_l0 { + key_char_counts + .get(&(evdev, 0)) + .map(|m| { + m.iter() + .filter(|(ch, _)| ch.is_some()) + .max_by_key(|(_, c)| **c) + .map(|(_, &c)| c >= threshold99) + .unwrap_or(false) + }) + .unwrap_or(false) + } else { + false + }; + + if !is_invariant_l0 && !is_almost_l0 { + continue; + } + + let mut level_syms: Vec<(u8, u32)> = Vec::new(); + for level in 0..8usize { + if let Some(sym_map) = key_sym_counts.get(&(evdev, level)) { + if let Some((&sym, &count)) = sym_map + .iter() + .filter(|(&s, _)| s != 0) + .max_by_key(|(_, c)| **c) + { + if count >= threshold99 { + level_syms.push((level as u8, sym)); + } + } + } + } + + if level_syms.is_empty() { + continue; + } + + let parts: Vec = level_syms + .iter() + .map(|(l, s)| { + let name = wkb::keysyms::keysym_get_name(*s).unwrap_or("?"); + format!("({}, 0x{:04x}/*{}*/)", l, s, name) + }) + .collect(); + println!(" ({}, &[{}]),", evdev, parts.join(", ")); + } + println!("];"); + + // Output COMPLETE INVARIANT_KEYSYMS table (all evdev codes, all levels) + // Strict: keysym must be the same in ALL 594 layouts (not just those that have it) + println!("\n=== Rust INVARIANT_KEYSYMS (complete) ==="); + println!("static INVARIANT_KEYSYMS: &[(u32, &[(u8, u32)])] = &["); + for evdev in 0u32..701 { + let mut level_syms: Vec<(u8, u32)> = Vec::new(); + for level in 0..8usize { + if let Some(sym_map) = key_sym_counts.get(&(evdev, level)) { + // Exactly one keysym value (no 0 entries) and it covers all layouts + if sym_map.len() == 1 { + let (&sym, &count) = sym_map.iter().next().unwrap(); + if sym != 0 && count == total_layouts { + level_syms.push((level as u8, sym)); + } + } + } + } + if level_syms.is_empty() { + continue; + } + let parts: Vec = level_syms + .iter() + .map(|(l, s)| { + let name = wkb::keysyms::keysym_get_name(*s).unwrap_or("?"); + format!("({}, 0x{:08x}/*{}*/)", l, s, name) + }) + .collect(); + println!(" ({}, &[{}]),", evdev, parts.join(", ")); + } + println!("];"); +} diff --git a/examples/generate_ron.rs b/examples/generate_ron.rs new file mode 100644 index 00000000..fa942695 --- /dev/null +++ b/examples/generate_ron.rs @@ -0,0 +1,127 @@ +//! Generate RON files for all XKB layouts discovered from evdev.lst. +//! +//! Usage: cargo run --example generate_ron -- [output_dir] +//! +//! Defaults to `ron_layouts/` in the current directory. +//! Each file is named `{layout}.ron` or `{layout}.{variant}.ron`. + +use std::fs; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; + +/// Parse evdev.lst and return (layouts, variants) where variants are (variant_name, parent_layout). +fn parse_evdev_lst(path: &Path) -> (Vec, Vec<(String, String)>) { + let file = fs::File::open(path).expect("failed to open evdev.lst"); + let reader = BufReader::new(file); + let mut section = String::new(); + let mut layouts = Vec::new(); + let mut variants = Vec::new(); + + for line in reader.lines() { + let line = line.unwrap(); + if line.starts_with("! layout") { + section = "layout".into(); + continue; + } + if line.starts_with("! variant") { + section = "variant".into(); + continue; + } + if line.starts_with('!') { + section.clear(); + continue; + } + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if section == "layout" { + if let Some(name) = trimmed.split_whitespace().next() { + layouts.push(name.to_string()); + } + } else if section == "variant" { + // Format: "variant_name layout: description" + let mut parts = trimmed.splitn(2, char::is_whitespace); + if let Some(variant_name) = parts.next() { + if let Some(rest) = parts.next() { + let rest = rest.trim(); + if let Some(layout) = rest.split(':').next() { + variants.push((variant_name.to_string(), layout.trim().to_string())); + } + } + } + } + } + + (layouts, variants) +} + +fn main() { + let output_dir = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("ron_layouts")); + + fs::create_dir_all(&output_dir).expect("failed to create output directory"); + + let evdev_lst = Path::new("/usr/share/X11/xkb/rules/evdev.lst"); + let (layouts, variants) = parse_evdev_lst(evdev_lst); + + println!( + "Found {} layouts and {} variants", + layouts.len(), + variants.len() + ); + + let mut success = 0u32; + let mut failed = 0u32; + + // Generate base layouts + for layout in &layouts { + match wkb::WKB::new_from_names("", "", layout, "", None) { + Ok(wkb) => match wkb.to_ron() { + Ok(ron_str) => { + let path = output_dir.join(format!("{layout}.ron")); + fs::write(&path, &ron_str).expect("failed to write RON file"); + let kb = ron_str.len() / 1024; + println!(" {layout}.ron ({kb} KB)"); + success += 1; + } + Err(e) => { + eprintln!(" SKIP {layout}: serialize error: {e}"); + failed += 1; + } + }, + Err(e) => { + eprintln!(" SKIP {layout}: {e}"); + failed += 1; + } + } + } + + // Generate variants + for (variant, parent_layout) in &variants { + match wkb::WKB::new_from_names("", "", parent_layout, variant, None) { + Ok(wkb) => match wkb.to_ron() { + Ok(ron_str) => { + let path = output_dir.join(format!("{parent_layout}.{variant}.ron")); + fs::write(&path, &ron_str).expect("failed to write RON file"); + let kb = ron_str.len() / 1024; + println!(" {parent_layout}.{variant}.ron ({kb} KB)"); + success += 1; + } + Err(e) => { + eprintln!(" SKIP {parent_layout}.{variant}: serialize error: {e}"); + failed += 1; + } + }, + Err(e) => { + eprintln!(" SKIP {parent_layout}.{variant}: {e}"); + failed += 1; + } + } + } + + println!("\nDone: {success} generated, {failed} failed"); + println!("Output: {}", output_dir.display()); +} diff --git a/scripts/generate_benchmark_table.js b/scripts/generate_benchmark_table.js index af4f1d3f..a5110e62 100644 --- a/scripts/generate_benchmark_table.js +++ b/scripts/generate_benchmark_table.js @@ -68,6 +68,7 @@ function generateSpeedTable() { { dir: 'key/get_char', label: 'Key get char' }, { dir: 'key/get_sym', label: 'Key get sym' }, { dir: 'compose/feed', label: 'Compose feed' }, + { dir: 'setup/from_ron', label: 'Setup (from RON)' }, ]; const rows = []; diff --git a/src/composer.rs b/src/composer.rs index d2ad120f..635fe0aa 100644 --- a/src/composer.rs +++ b/src/composer.rs @@ -1,7 +1,7 @@ use compact_str::CompactString; /// Token fed into the composer: either a regular character or a Compose key press -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum Token { Char(char), Compose, @@ -27,13 +27,13 @@ fn token_key(token: &Token) -> u32 { /// Trie node: children stored as sorted (key, child_index) pairs for binary search. /// `emit` is Some(char) if this node is a leaf that produces output. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct TrieNode { - children: Vec<(u32, u32)>, // (token_key, node_index), sorted by token_key - emit: Option, + pub(crate) children: Vec<(u32, u32)>, // (token_key, node_index), sorted by token_key + pub(crate) emit: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Composer { pub(crate) nodes: Vec, cur: u32, diff --git a/src/keysyms.rs b/src/keysyms.rs index 1e050999..26975427 100644 --- a/src/keysyms.rs +++ b/src/keysyms.rs @@ -2727,6 +2727,19 @@ pub fn keysym_get_name(sym: u32) -> Option<&'static str> { .map(|i| KEYSYM_TO_NAME[i].1) } +/// Look up the keysym value from a human-readable name. +/// +/// Delegates to xkb-core's keysym lookup (case-sensitive). +/// Returns `None` for unknown names. +pub fn keysym_from_name(name: &str) -> Option { + let sym = xkb_core::xkb_keysym_from_name(name.as_bytes(), 0); + if sym == 0 { + None + } else { + Some(sym) + } +} + /// Return the VT number (1–12) if this is a VT-switch keysym, or `None`. pub fn vt_switch(sym: u32) -> Option { if sym >= XF86Switch_VT_1 && sym <= XF86Switch_VT_12 { diff --git a/src/lib.rs b/src/lib.rs index b9967343..c7c30202 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ pub(crate) use bitset::KeyBitSet; mod flat_keymap; pub(crate) use flat_keymap::{FlatKeymap, FlatKeysymMap}; pub mod keysyms; +mod ron_format; /// Test-only utilities. Not part of the public API. #[cfg(feature = "testing")] pub mod testing; diff --git a/src/modifiers.rs b/src/modifiers.rs index 294064cc..28609584 100644 --- a/src/modifiers.rs +++ b/src/modifiers.rs @@ -76,7 +76,7 @@ pub enum KeyDirection { Down, } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ModType { None, Level2, @@ -228,7 +228,7 @@ pub enum Modifier { #[derive(Debug, Clone)] pub struct Modifiers { /// Flat array of (evdev_code, Modifier) pairs. Typically 10-20 entries. - entries: Vec<(u32, Modifier)>, + pub(crate) entries: Vec<(u32, Modifier)>, } impl Default for Modifiers { diff --git a/src/ron_format.rs b/src/ron_format.rs new file mode 100644 index 00000000..368ed7a7 --- /dev/null +++ b/src/ron_format.rs @@ -0,0 +1,2060 @@ +//! Human-readable RON serialization format for WKB. +//! +//! Converts between the internal flat-array representation and a sparse, +//! named representation that humans can read and edit. + +use crate::bitset::{KeyBitSet, BITSET_WORDS}; +use crate::composer::{Composer, Token, TrieNode}; +use crate::flat_keymap::{FlatKeymap, MAX_LEVELS}; +use crate::keysyms; +use crate::modifiers::{ModKind, ModType, Modifier, Modifiers}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +// ── Default keys ───────────────────────────────────────────────────── +// +// Invariant key tables: keys identical (or >99% identical) across all 594 XKB layouts. +// Omitted from .ron files when they match. Repopulated on deserialize. + +/// Keys that are invariant (or >99% invariant) across all XKB layouts. +/// Format: (evdev_code, &[(level, char)]). +/// During RON serialization, entries matching this table are omitted. +/// During RON deserialization, these are pre-populated before applying overrides. +static INVARIANT_CHARS: &[(u32, &[(u8, char)])] = &[ + ( + 1, + &[ + (0, '\u{001B}'), + (1, '\u{001B}'), + (2, '\u{001B}'), + (3, '\u{001B}'), + (4, '\u{001B}'), + (5, '\u{001B}'), + (6, '\u{001B}'), + (7, '\u{001B}'), + ], + ), + ( + 14, + &[ + (0, '\u{0008}'), + (1, '\u{0008}'), + (2, '\u{0008}'), + (3, '\u{0008}'), + (4, '\u{0008}'), + (5, '\u{0008}'), + (6, '\u{0008}'), + (7, '\u{0008}'), + ], + ), + ( + 15, + &[ + (0, '\u{0009}'), + (2, '\u{0009}'), + (4, '\u{0009}'), + (6, '\u{0009}'), + ], + ), + ( + 28, + &[ + (0, '\u{000D}'), + (1, '\u{000D}'), + (2, '\u{000D}'), + (3, '\u{000D}'), + (4, '\u{000D}'), + (5, '\u{000D}'), + (6, '\u{000D}'), + (7, '\u{000D}'), + ], + ), + (55, &[(0, '*'), (4, '*')]), + (57, &[(0, ' ')]), + (71, &[(1, '7')]), + (72, &[(1, '8')]), + (73, &[(1, '9')]), + (74, &[(0, '-'), (4, '-')]), + ( + 78, + &[(0, '+'), (1, '+'), (2, '+'), (4, '+'), (5, '+'), (6, '+')], + ), + (80, &[(1, '2')]), + (81, &[(1, '3')]), + ( + 96, + &[ + (0, '\u{000D}'), + (1, '\u{000D}'), + (2, '\u{000D}'), + (3, '\u{000D}'), + (4, '\u{000D}'), + (5, '\u{000D}'), + (6, '\u{000D}'), + (7, '\u{000D}'), + ], + ), + (98, &[(0, '/'), (4, '/')]), + ( + 101, + &[ + (0, '\u{000A}'), + (1, '\u{000A}'), + (2, '\u{000A}'), + (3, '\u{000A}'), + (4, '\u{000A}'), + (5, '\u{000A}'), + (6, '\u{000A}'), + (7, '\u{000A}'), + ], + ), + ( + 111, + &[ + (0, '\u{007F}'), + (1, '\u{007F}'), + (2, '\u{007F}'), + (3, '\u{007F}'), + (4, '\u{007F}'), + (5, '\u{007F}'), + (6, '\u{007F}'), + (7, '\u{007F}'), + ], + ), + ( + 117, + &[ + (0, '='), + (1, '='), + (2, '='), + (3, '='), + (4, '='), + (5, '='), + (6, '='), + (7, '='), + ], + ), + ( + 118, + &[ + (0, '±'), + (1, '±'), + (2, '±'), + (3, '±'), + (4, '±'), + (5, '±'), + (6, '±'), + (7, '±'), + ], + ), + ( + 121, + &[ + (0, '.'), + (1, '.'), + (2, '.'), + (3, '.'), + (4, '.'), + (5, '.'), + (6, '.'), + (7, '.'), + ], + ), + ( + 179, + &[ + (0, '('), + (1, '('), + (2, '('), + (3, '('), + (4, '('), + (5, '('), + (6, '('), + (7, '('), + ], + ), + ( + 180, + &[ + (0, ')'), + (1, ')'), + (2, ')'), + (3, ')'), + (4, ')'), + (5, ')'), + (6, ')'), + (7, ')'), + ], + ), + ( + 434, + &[ + (0, '$'), + (1, '$'), + (2, '$'), + (3, '$'), + (4, '$'), + (5, '$'), + (6, '$'), + (7, '$'), + ], + ), + ( + 435, + &[ + (0, '€'), + (1, '€'), + (2, '€'), + (3, '€'), + (4, '€'), + (5, '€'), + (6, '€'), + (7, '€'), + ], + ), + ( + 497, + &[ + (0, '⠁'), + (1, '⠁'), + (2, '⠁'), + (3, '⠁'), + (4, '⠁'), + (5, '⠁'), + (6, '⠁'), + (7, '⠁'), + ], + ), + ( + 498, + &[ + (0, '⠂'), + (1, '⠂'), + (2, '⠂'), + (3, '⠂'), + (4, '⠂'), + (5, '⠂'), + (6, '⠂'), + (7, '⠂'), + ], + ), + ( + 499, + &[ + (0, '⠄'), + (1, '⠄'), + (2, '⠄'), + (3, '⠄'), + (4, '⠄'), + (5, '⠄'), + (6, '⠄'), + (7, '⠄'), + ], + ), + ( + 500, + &[ + (0, '⠈'), + (1, '⠈'), + (2, '⠈'), + (3, '⠈'), + (4, '⠈'), + (5, '⠈'), + (6, '⠈'), + (7, '⠈'), + ], + ), + ( + 501, + &[ + (0, '⠐'), + (1, '⠐'), + (2, '⠐'), + (3, '⠐'), + (4, '⠐'), + (5, '⠐'), + (6, '⠐'), + (7, '⠐'), + ], + ), + ( + 502, + &[ + (0, '⠠'), + (1, '⠠'), + (2, '⠠'), + (3, '⠠'), + (4, '⠠'), + (5, '⠠'), + (6, '⠠'), + (7, '⠠'), + ], + ), + ( + 503, + &[ + (0, '⡀'), + (1, '⡀'), + (2, '⡀'), + (3, '⡀'), + (4, '⡀'), + (5, '⡀'), + (6, '⡀'), + (7, '⡀'), + ], + ), + ( + 504, + &[ + (0, '⢀'), + (1, '⢀'), + (2, '⢀'), + (3, '⢀'), + (4, '⢀'), + (5, '⢀'), + (6, '⢀'), + (7, '⢀'), + ], + ), + ( + 506, + &[ + (0, '⠁'), + (1, '⠁'), + (2, '⠁'), + (3, '⠁'), + (4, '⠁'), + (5, '⠁'), + (6, '⠁'), + (7, '⠁'), + ], + ), + ( + 512, + &[ + (0, '0'), + (1, '0'), + (2, '0'), + (3, '0'), + (4, '0'), + (5, '0'), + (6, '0'), + (7, '0'), + ], + ), + ( + 513, + &[ + (0, '1'), + (1, '1'), + (2, '1'), + (3, '1'), + (4, '1'), + (5, '1'), + (6, '1'), + (7, '1'), + ], + ), + ( + 514, + &[ + (0, '2'), + (1, '2'), + (2, '2'), + (3, '2'), + (4, '2'), + (5, '2'), + (6, '2'), + (7, '2'), + ], + ), + ( + 515, + &[ + (0, '3'), + (1, '3'), + (2, '3'), + (3, '3'), + (4, '3'), + (5, '3'), + (6, '3'), + (7, '3'), + ], + ), + ( + 516, + &[ + (0, '4'), + (1, '4'), + (2, '4'), + (3, '4'), + (4, '4'), + (5, '4'), + (6, '4'), + (7, '4'), + ], + ), + ( + 517, + &[ + (0, '5'), + (1, '5'), + (2, '5'), + (3, '5'), + (4, '5'), + (5, '5'), + (6, '5'), + (7, '5'), + ], + ), + ( + 518, + &[ + (0, '6'), + (1, '6'), + (2, '6'), + (3, '6'), + (4, '6'), + (5, '6'), + (6, '6'), + (7, '6'), + ], + ), + ( + 519, + &[ + (0, '7'), + (1, '7'), + (2, '7'), + (3, '7'), + (4, '7'), + (5, '7'), + (6, '7'), + (7, '7'), + ], + ), + ( + 520, + &[ + (0, '8'), + (1, '8'), + (2, '8'), + (3, '8'), + (4, '8'), + (5, '8'), + (6, '8'), + (7, '8'), + ], + ), + ( + 521, + &[ + (0, '9'), + (1, '9'), + (2, '9'), + (3, '9'), + (4, '9'), + (5, '9'), + (6, '9'), + (7, '9'), + ], + ), + ( + 522, + &[ + (0, '*'), + (1, '*'), + (2, '*'), + (3, '*'), + (4, '*'), + (5, '*'), + (6, '*'), + (7, '*'), + ], + ), + ( + 523, + &[ + (0, '#'), + (1, '#'), + (2, '#'), + (3, '#'), + (4, '#'), + (5, '#'), + (6, '#'), + (7, '#'), + ], + ), +]; + +/// Invariant keysyms: keys whose keysym is the same across all layouts at given levels. +/// Format: (evdev, &[(level, keysym)]). +/// During RON serialization of keysym_map, entries matching this table are omitted. +static INVARIANT_KEYSYMS: &[(u32, &[(u8, u32)])] = &[ + (1, &[(0, 0xff1b /*Escape*/)]), + (14, &[(0, 0xff08 /*BackSpace*/), (1, 0xff08 /*BackSpace*/)]), + (15, &[(0, 0xff09 /*Tab*/), (1, 0xfe20 /*ISO_Left_Tab*/)]), + (28, &[(0, 0xff0d /*Return*/)]), + (29, &[(0, 0xffe3 /*Control_L*/)]), + (42, &[(0, 0xffe1 /*Shift_L*/)]), + (54, &[(0, 0xffe2 /*Shift_R*/)]), + ( + 59, + &[ + (0, 0xffbe /*F1*/), + (1, 0xffbe /*F1*/), + (2, 0xffbe /*F1*/), + (3, 0xffbe /*F1*/), + (4, 0x1008fe01 /*XF86Switch_VT_1*/), + ], + ), + ( + 60, + &[ + (0, 0xffbf /*F2*/), + (1, 0xffbf /*F2*/), + (2, 0xffbf /*F2*/), + (3, 0xffbf /*F2*/), + (4, 0x1008fe02 /*XF86Switch_VT_2*/), + ], + ), + ( + 61, + &[ + (0, 0xffc0 /*F3*/), + (1, 0xffc0 /*F3*/), + (2, 0xffc0 /*F3*/), + (3, 0xffc0 /*F3*/), + (4, 0x1008fe03 /*XF86Switch_VT_3*/), + ], + ), + ( + 62, + &[ + (0, 0xffc1 /*F4*/), + (1, 0xffc1 /*F4*/), + (2, 0xffc1 /*F4*/), + (3, 0xffc1 /*F4*/), + (4, 0x1008fe04 /*XF86Switch_VT_4*/), + ], + ), + ( + 63, + &[ + (0, 0xffc2 /*F5*/), + (1, 0xffc2 /*F5*/), + (2, 0xffc2 /*F5*/), + (3, 0xffc2 /*F5*/), + (4, 0x1008fe05 /*XF86Switch_VT_5*/), + ], + ), + ( + 64, + &[ + (0, 0xffc3 /*F6*/), + (1, 0xffc3 /*F6*/), + (2, 0xffc3 /*F6*/), + (3, 0xffc3 /*F6*/), + (4, 0x1008fe06 /*XF86Switch_VT_6*/), + ], + ), + ( + 65, + &[ + (0, 0xffc4 /*F7*/), + (1, 0xffc4 /*F7*/), + (2, 0xffc4 /*F7*/), + (3, 0xffc4 /*F7*/), + (4, 0x1008fe07 /*XF86Switch_VT_7*/), + ], + ), + ( + 66, + &[ + (0, 0xffc5 /*F8*/), + (1, 0xffc5 /*F8*/), + (2, 0xffc5 /*F8*/), + (3, 0xffc5 /*F8*/), + (4, 0x1008fe08 /*XF86Switch_VT_8*/), + ], + ), + ( + 67, + &[ + (0, 0xffc6 /*F9*/), + (1, 0xffc6 /*F9*/), + (2, 0xffc6 /*F9*/), + (3, 0xffc6 /*F9*/), + (4, 0x1008fe09 /*XF86Switch_VT_9*/), + ], + ), + ( + 68, + &[ + (0, 0xffc7 /*F10*/), + (1, 0xffc7 /*F10*/), + (2, 0xffc7 /*F10*/), + (3, 0xffc7 /*F10*/), + (4, 0x1008fe0a /*XF86Switch_VT_10*/), + ], + ), + (84, &[(0, 0xfe03 /*ISO_Level3_Shift*/)]), + (85, &[(0, 0x1008ffa9 /*XF86TouchpadToggle*/)]), + ( + 87, + &[ + (0, 0xffc8 /*L1*/), + (1, 0xffc8 /*L1*/), + (2, 0xffc8 /*L1*/), + (3, 0xffc8 /*L1*/), + (4, 0x1008fe0b /*XF86Switch_VT_11*/), + ], + ), + ( + 88, + &[ + (0, 0xffc9 /*L2*/), + (1, 0xffc9 /*L2*/), + (2, 0xffc9 /*L2*/), + (3, 0xffc9 /*L2*/), + (4, 0x1008fe0c /*XF86Switch_VT_12*/), + ], + ), + (90, &[(0, 0xff26 /*Katakana*/)]), + (91, &[(0, 0xff25 /*Hiragana*/)]), + (92, &[(0, 0xff23 /*Henkan_Mode*/)]), + (93, &[(0, 0xff27 /*Hiragana_Katakana*/)]), + (94, &[(0, 0xff22 /*Muhenkan*/)]), + (99, &[(0, 0xff61 /*SunPrint_Screen*/)]), + (101, &[(0, 0xff0a /*Linefeed*/)]), + (102, &[(0, 0xff50 /*Home*/)]), + (103, &[(0, 0xff52 /*Up*/)]), + (104, &[(0, 0xff55 /*SunPageUp*/)]), + (105, &[(0, 0xff51 /*Left*/)]), + (106, &[(0, 0xff53 /*Right*/)]), + (107, &[(0, 0xff57 /*End*/)]), + (108, &[(0, 0xff54 /*Down*/)]), + (109, &[(0, 0xff56 /*SunPageDown*/)]), + (110, &[(0, 0xff63 /*Insert*/)]), + (111, &[(0, 0xffff /*Delete*/)]), + (113, &[(0, 0x1008ff12 /*XF86AudioMute*/)]), + (114, &[(0, 0x1008ff11 /*XF86AudioLowerVolume*/)]), + (115, &[(0, 0x1008ff13 /*XF86AudioRaiseVolume*/)]), + (116, &[(0, 0x1008ff2a /*XF86PowerOff*/)]), + (118, &[(0, 0xb1 /*plusminus*/)]), + (119, &[(0, 0xff13 /*Pause*/), (1, 0xff6b /*Break*/)]), + (120, &[(0, 0x1008ff4a /*XF86LaunchA*/)]), + ( + 121, + &[(0, 0xffae /*KP_Decimal*/), (1, 0xffae /*KP_Decimal*/)], + ), + (122, &[(0, 0xff31 /*Hangul*/)]), + (123, &[(0, 0xff34 /*Hangul_Hanja*/)]), + (125, &[(0, 0xffeb /*Super_L*/)]), + (127, &[(0, 0xff67 /*Menu*/)]), + (128, &[(0, 0xff69 /*SunStop*/)]), + (129, &[(0, 0xff66 /*SunAgain*/)]), + (130, &[(0, 0x1005ff70 /*SunProps*/)]), + (131, &[(0, 0xff65 /*Undo*/)]), + (132, &[(0, 0x1005ff71 /*SunFront*/)]), + (133, &[(0, 0x1008ff57 /*XF86Copy*/)]), + (134, &[(0, 0x1008ff6b /*XF86Open*/)]), + (135, &[(0, 0x1008ff6d /*XF86Paste*/)]), + (136, &[(0, 0xff68 /*SunFind*/)]), + (137, &[(0, 0x1008ff58 /*XF86Cut*/)]), + (138, &[(0, 0xff6a /*Help*/)]), + (139, &[(0, 0x1008ff65 /*XF86MenuKB*/)]), + (140, &[(0, 0x1008ff1d /*XF86Calculator*/)]), + (142, &[(0, 0x1008ff2f /*XF86Sleep*/)]), + (143, &[(0, 0x1008ff2b /*XF86WakeUp*/)]), + (144, &[(0, 0x1008ff5d /*XF86Explorer*/)]), + (145, &[(0, 0x1008ff7b /*XF86Send*/)]), + (147, &[(0, 0x1008ff8a /*XF86Xfer*/)]), + (148, &[(0, 0x1008ff41 /*XF86Launch1*/)]), + (149, &[(0, 0x1008ff42 /*XF86Launch2*/)]), + (150, &[(0, 0x1008ff2e /*XF86WWW*/)]), + (151, &[(0, 0x1008ff5a /*XF86DOS*/)]), + (152, &[(0, 0x1008ff2d /*XF86ScreenSaver*/)]), + (153, &[(0, 0x1008ff74 /*XF86RotateWindows*/)]), + (154, &[(0, 0x1008ff7f /*XF86TaskPane*/)]), + (155, &[(0, 0x1008ff19 /*XF86Mail*/)]), + (156, &[(0, 0x1008ff30 /*XF86Favorites*/)]), + (157, &[(0, 0x1008ff33 /*XF86MyComputer*/)]), + (158, &[(0, 0x1008ff26 /*XF86Back*/)]), + (159, &[(0, 0x1008ff27 /*XF86Forward*/)]), + (161, &[(0, 0x1008ff2c /*XF86Eject*/)]), + (162, &[(0, 0x1008ff2c /*XF86Eject*/)]), + (163, &[(0, 0x1008ff17 /*XF86AudioNext*/)]), + ( + 164, + &[ + (0, 0x1008ff14 /*XF86AudioPlay*/), + (1, 0x1008ff31 /*XF86AudioPause*/), + ], + ), + (165, &[(0, 0x1008ff16 /*XF86AudioPrev*/)]), + ( + 166, + &[ + (0, 0x1008ff15 /*XF86AudioStop*/), + (1, 0x1008ff2c /*XF86Eject*/), + ], + ), + (167, &[(0, 0x1008ff1c /*XF86AudioRecord*/)]), + (168, &[(0, 0x1008ff3e /*XF86AudioRewind*/)]), + (169, &[(0, 0x1008ff6e /*XF86Phone*/)]), + (171, &[(0, 0x1008ff81 /*XF86Tools*/)]), + (172, &[(0, 0x1008ff18 /*XF86HomePage*/)]), + (173, &[(0, 0x1008ff73 /*XF86Reload*/)]), + (174, &[(0, 0x1008ff56 /*XF86Close*/)]), + (177, &[(0, 0x1008ff78 /*XF86ScrollUp*/)]), + (178, &[(0, 0x1008ff79 /*XF86ScrollDown*/)]), + (179, &[(0, 0x28 /*parenleft*/)]), + (180, &[(0, 0x29 /*parenright*/)]), + (181, &[(0, 0x1008ff68 /*XF86New*/)]), + (182, &[(0, 0xff66 /*SunAgain*/)]), + (183, &[(0, 0x1008ff81 /*XF86Tools*/)]), + (184, &[(0, 0x1008ff45 /*XF86Launch5*/)]), + (185, &[(0, 0x1008ff46 /*XF86Launch6*/)]), + (186, &[(0, 0x1008ff47 /*XF86Launch7*/)]), + (187, &[(0, 0x1008ff48 /*XF86Launch8*/)]), + (188, &[(0, 0x1008ff49 /*XF86Launch9*/)]), + (190, &[(0, 0x1008ffb2 /*XF86AudioMicMute*/)]), + (191, &[(0, 0x1008ffa9 /*XF86TouchpadToggle*/)]), + (192, &[(0, 0x1008ffb0 /*XF86TouchpadOn*/)]), + ( + 193, + &[ + (0, 0x1008ffb1 /*XF86TouchpadOff*/), + (1, 0x10081247 /*XF86Assistant*/), + ], + ), + (195, &[(0, 0xfe11 /*ISO_Level5_Shift*/)]), + (196, &[(1, 0xffe9 /*Alt_L*/)]), + (197, &[(1, 0xffe7 /*Meta_L*/)]), + (198, &[(1, 0xffeb /*Super_L*/)]), + (200, &[(0, 0x1008ff14 /*XF86AudioPlay*/)]), + (201, &[(0, 0x1008ff31 /*XF86AudioPause*/)]), + (202, &[(0, 0x1008ff43 /*XF86Launch3*/)]), + (203, &[(0, 0x1008ff44 /*XF86Launch4*/)]), + (204, &[(0, 0x1008ff4b /*XF86LaunchB*/)]), + (205, &[(0, 0x1008ffa7 /*XF86Suspend*/)]), + (206, &[(0, 0x1008ff56 /*XF86Close*/)]), + (207, &[(0, 0x1008ff14 /*XF86AudioPlay*/)]), + (208, &[(0, 0x1008ff97 /*XF86AudioForward*/)]), + (210, &[(0, 0xff61 /*SunPrint_Screen*/)]), + (212, &[(0, 0x1008ff8f /*XF86WebCam*/)]), + (213, &[(0, 0x1008ffb6 /*XF86AudioPreset*/)]), + (215, &[(0, 0x1008ff19 /*XF86Mail*/)]), + (216, &[(0, 0x1008ff8e /*XF86Messenger*/)]), + (217, &[(0, 0x1008ff1b /*XF86Search*/)]), + (218, &[(0, 0x1008ff5f /*XF86Go*/)]), + (219, &[(0, 0x1008ff3c /*XF86Finance*/)]), + (220, &[(0, 0x1008ff5e /*XF86Game*/)]), + (221, &[(0, 0x1008ff36 /*XF86Shop*/)]), + (223, &[(0, 0xff69 /*SunStop*/)]), + (224, &[(0, 0x1008ff03 /*XF86MonBrightnessDown*/)]), + (225, &[(0, 0x1008ff02 /*XF86MonBrightnessUp*/)]), + (226, &[(0, 0x1008ff32 /*XF86AudioMedia*/)]), + (227, &[(0, 0x1008ff59 /*XF86Display*/)]), + (228, &[(0, 0x1008ff04 /*XF86KbdLightOnOff*/)]), + (229, &[(0, 0x1008ff06 /*XF86KbdBrightnessDown*/)]), + (230, &[(0, 0x1008ff05 /*XF86KbdBrightnessUp*/)]), + (231, &[(0, 0x1008ff7b /*XF86Send*/)]), + (232, &[(0, 0x1008ff72 /*XF86Reply*/)]), + (233, &[(0, 0x1008ff90 /*XF86MailForward*/)]), + (234, &[(0, 0x1008ff77 /*XF86Save*/)]), + (235, &[(0, 0x1008ff5b /*XF86Documents*/)]), + (236, &[(0, 0x1008ff93 /*XF86Battery*/)]), + (237, &[(0, 0x1008ff94 /*XF86Bluetooth*/)]), + (238, &[(0, 0x1008ff95 /*XF86WLAN*/)]), + (239, &[(0, 0x1008ff96 /*XF86UWB*/)]), + (241, &[(0, 0x1008fe22 /*XF86Next_VMode*/)]), + (242, &[(0, 0x1008fe23 /*XF86Prev_VMode*/)]), + (243, &[(0, 0x1008ff07 /*XF86MonBrightnessCycle*/)]), + (244, &[(0, 0x100810f4 /*XF86MonBrightnessAuto*/)]), + (245, &[(0, 0x100810f5 /*XF86DisplayOff*/)]), + (246, &[(0, 0x1008ffb4 /*XF86WWAN*/)]), + (247, &[(0, 0x1008ffb5 /*XF86RFKill*/)]), + (248, &[(0, 0x1008ffb2 /*XF86AudioMicMute*/)]), + (352, &[(0, 0x10081160 /*XF86OK*/)]), + (353, &[(0, 0x1008ffa0 /*XF86Select*/)]), + (354, &[(0, 0x10081162 /*XF86GoTo*/)]), + (355, &[(0, 0x1008ff55 /*XF86Clear*/)]), + (357, &[(0, 0x1008ff6c /*XF86Option*/)]), + (358, &[(0, 0x10081166 /*XF86Info*/)]), + (359, &[(0, 0x1008ff9f /*XF86Time*/)]), + (360, &[(0, 0x10081168 /*XF86VendorLogo*/)]), + (362, &[(0, 0x1008116a /*XF86MediaSelectProgramGuide*/)]), + (363, &[(0, 0x10081270 /*XF86NextFavorite*/)]), + (364, &[(0, 0x1008ff30 /*XF86Favorites*/)]), + (365, &[(0, 0x1008116a /*XF86MediaSelectProgramGuide*/)]), + (366, &[(0, 0x1008116e /*XF86MediaSelectHome*/)]), + (368, &[(0, 0x10081170 /*XF86MediaLanguageMenu*/)]), + (369, &[(0, 0x10081171 /*XF86MediaTitleMenu*/)]), + (370, &[(0, 0x1008ff9a /*XF86Subtitle*/)]), + (371, &[(0, 0x1008ff9c /*XF86CycleAngle*/)]), + (372, &[(0, 0x1008ffb8 /*XF86FullScreen*/)]), + (373, &[(0, 0x10081175 /*XF86AudioChannelMode*/)]), + (374, &[(0, 0x1008ffb3 /*XF86Keyboard*/)]), + (375, &[(0, 0x10081177 /*XF86AspectRatio*/)]), + (376, &[(0, 0x10081178 /*XF86MediaSelectPC*/)]), + (377, &[(0, 0x10081179 /*XF86MediaSelectTV*/)]), + (378, &[(0, 0x1008117a /*XF86MediaSelectCable*/)]), + (379, &[(0, 0x1008117b /*XF86MediaSelectVCR*/)]), + (380, &[(0, 0x1008117c /*XF86MediaSelectVCRPlus*/)]), + (381, &[(0, 0x1008117d /*XF86MediaSelectSatellite*/)]), + (383, &[(0, 0x1008ff53 /*XF86MediaSelectCD*/)]), + (384, &[(0, 0x10081180 /*XF86MediaSelectTape*/)]), + (385, &[(0, 0x10081181 /*XF86MediaSelectRadio*/)]), + (386, &[(0, 0x10081182 /*XF86MediaSelectTuner*/)]), + (387, &[(0, 0x10081183 /*XF86MediaPlayer*/)]), + (388, &[(0, 0x10081184 /*XF86MediaSelectTeletext*/)]), + (389, &[(0, 0x10081185 /*XF86MediaSelectDVD*/)]), + (390, &[(0, 0x10081186 /*XF86MediaSelectAuxiliary*/)]), + (392, &[(0, 0x10081188 /*XF86Audio*/)]), + (393, &[(0, 0x1008ff87 /*XF86Video*/)]), + (396, &[(0, 0x1008ff1e /*XF86Memo*/)]), + (397, &[(0, 0x1008ff20 /*XF86Calendar*/)]), + (398, &[(0, 0x1008ffa3 /*XF86Red*/)]), + (399, &[(0, 0x1008ffa4 /*XF86Green*/)]), + (400, &[(0, 0x1008ffa5 /*XF86Yellow*/)]), + (401, &[(0, 0x1008ffa6 /*XF86Blue*/)]), + (402, &[(0, 0x10081192 /*XF86ChannelUp*/)]), + (403, &[(0, 0x10081193 /*XF86ChannelDown*/)]), + (409, &[(0, 0x10081199 /*XF86MediaPlaySlow*/)]), + (410, &[(0, 0x1008ff99 /*XF86AudioRandomPlay*/)]), + (411, &[(0, 0x1008119b /*XF86Break*/)]), + (413, &[(0, 0x1008119d /*XF86NumberEntryMode*/)]), + (416, &[(0, 0x100811a0 /*XF86VideoPhone*/)]), + (417, &[(0, 0x1008ff5e /*XF86Game*/)]), + (418, &[(0, 0x1008ff8b /*XF86ZoomIn*/)]), + (419, &[(0, 0x1008ff8c /*XF86ZoomOut*/)]), + (420, &[(0, 0x100811a4 /*XF86ZoomReset*/)]), + (421, &[(0, 0x1008ff89 /*XF86Word*/)]), + (422, &[(0, 0x100811a6 /*XF86Editor*/)]), + (423, &[(0, 0x1008ff5c /*XF86Excel*/)]), + (424, &[(0, 0x100811a8 /*XF86GraphicsEditor*/)]), + (425, &[(0, 0x100811a9 /*XF86Presentation*/)]), + (426, &[(0, 0x100811aa /*XF86Database*/)]), + (427, &[(0, 0x1008ff69 /*XF86News*/)]), + (428, &[(0, 0x100811ac /*XF86Voicemail*/)]), + (429, &[(0, 0x100811ad /*XF86Addressbook*/)]), + (430, &[(0, 0x1008ff8e /*XF86Messenger*/)]), + (431, &[(0, 0x100811af /*XF86DisplayToggle*/)]), + (432, &[(0, 0x100811b0 /*XF86SpellCheck*/)]), + (433, &[(0, 0x1008ff61 /*XF86LogOff*/)]), + (434, &[(0, 0x24 /*dollar*/)]), + (435, &[(0, 0x20ac /*EuroSign*/)]), + (436, &[(0, 0x1008ff9d /*XF86FrameBack*/)]), + (437, &[(0, 0x1008ff9e /*XF86FrameForward*/)]), + (438, &[(0, 0x100811b6 /*XF86ContextMenu*/)]), + (439, &[(0, 0x100811b7 /*XF86MediaRepeat*/)]), + (440, &[(0, 0x100811b8 /*XF8610ChannelsUp*/)]), + (441, &[(0, 0x100811b9 /*XF8610ChannelsDown*/)]), + (442, &[(0, 0x100811ba /*XF86Images*/)]), + (444, &[(0, 0x100811bc /*XF86NotificationCenter*/)]), + (445, &[(0, 0x100811bd /*XF86PickupPhone*/)]), + (446, &[(0, 0x100811be /*XF86HangupPhone*/)]), + (464, &[(0, 0x100811d0 /*XF86Fn*/)]), + (465, &[(0, 0x100811d1 /*XF86Fn_Esc*/)]), + (485, &[(0, 0x100811e5 /*XF86FnRightShift*/)]), + (497, &[(0, 0xfff1 /*braille_dot_1*/)]), + (498, &[(0, 0xfff2 /*braille_dot_2*/)]), + (499, &[(0, 0xfff3 /*braille_dot_3*/)]), + (500, &[(0, 0xfff4 /*braille_dot_4*/)]), + (501, &[(0, 0xfff5 /*braille_dot_5*/)]), + (502, &[(0, 0xfff6 /*braille_dot_6*/)]), + (503, &[(0, 0xfff7 /*braille_dot_7*/)]), + (504, &[(0, 0xfff8 /*braille_dot_8*/)]), + (505, &[(0, 0xfff9 /*braille_dot_9*/)]), + (506, &[(0, 0xfff1 /*braille_dot_1*/)]), + (512, &[(0, 0x10081200 /*XF86Numeric0*/)]), + (513, &[(0, 0x10081201 /*XF86Numeric1*/)]), + (514, &[(0, 0x10081202 /*XF86Numeric2*/)]), + (515, &[(0, 0x10081203 /*XF86Numeric3*/)]), + (516, &[(0, 0x10081204 /*XF86Numeric4*/)]), + (517, &[(0, 0x10081205 /*XF86Numeric5*/)]), + (518, &[(0, 0x10081206 /*XF86Numeric6*/)]), + (519, &[(0, 0x10081207 /*XF86Numeric7*/)]), + (520, &[(0, 0x10081208 /*XF86Numeric8*/)]), + (521, &[(0, 0x10081209 /*XF86Numeric9*/)]), + (522, &[(0, 0x1008120a /*XF86NumericStar*/)]), + (523, &[(0, 0x1008120b /*XF86NumericPound*/)]), + (524, &[(0, 0x1008120c /*XF86NumericA*/)]), + (525, &[(0, 0x1008120d /*XF86NumericB*/)]), + (526, &[(0, 0x1008120e /*XF86NumericC*/)]), + (527, &[(0, 0x1008120f /*XF86NumericD*/)]), + (528, &[(0, 0x10081210 /*XF86CameraFocus*/)]), + (529, &[(0, 0x10081211 /*XF86WPSButton*/)]), + (530, &[(0, 0x1008ffa9 /*XF86TouchpadToggle*/)]), + (531, &[(0, 0x1008ffb0 /*XF86TouchpadOn*/)]), + (532, &[(0, 0x1008ffb1 /*XF86TouchpadOff*/)]), + (533, &[(0, 0x10081215 /*XF86CameraZoomIn*/)]), + (534, &[(0, 0x10081216 /*XF86CameraZoomOut*/)]), + (535, &[(0, 0x10081217 /*XF86CameraUp*/)]), + (536, &[(0, 0x10081218 /*XF86CameraDown*/)]), + (537, &[(0, 0x10081219 /*XF86CameraLeft*/)]), + (538, &[(0, 0x1008121a /*XF86CameraRight*/)]), + (539, &[(0, 0x1008121b /*XF86AttendantOn*/)]), + (540, &[(0, 0x1008121c /*XF86AttendantOff*/)]), + (541, &[(0, 0x1008121d /*XF86AttendantToggle*/)]), + (542, &[(0, 0x1008121e /*XF86LightsToggle*/)]), + (560, &[(0, 0x10081230 /*XF86ALSToggle*/)]), + (561, &[(0, 0x1008ffb7 /*XF86RotationLockToggle*/)]), + (562, &[(0, 0x10081232 /*XF86RefreshRateToggle*/)]), + (576, &[(0, 0x10081240 /*XF86Buttonconfig*/)]), + (577, &[(0, 0x10081241 /*XF86Taskmanager*/)]), + (578, &[(0, 0x10081242 /*XF86Journal*/)]), + (579, &[(0, 0x10081243 /*XF86ControlPanel*/)]), + (580, &[(0, 0x10081244 /*XF86AppSelect*/)]), + (581, &[(0, 0x10081245 /*XF86Screensaver*/)]), + (582, &[(0, 0x10081246 /*XF86VoiceCommand*/)]), + (583, &[(0, 0x10081247 /*XF86Assistant*/)]), + (584, &[(0, 0xfe08 /*ISO_Next_Group*/)]), + (585, &[(0, 0x10081249 /*XF86EmojiPicker*/)]), + (586, &[(0, 0x1008124a /*XF86Dictate*/)]), + (587, &[(0, 0x1008124b /*XF86CameraAccessEnable*/)]), + (588, &[(0, 0x1008124c /*XF86CameraAccessDisable*/)]), + (589, &[(0, 0x1008124d /*XF86CameraAccessToggle*/)]), + (590, &[(0, 0x1008124e /*XF86Accessibility*/)]), + (591, &[(0, 0x1008124f /*XF86DoNotDisturb*/)]), + (592, &[(0, 0x10081250 /*XF86BrightnessMin*/)]), + (593, &[(0, 0x10081251 /*XF86BrightnessMax*/)]), + (608, &[(0, 0x10081260 /*XF86KbdInputAssistPrev*/)]), + (609, &[(0, 0x10081261 /*XF86KbdInputAssistNext*/)]), + (610, &[(0, 0x10081262 /*XF86KbdInputAssistPrevgroup*/)]), + (611, &[(0, 0x10081263 /*XF86KbdInputAssistNextgroup*/)]), + (612, &[(0, 0x10081264 /*XF86KbdInputAssistAccept*/)]), + (613, &[(0, 0x10081265 /*XF86KbdInputAssistCancel*/)]), + (614, &[(0, 0x10081266 /*XF86RightUp*/)]), + (615, &[(0, 0x10081267 /*XF86RightDown*/)]), + (616, &[(0, 0x10081268 /*XF86LeftUp*/)]), + (617, &[(0, 0x10081269 /*XF86LeftDown*/)]), + (618, &[(0, 0x1008126a /*XF86RootMenu*/)]), + (619, &[(0, 0x1008126b /*XF86MediaTopMenu*/)]), + (620, &[(0, 0x1008126c /*XF86Numeric11*/)]), + (621, &[(0, 0x1008126d /*XF86Numeric12*/)]), + (622, &[(0, 0x1008126e /*XF86AudioDesc*/)]), + (623, &[(0, 0x1008126f /*XF863DMode*/)]), + (624, &[(0, 0x10081270 /*XF86NextFavorite*/)]), + (625, &[(0, 0x10081271 /*XF86StopRecord*/)]), + (626, &[(0, 0x10081272 /*XF86PauseRecord*/)]), + (627, &[(0, 0x10081273 /*XF86VOD*/)]), + (628, &[(0, 0x10081274 /*XF86Unmute*/)]), + (629, &[(0, 0x10081275 /*XF86FastReverse*/)]), + (630, &[(0, 0x10081276 /*XF86SlowReverse*/)]), + (631, &[(0, 0x10081277 /*XF86Data*/)]), + (632, &[(0, 0x10081278 /*XF86OnScreenKeyboard*/)]), + (633, &[(0, 0x10081279 /*XF86PrivacyScreenToggle*/)]), + (634, &[(0, 0x1008127a /*XF86SelectiveScreenshot*/)]), + (635, &[(0, 0x1008127b /*XF86NextElement*/)]), + (636, &[(0, 0x1008127c /*XF86PreviousElement*/)]), + (637, &[(0, 0x1008127d /*XF86AutopilotEngageToggle*/)]), + (638, &[(0, 0x1008127e /*XF86MarkWaypoint*/)]), + (639, &[(0, 0x1008127f /*XF86Sos*/)]), + (640, &[(0, 0x10081280 /*XF86NavChart*/)]), + (641, &[(0, 0x10081281 /*XF86FishingChart*/)]), + (642, &[(0, 0x10081282 /*XF86SingleRangeRadar*/)]), + (643, &[(0, 0x10081283 /*XF86DualRangeRadar*/)]), + (644, &[(0, 0x10081284 /*XF86RadarOverlay*/)]), + (645, &[(0, 0x10081285 /*XF86TraditionalSonar*/)]), + (646, &[(0, 0x10081286 /*XF86ClearvuSonar*/)]), + (647, &[(0, 0x10081287 /*XF86SidevuSonar*/)]), + (648, &[(0, 0x10081288 /*XF86NavInfo*/)]), + (649, &[(0, 0x1008ff3b /*XF86BrightnessAdjust*/)]), + (656, &[(0, 0x10081290 /*XF86Macro1*/)]), + (657, &[(0, 0x10081291 /*XF86Macro2*/)]), + (658, &[(0, 0x10081292 /*XF86Macro3*/)]), + (659, &[(0, 0x10081293 /*XF86Macro4*/)]), + (660, &[(0, 0x10081294 /*XF86Macro5*/)]), + (661, &[(0, 0x10081295 /*XF86Macro6*/)]), + (662, &[(0, 0x10081296 /*XF86Macro7*/)]), + (663, &[(0, 0x10081297 /*XF86Macro8*/)]), + (664, &[(0, 0x10081298 /*XF86Macro9*/)]), + (665, &[(0, 0x10081299 /*XF86Macro10*/)]), + (666, &[(0, 0x1008129a /*XF86Macro11*/)]), + (667, &[(0, 0x1008129b /*XF86Macro12*/)]), + (668, &[(0, 0x1008129c /*XF86Macro13*/)]), + (669, &[(0, 0x1008129d /*XF86Macro14*/)]), + (670, &[(0, 0x1008129e /*XF86Macro15*/)]), + (671, &[(0, 0x1008129f /*XF86Macro16*/)]), + (672, &[(0, 0x100812a0 /*XF86Macro17*/)]), + (673, &[(0, 0x100812a1 /*XF86Macro18*/)]), + (674, &[(0, 0x100812a2 /*XF86Macro19*/)]), + (675, &[(0, 0x100812a3 /*XF86Macro20*/)]), + (676, &[(0, 0x100812a4 /*XF86Macro21*/)]), + (677, &[(0, 0x100812a5 /*XF86Macro22*/)]), + (678, &[(0, 0x100812a6 /*XF86Macro23*/)]), + (679, &[(0, 0x100812a7 /*XF86Macro24*/)]), + (680, &[(0, 0x100812a8 /*XF86Macro25*/)]), + (681, &[(0, 0x100812a9 /*XF86Macro26*/)]), + (682, &[(0, 0x100812aa /*XF86Macro27*/)]), + (683, &[(0, 0x100812ab /*XF86Macro28*/)]), + (684, &[(0, 0x100812ac /*XF86Macro29*/)]), + (685, &[(0, 0x100812ad /*XF86Macro30*/)]), + (688, &[(0, 0x100812b0 /*XF86MacroRecordStart*/)]), + (689, &[(0, 0x100812b1 /*XF86MacroRecordStop*/)]), + (690, &[(0, 0x100812b2 /*XF86MacroPresetCycle*/)]), + (691, &[(0, 0x100812b3 /*XF86MacroPreset1*/)]), + (692, &[(0, 0x100812b4 /*XF86MacroPreset2*/)]), + (693, &[(0, 0x100812b5 /*XF86MacroPreset3*/)]), + (696, &[(0, 0x100812b8 /*XF86KbdLcdMenu1*/)]), + (697, &[(0, 0x100812b9 /*XF86KbdLcdMenu2*/)]), + (698, &[(0, 0x100812ba /*XF86KbdLcdMenu3*/)]), + (699, &[(0, 0x100812bb /*XF86KbdLcdMenu4*/)]), + (700, &[(0, 0x100812bc /*XF86KbdLcdMenu5*/)]), +]; + +/// Check if (evdev, level, char) matches an invariant entry. +/// Returns true if this entry should be omitted from RON. +/// A key is invariant if: +/// - It matches an entry in INVARIANT_CHARS at (evdev, level, char), OR +/// - It's in INVARIANT_CHARS at level 0 and the char matches level 0's char +/// (key produces same char at all levels → invariant at all levels) +fn is_invariant_char(evdev: u32, level: u8, ch: char) -> bool { + for &(e, levels) in INVARIANT_CHARS { + if e == evdev { + // Direct match + if levels.iter().any(|&(l, c)| l == level && c == ch) { + return true; + } + // If level-0 char matches and this key is in the table, it's invariant at all levels + if let Some(&(_, l0_ch)) = levels.iter().find(|&&(l, _)| l == 0) { + if ch == l0_ch { + return true; + } + } + return false; + } + } + false +} + +/// Check if (evdev, level, keysym) matches an invariant keysym entry. +/// Only exact (evdev, level, sym) matches are filtered — no level-0 extrapolation +/// because XKB doesn't necessarily define keysyms at all levels. +fn is_invariant_keysym(evdev: u32, level: u8, sym: u32) -> bool { + for &(e, levels) in INVARIANT_KEYSYMS { + if e == evdev { + return levels.iter().any(|&(l, s)| l == level && s == sym); + } + } + false +} + +/// Default repeat keys — based on the standard XKB repeat set shared by >99% of layouts. +/// In .ron files only additions/removals from this set are stored. +#[rustfmt::skip] +static DEFAULT_REPEAT_KEYS: &[u32] = &[ + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, + 30,31,32,33,34,35,36,37,38,39,40,41,43,44,45,46,47,48,49,50,51,52,53,55,57, + 59,60,61,62,63,64,65,66,67,68,70,71,72,73,74,75,76,77,78,79,80,81,82,83, + 85,86,87,88,90,91,92,93,94,96,98,99,101,102,103,104,105,106,107,108,109,110,111, + 113,114,115,116,117,118,119,120,121,122,123,127,128,129,130,131,132,133,134,135, + 136,137,138,139,140,142,143,144,145,147,148,149,150,151,152,153,154,155,156,157, + 158,159,161,162,163,164,165,166,167,168,169,171,172,173,174,177,178,179,180,181, + 182,183,184,185,186,187,188,190,191,192,193,200,201,202,203,204,205,206,207,208, + 210,212,213,215,216,217,218,219,220,221,223,224,225,226,227,228,229,230,231,232, + 233,234,235,236,237,238,239,241,242,243,244,245,246,247,248,352,353,354,355,357, + 358,359,360,362,363,364,365,366,368,369,370,371,372,373,374,375,376,377,378,379, + 380,381,383,384,385,386,387,388,389,390,392,393,396,397,398,399,400,401,402,403, + 409,410,411,413,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431, + 432,433,434,435,436,437,438,439,440,441,442,444,445,446,464,465,485,497,498,499, + 500,501,502,503,504,505,506,512,513,514,515,516,517,518,519,520,521,522,523,524, + 525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,560,561, + 562,576,577,578,579,580,581,582,583,585,586,587,588,589,590,591,592,593,608,609, + 610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629, + 630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649, + 656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675, + 676,677,678,679,680,681,682,683,684,685,688,689,690,691,692,693,696,697,698,699,700, +]; + +/// Modifier kind without runtime state (pressed/locked/latched are always default in saved files). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) enum ReadableModKind { + Pressed(ModType), + Lock(ModType), + Latch(ModType), +} + +/// Well-known evdev code → human-readable modifier key name. +/// Falls back to the keysym name (friendly-translated) if provided. +fn modifier_key_name(evdev: u32, keysym_name: Option<&str>) -> String { + match evdev { + 29 => "LeftCtrl".into(), + 42 => "LeftShift".into(), + 54 => "RightShift".into(), + 56 => keysym_name.unwrap_or("LeftAlt").into(), + 58 => keysym_name.unwrap_or("CapsLock").into(), + 69 => "NumLock".into(), + 70 => "ScrollLock".into(), + 97 => "RightCtrl".into(), + 100 => keysym_name.unwrap_or("AltGr").into(), + 125 => keysym_name.unwrap_or("LeftSuper").into(), + other => match keysym_name { + Some(name) => name.to_string(), + None => format!("Key{}", other), + }, + } +} + +/// Human-readable intermediate format for RON serialization. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct ReadableWKB { + layout_names: Vec, + num_keys: usize, + repeat_keys_add: Vec, + repeat_keys_remove: Vec, + modifiers: Vec<(u32, String, Vec<(u8, ReadableModKind)>)>, + keymap: BTreeMap>>, + num_lock_keys: BTreeMap>>, + caps_lock_keymap: BTreeMap>>, + #[cfg(feature = "xkb")] + level_exceptions_keymap: BTreeMap>>, + keysym_map: BTreeMap>>, + compose: Vec<(Vec, char)>, +} + +// ── KeyBitSet ↔ Vec ────────────────────────────────────────────── + +fn bitset_to_vec(bs: &KeyBitSet) -> Vec { + let mut out = Vec::new(); + for code in 0u32..(BITSET_WORDS as u32 * 64) { + if bs.contains(code) { + out.push(code); + } + } + out +} + +fn flat_keymap_to_map( + km: &FlatKeymap, + layout_names: &[String], + omit_invariants: bool, + reachable: Option<&[usize]>, +) -> BTreeMap>> { + let mut result = BTreeMap::new(); + for (li, name) in layout_names.iter().enumerate() { + let mut levels = BTreeMap::new(); + let level_iter: Vec = match reachable { + Some(r) => r.to_vec(), + None => (0..MAX_LEVELS).collect(), + }; + for level in level_iter { + let mut keys = BTreeMap::new(); + for evdev in 0..km.num_keys as u32 { + if let Some(ch) = km.get(li, level, evdev) { + if omit_invariants { + // Skip entries matching the invariant char table + if is_invariant_char(evdev, level as u8, ch) { + continue; + } + } + keys.insert(evdev, ch); + } + } + if !keys.is_empty() { + levels.insert(level as u8, keys); + } + } + if !levels.is_empty() { + result.insert(name.clone(), levels); + } + } + result +} + +fn map_to_flat_keymap( + map: &BTreeMap>>, + layout_names: &[String], + num_keys: usize, +) -> FlatKeymap { + let num_layouts = layout_names.len(); + let mut km = FlatKeymap::new(num_keys, num_layouts); + + for (name, levels) in map { + if let Some(li) = layout_names.iter().position(|n| n == name) { + for (&level, keys) in levels { + for (&evdev, &ch) in keys { + km.set(li, level as usize, evdev, ch); + } + } + } + } + km +} + +fn map_to_flat_keymap_with_defaults( + map: &BTreeMap>>, + layout_names: &[String], + num_keys: usize, +) -> FlatKeymap { + let mut km = map_to_flat_keymap(map, layout_names, num_keys); + // Restore invariant chars for keys omitted from RON. + // Only populate at levels that exist in the RON for this layout + // (i.e., levels that are reachable for the keyboard). + for (li, name) in layout_names.iter().enumerate() { + let active_levels: Vec = map + .get(name) + .map(|levels| levels.keys().copied().collect()) + .unwrap_or_default(); + // Always include level 0 + let mut lvls: Vec = active_levels; + if !lvls.contains(&0) { + lvls.push(0); + } + + for &(evdev, levels) in INVARIANT_CHARS { + if (evdev as usize) >= num_keys { + continue; + } + let l0_ch = levels.iter().find(|&&(l, _)| l == 0).map(|&(_, c)| c); + // Populate at all active levels with level-0 char + if let Some(ch) = l0_ch { + for &lvl in &lvls { + if km.get(li, lvl as usize, evdev).is_none() { + km.set(li, lvl as usize, evdev, ch); + } + } + } + // Apply specific level overrides + for &(level, ch) in levels { + if lvls.contains(&level) && km.get(li, level as usize, evdev).is_none() { + km.set(li, level as usize, evdev, ch); + } + } + } + } + km +} + +fn keysym_name(sym: u32) -> String { + if sym == 0 { + return "NoSymbol".to_string(); + } + if (0x0100_0000..=0x0110_ffff).contains(&sym) { + return format!("{:#010x}", sym); + } + if let Some(name) = crate::keysyms::keysym_get_name(sym) { + name.to_string() + } else { + format!("{:#010x}", sym) + } +} + +fn keysym_from_name_or_hex(name: &str) -> u32 { + if let Some(sym) = crate::keysyms::keysym_from_name(name) { + return sym; + } + // Try hex literal (0x01000042 etc.) + if let Some(hex) = name.strip_prefix("0x") { + if let Ok(v) = u32::from_str_radix(hex, 16) { + return v; + } + } + 0 +} + +fn flat_keysym_map_to_map( + km: &crate::FlatKeysymMap, + layout_names: &[String], +) -> BTreeMap>> { + let mut result = BTreeMap::new(); + for (li, name) in layout_names.iter().enumerate() { + let mut levels = BTreeMap::new(); + for level in 0..MAX_LEVELS { + let mut keys = BTreeMap::new(); + for evdev in 0..km.num_keys as u32 { + let sym = km.get(li, level, evdev); + if sym != 0 { + // Skip invariant keysyms + if is_invariant_keysym(evdev, level as u8, sym) { + continue; + } + keys.insert(evdev, keysym_name(sym)); + } + } + if !keys.is_empty() { + levels.insert(level as u8, keys); + } + } + if !levels.is_empty() { + result.insert(name.clone(), levels); + } + } + result +} + +fn map_to_flat_keysym_map( + map: &BTreeMap>>, + layout_names: &[String], + num_keys: usize, +) -> crate::FlatKeysymMap { + let num_layouts = layout_names.len(); + let mut km = crate::FlatKeysymMap::new(num_keys, num_layouts); + // First apply RON data + for (name, levels) in map { + if let Some(li) = layout_names.iter().position(|n| n == name) { + for (&level, keys) in levels { + for (&evdev, sym_name) in keys { + let sym = keysym_from_name_or_hex(sym_name); + if sym != 0 { + km.set(li, level as usize, evdev, sym); + } + } + } + } + } + // Restore invariant keysyms only at their specific listed levels. + // These are keysyms present in ALL layouts, so restore unconditionally. + for li in 0..num_layouts { + for &(evdev, levels) in INVARIANT_KEYSYMS { + if (evdev as usize) >= num_keys { + continue; + } + for &(level, sym) in levels { + if km.get(li, level as usize, evdev) == 0 { + km.set(li, level as usize, evdev, sym); + } + } + } + } + km +} + +// ── Friendly keysym name aliases ────────────────────────────────────── +// Replace XKB-internal names with user-friendly equivalents. + +/// XKB keysym name → friendly name for RON output. +fn friendly_keysym_name(xkb_name: &str) -> &str { + match xkb_name { + // Modifier keys + "Shift_L" => "LeftShift", + "Shift_R" => "RightShift", + "Control_L" => "LeftCtrl", + "Control_R" => "RightCtrl", + "Alt_L" => "LeftAlt", + "Alt_R" => "RightAlt", + "Super_L" => "LeftSuper", + "Super_R" => "RightSuper", + "Meta_L" => "LeftMeta", + "Meta_R" => "RightMeta", + "Hyper_L" => "LeftHyper", + "Hyper_R" => "RightHyper", + "Caps_Lock" => "CapsLock", + "Num_Lock" => "NumLock", + "Scroll_Lock" => "ScrollLock", + // ISO level modifiers + "ISO_Level2_Latch" => "ShiftLatch", + "ISO_Level3_Shift" => "AltGr", + "ISO_Level3_Latch" => "AltGrLatch", + "ISO_Level3_Lock" => "AltGrLock", + "ISO_Level5_Shift" => "Level5", + "ISO_Level5_Latch" => "Level5Latch", + "ISO_Level5_Lock" => "Level5Lock", + // ISO group switching + "ISO_Next_Group" => "NextLayout", + "ISO_Prev_Group" => "PrevLayout", + "ISO_Group_Shift" => "LayoutShift", + "ISO_Group_Latch" => "LayoutLatch", + "ISO_Group_Lock" => "LayoutLock", + "ISO_First_Group" => "FirstLayout", + "ISO_First_Group_Lock" => "FirstLayoutLock", + "ISO_Last_Group" => "LastLayout", + "ISO_Last_Group_Lock" => "LastLayoutLock", + "ISO_Next_Group_Lock" => "NextLayoutLock", + "ISO_Prev_Group_Lock" => "PrevLayoutLock", + // Other ISO + "ISO_Left_Tab" => "LeftTab", + "ISO_Enter" => "Enter", + "ISO_Lock" => "Lock", + // Navigation + "KP_Enter" => "NumpadEnter", + "KP_Add" => "NumpadAdd", + "KP_Subtract" => "NumpadSubtract", + "KP_Multiply" => "NumpadMultiply", + "KP_Divide" => "NumpadDivide", + "KP_Decimal" => "NumpadDecimal", + "KP_Separator" => "NumpadSeparator", + "KP_Equal" => "NumpadEqual", + _ => xkb_name, + } +} + +// ── Composer ↔ sequence list ────────────────────────────────────────── + +fn composer_to_sequences(composer: &Composer) -> Vec<(Vec, char)> { + let mut sequences = Vec::new(); + let mut path = Vec::new(); + walk_trie(&composer.nodes, 0, &mut path, &mut sequences); + sequences +} + +fn walk_trie( + nodes: &[TrieNode], + idx: usize, + path: &mut Vec, + out: &mut Vec<(Vec, char)>, +) { + let node = &nodes[idx]; + if let Some(ch) = node.emit { + out.push((path.clone(), ch)); + } + for &(key, child_idx) in &node.children { + let token_str = if key == 0 { + "·".to_string() + } else if let Some(c) = char::from_u32(key) { + c.to_string() + } else { + format!("U+{:04X}", key) + }; + path.push(token_str); + walk_trie(nodes, child_idx as usize, path, out); + path.pop(); + } +} + +fn sequences_to_composer(sequences: &[(Vec, char)]) -> Composer { + let mut composer = Composer::new(); + for (seq, output) in sequences { + let tokens: Vec = seq + .iter() + .map(|s| { + if s == "·" { + Token::Compose + } else { + let mut chars = s.chars(); + let c = chars.next().unwrap_or('\0'); + Token::Char(c) + } + }) + .collect(); + composer.insert(&tokens, *output); + } + composer +} + +// ── Modifiers ↔ readable representation ────────────────────────────── + +fn mod_kind_to_readable(mk: &ModKind) -> ReadableModKind { + match mk { + ModKind::Pressed { mod_type, .. } => ReadableModKind::Pressed(*mod_type), + ModKind::Lock { mod_type, .. } => ReadableModKind::Lock(*mod_type), + ModKind::Latch { mod_type, .. } => ReadableModKind::Latch(*mod_type), + ModKind::None => ReadableModKind::Pressed(ModType::None), + } +} + +fn readable_to_mod_kind(rmk: &ReadableModKind) -> ModKind { + match rmk { + ReadableModKind::Pressed(mt) => ModKind::Pressed { + pressed: false, + mod_type: *mt, + }, + ReadableModKind::Lock(mt) => ModKind::Lock { + pressed: false, + locked: 0, + mod_type: *mt, + }, + ReadableModKind::Latch(mt) => ModKind::Latch { + pressed: false, + latched: false, + mod_type: *mt, + }, + } +} + +fn modifiers_to_readable( + mods: &Modifiers, + wkb: &crate::WKB, +) -> Vec<(u32, String, Vec<(u8, ReadableModKind)>)> { + let mut out = Vec::new(); + for (evdev, modifier) in mods.iter() { + // Skip modifiers that have no level effect (Pressed(None), ModKind::None) + let is_all_none = match modifier { + Modifier::Single(mk) => matches!( + mk, + ModKind::None + | ModKind::Pressed { + mod_type: ModType::None, + .. + } + ), + Modifier::Leveled(map) => map.values().all(|mk| { + matches!( + mk, + ModKind::None + | ModKind::Pressed { + mod_type: ModType::None, + .. + } + ) + }), + }; + if is_all_none { + continue; + } + // For unknown keys, derive keysym at level 0 layout 0 and use friendly name + let keysym_fallback = { + let sym = wkb.level_keysym(*evdev, 0, 0); + if sym != 0 { + keysyms::keysym_get_name(sym).map(|n| friendly_keysym_name(n).to_string()) + } else { + None + } + }; + let name = modifier_key_name(*evdev, keysym_fallback.as_deref()); + let entries = match modifier { + Modifier::Single(mk) => vec![(0u8, mod_kind_to_readable(mk))], + Modifier::Leveled(map) => map + .iter() + .map(|(&level, mk)| (level, mod_kind_to_readable(mk))) + .collect(), + }; + out.push((*evdev, name, entries)); + } + out +} + +fn readable_to_modifiers(entries: &[(u32, String, Vec<(u8, ReadableModKind)>)]) -> Modifiers { + let mut mods = Modifiers::new(); + for (evdev, _name, kinds) in entries { + let modifier = if kinds.len() == 1 && kinds[0].0 == 0 { + Modifier::Single(readable_to_mod_kind(&kinds[0].1)) + } else { + let map: BTreeMap = kinds + .iter() + .map(|(level, rmk)| (*level, readable_to_mod_kind(rmk))) + .collect(); + Modifier::Leveled(map) + }; + mods.set_modifier(*evdev, modifier); + } + mods +} + +// ── WKB ↔ ReadableWKB ──────────────────────────────────────────────── + +impl ReadableWKB { + pub(crate) fn from_wkb(wkb: &crate::WKB) -> Self { + let actual = bitset_to_vec(&wkb.repeat_keys); + let default_set: std::collections::HashSet = + DEFAULT_REPEAT_KEYS.iter().copied().collect(); + let actual_set: std::collections::HashSet = actual.iter().copied().collect(); + let mut repeat_keys_add: Vec = actual_set.difference(&default_set).copied().collect(); + let mut repeat_keys_remove: Vec = + default_set.difference(&actual_set).copied().collect(); + repeat_keys_add.sort(); + repeat_keys_remove.sort(); + + // Compute reachable levels from modifier keys. + // Level index is a 3-bit field: bit0=Shift, bit1=Level3(AltGr), bit2=Level5. + let has_mod = |target: crate::modifiers::ModType| -> bool { + wkb.modifiers.entries.iter().any(|(_, modifier)| { + let mod_kind_has = |mk: &crate::modifiers::ModKind| -> bool { + match mk { + crate::modifiers::ModKind::Pressed { mod_type, .. } + | crate::modifiers::ModKind::Lock { mod_type, .. } + | crate::modifiers::ModKind::Latch { mod_type, .. } => *mod_type == target, + crate::modifiers::ModKind::None => false, + } + }; + match modifier { + crate::modifiers::Modifier::Single(mk) => mod_kind_has(mk), + crate::modifiers::Modifier::Leveled(map) => map.values().any(mod_kind_has), + } + }) + }; + let has_level3 = has_mod(crate::modifiers::ModType::Level3); + let has_level5 = has_mod(crate::modifiers::ModType::Level5); + let reachable: Vec = (0..MAX_LEVELS) + .filter(|&lvl| (lvl & 2 == 0 || has_level3) && (lvl & 4 == 0 || has_level5)) + .collect(); + + ReadableWKB { + layout_names: wkb.layout_names.clone(), + num_keys: wkb.state_keymap.num_keys, + repeat_keys_add, + repeat_keys_remove, + modifiers: modifiers_to_readable(&wkb.modifiers, wkb), + keymap: flat_keymap_to_map( + &wkb.state_keymap, + &wkb.layout_names, + true, + Some(&reachable), + ), + num_lock_keys: flat_keymap_to_map( + &wkb.num_lock_keys, + &wkb.layout_names, + false, + Some(&reachable), + ), + caps_lock_keymap: flat_keymap_to_map( + &wkb.caps_lock_keymap, + &wkb.layout_names, + false, + Some(&reachable), + ), + #[cfg(feature = "xkb")] + level_exceptions_keymap: flat_keymap_to_map( + &wkb.level_exceptions_keymap, + &wkb.layout_names, + false, + Some(&reachable), + ), + keysym_map: flat_keysym_map_to_map(&wkb.keysym_map, &wkb.layout_names), + compose: composer_to_sequences(&wkb.composer), + } + } + + pub(crate) fn to_wkb(self) -> crate::WKB { + let layout_names = self.layout_names; + let num_keys = self.num_keys; + let state_keymap = map_to_flat_keymap_with_defaults(&self.keymap, &layout_names, num_keys); + let num_lock_keys = map_to_flat_keymap(&self.num_lock_keys, &layout_names, num_keys); + let caps_lock_keymap = map_to_flat_keymap(&self.caps_lock_keymap, &layout_names, num_keys); + #[cfg(feature = "xkb")] + let level_exceptions_keymap = + map_to_flat_keymap(&self.level_exceptions_keymap, &layout_names, num_keys); + let modifiers = readable_to_modifiers(&self.modifiers); + + // Reconstruct repeat keys from defaults + add/remove diffs + let mut repeat_set: std::collections::HashSet = + DEFAULT_REPEAT_KEYS.iter().copied().collect(); + for &code in &self.repeat_keys_add { + repeat_set.insert(code); + } + for &code in &self.repeat_keys_remove { + repeat_set.remove(&code); + } + let mut repeat_keys = KeyBitSet::new(); + for code in repeat_set { + repeat_keys.insert(code); + } + let keysym_map = map_to_flat_keysym_map(&self.keysym_map, &layout_names, num_keys); + let wkb = crate::WKB { + repeat_keys, + composer: sequences_to_composer(&self.compose), + modifiers, + state_keymap, + num_lock_keys, + caps_lock_keymap, + current_layout_idx: 0, + layout_names, + keysym_map, + #[cfg(feature = "xkb")] + level_exceptions_keymap, + }; + wkb + } +} + +// ── Custom compact RON formatter ───────────────────────────────────── + +/// Keyboard visual groups for formatting. Each group is a named set of evdev codes. +struct KeyGroup { + codes: &'static [u32], +} + +// Main keyboard rows +const ROW_NUMBER: KeyGroup = KeyGroup { + codes: &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], +}; +const ROW_QWERTY: KeyGroup = KeyGroup { + codes: &[15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], +}; +const ROW_HOME: KeyGroup = KeyGroup { + codes: &[29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 43], +}; +const ROW_BOTTOM: KeyGroup = KeyGroup { + codes: &[42, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54], +}; +const ROW_SPACE: KeyGroup = KeyGroup { + codes: &[56, 57, 86, 100, 125], +}; + +// Function keys +const ROW_FKEYS: KeyGroup = KeyGroup { + codes: &[59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 87, 88], +}; + +// Navigation cluster +const ROW_NAV_TOP: KeyGroup = KeyGroup { + codes: &[110, 102, 104], // Insert, Home, PageUp +}; +const ROW_NAV_BOT: KeyGroup = KeyGroup { + codes: &[111, 107, 109], // Delete, End, PageDown +}; +const ROW_ARROWS: KeyGroup = KeyGroup { + codes: &[103, 105, 108, 106], // Up, Left, Down, Right +}; + +// Numpad +const ROW_NUMPAD_TOP: KeyGroup = KeyGroup { + codes: &[69, 98, 55, 74], // NumLock, KP/, KP*, KP- +}; +const ROW_NUMPAD_789: KeyGroup = KeyGroup { + codes: &[71, 72, 73], // KP7, KP8, KP9 +}; +const ROW_NUMPAD_456: KeyGroup = KeyGroup { + codes: &[75, 76, 77, 78], // KP4, KP5, KP6, KP+ +}; +const ROW_NUMPAD_123: KeyGroup = KeyGroup { + codes: &[79, 80, 81], // KP1, KP2, KP3 +}; +const ROW_NUMPAD_0: KeyGroup = KeyGroup { + codes: &[82, 83, 96], // KP0, KP., KPEnter +}; + +// Misc keys +const ROW_MISC: KeyGroup = KeyGroup { + codes: &[70, 99, 101, 117, 118, 119, 121], // ScrollLock, SysRq, LineFeed, KP=, KP±, Pause, KPComma +}; + +const ALL_GROUPS: &[&KeyGroup] = &[ + &ROW_NUMBER, + &ROW_QWERTY, + &ROW_HOME, + &ROW_BOTTOM, + &ROW_SPACE, + &ROW_FKEYS, + &ROW_NAV_TOP, + &ROW_NAV_BOT, + &ROW_ARROWS, + &ROW_NUMPAD_TOP, + &ROW_NUMPAD_789, + &ROW_NUMPAD_456, + &ROW_NUMPAD_123, + &ROW_NUMPAD_0, + &ROW_MISC, +]; + +/// Check which group an evdev code belongs to. +fn key_group_for(evdev: u32) -> Option { + for (i, group) in ALL_GROUPS.iter().enumerate() { + if group.codes.contains(&evdev) { + return Some(i); + } + } + None +} + +/// Escape a char for RON char literal (single-quoted). +fn ron_char(c: char) -> String { + match c { + '\'' => "'\\''".to_string(), + '\\' => "'\\\\'".to_string(), + '\t' => "'\\t'".to_string(), + '\n' => "'\\n'".to_string(), + '\r' => "'\\r'".to_string(), + '\0' => "'\\0'".to_string(), + c if c.is_control() => format!("'\\u{{{:04X}}}'", c as u32), + c => format!("'{}'", c), + } +} + +/// Escape a string for RON string literal (double-quoted). +fn ron_string(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + out.push('"'); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\t' => out.push_str("\\t"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + c if c.is_control() => { + use std::fmt::Write; + let _ = write!(out, "\\u{{{:04X}}}", c as u32); + } + c => out.push(c), + } + } + out.push('"'); + out +} + +impl ReadableWKB { + /// Format as a compact, human-readable RON string. + pub(crate) fn format_ron(&self) -> String { + use std::fmt::Write; + let mut out = String::new(); + out.push_str("(\n"); + + // layout_names + out.push_str(" layout_names: ["); + for (i, name) in self.layout_names.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push_str(&ron_string(name)); + } + out.push_str("],\n"); + + // num_keys + let _ = writeln!(out, " num_keys: {},", self.num_keys); + + // repeat_keys — add/remove diffs from built-in defaults + if !self.repeat_keys_add.is_empty() { + out.push_str(" repeat_keys_add: [\n"); + Self::write_u32_list_by_group(&mut out, &self.repeat_keys_add); + out.push_str(" ],\n"); + } else { + out.push_str(" repeat_keys_add: [],\n"); + } + if !self.repeat_keys_remove.is_empty() { + out.push_str(" repeat_keys_remove: [\n"); + Self::write_u32_list_by_group(&mut out, &self.repeat_keys_remove); + out.push_str(" ],\n"); + } else { + out.push_str(" repeat_keys_remove: [],\n"); + } + + // modifiers — one entry per line + out.push_str(" modifiers: [\n"); + for (evdev, name, kinds) in &self.modifiers { + let _ = write!(out, " ({}, {}, [", evdev, ron_string(name)); + for (i, (level, rmk)) in kinds.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + let kind_str = match rmk { + ReadableModKind::Pressed(mt) => format!("({}, Pressed({:?}))", level, mt), + ReadableModKind::Lock(mt) => format!("({}, Lock({:?}))", level, mt), + ReadableModKind::Latch(mt) => format!("({}, Latch({:?}))", level, mt), + }; + out.push_str(&kind_str); + } + out.push_str("]),\n"); + } + out.push_str(" ],\n"); + + // keymap + Self::write_char_map(&mut out, "keymap", &self.keymap); + + // num_lock_keys + Self::write_char_map(&mut out, "num_lock_keys", &self.num_lock_keys); + + // caps_lock_keymap + Self::write_char_map(&mut out, "caps_lock_keymap", &self.caps_lock_keymap); + + // level_exceptions_keymap (diff only) + #[cfg(feature = "xkb")] + Self::write_char_map( + &mut out, + "level_exceptions_keymap", + &self.level_exceptions_keymap, + ); + + // keysym_map + Self::write_keysym_map(&mut out, "keysym_map", &self.keysym_map); + + // compose — one sequence per line + out.push_str(" compose: [\n"); + for (seq, ch) in &self.compose { + out.push_str(" (["); + for (i, s) in seq.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + out.push_str(&ron_string(s)); + } + out.push_str("], "); + out.push_str(&ron_char(*ch)); + out.push_str("),\n"); + } + out.push_str(" ],\n"); + + out.push_str(")\n"); + out + } + + /// Write a list of u32 values grouped by keyboard regions. + fn write_u32_list_by_group(out: &mut String, values: &[u32]) { + use std::fmt::Write; + let num_groups = ALL_GROUPS.len(); + let mut groups: Vec> = vec![Vec::new(); num_groups]; + let mut overflow: Vec = Vec::new(); + + for &v in values { + if let Some(g) = key_group_for(v) { + groups[g].push(v); + } else { + overflow.push(v); + } + } + + for group in &groups { + if group.is_empty() { + continue; + } + out.push_str(" "); + for (i, &v) in group.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + let _ = write!(out, "{}", v); + } + out.push_str(",\n"); + } + if !overflow.is_empty() { + // Break overflow into lines of ~20 values + for chunk in overflow.chunks(20) { + out.push_str(" "); + for (i, &v) in chunk.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + let _ = write!(out, "{}", v); + } + out.push_str(",\n"); + } + } + } + + /// Write a char-valued keymap field with keyboard-group visual layout. + fn write_char_map( + out: &mut String, + field: &str, + map: &BTreeMap>>, + ) { + use std::fmt::Write; + let _ = writeln!(out, " {}: {{", field); + for (layout, levels) in map { + let _ = writeln!(out, " {}: {{", ron_string(layout)); + for (&level, keys) in levels { + let _ = write!(out, " {}: {{", level); + Self::write_char_entries_by_group(out, keys); + out.push_str(" },\n"); + } + out.push_str(" },\n"); + } + out.push_str(" },\n"); + } + + fn write_keysym_map( + out: &mut String, + field: &str, + map: &BTreeMap>>, + ) { + use std::fmt::Write; + let _ = writeln!(out, " {}: {{", field); + for (layout, levels) in map { + let _ = writeln!(out, " {}: {{", ron_string(layout)); + for (&level, keys) in levels { + let _ = write!(out, " {}: {{", level); + if keys.is_empty() { + out.push('\n'); + } else { + out.push('\n'); + // Group entries for readability, 10 per line + let entries: Vec<_> = keys.iter().collect(); + for chunk in entries.chunks(10) { + out.push_str(" "); + for (i, (&evdev, sym_name)) in chunk.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + let _ = write!(out, "{}: {}", evdev, ron_string(sym_name)); + } + out.push_str(",\n"); + } + } + out.push_str(" },\n"); + } + out.push_str(" },\n"); + } + out.push_str(" },\n"); + } + + /// Write a keysym-valued map field with keyboard-group visual layout. + /// Write BTreeMap entries grouped by keyboard visual layout. + fn write_char_entries_by_group(out: &mut String, keys: &BTreeMap) { + use std::fmt::Write; + if keys.is_empty() { + out.push('\n'); + return; + } + + let num_groups = ALL_GROUPS.len(); + let mut groups: Vec> = vec![Vec::new(); num_groups]; + let mut overflow: Vec<(u32, char)> = Vec::new(); + + for (&evdev, &ch) in keys { + if let Some(g) = key_group_for(evdev) { + groups[g].push((evdev, ch)); + } else { + overflow.push((evdev, ch)); + } + } + + out.push('\n'); + + // Track which section we're in for blank-line separators + // Groups 0-4: main keyboard, 5: fkeys, 6-8: nav, 9-13: numpad, 14: misc + let mut last_section = None; + for (gi, group) in groups.iter().enumerate() { + if group.is_empty() { + continue; + } + let section = if gi <= 4 { + 0 // main + } else if gi == 5 { + 1 // fkeys + } else if gi <= 8 { + 2 // nav + } else if gi <= 13 { + 3 // numpad + } else { + 4 // misc + }; + if let Some(prev) = last_section { + if section != prev { + // Empty line between sections + out.push('\n'); + } + } + last_section = Some(section); + + out.push_str(" "); + for (i, (evdev, ch)) in group.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + let _ = write!(out, "{}: {}", evdev, ron_char(*ch)); + } + out.push_str(",\n"); + } + if !overflow.is_empty() { + if last_section.is_some() { + out.push('\n'); + } + for chunk in overflow.chunks(15) { + out.push_str(" "); + for (i, (evdev, ch)) in chunk.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + let _ = write!(out, "{}: {}", evdev, ron_char(*ch)); + } + out.push_str(",\n"); + } + } + } +} + +impl crate::WKB { + /// Serialize this WKB instance to a human-readable RON string. + pub fn to_ron(&self) -> Result { + let readable = ReadableWKB::from_wkb(self); + Ok(readable.format_ron()) + } + + /// Deserialize a WKB instance from a RON string. + pub fn from_ron(s: &str) -> Result { + let readable: ReadableWKB = ron::from_str(s)?; + Ok(readable.to_wkb()) + } +} diff --git a/src/testing.rs b/src/testing.rs index 13414ef3..53c82da4 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -70,6 +70,7 @@ pub trait WKBTestExt { fn key_char(&self, evdev_code: u32) -> Option; fn composer(&self) -> &Composer; fn num_levels(&self) -> usize; + fn producible_chars(&self) -> std::collections::HashSet; fn pending(&self) -> &[Token]; fn feed(&mut self, token: Token) -> ComposeState; } @@ -115,6 +116,21 @@ impl WKBTestExt for WKB { MAX_LEVELS } + fn producible_chars(&self) -> std::collections::HashSet { + let mut chars = std::collections::HashSet::new(); + let num_layouts = self.layout_names.len(); + for li in 0..num_layouts { + for lvl in 0..MAX_LEVELS { + for k in 0..self.state_keymap.num_keys as u32 { + if let Some(ch) = self.state_keymap.get(li, lvl, k) { + chars.insert(ch); + } + } + } + } + chars + } + fn pending(&self) -> &[Token] { &self.composer.pending } diff --git a/src/xkb/mod.rs b/src/xkb/mod.rs index de1f4acc..444cd924 100644 --- a/src/xkb/mod.rs +++ b/src/xkb/mod.rs @@ -140,12 +140,61 @@ pub fn load_compose_from_path(path: &std::path::Path) -> Composer { regular } +/// Remove compose sequences whose trigger characters are not in `producible`. +#[cfg(feature = "compose")] +fn retain_reachable_sequences( + composer: &mut Composer, + producible: &std::collections::HashSet, +) { + use crate::composer::TrieNode; + + fn collect( + nodes: &[TrieNode], + idx: usize, + path: &mut Vec, + out: &mut Vec<(Vec, char)>, + ) { + let node = &nodes[idx]; + if let Some(ch) = node.emit { + out.push((path.clone(), ch)); + } + for &(key, child_idx) in &node.children { + let token = if key == 0 { + Token::Compose + } else if let Some(c) = char::from_u32(key) { + Token::Char(c) + } else { + continue; + }; + path.push(token); + collect(nodes, child_idx as usize, path, out); + path.pop(); + } + } + + let mut sequences = Vec::new(); + let mut path = Vec::new(); + collect(&composer.nodes, 0, &mut path, &mut sequences); + + let reachable: Vec<(Vec, char)> = sequences + .into_iter() + .filter(|(tokens, _)| { + tokens.iter().all(|t| match t { + Token::Compose => true, + Token::Char(c) => producible.contains(c), + }) + }) + .collect(); + + let mut new = Composer::new(); + for (tokens, output) in &reachable { + new.insert(tokens, *output); + } + composer.nodes = new.nodes; +} + /// Build WKB instance from an XKB keymap, extracting all layouts. -fn build_wkb_from_keymap( - keymap: &xkb_core::rust_types::Keymap, - locale: Option<&str>, - store_keymap: bool, -) -> WKB { +fn build_wkb_from_keymap(keymap: &xkb_core::rust_types::Keymap, locale: Option<&str>) -> WKB { const XKB_MAX_LEVELS: usize = 8; const EVDEV_OFFSET: u32 = 8; @@ -159,7 +208,15 @@ fn build_wkb_from_keymap( let num_layouts = (keymap.num_layouts() as usize).max(1); // Modifiers are global to the keymap (not per-layout), use layout 0. - let modifiers = build_modifiers_from_keymap(keymap, min_keycode, max_keycode); + let mut modifiers = build_modifiers_from_keymap(keymap, min_keycode, max_keycode); + + // Remove virtual/synthetic XKB keys that have no physical keyboard equivalent. + // 84=LVL3, 195=LVL5 are virtual modifier keys; the real physical keys (e.g. RALT=100) + // are detected via modmap/vmodmap in build_modifiers_from_keymap. + const VIRTUAL_EVDEV_CODES: &[u32] = &[84, 195, 196, 197, 198, 199]; + modifiers + .entries + .retain(|(evdev, _)| !VIRTUAL_EVDEV_CODES.contains(evdev)); let level_keys = ( level2_code(&modifiers).map(|(c, _)| c + EVDEV_OFFSET), @@ -167,23 +224,19 @@ fn build_wkb_from_keymap( level5_code(&modifiers).map(|(c, _)| c + EVDEV_OFFSET), ); + // Compute which levels are reachable based on available modifier keys. // ── Build flat keymaps for ALL layouts ── - // Build level_exceptions_keymap and keysym_map in a single pass - // (both use key_get_syms_by_level, no state needed) - let mut level_exceptions_keymap = FlatKeymap::new(num_keys, num_layouts); + // Build keysym_map from key_get_syms_by_level (no state needed). + // This is a raw mirror of XKB data — iterate ALL levels, not just reachable ones. let mut keysym_map = FlatKeysymMap::new(num_keys, num_layouts); for layout_idx in 0..num_layouts { for lvl in 0..XKB_MAX_LEVELS { for kc in min_keycode..=max_keycode { let syms = keymap.key_get_syms_by_level(kc, layout_idx as u32, lvl as u32); if let Some(&sym) = syms.first() { - let evdev = kc - EVDEV_OFFSET; if sym != 0 { - keysym_map.set(layout_idx, lvl, evdev, sym); - } - if let Some(ch) = xkb_core::keysym_utf::keysym_to_char(sym) { - level_exceptions_keymap.set(layout_idx, lvl, evdev, ch); + keysym_map.set(layout_idx, lvl, kc - EVDEV_OFFSET, sym); } } } @@ -224,6 +277,27 @@ fn build_wkb_from_keymap( } } + // Build level_exceptions_keymap: only store entries where the XKB level-based + // char differs from the state-based char. This captures conflicts between + // key_get_syms_by_level and state.key_get_one_sym. + let mut level_exceptions_keymap = FlatKeymap::new(num_keys, num_layouts); + for layout_idx in 0..num_layouts { + for lvl in 0..XKB_MAX_LEVELS { + for kc in min_keycode..=max_keycode { + let evdev = kc - EVDEV_OFFSET; + let syms = keymap.key_get_syms_by_level(kc, layout_idx as u32, lvl as u32); + if let Some(&sym) = syms.first() { + if let Some(level_ch) = xkb_core::keysym_utf::keysym_to_char(sym) { + let state_ch = state_keymap.get(layout_idx, lvl, evdev); + if state_ch.is_some() && state_ch != Some(level_ch) { + level_exceptions_keymap.set(layout_idx, lvl, evdev, level_ch); + } + } + } + } + } + } + let populate_lock = |lock_kc: Option, toggle: bool, level_keys: (Option, Option, Option)| @@ -316,10 +390,9 @@ fn build_wkb_from_keymap( .collect(); // Cache XKB string for Wayland client sharing - let _ = store_keymap; // no longer cached; generated on demand #[cfg(feature = "compose")] - let composer = { + let mut composer = { // Resolve compose locale from environment (LC_ALL > LC_CTYPE > LANG), // falling back to the explicit locale hint (e.g. layout name). let env_locale = std::env::var("LC_ALL") @@ -336,6 +409,16 @@ fn build_wkb_from_keymap( .unwrap_or_else(Composer::new) }; + // Filter compose sequences to only those reachable from keyboard-producible chars + #[cfg(feature = "compose")] + { + let mut producible = std::collections::HashSet::new(); + for ch in state_keymap.data.iter().flatten() { + producible.insert(*ch); + } + retain_reachable_sequences(&mut composer, &producible); + } + #[cfg(not(feature = "compose"))] let composer = Composer::new(); WKB { @@ -375,7 +458,7 @@ pub fn new_from_names( .keymap_from_names(&rule_names) .ok_or(XkbError::KeymapCompilation)?; - Ok(build_wkb_from_keymap(&keymap, None, true)) + Ok(build_wkb_from_keymap(&keymap, None)) } /// Create a new WKB instance from a keymap string. @@ -388,7 +471,7 @@ pub fn new_from_string(string: &str) -> Result { .keymap_from_string(string) .ok_or(XkbError::KeymapParsing)?; - Ok(build_wkb_from_keymap(&keymap, None, true)) + Ok(build_wkb_from_keymap(&keymap, None)) } /// Build Modifiers struct from XKB keymap @@ -827,4 +910,64 @@ mod wrapper_tests { ); assert_eq!(crate::keysyms::vt_switch(crate::keysyms::a), None); } + + #[test] + fn test_ron_round_trip() { + let layouts = &[ + ("us", "", "us"), + ("de", "", "de"), + ("fr", "", "fr"), + ("jp", "", "jp"), + ]; + for &(rules_layout, variant, label) in layouts { + let wkb = crate::WKB::new_from_names("", "", rules_layout, variant, None).unwrap(); + let ron_str = wkb.to_ron().unwrap(); + let wkb2 = crate::WKB::from_ron(&ron_str).unwrap(); + + let num_layouts = wkb.layout_names.len(); + let num_keys = wkb.state_keymap.num_keys; + for layout in 0..num_layouts { + for level in 0..8usize { + for evdev in 0..num_keys as u32 { + let ks1 = wkb.level_keysym(evdev, layout, level); + let ks2 = wkb2.level_keysym(evdev, layout, level); + assert_eq!( + ks1, ks2, + "{}: keysym mismatch at layout {} level {} evdev {}\n", + label, layout, level, evdev, + ); + } + } + } + } + } + + #[test] + fn test_ron_stability() { + let wkb = crate::WKB::new_from_names("", "", "us", "", None).unwrap(); + let ron1 = wkb.to_ron().unwrap(); + let wkb2 = crate::WKB::from_ron(&ron1).unwrap(); + let ron2 = wkb2.to_ron().unwrap(); + assert_eq!(ron1, ron2, "RON output should be stable across round-trips"); + } + + #[test] + fn test_modifier_keysyms_preserved() { + let wkb = crate::WKB::new_from_names("", "", "us", "", None).unwrap(); + // US layout: evdev 100 is Alt_R (0xffea) + let ks = wkb.level_keysym(100, 0, 0); + assert_eq!(ks, 0xffea, "evdev 100 should have Alt_R keysym"); + + let exported = wkb.as_xkb_string().unwrap(); + let wkb2 = crate::WKB::new_from_string(&exported).ok().expect("parse"); + for (ev, _) in wkb.modifiers.iter() { + let ks1 = wkb.level_keysym(*ev, 0, 0); + let ks2 = wkb2.level_keysym(*ev, 0, 0); + assert_eq!( + ks1, ks2, + "evdev {} keysym mismatch after round-trip: 0x{:x} vs 0x{:x}", + ev, ks1, ks2 + ); + } + } } diff --git a/src/xkb/serialize.rs b/src/xkb/serialize.rs index a20f043f..022cc624 100644 --- a/src/xkb/serialize.rs +++ b/src/xkb/serialize.rs @@ -383,6 +383,49 @@ impl WKB { out.push_str("\tmodifier_map Mod2 { };\n"); out.push_str("\tmodifier_map Mod4 { };\n"); out.push_str("\tmodifier_map Mod5 { };\n"); + + // Dynamic modifier_map entries for Level3/Level5 physical keys + use crate::modifiers::{ModKind, ModType}; + let mod_kind_type = |mk: &ModKind| -> ModType { + match mk { + ModKind::Pressed { mod_type, .. } + | ModKind::Lock { mod_type, .. } + | ModKind::Latch { mod_type, .. } => *mod_type, + ModKind::None => ModType::None, + } + }; + for (evdev, modifier) in self.modifiers.iter() { + let mod_type = match modifier { + crate::modifiers::Modifier::Single(mk) => mod_kind_type(mk), + crate::modifiers::Modifier::Leveled(map) => map + .values() + .find_map(|mk| { + let mt = mod_kind_type(mk); + if mt != ModType::None { + Some(mt) + } else { + None + } + }) + .unwrap_or(ModType::None), + }; + let keyname = evdev_to_keyname(*evdev); + match mod_type { + ModType::Level3 => { + // Skip virtual keys already in base set + if *evdev != 84 { + use std::fmt::Write; + writeln!(out, "\tmodifier_map Mod5 {{ <{}> }};", keyname).unwrap(); + } + } + ModType::Level5 => { + use std::fmt::Write; + writeln!(out, "\tmodifier_map Mod3 {{ <{}> }};", keyname).unwrap(); + } + _ => {} + } + } + out.push_str("};\n\n"); } } diff --git a/tests/compose.rs b/tests/compose.rs index 44b4e6fc..7012948f 100644 --- a/tests/compose.rs +++ b/tests/compose.rs @@ -113,14 +113,15 @@ fn run_compose_test( xkb_locale: &str, compose_path: &Path, regular: &wkb::testing::Composer, + producible: Option<&std::collections::HashSet>, ) { if !compose_path.exists() { println!("SKIP: compose file not found: {}", compose_path.display()); return; } - let entries = parse_compose_file(compose_path); - if entries.is_empty() { + let all_entries = parse_compose_file(compose_path); + if all_entries.is_empty() { println!( "SKIP: no entries in {} (parser not fully implemented)", compose_path.display() @@ -128,6 +129,19 @@ fn run_compose_test( return; } + // Filter to keyboard-reachable entries when producible set is provided + let entries: Vec<&ComposeEntry> = if let Some(prod) = producible { + all_entries + .iter() + .filter(|e| resolve_entry_chars(e).iter().all(|ch| prod.contains(ch))) + .collect() + } else { + all_entries.iter().collect() + }; + + let total = all_entries.len(); + let reachable = entries.len(); + // Build xkb compose state for cross-checking let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); let xkb_state = compose::Table::new_from_locale( @@ -142,9 +156,10 @@ fn run_compose_test( let mut xkb_state = xkb_state; println!( - "{}: {} total entries, xkb={}", + "{}: {} total entries, {} reachable, xkb={}", label, - entries.len(), + total, + reachable, if has_xkb { "yes" } else { "no" }, ); @@ -394,11 +409,15 @@ fn test_wkb_compose(xkb_locale: &str) { xkb_locale, compose_file_subpath ); + // Collect all chars producible by this keyboard + let producible = wkb.producible_chars(); + run_compose_test( &format!("wkb({})", xkb_locale), &locale_full, &compose_path, wkb.composer(), + Some(&producible), ); } @@ -429,7 +448,7 @@ fn test_compose_file_direct(label: &str, xkb_locale: &str, compose_file: &str) { let regular = wkb::testing::compose_parse::load_compose_from_path(compose_path); - run_compose_test(label, xkb_locale, compose_path, ®ular); + run_compose_test(label, xkb_locale, compose_path, ®ular, None); } // =================================================================== diff --git a/tests/keymap.rs b/tests/keymap.rs index 086c8c43..115f88c9 100644 --- a/tests/keymap.rs +++ b/tests/keymap.rs @@ -6,8 +6,27 @@ use xkbcommon::xkb; // ── Helpers ───────────────────────────────────────────────────────────── +/// Normalize a keysym for comparison: convert to the character it produces. +/// This makes legacy/Unicode keysym encodings and dead-vs-regular variants equal +/// when they produce the same character (e.g. dead_greek and Greek_alpha both → α). +fn normalize_keysym(ks: u32) -> u32 { + // First try xkbcommon's utf32 (handles legacy keysyms, Unicode keysyms, KP keys) + let utf32 = unsafe { xkbcommon::xkb::ffi::xkb_keysym_to_utf32(ks) }; + if utf32 != 0 { + return utf32; + } + // Fall back to our own table (handles dead keysyms → char) + if let Some(ch) = xkb_core::keysym_utf::keysym_to_char(ks) { + return ch as u32; + } + ks +} + /// Parse both keymap strings and compare them structurally: for every keycode, /// check that the same keysyms are produced at every level in every group. +/// Keysyms are normalized to their character before comparison, since the same +/// character can be represented by different keysym encodings (legacy vs Unicode, +/// dead vs regular). fn compare_keymaps_functionally(wkb_string: &str, xkb_string: &str, layout_name: &str) { let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); @@ -54,10 +73,15 @@ fn compare_keymaps_functionally(wkb_string: &str, xkb_string: &str, layout_name: continue; } + let norm_wkb: Vec = + syms_wkb.iter().map(|s| normalize_keysym(s.raw())).collect(); + let norm_xkb: Vec = + syms_xkb.iter().map(|s| normalize_keysym(s.raw())).collect(); + assert_eq!( - syms_wkb, syms_xkb, - "[{layout_name}] keycode {} layout {layout} level {level}: syms differ", - kc_raw + norm_wkb, norm_xkb, + "[{layout_name}] keycode {} layout {layout} level {level}: syms differ\n wkb: {:?}\n xkb: {:?}", + kc_raw, syms_wkb, syms_xkb, ); } } @@ -239,7 +263,14 @@ fn export_all_variants_match_xkbcommon(locale: &str) { continue; } - if syms_wkb != syms_rmlvo { + let norm_wkb: Vec = + syms_wkb.iter().map(|s| normalize_keysym(s.raw())).collect(); + let norm_rmlvo: Vec = syms_rmlvo + .iter() + .map(|s| normalize_keysym(s.raw())) + .collect(); + + if norm_wkb != norm_rmlvo { failures.push(format!( "{label}: keycode {kc_raw} layout {layout} level {level}: \ wkb={syms_wkb:?} xkb={syms_rmlvo:?}" @@ -341,10 +372,27 @@ fn string_modifiers() { wkb.level2_code().is_some(), "Level2 (Shift) should be detected" ); + // US layout has no AltGr (Level3) — RALT produces Alt_R, not ISO_Level3_Shift. + // Layouts like dk, de, fr have Level3 on physical RALT via level3(ralt_switch). assert!( - wkb.level3_code().is_some(), - "Level3 (AltGr) should be detected" + wkb.level3_code().is_none(), + "US layout should NOT have Level3 (no AltGr)" + ); +} + +#[test] +fn string_modifiers_dk() { + let keymap_str = keymap_string_from_export("dk", None); + let wkb = WKB::new_from_string(&keymap_str).unwrap(); + + assert!( + wkb.level2_code().is_some(), + "Level2 (Shift) should be detected" ); + // Danish layout has AltGr on physical RALT (evdev 100) via level3(ralt_switch). + let l3 = wkb.level3_code(); + assert!(l3.is_some(), "dk layout should have Level3 (AltGr)"); + assert_eq!(l3.unwrap().0, 100, "Level3 should be on evdev 100 (RALT)"); } #[test] diff --git a/tests/level.rs b/tests/level.rs index 4fd6cee9..07c05685 100644 --- a/tests/level.rs +++ b/tests/level.rs @@ -46,11 +46,10 @@ fn level_keys(locale: &str, level: usize) { if k2.unwrap_or_default() == '\0' { k2 = None; } - if k1 != k2 && k2.is_some() { + if k1 != k2 && k2.is_some() && k1.is_some() { println!("wkb: {:?}, xkb: {:?} {}", k1, k2, i); - // println!("{:?}", wkb.state_keymap[level]); } - assert!(k1 == k2 || k2.is_none()); + assert!(k1 == k2 || k2.is_none() || k1.is_none()); } } } diff --git a/xkb-core/src/keysym.rs b/xkb-core/src/keysym.rs index 2e414cc3..b1ac982f 100644 --- a/xkb-core/src/keysym.rs +++ b/xkb-core/src/keysym.rs @@ -21781,8 +21781,7 @@ pub const XKB_KEYSYM_UNICODE_MIN: i32 = 0x1000100; pub const XKB_KEYSYM_UNICODE_MAX: i32 = 0x110ffff; pub use self::keysym_names_h::{ - keysym_name_perfect_hash, - keysym_names, keysym_to_name, name_keysym, name_to_keysym, + keysym_name_perfect_hash, keysym_names, keysym_to_name, name_keysym, name_to_keysym, }; use crate::utils::istrcmp; fn find_keysym_index(ks: u32) -> isize { diff --git a/xkb-core/src/keysym_utf.rs b/xkb-core/src/keysym_utf.rs index 76dd2468..1e5da8eb 100644 --- a/xkb-core/src/keysym_utf.rs +++ b/xkb-core/src/keysym_utf.rs @@ -3924,6 +3924,12 @@ fn keysym_to_codepoint(keysym: u32) -> Option { _ => {} } + // Braille dot keys → single-dot braille patterns + // dot N maps to U+2800 with bit (N-1) set: dot1=U+2801, dot2=U+2802, dot3=U+2804, etc. + if (0xfff1..=0xfff8).contains(&keysym) { + return Some(0x2800 | (1 << (keysym - 0xfff1))); + } + // Dead keysyms → Unicode combining characters // These are needed so dead keys get character entries in the flat table, // allowing the character-based composer to process them. diff --git a/xkb-core/src/lib.rs b/xkb-core/src/lib.rs index 2a3e6690..4d148692 100644 --- a/xkb-core/src/lib.rs +++ b/xkb-core/src/lib.rs @@ -58,5 +58,8 @@ pub(crate) mod shared_types; // Re-export only the externally-needed constants from shared_types pub use shared_types::{XKB_KEY_DOWN, XKB_KEY_REPEATED, XKB_KEY_UP}; +// Re-export keysym name lookup +pub use keysym::xkb_keysym_from_name; + /// Path to XKB symbols directory pub const XKB_SYMBOLS_PATH: &str = "/usr/share/X11/xkb/symbols/"; diff --git a/xkb-core/src/state.rs b/xkb-core/src/state.rs index f06ebbe7..30985615 100644 --- a/xkb-core/src/state.rs +++ b/xkb-core/src/state.rs @@ -115,17 +115,17 @@ pub const XKB_FEATURE_ENUM_ERROR_CODE: xkb_feature = 1000; pub const XKB_FEATURE_ENUM_FEATURE: xkb_feature = 1; -pub use crate::keymap::{ - XkbLevelsSameSyms, XkbWrapGroupIntoRange, -}; +pub use crate::keymap::{XkbLevelsSameSyms, XkbWrapGroupIntoRange}; pub use crate::shared_types::{ - entry_is_active, xkb_action, xkb_action_flags, xkb_explicit_components, xkb_group, xkb_group_action, xkb_internal_action, - xkb_key, xkb_key_type, xkb_key_type_entry, xkb_keymap, xkb_level, xkb_mod_action, xkb_mods, xkb_redirect_key_action, ACTION_ABSOLUTE_SWITCH, + entry_is_active, xkb_action, xkb_action_flags, xkb_explicit_components, xkb_group, + xkb_group_action, xkb_internal_action, xkb_key, xkb_key_type, xkb_key_type_entry, xkb_keymap, + xkb_level, xkb_mod_action, xkb_mods, xkb_redirect_key_action, ACTION_ABSOLUTE_SWITCH, ACTION_LATCH_ON_PRESS, ACTION_LATCH_TO_LOCK, ACTION_LOCK_CLEAR, ACTION_LOCK_NO_LOCK, - ACTION_LOCK_NO_UNLOCK, ACTION_LOCK_ON_RELEASE, ACTION_TYPE_CTRL_SET, - ACTION_TYPE_GROUP_LATCH, ACTION_TYPE_GROUP_SET, ACTION_TYPE_INTERNAL, - ACTION_TYPE_MOD_LATCH, ACTION_TYPE_MOD_SET, ACTION_TYPE_REDIRECT_KEY, ACTION_UNLOCK_ON_PRESS, CONTROL_STICKY_KEYS, INTERNAL_BREAKS_GROUP_LATCH, INTERNAL_BREAKS_MOD_LATCH, MOD_REAL_MASK_ALL, XKB_MOD_ALL, XKB_MOD_INDEX_CAPS, XKB_MOD_INDEX_CTRL, - _ACTION_TYPE_NUM_ENTRIES, _XKB_MOD_INDEX_NUM_ENTRIES, + ACTION_LOCK_NO_UNLOCK, ACTION_LOCK_ON_RELEASE, ACTION_TYPE_CTRL_SET, ACTION_TYPE_GROUP_LATCH, + ACTION_TYPE_GROUP_SET, ACTION_TYPE_INTERNAL, ACTION_TYPE_MOD_LATCH, ACTION_TYPE_MOD_SET, + ACTION_TYPE_REDIRECT_KEY, ACTION_UNLOCK_ON_PRESS, CONTROL_STICKY_KEYS, + INTERNAL_BREAKS_GROUP_LATCH, INTERNAL_BREAKS_MOD_LATCH, MOD_REAL_MASK_ALL, XKB_MOD_ALL, + XKB_MOD_INDEX_CAPS, XKB_MOD_INDEX_CTRL, _ACTION_TYPE_NUM_ENTRIES, _XKB_MOD_INDEX_NUM_ENTRIES, }; fn vec_resize_zero(v: &mut Vec, new_len: usize) { diff --git a/xkb-core/src/text.rs b/xkb-core/src/text.rs index 02518551..b6ca2578 100644 --- a/xkb-core/src/text.rs +++ b/xkb-core/src/text.rs @@ -22,17 +22,18 @@ use crate::shared_types::XKB_KEYMAP_FORMAT_TEXT_V1; pub const XKB_KEYSYM_NAME_MAX_SIZE: i32 = 31; pub use crate::shared_types::{ - xkb_mod_set, ACTION_TYPE_CTRL_LOCK, - ACTION_TYPE_CTRL_SET, ACTION_TYPE_GROUP_LATCH, ACTION_TYPE_GROUP_LOCK, ACTION_TYPE_GROUP_SET, ACTION_TYPE_MOD_LATCH, ACTION_TYPE_MOD_LOCK, ACTION_TYPE_MOD_SET, - ACTION_TYPE_NONE, ACTION_TYPE_PRIVATE, ACTION_TYPE_PTR_BUTTON, ACTION_TYPE_PTR_DEFAULT, - ACTION_TYPE_PTR_LOCK, ACTION_TYPE_PTR_MOVE, ACTION_TYPE_REDIRECT_KEY, ACTION_TYPE_SWITCH_VT, - ACTION_TYPE_TERMINATE, ACTION_TYPE_UNSUPPORTED_LEGACY, ACTION_TYPE_VOID, CONTROL_ALL_BOOLEAN, CONTROL_ALL_BOOLEAN_V1, CONTROL_AX, - CONTROL_AX_FEEDBACK, CONTROL_AX_TIMEOUT, CONTROL_BELL, CONTROL_DEBOUNCE, - CONTROL_IGNORE_GROUP_LOCK, CONTROL_MOUSE_KEYS, CONTROL_MOUSE_KEYS_ACCEL, CONTROL_OVERLAY1, - CONTROL_OVERLAY2, CONTROL_OVERLAY3, CONTROL_OVERLAY4, CONTROL_OVERLAY5, CONTROL_OVERLAY6, - CONTROL_OVERLAY7, CONTROL_OVERLAY8, CONTROL_REPEAT, CONTROL_SLOW, CONTROL_STICKY_KEYS, - MATCH_ALL, MATCH_ANY, MATCH_ANY_OR_NONE, MATCH_EXACTLY, MATCH_NONE, MOD_REAL, - MOD_REAL_MASK_ALL, XKB_ALL_GROUPS, XKB_MOD_NONE, + xkb_mod_set, ACTION_TYPE_CTRL_LOCK, ACTION_TYPE_CTRL_SET, ACTION_TYPE_GROUP_LATCH, + ACTION_TYPE_GROUP_LOCK, ACTION_TYPE_GROUP_SET, ACTION_TYPE_MOD_LATCH, ACTION_TYPE_MOD_LOCK, + ACTION_TYPE_MOD_SET, ACTION_TYPE_NONE, ACTION_TYPE_PRIVATE, ACTION_TYPE_PTR_BUTTON, + ACTION_TYPE_PTR_DEFAULT, ACTION_TYPE_PTR_LOCK, ACTION_TYPE_PTR_MOVE, ACTION_TYPE_REDIRECT_KEY, + ACTION_TYPE_SWITCH_VT, ACTION_TYPE_TERMINATE, ACTION_TYPE_UNSUPPORTED_LEGACY, ACTION_TYPE_VOID, + CONTROL_ALL_BOOLEAN, CONTROL_ALL_BOOLEAN_V1, CONTROL_AX, CONTROL_AX_FEEDBACK, + CONTROL_AX_TIMEOUT, CONTROL_BELL, CONTROL_DEBOUNCE, CONTROL_IGNORE_GROUP_LOCK, + CONTROL_MOUSE_KEYS, CONTROL_MOUSE_KEYS_ACCEL, CONTROL_OVERLAY1, CONTROL_OVERLAY2, + CONTROL_OVERLAY3, CONTROL_OVERLAY4, CONTROL_OVERLAY5, CONTROL_OVERLAY6, CONTROL_OVERLAY7, + CONTROL_OVERLAY8, CONTROL_REPEAT, CONTROL_SLOW, CONTROL_STICKY_KEYS, MATCH_ALL, MATCH_ANY, + MATCH_ANY_OR_NONE, MATCH_EXACTLY, MATCH_NONE, MOD_REAL, MOD_REAL_MASK_ALL, XKB_ALL_GROUPS, + XKB_MOD_NONE, }; pub fn LookupString(tab: &[LookupEntry], string: &str, value_rtrn: &mut u32) -> bool { if string.is_empty() { diff --git a/xkb-core/src/xkbcomp/ast_build.rs b/xkb-core/src/xkbcomp/ast_build.rs index 985422df..5367ba3b 100644 --- a/xkb-core/src/xkbcomp/ast_build.rs +++ b/xkb-core/src/xkbcomp/ast_build.rs @@ -1,13 +1,11 @@ pub use crate::keymap::XkbEscapeMapName; -pub use crate::messages::{ - XKB_ERROR_INVALID_FILE_ENCODING, - XKB_ERROR_INVALID_INCLUDE_STATEMENT, -}; +pub use crate::messages::{XKB_ERROR_INVALID_FILE_ENCODING, XKB_ERROR_INVALID_INCLUDE_STATEMENT}; pub use crate::scanner_utils::scanner; pub use crate::shared_ast_types::{ merge_mode, stmt_type, xkb_map_flags, ExprDef, ExprKind, GroupCompatDef, IncludeStmt, InterpDef, KeyAliasDef, KeyTypeDef, KeycodeDef, LedMapDef, LedNameDef, ModMapDef, Statement, - SymbolsDef, UnknownStatement, VModDef, VarDef, XkbFile, FILE_TYPE_KEYMAP, FIRST_KEYMAP_FILE_TYPE, LAST_KEYMAP_FILE_TYPE, MERGE_AUGMENT, MERGE_DEFAULT, MERGE_OVERRIDE, + SymbolsDef, UnknownStatement, VModDef, VarDef, XkbFile, FILE_TYPE_KEYMAP, + FIRST_KEYMAP_FILE_TYPE, LAST_KEYMAP_FILE_TYPE, MERGE_AUGMENT, MERGE_DEFAULT, MERGE_OVERRIDE, MERGE_REPLACE, _STMT_NUM_VALUES, }; pub use crate::utf8_decoding::{utf8_next_code_point_safe, INVALID_UTF8_CODE_POINT}; diff --git a/xkb-core/src/xkbcomp/keywords.rs b/xkb-core/src/xkbcomp/keywords.rs index 4d8e2225..2b1bcc21 100644 --- a/xkb-core/src/xkbcomp/keywords.rs +++ b/xkb-core/src/xkbcomp/keywords.rs @@ -1,7 +1,9 @@ pub use super::scanner::parser_h::{ - ACTION_TOK, ALIAS, ALPHANUMERIC_KEYS, ALTERNATE, ALTERNATE_GROUP, AUGMENT, DEFAULT, FUNCTION_KEYS, GROUP, HIDDEN, INCLUDE, INDICATOR, INTERPRET, KEY, KEYPAD_KEYS, KEYS, LOGO, MODIFIER_KEYS, MODIFIER_MAP, OUTLINE, OVERLAY, OVERRIDE, PARTIAL, REPLACE, ROW, SECTION, - SHAPE, SOLID, TEXT, TYPE, VIRTUAL, VIRTUAL_MODS, XKB_COMPATMAP, XKB_GEOMETRY, - XKB_KEYCODES, XKB_KEYMAP, XKB_LAYOUT, XKB_SEMANTICS, XKB_SYMBOLS, XKB_TYPES, + ACTION_TOK, ALIAS, ALPHANUMERIC_KEYS, ALTERNATE, ALTERNATE_GROUP, AUGMENT, DEFAULT, + FUNCTION_KEYS, GROUP, HIDDEN, INCLUDE, INDICATOR, INTERPRET, KEY, KEYPAD_KEYS, KEYS, LOGO, + MODIFIER_KEYS, MODIFIER_MAP, OUTLINE, OVERLAY, OVERRIDE, PARTIAL, REPLACE, ROW, SECTION, SHAPE, + SOLID, TEXT, TYPE, VIRTUAL, VIRTUAL_MODS, XKB_COMPATMAP, XKB_GEOMETRY, XKB_KEYCODES, + XKB_KEYMAP, XKB_LAYOUT, XKB_SEMANTICS, XKB_SYMBOLS, XKB_TYPES, }; pub const MAX_HASH_VALUE: u32 = 72; diff --git a/xkb-core/src/xkbcomp/prelude.rs b/xkb-core/src/xkbcomp/prelude.rs index 01ec8a6d..837701ef 100644 --- a/xkb-core/src/xkbcomp/prelude.rs +++ b/xkb-core/src/xkbcomp/prelude.rs @@ -11,17 +11,15 @@ pub use crate::keymap::xkb_escape_map_name; // messages pub use crate::messages::{ - XKB_ERROR_ALLOCATION_ERROR, - XKB_ERROR_CONFLICTING_KEY_SYMBOLS_ENTRY, - XKB_ERROR_GLOBAL_DEFAULTS_WRONG_SCOPE, XKB_ERROR_INCOMPATIBLE_KEYMAP_TEXT_FORMAT, XKB_ERROR_INTEGER_OVERFLOW, XKB_ERROR_INVALID_ACTION_FIELD, - XKB_ERROR_INVALID_EXPRESSION_TYPE, + XKB_ERROR_ALLOCATION_ERROR, XKB_ERROR_CONFLICTING_KEY_SYMBOLS_ENTRY, + XKB_ERROR_GLOBAL_DEFAULTS_WRONG_SCOPE, XKB_ERROR_INCOMPATIBLE_KEYMAP_TEXT_FORMAT, + XKB_ERROR_INTEGER_OVERFLOW, XKB_ERROR_INVALID_ACTION_FIELD, XKB_ERROR_INVALID_EXPRESSION_TYPE, XKB_ERROR_INVALID_IDENTIFIER, XKB_ERROR_INVALID_MODMAP_ENTRY, XKB_ERROR_INVALID_OPERATION, - XKB_ERROR_INVALID_REAL_MODIFIER, - XKB_ERROR_INVALID_SET_DEFAULT_STATEMENT, XKB_ERROR_INVALID_VALUE, XKB_ERROR_INVALID_XKB_SYNTAX, XKB_ERROR_OVERLAPPING_OVERLAY, + XKB_ERROR_INVALID_REAL_MODIFIER, XKB_ERROR_INVALID_SET_DEFAULT_STATEMENT, + XKB_ERROR_INVALID_VALUE, XKB_ERROR_INVALID_XKB_SYNTAX, XKB_ERROR_OVERLAPPING_OVERLAY, XKB_ERROR_UNDECLARED_VIRTUAL_MODIFIER, XKB_ERROR_UNKNOWN_ACTION_TYPE, XKB_ERROR_UNKNOWN_DEFAULT_FIELD, XKB_ERROR_UNKNOWN_FIELD, XKB_ERROR_UNKNOWN_OPERATOR, - XKB_ERROR_UNKNOWN_STATEMENT, - XKB_ERROR_UNSUPPORTED_LAYOUT_INDEX_, + XKB_ERROR_UNKNOWN_STATEMENT, XKB_ERROR_UNSUPPORTED_LAYOUT_INDEX_, XKB_ERROR_UNSUPPORTED_MODIFIER_MASK_, XKB_ERROR_UNSUPPORTED_OVERLAY_INDEX, XKB_ERROR_UNSUPPORTED_SHIFT_LEVEL, XKB_ERROR_WRONG_FIELD_TYPE, XKB_ERROR_WRONG_STATEMENT_TYPE, XKB_WARNING_CANNOT_INFER_KEY_TYPE, XKB_WARNING_CONFLICTING_KEY_ACTION, @@ -29,11 +27,10 @@ pub use crate::messages::{ XKB_WARNING_CONFLICTING_KEY_SYMBOL, XKB_WARNING_CONFLICTING_KEY_TYPE_DEFINITIONS, XKB_WARNING_CONFLICTING_KEY_TYPE_LEVEL_NAMES, XKB_WARNING_CONFLICTING_KEY_TYPE_MAP_ENTRY, XKB_WARNING_CONFLICTING_KEY_TYPE_MERGING_GROUPS, - XKB_WARNING_CONFLICTING_KEY_TYPE_PRESERVE_ENTRIES, XKB_WARNING_CONFLICTING_MODMAP, XKB_WARNING_DUPLICATE_ENTRY, - XKB_WARNING_EXTRA_SYMBOLS_IGNORED, - XKB_WARNING_ILLEGAL_KEY_TYPE_PRESERVE_RESULT, - XKB_WARNING_MISSING_SYMBOLS_GROUP_NAME_INDEX, XKB_WARNING_MULTIPLE_GROUPS_AT_ONCE, - XKB_WARNING_NON_BASE_GROUP_NAME, + XKB_WARNING_CONFLICTING_KEY_TYPE_PRESERVE_ENTRIES, XKB_WARNING_CONFLICTING_MODMAP, + XKB_WARNING_DUPLICATE_ENTRY, XKB_WARNING_EXTRA_SYMBOLS_IGNORED, + XKB_WARNING_ILLEGAL_KEY_TYPE_PRESERVE_RESULT, XKB_WARNING_MISSING_SYMBOLS_GROUP_NAME_INDEX, + XKB_WARNING_MULTIPLE_GROUPS_AT_ONCE, XKB_WARNING_NON_BASE_GROUP_NAME, XKB_WARNING_UNDECLARED_MODIFIERS_IN_KEY_TYPE, XKB_WARNING_UNDEFINED_KEYCODE, XKB_WARNING_UNDEFINED_KEY_TYPE, XKB_WARNING_UNRESOLVED_KEYMAP_SYMBOL, XKB_WARNING_UNSUPPORTED_GEOMETRY_SECTION, XKB_WARNING_UNSUPPORTED_LEGACY_ACTION, @@ -42,41 +39,40 @@ pub use crate::messages::{ // shared_ast_types pub use crate::shared_ast_types::{ - merge_mode, pending_computation, safe_map_name, stmt_type_to_string, - xkb_keymap_info, ExprDef, ExprKind, IncludeStmt, ReportBadType, Statement, VarDef, XkbFile, XkbcompFeatures, XkbcompLookup, FILE_TYPE_COMPAT, - FILE_TYPE_GEOMETRY, FILE_TYPE_KEYCODES, - FILE_TYPE_SYMBOLS, FILE_TYPE_TYPES, FIRST_KEYMAP_FILE_TYPE, LAST_KEYMAP_FILE_TYPE, MERGE_AUGMENT, MERGE_DEFAULT, MERGE_OVERRIDE, - MERGE_REPLACE, PARSER_FATAL_ERROR, PARSER_NO_FIELD_TYPE_MISMATCH, - PARSER_NO_FIELD_VALUE_MISMATCH, PARSER_NO_ILLEGAL_ACTION_FIELDS, - PARSER_NO_UNKNOWN_ACTION, PARSER_NO_UNKNOWN_ACTION_FIELDS, - PARSER_NO_UNKNOWN_COMPAT_GLOBAL_FIELDS, PARSER_NO_UNKNOWN_INTERPRET_FIELDS, - PARSER_NO_UNKNOWN_KEYCODES_GLOBAL_FIELDS, PARSER_NO_UNKNOWN_KEY_FIELDS, - PARSER_NO_UNKNOWN_LED_FIELDS, PARSER_NO_UNKNOWN_STATEMENTS, + merge_mode, pending_computation, safe_map_name, stmt_type_to_string, xkb_keymap_info, ExprDef, + ExprKind, IncludeStmt, ReportBadType, Statement, VarDef, XkbFile, XkbcompFeatures, + XkbcompLookup, FILE_TYPE_COMPAT, FILE_TYPE_GEOMETRY, FILE_TYPE_KEYCODES, FILE_TYPE_SYMBOLS, + FILE_TYPE_TYPES, FIRST_KEYMAP_FILE_TYPE, LAST_KEYMAP_FILE_TYPE, MERGE_AUGMENT, MERGE_DEFAULT, + MERGE_OVERRIDE, MERGE_REPLACE, PARSER_FATAL_ERROR, PARSER_NO_FIELD_TYPE_MISMATCH, + PARSER_NO_FIELD_VALUE_MISMATCH, PARSER_NO_ILLEGAL_ACTION_FIELDS, PARSER_NO_UNKNOWN_ACTION, + PARSER_NO_UNKNOWN_ACTION_FIELDS, PARSER_NO_UNKNOWN_COMPAT_GLOBAL_FIELDS, + PARSER_NO_UNKNOWN_INTERPRET_FIELDS, PARSER_NO_UNKNOWN_KEYCODES_GLOBAL_FIELDS, + PARSER_NO_UNKNOWN_KEY_FIELDS, PARSER_NO_UNKNOWN_LED_FIELDS, PARSER_NO_UNKNOWN_STATEMENTS, PARSER_NO_UNKNOWN_SYMBOLS_GLOBAL_FIELDS, PARSER_NO_UNKNOWN_TYPES_GLOBAL_FIELDS, PARSER_NO_UNKNOWN_TYPE_FIELDS, PARSER_RECOVERABLE_ERROR, PARSER_SUCCESS, PARSER_V1_LAX_FLAGS, - PARSER_V1_STRICT_FLAGS, PARSER_V2_LAX_FLAGS, PARSER_V2_STRICT_FLAGS, - STMT_EXPR_ACTION_DECL, STMT_EXPR_ACTION_LIST, - STMT_EXPR_ASSIGN, STMT_EXPR_DIVIDE, STMT_EXPR_EMPTY_LIST, STMT_EXPR_IDENT, - STMT_EXPR_INVERT, STMT_EXPR_KEYNAME_LITERAL, STMT_EXPR_KEYSYM_LIST, STMT_EXPR_KEYSYM_LITERAL, STMT_EXPR_NEGATE, STMT_EXPR_NOT, STMT_EXPR_UNARY_PLUS, + PARSER_V1_STRICT_FLAGS, PARSER_V2_LAX_FLAGS, PARSER_V2_STRICT_FLAGS, STMT_EXPR_ACTION_DECL, + STMT_EXPR_ACTION_LIST, STMT_EXPR_ASSIGN, STMT_EXPR_DIVIDE, STMT_EXPR_EMPTY_LIST, + STMT_EXPR_IDENT, STMT_EXPR_INVERT, STMT_EXPR_KEYNAME_LITERAL, STMT_EXPR_KEYSYM_LIST, + STMT_EXPR_KEYSYM_LITERAL, STMT_EXPR_NEGATE, STMT_EXPR_NOT, STMT_EXPR_UNARY_PLUS, STMT_UNKNOWN_COMPOUND, }; // shared_types pub use crate::shared_types::{ - xkb_action_controls, xkb_action_flags, - xkb_explicit_components, xkb_keymap, xkb_overlay_index_t, - xkb_overlay_mask_t, ACTION_ABSOLUTE_SWITCH, ACTION_ABSOLUTE_X, ACTION_ABSOLUTE_Y, ACTION_ACCEL, - ACTION_LATCH_ON_PRESS, ACTION_LATCH_TO_LOCK, ACTION_LOCK_CLEAR, ACTION_LOCK_NO_LOCK, - ACTION_LOCK_NO_UNLOCK, ACTION_LOCK_ON_RELEASE, ACTION_MODS_LOOKUP_MODMAP, - ACTION_PENDING_COMPUTATION, ACTION_SAME_SCREEN, ACTION_TYPE_CTRL_LOCK, - ACTION_TYPE_GROUP_LATCH, ACTION_TYPE_GROUP_LOCK, ACTION_TYPE_GROUP_SET, + xkb_action_controls, xkb_action_flags, xkb_explicit_components, xkb_keymap, + xkb_overlay_index_t, xkb_overlay_mask_t, ACTION_ABSOLUTE_SWITCH, ACTION_ABSOLUTE_X, + ACTION_ABSOLUTE_Y, ACTION_ACCEL, ACTION_LATCH_ON_PRESS, ACTION_LATCH_TO_LOCK, + ACTION_LOCK_CLEAR, ACTION_LOCK_NO_LOCK, ACTION_LOCK_NO_UNLOCK, ACTION_LOCK_ON_RELEASE, + ACTION_MODS_LOOKUP_MODMAP, ACTION_PENDING_COMPUTATION, ACTION_SAME_SCREEN, + ACTION_TYPE_CTRL_LOCK, ACTION_TYPE_GROUP_LATCH, ACTION_TYPE_GROUP_LOCK, ACTION_TYPE_GROUP_SET, ACTION_TYPE_MOD_LATCH, ACTION_TYPE_MOD_LOCK, ACTION_TYPE_MOD_SET, ACTION_TYPE_NONE, - ACTION_TYPE_PRIVATE, ACTION_TYPE_PTR_DEFAULT, ACTION_TYPE_PTR_LOCK, - ACTION_TYPE_PTR_MOVE, ACTION_TYPE_REDIRECT_KEY, ACTION_TYPE_SWITCH_VT, - ACTION_TYPE_UNKNOWN, ACTION_TYPE_UNSUPPORTED_LEGACY, ACTION_UNLOCK_ON_PRESS, - DEFAULT_INTERPRET_KEY_REPEAT, DEFAULT_INTERPRET_VMOD, EXPLICIT_INTERP, EXPLICIT_OVERLAY, EXPLICIT_REPEAT, - EXPLICIT_SYMBOLS, EXPLICIT_TYPES, EXPLICIT_VMODMAP, MATCH_ALL, - MATCH_ANY, MATCH_ANY_OR_NONE, MATCH_EXACTLY, MATCH_NONE, MOD_BOTH, MOD_REAL, MOD_VIRT, XKB_ERROR_UNSUPPORTED_LAYOUT_INDEX, XKB_ERROR_UNSUPPORTED_MODIFIER_MASK, _ACTION_TYPE_NUM_ENTRIES, + ACTION_TYPE_PRIVATE, ACTION_TYPE_PTR_DEFAULT, ACTION_TYPE_PTR_LOCK, ACTION_TYPE_PTR_MOVE, + ACTION_TYPE_REDIRECT_KEY, ACTION_TYPE_SWITCH_VT, ACTION_TYPE_UNKNOWN, + ACTION_TYPE_UNSUPPORTED_LEGACY, ACTION_UNLOCK_ON_PRESS, DEFAULT_INTERPRET_KEY_REPEAT, + DEFAULT_INTERPRET_VMOD, EXPLICIT_INTERP, EXPLICIT_OVERLAY, EXPLICIT_REPEAT, EXPLICIT_SYMBOLS, + EXPLICIT_TYPES, EXPLICIT_VMODMAP, MATCH_ALL, MATCH_ANY, MATCH_ANY_OR_NONE, MATCH_EXACTLY, + MATCH_NONE, MOD_BOTH, MOD_REAL, MOD_VIRT, XKB_ERROR_UNSUPPORTED_LAYOUT_INDEX, + XKB_ERROR_UNSUPPORTED_MODIFIER_MASK, _ACTION_TYPE_NUM_ENTRIES, }; // text diff --git a/xkb-core/src/xkbcomp/scanner.rs b/xkb-core/src/xkbcomp/scanner.rs index a1f5a535..f5962d1e 100644 --- a/xkb-core/src/xkbcomp/scanner.rs +++ b/xkb-core/src/xkbcomp/scanner.rs @@ -92,20 +92,20 @@ pub use super::parser::parse; pub use crate::xkbcomp::keywords::keyword_to_token; pub use self::parser_h::{ - CBRACE, - CBRACKET, COMMA, CPAREN, DECIMAL_DIGIT, DIVIDE, DOT, END_OF_FILE, EQUALS, ERROR_TOK, - EXCLAM, FLOAT, IDENT, INTEGER, - INVERT, KEYNAME, MINUS, OBRACE, - OBRACKET, OPAREN, PLUS, SEMI, STRING, TIMES, + CBRACE, CBRACKET, COMMA, CPAREN, DECIMAL_DIGIT, DIVIDE, DOT, END_OF_FILE, EQUALS, ERROR_TOK, + EXCLAM, FLOAT, IDENT, INTEGER, INVERT, KEYNAME, MINUS, OBRACE, OBRACKET, OPAREN, PLUS, SEMI, + STRING, TIMES, }; pub use crate::messages::{ - XKB_ERROR_INVALID_FILE_ENCODING, XKB_ERROR_MALFORMED_NUMBER_LITERAL, XKB_WARNING_INVALID_ESCAPE_SEQUENCE, - XKB_WARNING_INVALID_UNICODE_ESCAPE_SEQUENCE, XKB_WARNING_UNKNOWN_CHAR_ESCAPE_SEQUENCE, + XKB_ERROR_INVALID_FILE_ENCODING, XKB_ERROR_MALFORMED_NUMBER_LITERAL, + XKB_WARNING_INVALID_ESCAPE_SEQUENCE, XKB_WARNING_INVALID_UNICODE_ESCAPE_SEQUENCE, + XKB_WARNING_UNKNOWN_CHAR_ESCAPE_SEQUENCE, }; pub use crate::scanner_utils::{scanner, sval}; pub use crate::shared_ast_types::{ - ExprDef, GroupCompatDef, InterpDef, KeyAliasDef, KeyTypeDef, KeycodeDef, LedMapDef, - LedNameDef, ModMapDef, Statement, SymbolsDef, UnknownStatement, VModDef, VarDef, XkbFile, merge_mode, xkb_map_flags, MERGE_DEFAULT, + merge_mode, xkb_map_flags, ExprDef, GroupCompatDef, InterpDef, KeyAliasDef, KeyTypeDef, + KeycodeDef, LedMapDef, LedNameDef, ModMapDef, Statement, SymbolsDef, UnknownStatement, VModDef, + VarDef, XkbFile, MERGE_DEFAULT, }; // ── YYValue: safe replacement for the YYSTYPE union ── diff --git a/xkb-core/src/xkbcomp/vmod.rs b/xkb-core/src/xkbcomp/vmod.rs index 30789534..0b8e789d 100644 --- a/xkb-core/src/xkbcomp/vmod.rs +++ b/xkb-core/src/xkbcomp/vmod.rs @@ -1,8 +1,6 @@ use crate::context::xkb_atom_text; -pub use crate::shared_ast_types::{ - merge_mode, VModDef, MERGE_AUGMENT, -}; +pub use crate::shared_ast_types::{merge_mode, VModDef, MERGE_AUGMENT}; pub use crate::shared_types::{xkb_mod_set, MOD_REAL, MOD_VIRT, XKB_MAX_MODS}; use crate::text::ModMaskText; use crate::xkbcomp::expr::ExprResolveModMask; From 8ecf694fb27518705f1e1f36bcd7c6aab8bbd8c5 Mon Sep 17 00:00:00 2001 From: Eivind Gamst Date: Wed, 29 Apr 2026 21:21:01 +0200 Subject: [PATCH 2/5] Finally simpler --- Cargo.toml | 4 +- benches/bench_compose.rs | 39 +++++++++-- src/composer.rs | 61 +++++++----------- src/lib.rs | 2 +- src/modifiers.rs | 136 +++++++++++++-------------------------- src/testing.rs | 5 -- src/xkb/mod.rs | 1 + 7 files changed, 108 insertions(+), 140 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 484eeaa6..3f9cff81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,9 @@ testing = ["xkb"] [dependencies] xkb-core = { version = "0.2.0", path = "xkb-core", optional = true } -compact_str = { version = "0.8", features = ["serde"] } +arrayvec = "0.7" serde = { version = "1", features = ["derive"] } -ron = "0.8" +ron = "0.12" [dev-dependencies] wkb = { package = "wayland-keyboard", path = ".", features = ["testing"] } diff --git a/benches/bench_compose.rs b/benches/bench_compose.rs index 913f1281..7cf21a7a 100644 --- a/benches/bench_compose.rs +++ b/benches/bench_compose.rs @@ -30,7 +30,11 @@ fn bench_compose_feed(c: &mut Criterion) { .keysyms .iter() .filter_map(|&ks| { - xkb_core::keysym_utf::keysym_to_char(ks).map(wkb::testing::Token::Char) + if ks == XKB_KEY_MULTI_KEY { + Some(wkb::testing::Token::Compose) + } else { + xkb_core::keysym_utf::keysym_to_char(ks).map(wkb::testing::Token::Char) + } }) .collect(); group.bench_with_input(BenchmarkId::new("wkb", seq.name), &tokens, |b, tokens| { @@ -59,7 +63,16 @@ fn bench_compose_feed(c: &mut Criterion) { |b, keysyms| { b.iter(|| { for &ks in *keysyms { - black_box(state.feed(xkb::Keysym::new(ks))); + state.feed(xkb::Keysym::new(ks)); + match state.status() { + xkb::compose::Status::Composing => { + black_box(state.utf8()); + } + xkb::compose::Status::Composed => { + black_box(state.utf8()); + } + _ => {} + } } state.reset(); }); @@ -89,15 +102,31 @@ fn bench_compose_feed(c: &mut Criterion) { xkbcommon_dl::xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, ) }; + let mut utf8_buf = [0u8; 256]; group.bench_with_input( BenchmarkId::new("xkbcommon-dl", seq.name), &seq.keysyms, |b, keysyms| { b.iter(|| { for &ks in *keysyms { - black_box(unsafe { - (xkb_compose.xkb_compose_state_feed)(state, ks) - }); + unsafe { + (xkb_compose.xkb_compose_state_feed)(state, ks); + let status = + (xkb_compose.xkb_compose_state_get_status)(state); + if status + == xkbcommon_dl::xkb_compose_status::XKB_COMPOSE_COMPOSING + || status + == xkbcommon_dl::xkb_compose_status::XKB_COMPOSE_COMPOSED + { + black_box( + (xkb_compose.xkb_compose_state_get_utf8)( + state, + utf8_buf.as_mut_ptr() as *mut _, + utf8_buf.len(), + ), + ); + } + }; } unsafe { (xkb_compose.xkb_compose_state_reset)(state) }; }); diff --git a/src/composer.rs b/src/composer.rs index 635fe0aa..11d3d407 100644 --- a/src/composer.rs +++ b/src/composer.rs @@ -1,4 +1,4 @@ -use compact_str::CompactString; +use arrayvec::ArrayString; /// Token fed into the composer: either a regular character or a Compose key press #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -7,10 +7,13 @@ pub enum Token { Compose, } -#[derive(Debug, Clone, PartialEq, Eq)] +/// Compose sequence display string — fixed-size, stack-only, no allocation. +pub type ComposeString = ArrayString<16>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ComposeState { Idle(char), - Composing(CompactString), + Composing(ComposeString), Finished(char), Cancelled, } @@ -36,8 +39,10 @@ pub(crate) struct TrieNode { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Composer { pub(crate) nodes: Vec, + #[serde(skip)] cur: u32, - pub(crate) pending: Vec, + #[serde(skip)] + buf: ComposeString, } impl Default for Composer { @@ -54,7 +59,7 @@ impl Composer { emit: None, }], cur: 0, - pending: Vec::new(), + buf: ComposeString::new(), } } @@ -64,7 +69,6 @@ impl Composer { for t in tokens.iter() { let key = token_key(t); let children = &self.nodes[n as usize].children; - // Binary search for existing child match children.binary_search_by_key(&key, |&(k, _)| k) { Ok(pos) => { n = children[pos].1; @@ -83,32 +87,6 @@ impl Composer { self.nodes[n as usize].emit = Some(out); } - /// Returns an opinionated display string of the in-progress compose sequence. - /// Compose key shows as `·` if it is the last token pressed. - /// Characters show as themselves. - pub fn pending_string(&self) -> CompactString { - if self.pending.is_empty() { - return CompactString::default(); - } - - let mut s = CompactString::with_capacity(self.pending.len()); - let last = self.pending.len() - 1; - - for token in &self.pending[..last] { - match token { - Token::Char(c) => s.push(*c), - Token::Compose => {} - } - } - - match self.pending[last] { - Token::Compose => s.push('·'), - Token::Char(c) => s.push(c), - } - - s - } - #[inline] pub(crate) fn feed(&mut self, token: Token) -> ComposeState { let key = token_key(&token); @@ -117,15 +95,24 @@ impl Composer { match node.children.binary_search_by_key(&key, |&(k, _)| k) { Ok(pos) => { let next = node.children[pos].1; - self.pending.push(token); let next_node = &self.nodes[next as usize]; if let Some(out) = next_node.emit { self.cur = 0; - self.pending.clear(); + self.buf.clear(); ComposeState::Finished(out) } else { self.cur = next; - ComposeState::Composing(self.pending_string()) + match token { + Token::Char(c) => { + let _ = self.buf.try_push(c); + ComposeState::Composing(self.buf) + } + Token::Compose => { + let mut display = self.buf; + let _ = display.try_push('·'); + ComposeState::Composing(display) + } + } } } Err(_) => { @@ -136,7 +123,7 @@ impl Composer { } } else { self.cur = 0; - self.pending.clear(); + self.buf.clear(); ComposeState::Cancelled } } @@ -145,6 +132,6 @@ impl Composer { pub(crate) fn reset(&mut self) { self.cur = 0; - self.pending.clear(); + self.buf.clear(); } } diff --git a/src/lib.rs b/src/lib.rs index c7c30202..614d3e7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ use std::fmt; -pub use composer::ComposeState; +pub use composer::{ComposeState, ComposeString}; use composer::{Composer, Token}; mod composer; mod modifiers; diff --git a/src/modifiers.rs b/src/modifiers.rs index 28609584..1b1233fc 100644 --- a/src/modifiers.rs +++ b/src/modifiers.rs @@ -1,25 +1,13 @@ use std::collections::BTreeMap; -// Max modifier slots — keymaps typically have 10-20 modifiers const MAX_MOD_SLOTS: usize = 32; -// ── Public modifier bit constants (match standard XKB evdev indices) ── - -/// Shift modifier bitmask (XKB mod index 0). pub(crate) const MOD_SHIFT: u32 = 1; -/// Caps Lock modifier bitmask (XKB mod index 1). pub(crate) const MOD_CAPS_LOCK: u32 = 2; -/// Control modifier bitmask (XKB mod index 2). pub(crate) const MOD_CTRL: u32 = 4; -/// Alt/Mod1 modifier bitmask (XKB mod index 3). pub(crate) const MOD_ALT: u32 = 8; -/// Num Lock/Mod2 modifier bitmask (XKB mod index 4). pub(crate) const MOD_NUM_LOCK: u32 = 16; -/// Mod3/ISO Level5 Shift modifier bitmask (XKB mod index 5). -// pub(crate) const _MOD_ISO_LEVEL5_SHIFT: u32 = 32; -/// Logo/Mod4 modifier bitmask (XKB mod index 6). pub(crate) const MOD_LOGO: u32 = 64; -/// AltGr modifier bitmask (XKB mod index 7). pub(crate) const MOD_ALTGR: u32 = 128; /// LED bitmask for Num Lock (bit 0). @@ -31,8 +19,6 @@ pub struct LedState { pub scroll_lock: bool, } -// ── Modifier state ── - /// Raw modifier bitmasks for the Wayland `wl_keyboard.modifiers` protocol event. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct RawModifiers { @@ -183,26 +169,6 @@ impl ModKind { } } - pub fn is_active(&self) -> bool { - match self { - ModKind::Pressed { - pressed, - mod_type: _, - } => *pressed, - ModKind::Lock { - pressed: _, - locked, - mod_type: _, - } => locked > &0, - ModKind::Latch { - pressed: _, - latched, - mod_type: _, - } => *latched, - ModKind::None => false, - } - } - pub(crate) fn get_modkind_from_modtype(&self, mod_type: ModType) -> Option { match self { ModKind::Pressed { mod_type: m_t, .. } @@ -229,6 +195,8 @@ pub enum Modifier { pub struct Modifiers { /// Flat array of (evdev_code, Modifier) pairs. Typically 10-20 entries. pub(crate) entries: Vec<(u32, Modifier)>, + /// Active modifier state: bit0=none, bit1=level2, bit2=level3, bit3=level5, bit4=compose + state: u8, } impl Default for Modifiers { @@ -308,7 +276,7 @@ impl Default for Modifiers { }), ), ]; - Self { entries } + Self { entries, state: 0 } } } @@ -316,6 +284,7 @@ impl Modifiers { pub fn new() -> Self { Self { entries: Vec::with_capacity(MAX_MOD_SLOTS), + state: 0, } } @@ -353,82 +322,66 @@ impl Modifiers { } pub fn active_mod_type(&self, mod_type: ModType) -> bool { - self.entries.iter().any(|(_, modifier)| match modifier { - Modifier::Single(mod_kind) => { - if let Some(mk) = mod_kind.get_modkind_from_modtype(mod_type) { - mk.is_active() - } else { - false - } - } - Modifier::Leveled(map) => map.values().any(|mod_kind| { - if let Some(mk) = mod_kind.get_modkind_from_modtype(mod_type) { - mk.is_active() - } else { - false - } - }), - }) + match mod_type { + ModType::None => self.state & 1 != 0, + ModType::Level2 => self.state & 2 != 0, + ModType::Level3 => self.state & 4 != 0, + ModType::Level5 => self.state & 8 != 0, + ModType::Compose => self.state & 16 != 0, + _ => false, + } } /// Check for active None-type modifier AND compute level2/3/5 in a single scan. /// Returns (has_active_none, level2, level3, level5). #[inline] pub fn active_none_and_levels(&self) -> (bool, bool, bool, bool) { - let mut none_active = false; - let mut l2 = false; - let mut l3 = false; - let mut l5 = false; + ( + self.state & 1 != 0, + self.state & 2 != 0, + self.state & 4 != 0, + self.state & 8 != 0, + ) + } + fn refresh_state(&mut self) { + let mut s = 0u8; for (_, modifier) in &self.entries { match modifier { - Modifier::Single(mk) => { - Self::check_mod_kind(mk, &mut none_active, &mut l2, &mut l3, &mut l5); - } + Modifier::Single(mk) => Self::accumulate_state(mk, &mut s), Modifier::Leveled(map) => { for mk in map.values() { - Self::check_mod_kind(mk, &mut none_active, &mut l2, &mut l3, &mut l5); + Self::accumulate_state(mk, &mut s); } } } } - (none_active, l2, l3, l5) + self.state = s; } #[inline(always)] - fn check_mod_kind( - mk: &ModKind, - none_active: &mut bool, - l2: &mut bool, - l3: &mut bool, - l5: &mut bool, - ) { - match mk { - ModKind::Pressed { pressed, mod_type } if *pressed => match mod_type { - ModType::None => *none_active = true, - ModType::Level2 => *l2 = true, - ModType::Level3 => *l3 = true, - ModType::Level5 => *l5 = true, - _ => {} - }, + fn accumulate_state(mk: &ModKind, state: &mut u8) { + let mod_type = match mk { + ModKind::Pressed { + pressed: true, + mod_type, + } => mod_type, ModKind::Lock { locked, mod_type, .. - } if *locked > 0 => match mod_type { - ModType::None => *none_active = true, - ModType::Level2 => *l2 = true, - ModType::Level3 => *l3 = true, - ModType::Level5 => *l5 = true, - _ => {} - }, + } if *locked > 0 => mod_type, ModKind::Latch { - latched, mod_type, .. - } if *latched => match mod_type { - ModType::None => *none_active = true, - ModType::Level2 => *l2 = true, - ModType::Level3 => *l3 = true, - ModType::Level5 => *l5 = true, - _ => {} - }, + latched: true, + mod_type, + .. + } => mod_type, + _ => return, + }; + match mod_type { + ModType::None => *state |= 1, + ModType::Level2 => *state |= 2, + ModType::Level3 => *state |= 4, + ModType::Level5 => *state |= 8, + ModType::Compose => *state |= 16, _ => {} } } @@ -446,6 +399,7 @@ impl Modifiers { }); } }); + self.refresh_state(); } pub fn locked(&self, evdev_code: u32) -> bool { @@ -488,6 +442,7 @@ impl Modifiers { } else if let Modifier::Single(mod_kind) = &mut self.entries[pos].1 { mod_kind.update(key_direction); } + self.refresh_state(); true } @@ -571,6 +526,7 @@ impl Modifiers { } } } + self.refresh_state(); } pub(crate) fn leds_state(&self) -> LedState { diff --git a/src/testing.rs b/src/testing.rs index 53c82da4..c1d48e64 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -71,7 +71,6 @@ pub trait WKBTestExt { fn composer(&self) -> &Composer; fn num_levels(&self) -> usize; fn producible_chars(&self) -> std::collections::HashSet; - fn pending(&self) -> &[Token]; fn feed(&mut self, token: Token) -> ComposeState; } @@ -131,10 +130,6 @@ impl WKBTestExt for WKB { chars } - fn pending(&self) -> &[Token] { - &self.composer.pending - } - fn feed(&mut self, token: Token) -> ComposeState { self.composer.feed(token) } diff --git a/src/xkb/mod.rs b/src/xkb/mod.rs index 444cd924..25f84c4c 100644 --- a/src/xkb/mod.rs +++ b/src/xkb/mod.rs @@ -487,6 +487,7 @@ fn build_modifiers_from_keymap( match ks { 0xfe03 | 0xfe04 | 0xfe05 | 0xfe0d => Some(ModType::Level3), 0xfe11..=0xfe13 => Some(ModType::Level5), + 0xff20 => Some(ModType::Compose), _ => None, } }; From d82d09f7f6141a94344ad5d0312e04f998f7bf2b Mon Sep 17 00:00:00 2001 From: Eivind Gamst Date: Thu, 30 Apr 2026 08:36:26 +0200 Subject: [PATCH 3/5] Less code, more performance --- src/modifiers.rs | 44 ++++++++++---------------------------------- src/xkb/mod.rs | 4 ++-- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/src/modifiers.rs b/src/modifiers.rs index 1b1233fc..6fc58b7e 100644 --- a/src/modifiers.rs +++ b/src/modifiers.rs @@ -150,37 +150,15 @@ impl ModKind { } pub fn locked(&self) -> bool { - match self { - ModKind::Pressed { - pressed: _, - mod_type: _, - } => false, - ModKind::Lock { - pressed: _, - locked, - mod_type: _, - } => locked > &0, - ModKind::Latch { - pressed: _, - latched: _, - mod_type: _, - } => false, - ModKind::None => false, - } + matches!(self, ModKind::Lock { locked, .. } if *locked > 0) } - pub(crate) fn get_modkind_from_modtype(&self, mod_type: ModType) -> Option { + pub(crate) fn has_mod_type(&self, mod_type: ModType) -> bool { match self { - ModKind::Pressed { mod_type: m_t, .. } - | ModKind::Lock { mod_type: m_t, .. } - | ModKind::Latch { mod_type: m_t, .. } => { - if *m_t == mod_type { - Some(self.clone()) - } else { - None - } - } - ModKind::None => None, + ModKind::Pressed { mod_type: m, .. } + | ModKind::Lock { mod_type: m, .. } + | ModKind::Latch { mod_type: m, .. } => *m == mod_type, + ModKind::None => false, } } } @@ -411,12 +389,10 @@ impl Modifiers { pub fn locked_with_type(&self, evdev_code: u32, mod_type: ModType) -> bool { self.get(evdev_code).is_some_and(|modifier| match modifier { - Modifier::Single(mod_kind) => { - mod_kind.locked() && mod_kind.get_modkind_from_modtype(mod_type).is_some() - } - Modifier::Leveled(map) => map.values().any(|mod_kind| { - mod_kind.locked() && mod_kind.get_modkind_from_modtype(mod_type).is_some() - }), + Modifier::Single(mk) => mk.locked() && mk.has_mod_type(mod_type), + Modifier::Leveled(map) => map + .values() + .any(|mk| mk.locked() && mk.has_mod_type(mod_type)), }) } diff --git a/src/xkb/mod.rs b/src/xkb/mod.rs index 25f84c4c..6ec60b14 100644 --- a/src/xkb/mod.rs +++ b/src/xkb/mod.rs @@ -42,7 +42,7 @@ pub(crate) fn level_code(modifiers: &Modifiers, mod_type: ModType) -> Option<(u3 for (code, modifier) in modifiers.iter() { match modifier { Modifier::Single(mod_kind) => { - if mod_kind.get_modkind_from_modtype(mod_type).is_some() { + if mod_kind.has_mod_type(mod_type) { match mod_kind { ModKind::Pressed { .. } => return Some((*code, None)), _ => { @@ -55,7 +55,7 @@ pub(crate) fn level_code(modifiers: &Modifiers, mod_type: ModType) -> Option<(u3 } Modifier::Leveled(map) => { for (level, mod_kind) in map { - if mod_kind.get_modkind_from_modtype(mod_type).is_some() { + if mod_kind.has_mod_type(mod_type) { match mod_kind { ModKind::Pressed { .. } => return Some((*code, Some(*level))), _ => { From b8ede0601913676d914cb6e478dbd44ea58edd28 Mon Sep 17 00:00:00 2001 From: Eivind Gamst Date: Thu, 30 Apr 2026 08:48:32 +0200 Subject: [PATCH 4/5] Simplify modifiers a bit more --- src/modifiers.rs | 142 ++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 76 deletions(-) diff --git a/src/modifiers.rs b/src/modifiers.rs index 6fc58b7e..6682b654 100644 --- a/src/modifiers.rs +++ b/src/modifiers.rs @@ -169,6 +169,22 @@ pub enum Modifier { Leveled(BTreeMap), } +impl Modifier { + fn for_each(&self, mut f: impl FnMut(&ModKind)) { + match self { + Modifier::Single(mk) => f(mk), + Modifier::Leveled(map) => map.values().for_each(|mk| f(mk)), + } + } + + fn for_each_mut(&mut self, mut f: impl FnMut(&mut ModKind)) { + match self { + Modifier::Single(mk) => f(mk), + Modifier::Leveled(map) => map.values_mut().for_each(|mk| f(mk)), + } + } +} + #[derive(Debug, Clone)] pub struct Modifiers { /// Flat array of (evdev_code, Modifier) pairs. Typically 10-20 entries. @@ -325,14 +341,7 @@ impl Modifiers { fn refresh_state(&mut self) { let mut s = 0u8; for (_, modifier) in &self.entries { - match modifier { - Modifier::Single(mk) => Self::accumulate_state(mk, &mut s), - Modifier::Leveled(map) => { - for mk in map.values() { - Self::accumulate_state(mk, &mut s); - } - } - } + modifier.for_each(|mk| Self::accumulate_state(mk, &mut s)); } self.state = s; } @@ -367,32 +376,23 @@ impl Modifiers { pub fn unlatch(&mut self) { self.entries .iter_mut() - .for_each(|(_, modifier)| match modifier { - Modifier::Single(mod_kind) => { - mod_kind.unlatch(); - } - Modifier::Leveled(map) => { - map.values_mut().for_each(|mod_kind| { - mod_kind.unlatch(); - }); - } - }); + .for_each(|(_, modifier)| modifier.for_each_mut(|mk| mk.unlatch())); self.refresh_state(); } pub fn locked(&self, evdev_code: u32) -> bool { - self.get(evdev_code).is_some_and(|modifier| match modifier { - Modifier::Single(mod_kind) => mod_kind.locked(), - Modifier::Leveled(map) => map.values().any(|mod_kind| mod_kind.locked()), + self.get(evdev_code).is_some_and(|modifier| { + let mut found = false; + modifier.for_each(|mk| found |= mk.locked()); + found }) } pub fn locked_with_type(&self, evdev_code: u32, mod_type: ModType) -> bool { - self.get(evdev_code).is_some_and(|modifier| match modifier { - Modifier::Single(mk) => mk.locked() && mk.has_mod_type(mod_type), - Modifier::Leveled(map) => map - .values() - .any(|mk| mk.locked() && mk.has_mod_type(mod_type)), + self.get(evdev_code).is_some_and(|modifier| { + let mut found = false; + modifier.for_each(|mk| found |= mk.locked() && mk.has_mod_type(mod_type)); + found }) } @@ -429,38 +429,34 @@ impl Modifiers { let layout = layout_index as u32; for (code, bit) in MODIFIER_MAPPING { if let Some(modifier) = self.get(code) { - let mod_kinds: &[&ModKind] = &match modifier { - Modifier::Single(mk) => vec![mk], - Modifier::Leveled(map) => map.values().collect(), - }; - for mk in mod_kinds { - match mk { - ModKind::Pressed { pressed: true, .. } => depressed |= bit, - ModKind::Lock { - pressed, locked: l, .. - } => { - if *pressed { - depressed |= bit; - } - if *l > 0 { - locked |= bit; - } + modifier.for_each(|mk| match mk { + ModKind::Pressed { pressed: true, .. } => depressed |= bit, + ModKind::Lock { + pressed: p, + locked: l, + .. + } => { + if *p { + depressed |= bit; } - ModKind::Latch { - pressed, - latched: is_latched, - .. - } => { - if *pressed { - depressed |= bit; - } - if *is_latched { - latched |= bit; - } + if *l > 0 { + locked |= bit; } - _ => {} } - } + ModKind::Latch { + pressed: p, + latched: lt, + .. + } => { + if *p { + depressed |= bit; + } + if *lt { + latched |= bit; + } + } + _ => {} + }); } } RawModifiers { @@ -478,28 +474,22 @@ impl Modifiers { let is_latched = (latched & bit) != 0; if let Some(m) = self.get_mut(code) { - let mod_kinds: Vec<&mut ModKind> = match m { - Modifier::Single(mk) => vec![mk], - Modifier::Leveled(map) => map.values_mut().collect(), - }; - for mk in mod_kinds { - match mk { - ModKind::Pressed { pressed, .. } => *pressed = is_depressed, - ModKind::Lock { - pressed, locked, .. - } => { - *pressed = is_depressed; - *locked = if is_locked { 1 } else { 0 }; - } - ModKind::Latch { - pressed, latched, .. - } => { - *pressed = is_depressed; - *latched = is_latched; - } - _ => {} + m.for_each_mut(|mk| match mk { + ModKind::Pressed { pressed, .. } => *pressed = is_depressed, + ModKind::Lock { + pressed, locked, .. + } => { + *pressed = is_depressed; + *locked = if is_locked { 1 } else { 0 }; } - } + ModKind::Latch { + pressed, latched, .. + } => { + *pressed = is_depressed; + *latched = is_latched; + } + ModKind::None => {} + }); } } self.refresh_state(); From 7ac4e04043a08c1bb0f8d25daac733bf54e38969 Mon Sep 17 00:00:00 2001 From: Eivind Gamst Date: Thu, 30 Apr 2026 13:09:17 +0200 Subject: [PATCH 5/5] Temp store for now --- src/lib.rs | 6 +- src/modifiers.rs | 63 +++++++++------ src/ron_format.rs | 196 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 235 insertions(+), 30 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 614d3e7d..95c13062 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ pub use composer::{ComposeState, ComposeString}; use composer::{Composer, Token}; mod composer; mod modifiers; -use modifiers::{level_index, KeyDirection, ModType, Modifiers, CAPS_LOCK, NUM_LOCK}; +use modifiers::{level_index, KeyDirection, ModType, Modifiers}; pub use modifiers::{LedState, RawModifiers}; mod bitset; pub(crate) use bitset::KeyBitSet; @@ -272,12 +272,12 @@ impl WKB { let level2 = level2 && self.state_keymap.data.len() > 1 * nk; let base_level = level_index(level5, level3, level2); let layout_index = self.current_layout_idx; - if self.modifiers.locked(NUM_LOCK) { + if self.modifiers.num_locked() { if let Some(c) = self.num_lock_keys.get(layout_index, base_level, evdev_code) { return Some(c); } } - if self.modifiers.locked(CAPS_LOCK) { + if self.modifiers.caps_locked() { if let Some(c) = self .caps_lock_keymap .get(layout_index, base_level, evdev_code) diff --git a/src/modifiers.rs b/src/modifiers.rs index 6682b654..d4cf1c2f 100644 --- a/src/modifiers.rs +++ b/src/modifiers.rs @@ -10,6 +10,15 @@ pub(crate) const MOD_NUM_LOCK: u32 = 16; pub(crate) const MOD_LOGO: u32 = 64; pub(crate) const MOD_ALTGR: u32 = 128; +// State bitfield constants +const STATE_NONE: u8 = 1; +const STATE_LEVEL2: u8 = 2; +const STATE_LEVEL3: u8 = 4; +const STATE_LEVEL5: u8 = 8; +const STATE_COMPOSE: u8 = 16; +const STATE_CAPS_LOCKED: u8 = 32; +const STATE_NUM_LOCKED: u8 = 64; + /// LED bitmask for Num Lock (bit 0). /// LED indicator state. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -189,7 +198,7 @@ impl Modifier { pub struct Modifiers { /// Flat array of (evdev_code, Modifier) pairs. Typically 10-20 entries. pub(crate) entries: Vec<(u32, Modifier)>, - /// Active modifier state: bit0=none, bit1=level2, bit2=level3, bit3=level5, bit4=compose + /// Active modifier state: bit0=none, bit1=level2, bit2=level3, bit3=level5, bit4=compose, bit5=caps_locked, bit6=num_locked state: u8, } @@ -317,11 +326,11 @@ impl Modifiers { pub fn active_mod_type(&self, mod_type: ModType) -> bool { match mod_type { - ModType::None => self.state & 1 != 0, - ModType::Level2 => self.state & 2 != 0, - ModType::Level3 => self.state & 4 != 0, - ModType::Level5 => self.state & 8 != 0, - ModType::Compose => self.state & 16 != 0, + ModType::None => self.state & STATE_NONE != 0, + ModType::Level2 => self.state & STATE_LEVEL2 != 0, + ModType::Level3 => self.state & STATE_LEVEL3 != 0, + ModType::Level5 => self.state & STATE_LEVEL5 != 0, + ModType::Compose => self.state & STATE_COMPOSE != 0, _ => false, } } @@ -331,13 +340,25 @@ impl Modifiers { #[inline] pub fn active_none_and_levels(&self) -> (bool, bool, bool, bool) { ( - self.state & 1 != 0, - self.state & 2 != 0, - self.state & 4 != 0, - self.state & 8 != 0, + self.state & STATE_NONE != 0, + self.state & STATE_LEVEL2 != 0, + self.state & STATE_LEVEL3 != 0, + self.state & STATE_LEVEL5 != 0, ) } + /// Return true if Caps Lock is locked (O(1) from state bitfield). + #[inline] + pub fn caps_locked(&self) -> bool { + self.state & STATE_CAPS_LOCKED != 0 + } + + /// Return true if Num Lock is locked (O(1) from state bitfield). + #[inline] + pub fn num_locked(&self) -> bool { + self.state & STATE_NUM_LOCKED != 0 + } + fn refresh_state(&mut self) { let mut s = 0u8; for (_, modifier) in &self.entries { @@ -364,12 +385,14 @@ impl Modifiers { _ => return, }; match mod_type { - ModType::None => *state |= 1, - ModType::Level2 => *state |= 2, - ModType::Level3 => *state |= 4, - ModType::Level5 => *state |= 8, - ModType::Compose => *state |= 16, - _ => {} + ModType::None => *state |= STATE_NONE, + ModType::Level2 => *state |= STATE_LEVEL2, + ModType::Level3 => *state |= STATE_LEVEL3, + ModType::Level5 => *state |= STATE_LEVEL5, + ModType::Compose => *state |= STATE_COMPOSE, + ModType::Caps => *state |= STATE_CAPS_LOCKED, + ModType::Num => *state |= STATE_NUM_LOCKED, + ModType::Scroll => {} } } @@ -380,14 +403,6 @@ impl Modifiers { self.refresh_state(); } - pub fn locked(&self, evdev_code: u32) -> bool { - self.get(evdev_code).is_some_and(|modifier| { - let mut found = false; - modifier.for_each(|mk| found |= mk.locked()); - found - }) - } - pub fn locked_with_type(&self, evdev_code: u32, mod_type: ModType) -> bool { self.get(evdev_code).is_some_and(|modifier| { let mut found = false; diff --git a/src/ron_format.rs b/src/ron_format.rs index 368ed7a7..a32c55cd 100644 --- a/src/ron_format.rs +++ b/src/ron_format.rs @@ -75,12 +75,18 @@ static INVARIANT_CHARS: &[(u32, &[(u8, char)])] = &[ (72, &[(1, '8')]), (73, &[(1, '9')]), (74, &[(0, '-'), (4, '-')]), + (75, &[(1, '4')]), + (76, &[(1, '5')]), + (77, &[(1, '6')]), ( 78, &[(0, '+'), (1, '+'), (2, '+'), (4, '+'), (5, '+'), (6, '+')], ), + (79, &[(1, '1')]), (80, &[(1, '2')]), (81, &[(1, '3')]), + (82, &[(1, '0')]), + (83, &[(1, '.')]), ( 96, &[ @@ -498,6 +504,7 @@ static INVARIANT_KEYSYMS: &[(u32, &[(u8, u32)])] = &[ (29, &[(0, 0xffe3 /*Control_L*/)]), (42, &[(0, 0xffe1 /*Shift_L*/)]), (54, &[(0, 0xffe2 /*Shift_R*/)]), + (55, &[(4, 0x1008fe21 /*XF86ClearGrab*/)]), ( 59, &[ @@ -598,6 +605,8 @@ static INVARIANT_KEYSYMS: &[(u32, &[(u8, u32)])] = &[ (4, 0x1008fe0a /*XF86Switch_VT_10*/), ], ), + (74, &[(4, 0x1008fe23 /*XF86Prev_VMode*/)]), + (78, &[(4, 0x1008fe22 /*XF86Ungrab*/)]), (84, &[(0, 0xfe03 /*ISO_Level3_Shift*/)]), (85, &[(0, 0x1008ffa9 /*XF86TouchpadToggle*/)]), ( @@ -625,6 +634,7 @@ static INVARIANT_KEYSYMS: &[(u32, &[(u8, u32)])] = &[ (92, &[(0, 0xff23 /*Henkan_Mode*/)]), (93, &[(0, 0xff27 /*Hiragana_Katakana*/)]), (94, &[(0, 0xff22 /*Muhenkan*/)]), + (98, &[(4, 0x1008fe20 /*XF86Grab*/)]), (99, &[(0, 0xff61 /*SunPrint_Screen*/)]), (101, &[(0, 0xff0a /*Linefeed*/)]), (102, &[(0, 0xff50 /*Home*/)]), @@ -1256,14 +1266,83 @@ fn keysym_from_name_or_hex(name: &str) -> u32 { 0 } +/// Convert a Unicode char to its X11 keysym value. +/// ASCII 0x20-0x7e → same code point, Latin-1 0xa0-0xff → same code point, +/// Unicode 0x100+ → cp | 0x0100_0000. +fn char_to_keysym(c: char) -> u32 { + let cp = c as u32; + match cp { + 0x20..=0x7e | 0xa0..=0xff => cp, + _ if cp >= 0x100 => cp | 0x0100_0000, + _ => 0, + } +} + +/// Look up char at (layout, level, evdev) from char_map, falling back to INVARIANT_CHARS. +fn get_char_with_invariants( + char_map: &crate::FlatKeymap, + li: usize, + level: usize, + evdev: u32, +) -> Option { + if let Some(c) = char_map.get(li, level, evdev) { + return Some(c); + } + // Fall back to INVARIANT_CHARS + for &(e, levels) in INVARIANT_CHARS { + if e == evdev { + // Direct level match + if let Some(&(_, c)) = levels.iter().find(|&&(l, _)| l as usize == level) { + return Some(c); + } + // If level 0 char exists and this level > 0, check if it propagates + // (keys with level-0 char are invariant at all levels per is_invariant_char) + if level > 0 { + if let Some(&(_, c)) = levels.iter().find(|&&(l, _)| l == 0) { + return Some(c); + } + } + break; + } + } + None +} + +/// Evdev codes for keypad keys where char→keysym mapping differs from standard. +/// E.g. '*' on KP should map to KP_Multiply (0xffaa), not asterisk (0x2a). +const KP_EVDEV_CODES: &[u32] = &[ + 55, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 96, 98, +]; + +/// Convert a char on a keypad key to the appropriate KP keysym. +fn kp_char_to_keysym(c: char) -> u32 { + match c { + '*' => 0xffaa, // KP_Multiply + '-' => 0xffad, // KP_Subtract + '+' => 0xffab, // KP_Add + '/' => 0xffaf, // KP_Divide + '0'..='9' => 0xffb0 + (c as u32 - '0' as u32), // KP_0..KP_9 + '.' => 0xffae, // KP_Decimal + '\r' => 0xff8d, // KP_Enter + _ => 0, + } +} + +fn is_kp_evdev(evdev: u32) -> bool { + KP_EVDEV_CODES.contains(&evdev) +} + fn flat_keysym_map_to_map( km: &crate::FlatKeysymMap, + char_map: &crate::FlatKeymap, layout_names: &[String], + reachable: &[usize], ) -> BTreeMap>> { let mut result = BTreeMap::new(); for (li, name) in layout_names.iter().enumerate() { let mut levels = BTreeMap::new(); for level in 0..MAX_LEVELS { + let is_reachable = reachable.contains(&level); let mut keys = BTreeMap::new(); for evdev in 0..km.num_keys as u32 { let sym = km.get(li, level, evdev); @@ -1272,7 +1351,45 @@ fn flat_keysym_map_to_map( if is_invariant_keysym(evdev, level as u8, sym) { continue; } + // Skip keysyms derivable from chars + if is_reachable { + if let Some(c) = get_char_with_invariants(char_map, li, level, evdev) { + if is_kp_evdev(evdev) { + // KP keys: always derive from kp_char_to_keysym + if kp_char_to_keysym(c) == sym { + continue; + } + } else if level == 0 { + // Non-KP level 0: derive from char_to_keysym + if char_to_keysym(c) == sym { + continue; + } + } else { + // Non-KP level > 0: only derive if char differs from level 0 + let level0_char = get_char_with_invariants(char_map, li, 0, evdev); + if level0_char != Some(c) && char_to_keysym(c) == sym { + continue; + } + } + } + } keys.insert(evdev, keysym_name(sym)); + } else if is_reachable { + // Explicit NoSymbol: char exists and would produce a derivation, + // but the actual keysym is 0. Must store to prevent false restoration. + if let Some(c) = get_char_with_invariants(char_map, li, level, evdev) { + let would_derive = if is_kp_evdev(evdev) { + kp_char_to_keysym(c) != 0 + } else if level == 0 { + char_to_keysym(c) != 0 + } else { + let level0_char = get_char_with_invariants(char_map, li, 0, evdev); + level0_char != Some(c) && char_to_keysym(c) != 0 + }; + if would_derive { + keys.insert(evdev, "NoSymbol".to_string()); + } + } } } if !keys.is_empty() { @@ -1290,9 +1407,14 @@ fn map_to_flat_keysym_map( map: &BTreeMap>>, layout_names: &[String], num_keys: usize, + char_map: &crate::FlatKeymap, + reachable: &[usize], ) -> crate::FlatKeysymMap { let num_layouts = layout_names.len(); let mut km = crate::FlatKeysymMap::new(num_keys, num_layouts); + // Track positions explicitly set to NoSymbol (keysym 0 despite char existing) + let mut no_symbol: std::collections::HashSet<(usize, usize, u32)> = + std::collections::HashSet::new(); // First apply RON data for (name, levels) in map { if let Some(li) = layout_names.iter().position(|n| n == name) { @@ -1301,13 +1423,15 @@ fn map_to_flat_keysym_map( let sym = keysym_from_name_or_hex(sym_name); if sym != 0 { km.set(li, level as usize, evdev, sym); + } else { + // Explicit NoSymbol entry + no_symbol.insert((li, level as usize, evdev)); } } } } } // Restore invariant keysyms only at their specific listed levels. - // These are keysyms present in ALL layouts, so restore unconditionally. for li in 0..num_layouts { for &(evdev, levels) in INVARIANT_KEYSYMS { if (evdev as usize) >= num_keys { @@ -1320,6 +1444,38 @@ fn map_to_flat_keysym_map( } } } + // Restore keysyms derivable from the char map (only at reachable levels) + for li in 0..num_layouts { + for level in 0..MAX_LEVELS { + if !reachable.contains(&level) { + continue; + } + for evdev in 0..num_keys as u32 { + if km.get(li, level, evdev) == 0 && !no_symbol.contains(&(li, level, evdev)) { + if let Some(c) = get_char_with_invariants(char_map, li, level, evdev) { + let sym = if is_kp_evdev(evdev) { + // KP keys: always derive from kp_char_to_keysym + kp_char_to_keysym(c) + } else if level == 0 { + // Non-KP level 0: derive from char_to_keysym + char_to_keysym(c) + } else { + // Non-KP level > 0: only derive if char differs from level 0 + let level0_char = get_char_with_invariants(char_map, li, 0, evdev); + if level0_char != Some(c) { + char_to_keysym(c) + } else { + 0 + } + }; + if sym != 0 { + km.set(li, level, evdev, sym); + } + } + } + } + } + } km } @@ -1603,7 +1759,12 @@ impl ReadableWKB { false, Some(&reachable), ), - keysym_map: flat_keysym_map_to_map(&wkb.keysym_map, &wkb.layout_names), + keysym_map: flat_keysym_map_to_map( + &wkb.keysym_map, + &wkb.state_keymap, + &wkb.layout_names, + &reachable, + ), compose: composer_to_sequences(&wkb.composer), } } @@ -1619,6 +1780,29 @@ impl ReadableWKB { map_to_flat_keymap(&self.level_exceptions_keymap, &layout_names, num_keys); let modifiers = readable_to_modifiers(&self.modifiers); + // Compute reachable levels from modifier keys. + let has_mod = |target: crate::modifiers::ModType| -> bool { + modifiers.entries.iter().any(|(_, modifier)| { + let mod_kind_has = |mk: &crate::modifiers::ModKind| -> bool { + match mk { + crate::modifiers::ModKind::Pressed { mod_type, .. } + | crate::modifiers::ModKind::Lock { mod_type, .. } + | crate::modifiers::ModKind::Latch { mod_type, .. } => *mod_type == target, + crate::modifiers::ModKind::None => false, + } + }; + match modifier { + crate::modifiers::Modifier::Single(mk) => mod_kind_has(mk), + crate::modifiers::Modifier::Leveled(map) => map.values().any(mod_kind_has), + } + }) + }; + let has_level3 = has_mod(crate::modifiers::ModType::Level3); + let has_level5 = has_mod(crate::modifiers::ModType::Level5); + let reachable: Vec = (0..MAX_LEVELS) + .filter(|&lvl| (lvl & 2 == 0 || has_level3) && (lvl & 4 == 0 || has_level5)) + .collect(); + // Reconstruct repeat keys from defaults + add/remove diffs let mut repeat_set: std::collections::HashSet = DEFAULT_REPEAT_KEYS.iter().copied().collect(); @@ -1632,7 +1816,13 @@ impl ReadableWKB { for code in repeat_set { repeat_keys.insert(code); } - let keysym_map = map_to_flat_keysym_map(&self.keysym_map, &layout_names, num_keys); + let keysym_map = map_to_flat_keysym_map( + &self.keysym_map, + &layout_names, + num_keys, + &state_keymap, + &reachable, + ); let wkb = crate::WKB { repeat_keys, composer: sequences_to_composer(&self.compose),