use std::collections::HashMap;
use std::io::Write;
use std::time::Duration;
use anyhow::{anyhow, Context as _};
use wayland_client::globals::{registry_queue_init, GlobalListContents};
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1,
zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1,
};
use xkbcommon::xkb;
use crate::KeyInjector;
#[cfg(feature = "logging")]
use log::{debug, warn};
#[cfg(not(feature = "logging"))]
macro_rules! debug {
($($arg:tt)*) => {};
}
#[cfg(not(feature = "logging"))]
macro_rules! warn {
($($arg:tt)*) => {};
}
const KEYMAP_FORMAT_TEXT_V1: u32 = 1;
const KEY_STATE_RELEASED: u32 = 0;
const KEY_STATE_PRESSED: u32 = 1;
const KEYMAP_SETTLE: Duration = Duration::from_millis(20);
const EVDEV_XKB_OFFSET: u32 = 8;
pub(crate) fn xkb_to_evdev_keycode(xkb_keycode: u32) -> u32 {
debug_assert!(
xkb_keycode >= EVDEV_XKB_OFFSET,
"XKB keycode {xkb_keycode} is below the evdev offset {EVDEV_XKB_OFFSET}"
);
xkb_keycode.saturating_sub(EVDEV_XKB_OFFSET)
}
const SHIFT_XKB_KEYCODE: u32 = 50;
const ALTGR_XKB_KEYCODE: u32 = 108;
const SHIFT_MOD_MASK: u32 = 1 << 0;
const ALTGR_MOD_MASK: u32 = 1 << 7;
const CTRL_XKB_KEYCODE: u32 = 37;
const CTRL_MOD_MASK: u32 = 1 << 2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Level {
Base,
Shift,
AltGr,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct CharKey {
pub(crate) keycode: u32,
pub(crate) level: Level,
}
const PERMANENT_ASCII_KEYS: &[(u32, &str, &str)] = &[
(10, "1", "exclam"),
(11, "2", "at"),
(12, "3", "numbersign"),
(13, "4", "dollar"),
(14, "5", "percent"),
(15, "6", "asciicircum"),
(16, "7", "ampersand"),
(17, "8", "asterisk"),
(18, "9", "parenleft"),
(19, "0", "parenright"),
(20, "minus", "underscore"),
(21, "equal", "plus"),
(24, "q", "Q"),
(25, "w", "W"),
(26, "e", "E"),
(27, "r", "R"),
(28, "t", "T"),
(29, "y", "Y"),
(30, "u", "U"),
(31, "i", "I"),
(32, "o", "O"),
(33, "p", "P"),
(34, "bracketleft", "braceleft"),
(35, "bracketright", "braceright"),
(38, "a", "A"),
(39, "s", "S"),
(40, "d", "D"),
(41, "f", "F"),
(42, "g", "G"),
(43, "h", "H"),
(44, "j", "J"),
(45, "k", "K"),
(46, "l", "L"),
(47, "semicolon", "colon"),
(48, "apostrophe", "quotedbl"),
(52, "z", "Z"),
(53, "x", "X"),
(54, "c", "C"),
(55, "v", "V"),
(56, "b", "B"),
(57, "n", "N"),
(58, "m", "M"),
(59, "comma", "less"),
(60, "period", "greater"),
(61, "slash", "question"),
(49, "grave", "asciitilde"), (51, "backslash", "bar"), (65, "space", "space"), ];
const SAFE_ALNUM_XKB_KEYCODES: &[u32] = &[
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
];
fn dynamic_keycode_pool() -> Vec<u32> {
SAFE_ALNUM_XKB_KEYCODES.to_vec()
}
fn dynamic_capacity() -> usize {
dynamic_keycode_pool().len()
}
pub(crate) fn is_permanent_ascii(ch: char) -> bool {
permanent_ascii_map().contains_key(&ch)
}
pub(crate) fn permanent_ascii_map() -> HashMap<char, CharKey> {
let mut map = HashMap::new();
for &(keycode, l0, l1) in PERMANENT_ASCII_KEYS {
for (name, level) in [(l0, Level::Base), (l1, Level::Shift)] {
let sym = xkb::keysym_from_name(name, xkb::KEYSYM_NO_FLAGS);
let cp = xkb::keysym_to_utf32(sym);
if cp == 0 {
continue;
}
if let Some(ch) = char::from_u32(cp) {
map.entry(ch).or_insert(CharKey { keycode, level });
}
}
}
map
}
struct BaseKey {
keycode: u32,
keysym_name: &'static str,
}
fn base_keys() -> Vec<BaseKey> {
use evdev::Key;
let entries: &[(Key, &'static str)] = &[
(Key::KEY_BACKSPACE, "BackSpace"),
(Key::KEY_LEFTCTRL, "Control_L"),
(Key::KEY_RIGHTCTRL, "Control_R"),
(Key::KEY_LEFTSHIFT, "Shift_L"),
(Key::KEY_LEFTALT, "Alt_L"),
(Key::KEY_RIGHTALT, "ISO_Level3_Shift"),
(Key::KEY_ENTER, "Return"),
(Key::KEY_TAB, "Tab"),
];
entries
.iter()
.map(|(key, name)| BaseKey {
keycode: u32::from(key.code()) + 8,
keysym_name: name,
})
.collect()
}
pub(crate) fn build_keymap_string(chars: &[char]) -> (String, HashMap<char, CharKey>) {
let base = base_keys();
let mut char_to_key: HashMap<char, CharKey> = permanent_ascii_map();
let pool = dynamic_keycode_pool();
let mut pool_iter = pool.iter().copied();
let mut dynamic_level2: HashMap<u32, String> = HashMap::new();
let mut placed_dynamic: std::collections::HashSet<char> = std::collections::HashSet::new();
for &ch in chars {
if is_permanent_ascii(ch) || placed_dynamic.contains(&ch) {
continue;
}
let keysym = xkb::utf32_to_keysym(ch as u32);
if keysym == xkb::keysyms::KEY_NoSymbol.into() {
warn!("wayland-vk: no keysym for char {ch:?}; skipping");
continue;
}
let name = xkb::keysym_get_name(keysym);
if name.is_empty() {
warn!("wayland-vk: empty keysym name for char {ch:?}; skipping");
continue;
}
let Some(keycode) = pool_iter.next() else {
break;
};
dynamic_level2.insert(keycode, name);
char_to_key.insert(
ch,
CharKey {
keycode,
level: Level::AltGr,
},
);
placed_dynamic.insert(ch);
}
let max_keycode = base
.iter()
.map(|b| b.keycode)
.chain(PERMANENT_ASCII_KEYS.iter().map(|(kc, _, _)| *kc))
.max()
.unwrap_or(*SAFE_ALNUM_XKB_KEYCODES.last().unwrap_or(&8));
let mut keymap = String::new();
keymap.push_str("xkb_keymap {\n");
keymap.push_str("xkb_keycodes \"whisrs\" {\n");
keymap.push_str("minimum = 8;\n");
keymap.push_str(&format!("maximum = {max_keycode};\n"));
for b in &base {
keymap.push_str(&format!("<K{}> = {};\n", b.keycode, b.keycode));
}
for &(kc, _, _) in PERMANENT_ASCII_KEYS {
keymap.push_str(&format!("<K{kc}> = {kc};\n"));
}
keymap.push_str("};\n");
keymap.push_str("xkb_types \"whisrs\" { include \"complete\" };\n");
keymap.push_str("xkb_compatibility \"whisrs\" { include \"complete\" };\n");
keymap.push_str("xkb_symbols \"whisrs\" {\n");
for b in &base {
keymap.push_str(&format!(
"key <K{}> {{ [ {} ] }};\n",
b.keycode, b.keysym_name
));
}
keymap.push_str(&format!(
"modifier_map Shift {{ <K{SHIFT_XKB_KEYCODE}> }};\n"
));
keymap.push_str(&format!(
"modifier_map Mod5 {{ <K{ALTGR_XKB_KEYCODE}> }};\n"
));
keymap.push_str(&format!(
"modifier_map Control {{ <K{CTRL_XKB_KEYCODE}> }};\n"
));
for &(kc, l0, l1) in PERMANENT_ASCII_KEYS {
let l2 = dynamic_level2
.get(&kc)
.map(String::as_str)
.unwrap_or("NoSymbol");
keymap.push_str(&format!(
"key <K{kc}> {{ type=\"FOUR_LEVEL\", [ {l0}, {l1}, {l2} ] }};\n"
));
}
keymap.push_str("};\n");
keymap.push_str("};\n");
(keymap, char_to_key)
}
pub(crate) fn special_base_keycode(ch: char) -> Option<u32> {
use evdev::Key;
match ch {
'\n' => Some(u32::from(Key::KEY_ENTER.code()) + 8),
'\t' => Some(u32::from(Key::KEY_TAB.code()) + 8),
_ => None,
}
}
pub(crate) fn distinct_dynamic_chars(text: &str) -> Vec<char> {
let mut seen: std::collections::HashSet<char> = std::collections::HashSet::new();
let mut out: Vec<char> = Vec::new();
for ch in text.chars() {
if special_base_keycode(ch).is_some() || is_permanent_ascii(ch) {
continue;
}
if !seen.insert(ch) {
continue;
}
let keysym = xkb::utf32_to_keysym(ch as u32);
if keysym == xkb::keysyms::KEY_NoSymbol.into() {
warn!("wayland-vk: no keysym for char {ch:?}; skipping");
continue;
}
out.push(ch);
}
out
}
struct State;
impl Dispatch<WlRegistry, GlobalListContents> for State {
fn event(
_state: &mut Self,
_proxy: &WlRegistry,
_event: <WlRegistry as Proxy>::Event,
_data: &GlobalListContents,
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WlSeat, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WlSeat,
_event: <WlSeat as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwpVirtualKeyboardManagerV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &ZwpVirtualKeyboardManagerV1,
_event: <ZwpVirtualKeyboardManagerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwpVirtualKeyboardV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &ZwpVirtualKeyboardV1,
_event: <ZwpVirtualKeyboardV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
pub struct WaylandVkKeyboard {
conn: Connection,
queue: wayland_client::EventQueue<State>,
qh: QueueHandle<State>,
vk: ZwpVirtualKeyboardV1,
char_to_key: HashMap<char, CharKey>,
ordered_chars: Vec<char>,
keymap_uploaded: bool,
key_delay: Duration,
time: u32,
}
impl WaylandVkKeyboard {
pub fn new(key_delay: Duration) -> anyhow::Result<Self> {
let conn = Connection::connect_to_env()
.context("failed to connect to Wayland display (is WAYLAND_DISPLAY set?)")?;
let (globals, mut queue) = registry_queue_init::<State>(&conn)
.context("failed to initialise Wayland registry/globals")?;
let qh = queue.handle();
let mut state = State;
let seat: WlSeat = globals
.bind(&qh, 1..=1, ())
.context("compositor exposes no wl_seat")?;
let manager: ZwpVirtualKeyboardManagerV1 = globals
.bind(&qh, 1..=1, ())
.map_err(|_| anyhow!("compositor does not support zwp_virtual_keyboard_v1"))?;
let vk = manager.create_virtual_keyboard(&seat, &qh, ());
queue
.roundtrip(&mut state)
.context("initial Wayland roundtrip failed")?;
debug!("wayland-vk virtual keyboard created");
Ok(Self {
conn,
queue,
qh,
vk,
char_to_key: HashMap::new(),
ordered_chars: Vec::new(),
keymap_uploaded: false,
key_delay,
time: 0,
})
}
fn upload_keymap(&mut self, chars: &[char]) -> anyhow::Result<()> {
let (keymap_str, map) = build_keymap_string(chars);
let bytes = keymap_str.into_bytes();
let fd = create_keymap_fd(&bytes)?;
let size = (bytes.len() + 1) as u32;
use std::os::fd::AsFd;
self.vk.keymap(KEYMAP_FORMAT_TEXT_V1, fd.as_fd(), size);
self.conn.flush().context("flush after keymap upload")?;
let mut state = State;
self.queue
.roundtrip(&mut state)
.context("roundtrip after keymap upload")?;
std::thread::sleep(KEYMAP_SETTLE);
self.char_to_key = map;
self.keymap_uploaded = true;
Ok(())
}
fn tap_keycode(&mut self, keycode: u32) -> anyhow::Result<()> {
self.emit_key(keycode, KEY_STATE_PRESSED)?;
self.emit_key(keycode, KEY_STATE_RELEASED)?;
Ok(())
}
fn tap_char_key(&mut self, key: CharKey) -> anyhow::Result<()> {
let mask = match key.level {
Level::Base => 0,
Level::Shift => SHIFT_MOD_MASK,
Level::AltGr => ALTGR_MOD_MASK,
};
if mask != 0 {
self.set_modifiers(mask)?;
}
self.tap_keycode(key.keycode)?;
if mask != 0 {
self.set_modifiers(0)?;
}
Ok(())
}
fn set_modifiers(&mut self, depressed: u32) -> anyhow::Result<()> {
self.vk.modifiers(depressed, 0, 0, 0);
self.conn.flush().context("flush after modifiers")?;
std::thread::sleep(self.key_delay);
Ok(())
}
fn emit_key(&mut self, keycode: u32, key_state: u32) -> anyhow::Result<()> {
let evdev_keycode = xkb_to_evdev_keycode(keycode);
self.vk.key(self.time, evdev_keycode, key_state);
self.time = self.time.saturating_add(self.key_delay.as_millis() as u32);
self.conn.flush().context("flush after key event")?;
std::thread::sleep(self.key_delay);
Ok(())
}
fn type_text_inner(&mut self, text: &str) -> anyhow::Result<()> {
if text.is_empty() {
return Ok(());
}
let wanted = distinct_dynamic_chars(text);
let capacity = dynamic_capacity();
if wanted.len() > capacity {
self.ordered_chars.clear();
self.char_to_key = permanent_ascii_map();
self.type_text_batched(text)?;
self.ordered_chars.clear();
self.keymap_uploaded = false;
return Ok(());
}
let new_chars: Vec<char> = wanted
.iter()
.copied()
.filter(|ch| !self.char_to_key.contains_key(ch))
.collect();
if new_chars.is_empty() {
if !self.keymap_uploaded {
let ordered = self.ordered_chars.clone();
self.upload_keymap(&ordered)?;
}
return self.type_chars(text);
}
let mut next: Vec<char> = self.ordered_chars.clone();
next.extend(new_chars.iter().copied());
if next.len() > capacity {
let needed: std::collections::HashSet<char> = wanted.iter().copied().collect();
let overflow = next.len() - capacity;
let mut to_remove = overflow;
next.retain(|ch| {
if to_remove > 0 && !needed.contains(ch) {
to_remove -= 1;
false
} else {
true
}
});
debug_assert!(next.len() <= capacity);
}
self.ordered_chars = next;
let ordered = self.ordered_chars.clone();
self.upload_keymap(&ordered)?;
self.type_chars(text)
}
fn type_chars(&mut self, text: &str) -> anyhow::Result<()> {
for ch in text.chars() {
if let Some(keycode) = special_base_keycode(ch) {
self.tap_keycode(keycode)?;
continue;
}
let Some(&key) = self.char_to_key.get(&ch) else {
warn!("wayland-vk: cannot type char {ch:?} (no keysym); skipping");
continue;
};
self.tap_char_key(key)?;
}
Ok(())
}
fn type_text_batched(&mut self, text: &str) -> anyhow::Result<()> {
let capacity = dynamic_capacity();
let chars: Vec<char> = text.chars().collect();
let mut idx = 0;
while idx < chars.len() {
let mut distinct: std::collections::HashSet<char> = std::collections::HashSet::new();
let start = idx;
while idx < chars.len() {
let ch = chars[idx];
let counts = special_base_keycode(ch).is_none() && !is_permanent_ascii(ch);
if counts && !distinct.contains(&ch) && distinct.len() >= capacity {
break;
}
if counts {
distinct.insert(ch);
}
idx += 1;
}
let batch = &chars[start..idx];
let batch_dynamic: Vec<char> = {
let mut seen = std::collections::HashSet::new();
batch
.iter()
.copied()
.filter(|&c| {
special_base_keycode(c).is_none()
&& !is_permanent_ascii(c)
&& seen.insert(c)
})
.collect()
};
self.upload_keymap(&batch_dynamic)?;
for &ch in batch {
if let Some(keycode) = special_base_keycode(ch) {
self.tap_keycode(keycode)?;
continue;
}
let Some(&key) = self.char_to_key.get(&ch) else {
warn!("wayland-vk: cannot type char {ch:?} (no keysym); skipping");
continue;
};
self.tap_char_key(key)?;
}
}
Ok(())
}
}
impl KeyInjector for WaylandVkKeyboard {
fn type_text(&mut self, text: &str) -> anyhow::Result<()> {
self.type_text_inner(text)
}
fn backspace(&mut self, count: usize) -> anyhow::Result<()> {
if !self.keymap_uploaded {
self.upload_keymap(&[])?;
}
let backspace = u32::from(evdev::Key::KEY_BACKSPACE.code()) + 8;
for _ in 0..count {
self.tap_keycode(backspace)?;
}
Ok(())
}
fn send_combo(&mut self, keys: &[evdev::Key]) -> anyhow::Result<()> {
if !self.keymap_uploaded {
self.upload_keymap(&[])?;
}
use evdev::Key;
let mut mask = 0u32;
let mut real_keys = Vec::with_capacity(keys.len());
for &key in keys {
match key {
Key::KEY_LEFTCTRL | Key::KEY_RIGHTCTRL => mask |= CTRL_MOD_MASK,
Key::KEY_LEFTSHIFT | Key::KEY_RIGHTSHIFT => mask |= SHIFT_MOD_MASK,
other => real_keys.push(other),
}
}
if mask != 0 {
self.set_modifiers(mask)?;
}
for key in &real_keys {
let keycode = u32::from(key.code()) + 8;
self.emit_key(keycode, KEY_STATE_PRESSED)?;
}
for key in real_keys.iter().rev() {
let keycode = u32::from(key.code()) + 8;
self.emit_key(keycode, KEY_STATE_RELEASED)?;
}
if mask != 0 {
self.set_modifiers(0)?;
}
Ok(())
}
fn set_key_delay(&mut self, delay: Duration) {
self.key_delay = delay;
}
}
impl Drop for WaylandVkKeyboard {
fn drop(&mut self) {
self.vk.destroy();
let _ = self.conn.flush();
let _ = self.qh; }
}
fn create_keymap_fd(bytes: &[u8]) -> anyhow::Result<std::os::fd::OwnedFd> {
use rustix::fs::{memfd_create, MemfdFlags};
let fd = memfd_create("whisrs-keymap", MemfdFlags::CLOEXEC)
.context("memfd_create for keymap failed")?;
let mut file = std::fs::File::from(fd);
file.write_all(bytes).context("writing keymap to memfd")?;
file.write_all(&[0])
.context("writing keymap NUL terminator")?;
file.flush().ok();
let fd: std::os::fd::OwnedFd = file.into();
Ok(fd)
}
#[cfg(test)]
mod tests {
use super::*;
fn compile(keymap_str: &str) -> xkb::Keymap {
let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
xkb::Keymap::new_from_string(
&ctx,
keymap_str.to_string(),
xkb::KEYMAP_FORMAT_TEXT_V1,
xkb::KEYMAP_COMPILE_NO_FLAGS,
)
.unwrap_or_else(|| panic!("keymap failed to compile:\n{keymap_str}"))
}
fn resolve_via_state(keymap: &xkb::Keymap, key: CharKey) -> String {
let mut state = xkb::State::new(keymap);
let mask = match key.level {
Level::Base => 0,
Level::Shift => SHIFT_MOD_MASK,
Level::AltGr => ALTGR_MOD_MASK,
};
state.update_mask(mask, 0, 0, 0, 0, 0);
let out = state.key_get_utf8(xkb::Keycode::new(key.keycode));
state.update_mask(0, 0, 0, 0, 0, 0);
out
}
fn assert_roundtrip(input: &str) {
let dynamic = distinct_dynamic_chars(input);
let (keymap_str, map) = build_keymap_string(&dynamic);
let keymap = compile(&keymap_str);
let mut checked = 0usize;
for ch in input.chars() {
if special_base_keycode(ch).is_some() {
continue; }
let key = *map
.get(&ch)
.unwrap_or_else(|| panic!("char {ch:?} was not assigned a CharKey"));
let got = resolve_via_state(&keymap, key);
assert_eq!(
got,
ch.to_string(),
"char {ch:?} resolved to {got:?} at {key:?} (compositor-sim mismatch)"
);
checked += 1;
}
eprintln!(
"roundtrip OK: {checked} chars for {input:?} (dynamic glyphs: {})",
dynamic.len()
);
}
#[test]
fn roundtrip_mixed_scripts() {
let input = "Ammeter اميتر ω д 1+2=3! UPPER lower";
assert_roundtrip(input);
let dynamic = distinct_dynamic_chars(input);
assert!(dynamic.len() <= dynamic_capacity());
assert!(
dynamic.iter().all(|c| (*c as u32) >= 0x80),
"distinct_dynamic_chars must contain only non-ASCII, got {dynamic:?}"
);
}
#[test]
fn all_printable_ascii_is_permanent_and_resolvable() {
let (keymap_str, map) = build_keymap_string(&[]);
let keymap = compile(&keymap_str);
let mut checked = 0usize;
for cp in 0x20u32..=0x7e {
let ch = char::from_u32(cp).unwrap();
assert!(
is_permanent_ascii(ch),
"printable ASCII {ch:?} (U+{cp:04X}) must be permanent"
);
let key = *map
.get(&ch)
.unwrap_or_else(|| panic!("permanent ASCII {ch:?} missing from empty-keymap map"));
let got = resolve_via_state(&keymap, key);
assert_eq!(
got,
ch.to_string(),
"permanent ASCII {ch:?} resolved to {got:?} at {key:?}"
);
checked += 1;
}
assert_eq!(checked, 95, "expected all 95 printable ASCII chars");
eprintln!("ascii-permanence OK: all 95 printable ASCII resolvable with zero dynamics");
}
#[test]
fn streaming_accumulation_keeps_all_chars_resolvable() {
let deltas = [
"the quick ",
"brown اهلا ",
"fox مرحبا ",
"привет jumps ",
"over мир ",
"the lazy ",
"صباح dog ",
"доброе утро!",
];
let mut ordered: Vec<char> = Vec::new(); let mut typed_so_far = String::new();
for delta in deltas {
for ch in distinct_dynamic_chars(delta) {
if !ordered.contains(&ch) {
ordered.push(ch);
}
}
typed_so_far.push_str(delta);
assert!(
ordered.len() <= dynamic_capacity(),
"test fixture exceeded capacity ({} > {})",
ordered.len(),
dynamic_capacity()
);
let (keymap_str, map) = build_keymap_string(&ordered);
let keymap = compile(&keymap_str);
for ch in typed_so_far.chars() {
if special_base_keycode(ch).is_some() {
continue;
}
let key = *map.get(&ch).unwrap_or_else(|| {
panic!("after delta {delta:?}: char {ch:?} lost from keymap")
});
let got = resolve_via_state(&keymap, key);
assert_eq!(
got,
ch.to_string(),
"after delta {delta:?}: char {ch:?} no longer resolves ({got:?} at {key:?})"
);
}
}
eprintln!(
"streaming-accumulation OK: {} deltas, {} accumulated non-ASCII glyphs, all chars \
stayed resolvable across every re-upload",
deltas.len(),
ordered.len()
);
}
#[test]
fn accumulation_is_stable_across_reuploads() {
let (_k1, map1) = build_keymap_string(&distinct_dynamic_chars("اهلا"));
let m1 = *map1.get(&'ا').unwrap();
let (_k2, map2) = build_keymap_string(&distinct_dynamic_chars("اهلا مرحبا привет"));
let m2 = *map2.get(&'ا').unwrap();
assert_eq!(
m1, m2,
"glyph 'ا' relocated across re-upload: {m1:?} -> {m2:?}"
);
assert_eq!(map1.get(&'a'), map2.get(&'a'));
assert_eq!(map1.get(&' '), map2.get(&' '));
}
#[test]
fn roundtrip_batches_over_capacity() {
let needed = dynamic_capacity() + 20;
let mut input = String::new();
let mut cp = 0x0410u32; let mut count = 0;
while count < needed {
if let Some(c) = char::from_u32(cp) {
if !c.is_control()
&& (c as u32) >= 0x80
&& xkb::utf32_to_keysym(cp) != xkb::keysyms::KEY_NoSymbol.into()
{
input.push(c);
count += 1;
}
}
cp += 1;
}
let dynamic = distinct_dynamic_chars(&input);
assert!(dynamic.len() > dynamic_capacity());
let mut idx = 0;
let mut total = 0usize;
let mut batches = 0usize;
while idx < dynamic.len() {
let end = (idx + dynamic_capacity()).min(dynamic.len());
let batch = &dynamic[idx..end];
let (keymap_str, map) = build_keymap_string(batch);
let keymap = compile(&keymap_str);
for &ch in batch {
let key = *map.get(&ch).expect("batch char has CharKey");
assert_eq!(resolve_via_state(&keymap, key), ch.to_string());
total += 1;
}
batches += 1;
idx = end;
}
assert_eq!(total, dynamic.len());
assert!(batches >= 2, "expected >=2 batches, got {batches}");
eprintln!(
"batching OK: {} non-ASCII glyphs across {batches} batches (capacity {})",
dynamic.len(),
dynamic_capacity()
);
}
#[test]
fn special_base_keycode_maps_newline_and_tab() {
use evdev::Key;
assert_eq!(
special_base_keycode('\n'),
Some(u32::from(Key::KEY_ENTER.code()) + 8)
);
assert_eq!(
special_base_keycode('\t'),
Some(u32::from(Key::KEY_TAB.code()) + 8)
);
assert_eq!(special_base_keycode('a'), None);
assert_eq!(special_base_keycode(' '), None);
assert_eq!(special_base_keycode('م'), None);
}
#[test]
fn newline_tab_are_not_dynamic_but_compile_at_base_keycodes() {
use evdev::Key;
let dynamic = distinct_dynamic_chars("\n\t");
assert!(dynamic.is_empty(), "got {dynamic:?}");
let (keymap_str, _map) = build_keymap_string(&dynamic);
let keymap = compile(&keymap_str);
let enter_kc = u32::from(Key::KEY_ENTER.code()) + 8;
let enter_syms = keymap.key_get_syms_by_level(xkb::Keycode::new(enter_kc), 0, 0);
assert_eq!(enter_syms.len(), 1);
assert_eq!(xkb::keysym_get_name(enter_syms[0]), "Return");
let tab_kc = u32::from(Key::KEY_TAB.code()) + 8;
let tab_syms = keymap.key_get_syms_by_level(xkb::Keycode::new(tab_kc), 0, 0);
assert_eq!(tab_syms.len(), 1);
assert_eq!(xkb::keysym_get_name(tab_syms[0]), "Tab");
}
#[test]
fn distinct_dynamic_chars_excludes_ascii_and_whitespace_base_keys() {
let got = distinct_dynamic_chars("a\tb\n ab مرحبا مرحبا");
assert_eq!(got, vec!['م', 'ر', 'ح', 'ب', 'ا']);
}
#[test]
fn accumulation_detects_no_new_chars_on_reused_or_ascii_glyphs() {
let (_keymap, char_to_key) = build_keymap_string(&distinct_dynamic_chars("مرحبا"));
for reuse in [" احبرم", "hello world مرحبا", "ABC 123 !@# مرم"] {
let new_chars: Vec<char> = distinct_dynamic_chars(reuse)
.into_iter()
.filter(|ch| !char_to_key.contains_key(ch))
.collect();
assert!(
new_chars.is_empty(),
"reuse {reuse:?} should introduce no new dynamic glyph, got {new_chars:?}"
);
}
let new_chars: Vec<char> = distinct_dynamic_chars("مرحبا привет")
.into_iter()
.filter(|ch| !char_to_key.contains_key(ch))
.collect();
assert_eq!(new_chars, vec!['п', 'р', 'и', 'в', 'е', 'т']);
}
fn is_dangerous_evdev_keycode(evdev: u32) -> bool {
if (183..=247).contains(&evdev) {
return true;
}
matches!(
evdev,
99 | 113 | 114 | 115 | 139 | 148 | 149 | 163 | 164 | 165
)
}
#[test]
fn no_emitted_keycode_is_a_special_key() {
let text = "Hello, мир! اهلا ω 1234567890 -=[];'/.,";
let dynamic = distinct_dynamic_chars(text);
let (_keymap_str, char_to_key) = build_keymap_string(&dynamic);
assert!(!char_to_key.is_empty());
for (&ch, key) in &char_to_key {
let evdev = xkb_to_evdev_keycode(key.keycode);
assert!(
!is_dangerous_evdev_keycode(evdev),
"char {ch:?}: emitted evdev keycode {evdev} (XKB {}) is a special key",
key.keycode
);
if key.level == Level::AltGr {
assert!(
SAFE_ALNUM_XKB_KEYCODES.contains(&key.keycode),
"char {ch:?}: dynamic XKB keycode {} outside safe alphanumeric block",
key.keycode
);
}
assert!(
PERMANENT_ASCII_KEYS
.iter()
.any(|(kc, _, _)| *kc == key.keycode),
"char {ch:?}: keycode {} is not a permanent-ASCII keycode",
key.keycode
);
}
use evdev::Key;
for key in [
Key::KEY_BACKSPACE,
Key::KEY_LEFTCTRL,
Key::KEY_RIGHTCTRL,
Key::KEY_LEFTSHIFT,
Key::KEY_LEFTALT,
Key::KEY_RIGHTALT,
Key::KEY_ENTER,
Key::KEY_TAB,
] {
let evdev = u32::from(key.code());
assert!(
!is_dangerous_evdev_keycode(evdev),
"base key {key:?}: evdev keycode {evdev} is a special key"
);
}
assert!(is_dangerous_evdev_keycode(200u32 - EVDEV_XKB_OFFSET)); assert!(is_dangerous_evdev_keycode(210)); eprintln!(
"no-dangerous-keycode OK: {} printable glyphs + base keys all in safe evdev space",
char_to_key.len()
);
}
#[test]
fn emitted_combos_are_plain_keys_in_stock_keymap() {
let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let stock = xkb::Keymap::new_from_names(
&ctx,
"evdev",
"pc105",
"us",
"",
None,
xkb::KEYMAP_COMPILE_NO_FLAGS,
)
.expect("stock us keymap compiles");
let check_plain = |kc: u32, level: u32, what: &str| {
let syms = stock.key_get_syms_by_level(xkb::Keycode::new(kc), 0, level);
if syms.is_empty() {
return;
}
let cp = xkb::keysym_to_utf32(syms[0]);
if cp == 0 {
let name = xkb::keysym_get_name(syms[0]);
assert!(
!name.starts_with("XF86")
&& !name.starts_with('F')
&& name != "Print"
&& name != "Sys_Req",
"{what}: keycode {kc} level {level} resolves to special keysym {name} in \
stock us"
);
return;
}
let c = char::from_u32(cp).unwrap_or('\u{0}');
assert!(
!c.is_control(),
"{what}: keycode {kc} level {level} resolves to control char in stock us"
);
};
for &(kc, _, _) in PERMANENT_ASCII_KEYS {
check_plain(kc, 0, "permanent-ascii level0");
check_plain(kc, 1, "permanent-ascii level1 (Shift)");
}
for kc in dynamic_keycode_pool() {
check_plain(kc, 2, "dynamic AltGr level2");
}
eprintln!(
"stock-keymap bind-safety OK: level0/level1 of {} permanent ASCII keys and AltGr \
level2 of {} safe keycodes are all plain/non-dangerous in stock us",
PERMANENT_ASCII_KEYS.len(),
dynamic_keycode_pool().len()
);
}
#[test]
fn vk_key_arg_plus_8_resolves_to_intended_glyph() {
let text = "ab AB م ω д 1+2=3 !@#";
let dynamic = distinct_dynamic_chars(text);
let (keymap_str, char_to_key) = build_keymap_string(&dynamic);
let keymap = compile(&keymap_str);
assert!(!char_to_key.is_empty());
for ch in text.chars() {
if special_base_keycode(ch).is_some() || ch == ' ' {
}
let Some(key) = char_to_key.get(&ch).copied() else {
continue;
};
let evdev = xkb_to_evdev_keycode(key.keycode);
assert_eq!(
evdev + EVDEV_XKB_OFFSET,
key.keycode,
"char {ch:?}: vk.key arg {evdev} + {EVDEV_XKB_OFFSET} must equal XKB {}",
key.keycode
);
let got = resolve_via_state(&keymap, key);
assert_eq!(
got,
ch.to_string(),
"char {ch:?} compositor-sim mismatch at {key:?}"
);
}
use evdev::Key;
for (key, expected_name) in [
(Key::KEY_BACKSPACE, "BackSpace"),
(Key::KEY_ENTER, "Return"),
(Key::KEY_TAB, "Tab"),
(Key::KEY_LEFTCTRL, "Control_L"),
(Key::KEY_RIGHTALT, "ISO_Level3_Shift"),
] {
let xkb_keycode = u32::from(key.code()) + EVDEV_XKB_OFFSET;
let syms = keymap.key_get_syms_by_level(xkb::Keycode::new(xkb_keycode), 0, 0);
assert_eq!(syms.len(), 1, "base key {expected_name} keysym count");
assert_eq!(xkb::keysym_get_name(syms[0]), expected_name);
}
eprintln!(
"keycode-arithmetic OK: vk.key arg + {EVDEV_XKB_OFFSET} == XKB keycode for all glyphs"
);
}
#[test]
fn keysym_name_roundtrip_tricky_chars() {
for &ch in &['é', 'م', '😀', 'ω', 'д'] {
let (keymap_str, map) = build_keymap_string(&[ch]);
let keymap = compile(&keymap_str);
let key = *map
.get(&ch)
.unwrap_or_else(|| panic!("char {ch:?} not assigned a CharKey"));
assert_eq!(
key.level,
Level::AltGr,
"non-ASCII {ch:?} should be AltGr-level"
);
assert_eq!(resolve_via_state(&keymap, key), ch.to_string());
}
}
#[test]
fn shifted_ascii_uses_shift_level() {
let map = permanent_ascii_map();
let lower = map[&'a'];
let upper = map[&'A'];
assert_eq!(lower.keycode, upper.keycode, "'a'/'A' share a keycode");
assert_eq!(lower.level, Level::Base);
assert_eq!(upper.level, Level::Shift);
let one = map[&'1'];
let bang = map[&'!'];
assert_eq!(one.keycode, bang.keycode);
assert_eq!(bang.level, Level::Shift);
let (keymap_str, m) = build_keymap_string(&[]);
let keymap = compile(&keymap_str);
assert_eq!(resolve_via_state(&keymap, m[&'A']), "A");
assert_eq!(resolve_via_state(&keymap, m[&'a']), "a");
assert_eq!(resolve_via_state(&keymap, m[&'!']), "!");
assert_eq!(resolve_via_state(&keymap, m[&'1']), "1");
assert_eq!(resolve_via_state(&keymap, m[&' ']), " ");
}
#[test]
fn space_is_permanent_base_level() {
assert!(is_permanent_ascii(' '));
let key = permanent_ascii_map()[&' '];
assert_eq!(
key.level,
Level::Base,
"space must be base-level (no modifier)"
);
let (keymap_str, m) = build_keymap_string(&[]);
let keymap = compile(&keymap_str);
assert_eq!(resolve_via_state(&keymap, m[&' ']), " ");
}
#[test]
fn modifier_masks_select_levels() {
let (keymap_str, map) = build_keymap_string(&['م']);
let keymap = compile(&keymap_str);
assert_eq!(
keymap.mod_get_index(xkb::MOD_NAME_SHIFT),
SHIFT_MOD_MASK.trailing_zeros(),
"Shift modifier index mismatch with SHIFT_MOD_MASK"
);
assert_eq!(
keymap.mod_get_index("Mod5"),
ALTGR_MOD_MASK.trailing_zeros(),
"Mod5/AltGr modifier index mismatch with ALTGR_MOD_MASK"
);
assert_eq!(
keymap.mod_get_index(xkb::MOD_NAME_CTRL),
CTRL_MOD_MASK.trailing_zeros(),
"Control modifier index mismatch with CTRL_MOD_MASK"
);
let a = map[&'a'];
let mut st = xkb::State::new(&keymap);
st.update_mask(0, 0, 0, 0, 0, 0);
assert_eq!(st.key_get_utf8(xkb::Keycode::new(a.keycode)), "a");
st.update_mask(SHIFT_MOD_MASK, 0, 0, 0, 0, 0);
assert_eq!(
st.key_get_utf8(xkb::Keycode::new(a.keycode)),
"A",
"SHIFT_MOD_MASK must select the Shift level"
);
let meem = map[&'م'];
assert_eq!(meem.level, Level::AltGr);
st.update_mask(ALTGR_MOD_MASK, 0, 0, 0, 0, 0);
assert_eq!(
st.key_get_utf8(xkb::Keycode::new(meem.keycode)),
"م",
"ALTGR_MOD_MASK must select the AltGr (dynamic) level"
);
eprintln!(
"modifier-mask OK: Shift={SHIFT_MOD_MASK} Mod5={ALTGR_MOD_MASK} select levels 1/2, \
Control={CTRL_MOD_MASK} is a real modifier"
);
}
}