use anyhow::Context;
use x11rb::connection::{Connection, RequestConnection};
use x11rb::protocol::xinput::{self, ConnectionExt as _};
use x11rb::protocol::xkb::{self, ConnectionExt as _};
use x11rb::protocol::xproto::{ConnectionExt as _, Window, KEY_PRESS_EVENT, KEY_RELEASE_EVENT};
use x11rb::protocol::xtest::{self, ConnectionExt as _};
use x11rb::rust_connection::RustConnection;
use xkbcommon::xkb::keysyms::{KEY_Control_L, KEY_ISO_Level3_Shift, KEY_Shift_L};
use crate::keymap::Group1Keysyms;
use crate::{Key, KeyMapping, KeyTap, XkbKeymap};
const EVDEV_TO_X11_KEYCODE: u16 = 8;
const XTEST_KEYBOARD_NAME: &[u8] = b"Virtual core XTEST keyboard";
fn group1_keysyms(first_keycode: u8, maps: &[xkb::KeySymMap]) -> Group1Keysyms {
let levels = maps
.iter()
.zip(u16::from(first_keycode)..)
.filter_map(|(map, x11_keycode)| {
let width = usize::from(map.width);
let levels = [0, 1, 2, 3].map(|level| {
if level < width {
map.syms.get(level).copied().unwrap_or(0)
} else {
0
}
});
Some((x11_keycode.checked_sub(EVDEV_TO_X11_KEYCODE)?, levels))
})
.collect();
Group1Keysyms(levels)
}
const MODIFIERS: [(Key, u32, &str); 2] = [
(Key::KEY_LEFTSHIFT, KEY_Shift_L, "Shift_L"),
(Key::KEY_LEFTCTRL, KEY_Control_L, "Control_L"),
];
#[derive(Debug, Default)]
struct Mismatches {
modifiers: Vec<&'static str>,
chars: Vec<char>,
}
impl Mismatches {
fn is_empty(&self) -> bool {
self.modifiers.is_empty() && self.chars.is_empty()
}
fn sample(&self) -> String {
let mut chars = self.chars.clone();
chars.sort_unstable();
let items: Vec<String> = self
.modifiers
.iter()
.map(|name| name.to_string())
.chain(chars.iter().map(|ch| format!("{ch:?}")))
.take(12)
.collect();
format!("[{}]", items.join(", "))
}
}
fn taps(mapping: &KeyMapping) -> impl Iterator<Item = &KeyTap> {
std::iter::once(&mapping.main).chain(&mapping.follow)
}
fn layout_mismatches(server: &Group1Keysyms, keymap: &XkbKeymap) -> Mismatches {
let base = |keycode| KeyTap {
keycode,
shift: false,
altgr: false,
};
let typed = || {
keymap
.iter()
.filter(|(ch, _)| !ch.is_control() || matches!(ch, '\t' | '\n' | '\r'))
};
let mut modifiers: Vec<&'static str> = MODIFIERS
.iter()
.filter(|(key, keysym, _)| !same_symbol(server.keysym(&base(key.code())), *keysym))
.map(|(_, _, name)| *name)
.collect();
if typed().any(|(_, m)| taps(m).any(|tap| tap.altgr))
&& !same_symbol(
server.keysym(&base(keymap.level3_keycode())),
KEY_ISO_Level3_Shift,
)
{
modifiers.push("ISO_Level3_Shift");
}
let chars = typed()
.filter(|(_, m)| {
taps(m).any(|tap| !same_symbol(server.keysym(tap), keymap.group1().keysym(tap)))
})
.map(|(ch, _)| ch)
.collect();
Mismatches { modifiers, chars }
}
fn drop_unsendable(keymap: &mut XkbKeymap, min: u8, max: u8) {
let sendable = |tap: &KeyTap| {
tap.keycode
.checked_add(EVDEV_TO_X11_KEYCODE)
.and_then(|x11| u8::try_from(x11).ok())
.is_some_and(|x11| (min..=max).contains(&x11))
};
keymap.retain(|m| taps(m).all(sendable));
}
fn same_symbol(a: u32, b: u32) -> bool {
let ch = xkbcommon::xkb::keysym_to_utf32(a.into());
a == b || (ch != 0 && ch == xkbcommon::xkb::keysym_to_utf32(b.into()))
}
pub(crate) struct XtestSink {
conn: RustConnection,
root: Window,
pressed: [bool; 256],
}
impl XtestSink {
pub(crate) fn connect(keymap: &mut XkbKeymap) -> anyhow::Result<Self> {
let (conn, screen) = x11rb::connect(None).context("failed to connect to the X server")?;
if conn
.extension_information(xtest::X11_EXTENSION_NAME)
.context("failed to query the XTEST extension")?
.is_none()
{
anyhow::bail!("the X server does not support the XTEST extension");
}
if conn
.extension_information("XWAYLAND")
.context("failed to query the XWAYLAND extension")?
.is_some()
{
anyhow::bail!("the X server is Xwayland; XTEST keys would only reach X11 clients");
}
let setup = conn.setup();
drop_unsendable(keymap, setup.min_keycode, setup.max_keycode);
let root = setup.roots[screen].root;
let mut sink = Self {
conn,
root,
pressed: [false; 256],
};
sink.check_keymap(keymap)?;
sink.release_all_keys()?;
Ok(sink)
}
fn check_keymap(&self, keymap: &XkbKeymap) -> anyhow::Result<()> {
let device = self.xtest_keyboard_id()?;
if !self
.conn
.xkb_use_extension(1, 0)
.context("failed to query the XKB extension")?
.reply()
.context("failed to query the XKB extension")?
.supported
{
anyhow::bail!("the X server does not support the XKB extension");
}
let reply = self
.conn
.xkb_get_map(
device.into(),
xkb::MapPart::KEY_SYMS,
xkb::MapPart::default(),
0,
0,
0,
0,
0,
0,
0,
0,
xkb::VMod::default(),
0,
0,
0,
0,
0,
0,
)
.context("failed to request the XTEST keyboard mapping")?
.reply()
.context("failed to read the XTEST keyboard mapping")?;
let server = group1_keysyms(
reply.first_key_sym,
reply.map.syms_rtrn.as_deref().unwrap_or_default(),
);
let mismatches = layout_mismatches(&server, keymap);
if !mismatches.is_empty() {
anyhow::bail!(
"the X server's XTEST keyboard types differently than the detected layout \
(keys for {} differ); run setxkbmap with your layout, or, if an XKB option \
moves one of these keys (e.g. ctrl:swapcaps), use backend = \"auto\"",
mismatches.sample()
);
}
Ok(())
}
fn xtest_keyboard_id(&self) -> anyhow::Result<u8> {
self.conn
.xinput_xi_query_version(2, 0)
.context("failed to query XInput")?
.reply()
.context("the X server does not support XInput 2")?;
let devices = self
.conn
.xinput_xi_query_device(xinput::Device::ALL)
.context("failed to list input devices")?
.reply()
.context("failed to list input devices")?;
let device = devices
.infos
.iter()
.find(|d| {
d.type_ == xinput::DeviceType::SLAVE_KEYBOARD && d.name == XTEST_KEYBOARD_NAME
})
.context("the X server has no XTEST keyboard")?;
u8::try_from(device.deviceid).context("XTEST keyboard device id out of range")
}
fn release_all_keys(&mut self) -> anyhow::Result<()> {
let setup = self.conn.setup();
for detail in setup.min_keycode..=setup.max_keycode {
self.fake_key(detail, false)?;
}
self.sync()
}
fn fake_key(&mut self, detail: u8, pressed: bool) -> anyhow::Result<()> {
let kind = if pressed {
KEY_PRESS_EVENT
} else {
KEY_RELEASE_EVENT
};
self.conn
.xtest_fake_input(kind, detail, 0, self.root, 0, 0, 0)
.context("failed to send XTEST key event")?;
self.pressed[usize::from(detail)] = pressed;
Ok(())
}
pub(crate) fn key(&mut self, code: u16, pressed: bool) -> anyhow::Result<()> {
let detail = u8::try_from(code + EVDEV_TO_X11_KEYCODE)
.with_context(|| format!("evdev keycode {code} has no X11 keycode"))?;
self.fake_key(detail, pressed)
}
pub(crate) fn flush(&mut self) -> anyhow::Result<()> {
self.conn.flush().context("failed to flush X connection")?;
Ok(())
}
pub(crate) fn sync(&mut self) -> anyhow::Result<()> {
self.conn
.get_input_focus()
.context("failed to send X round-trip")?
.reply()
.context("X server round-trip failed")?;
while self
.conn
.poll_for_event()
.context("failed to read X events")?
.is_some()
{}
Ok(())
}
}
impl Drop for XtestSink {
fn drop(&mut self) {
for detail in 0..=u8::MAX {
if self.pressed[usize::from(detail)] {
let _ = self.fake_key(detail, false);
}
}
let _ = self.conn.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::KeyboardLayout;
fn detected(layout: &str, variant: &str) -> XkbKeymap {
XkbKeymap::from_layout(&KeyboardLayout {
layout: layout.to_string(),
variant: variant.to_string(),
})
.unwrap()
}
fn server(layout: &str, variant: &str) -> Group1Keysyms {
server_with_options(layout, variant, "")
}
fn server_with_options(layout: &str, variant: &str, options: &str) -> Group1Keysyms {
let context = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
let keymap = xkbcommon::xkb::Keymap::new_from_names(
&context,
"",
"",
layout,
variant,
Some(options.to_string()),
xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
)
.unwrap();
let mut keysyms = Group1Keysyms::from_keymap(&keymap);
keysyms
.0
.retain(|&keycode, _| keycode + EVDEV_TO_X11_KEYCODE <= u16::from(u8::MAX));
keysyms
}
fn mismatches_on(detected_layout: (&str, &str), server: &Group1Keysyms) -> Mismatches {
let mut keymap = detected(detected_layout.0, detected_layout.1);
drop_unsendable(&mut keymap, 8, 255);
layout_mismatches(server, &keymap)
}
fn mismatches(detected_layout: (&str, &str), server_layout: (&str, &str)) -> Mismatches {
mismatches_on(detected_layout, &server(server_layout.0, server_layout.1))
}
#[test]
fn de_on_us_xtest_keyboard_fails() {
let m = mismatches(("de", ""), ("us", ""));
assert!(m.chars.contains(&'y') && m.chars.contains(&'z'), "{m:?}");
}
#[test]
fn altgr_level_differences_fail() {
let m = mismatches(("us", "altgr-intl"), ("us", ""));
assert!(m.chars.contains(&'é'), "{m:?}");
}
#[test]
fn dead_key_layout_on_us_xtest_keyboard_fails() {
let m = mismatches(("us", "intl"), ("us", ""));
assert!(m.chars.contains(&'\''), "{m:?}");
}
#[test]
fn matching_layouts_pass() {
for (layout, variant) in [("us", ""), ("us", "altgr-intl"), ("us", "intl"), ("de", "")] {
let m = mismatches((layout, variant), (layout, variant));
assert!(m.is_empty(), "{layout}({variant}): {m:?}");
}
}
#[test]
fn second_group_passes_on_its_first_group() {
assert!(detected("us,ru", "").lookup('ф').is_some());
let m = mismatches(("us,ru", ""), ("us", ""));
assert!(m.is_empty(), "{m:?}");
let m = mismatches(("us,ru", ""), ("us,ru", ""));
assert!(m.is_empty(), "{m:?}");
}
#[test]
fn altgr_characters_fail_without_level3_shift() {
let keymap = detected("us", "altgr-intl");
let mut server = server("us", "altgr-intl");
server.0.get_mut(&keymap.level3_keycode()).unwrap()[0] = xkbcommon::xkb::keysyms::KEY_Alt_R;
let m = mismatches_on(("us", "altgr-intl"), &server);
assert_eq!(m.modifiers, ["ISO_Level3_Shift"], "{m:?}");
assert_eq!(m.sample(), "[ISO_Level3_Shift]");
}
#[test]
fn moved_escape_passes() {
let server = server_with_options("us", "", "caps:swapescape");
let esc = KeyTap {
keycode: Key::KEY_ESC.code(),
shift: false,
altgr: false,
};
assert_eq!(server.keysym(&esc), xkbcommon::xkb::keysyms::KEY_Caps_Lock);
let m = mismatches_on(("us", ""), &server);
assert!(m.is_empty(), "{m:?}");
}
#[test]
fn moved_ctrl_fails() {
let m = mismatches_on(("us", ""), &server_with_options("us", "", "ctrl:swapcaps"));
assert_eq!(m.modifiers, ["Control_L"], "{m:?}");
assert_eq!(m.sample(), "[Control_L]");
}
#[test]
fn keys_past_the_x_keycode_range_are_pasted() {
let mut keymap = detected("us", "");
assert!(keymap.lookup('€').is_some(), "`€` is on KEY_EURO");
drop_unsendable(&mut keymap, 8, 255);
assert!(keymap.lookup('€').is_none());
assert!(keymap.lookup('a').is_some());
}
#[test]
fn keys_outside_the_server_keycodes_are_pasted() {
let mut keymap = detected("de", "");
let acute_u = *keymap.lookup('ú').unwrap();
assert_eq!(acute_u.main.keycode + EVDEV_TO_X11_KEYCODE, 21);
assert_eq!(acute_u.follow.unwrap().keycode + EVDEV_TO_X11_KEYCODE, 30);
drop_unsendable(&mut keymap, 8, 25);
assert!(keymap.lookup('q').is_some());
assert!(keymap.lookup('w').is_some(), "`w` (25) is the last keycode");
assert!(keymap.lookup('ú').is_none());
assert!(keymap.lookup('u').is_none());
let mut keymap = detected("us", "");
drop_unsendable(&mut keymap, 25, 255);
assert!(keymap.lookup('q').is_none());
assert!(
keymap.lookup('w').is_some(),
"`w` (25) is the first keycode"
);
assert!(keymap.lookup('a').is_some());
}
#[test]
fn group1_keysyms_skips_later_groups() {
let maps = [xkb::KeySymMap {
kt_index: [0; 4],
group_info: 2,
width: 4,
syms: vec![0x38, 0x2a, 0, 0, 0x38, 0x2a, 0x10020bd, 0],
}];
let server = group1_keysyms(17, &maps);
assert_eq!(server.0.get(&9), Some(&[0x38, 0x2a, 0, 0]));
}
}