use core::ffi::{CStr, c_char, c_int, c_void};
use core::ptr;
use std::os::fd::{AsRawFd, FromRawFd};
use std::sync::OnceLock;
use crcbl_core::KeyCode;
use crcbl_core::input::{Keysym, Modifiers};
mod compose;
use compose::{Compose, ComposeLib, Composed, load_compose};
unsafe extern "C" {
fn mmap(
addr: *mut c_void,
length: usize,
prot: c_int,
flags: c_int,
fd: c_int,
offset: i64,
) -> *mut c_void;
fn munmap(addr: *mut c_void, length: usize) -> c_int;
fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}
const PROT_READ: c_int = 1;
const MAP_PRIVATE: c_int = 2;
const MAP_FAILED: *mut c_void = usize::MAX as *mut c_void;
const RTLD_NOW: c_int = 2;
const KEYMAP_FORMAT_TEXT_V1: c_int = 1;
const STATE_MODS_EFFECTIVE: c_int = 1 << 3;
const MOD_INVALID: u32 = u32::MAX;
pub const EVDEV_OFFSET: u32 = 8;
#[repr(C)]
struct XkbContext {
_opaque: [u8; 0],
}
#[repr(C)]
struct XkbKeymap {
_opaque: [u8; 0],
}
#[repr(C)]
struct XkbState {
_opaque: [u8; 0],
}
const TEXT_BUFFER: usize = 64;
mod prototype {
use super::{XkbContext, XkbKeymap, XkbState, c_char, c_int};
pub type ContextNew = unsafe extern "C" fn(c_int) -> *mut XkbContext;
pub type ContextUnref = unsafe extern "C" fn(*mut XkbContext);
pub type KeymapNewFromBuffer =
unsafe extern "C" fn(*mut XkbContext, *const c_char, usize, c_int, c_int) -> *mut XkbKeymap;
pub type KeymapUnref = unsafe extern "C" fn(*mut XkbKeymap);
pub type KeymapModGetIndex = unsafe extern "C" fn(*mut XkbKeymap, *const c_char) -> u32;
pub type KeymapKeyRepeats = unsafe extern "C" fn(*mut XkbKeymap, u32) -> c_int;
pub type StateNew = unsafe extern "C" fn(*mut XkbKeymap) -> *mut XkbState;
pub type StateUnref = unsafe extern "C" fn(*mut XkbState);
pub type StateUpdateMask =
unsafe extern "C" fn(*mut XkbState, u32, u32, u32, u32, u32, u32) -> c_int;
pub type StateKeyGetOneSym = unsafe extern "C" fn(*mut XkbState, u32) -> u32;
pub type StateKeyGetUtf8 =
unsafe extern "C" fn(*mut XkbState, u32, *mut c_char, usize) -> c_int;
pub type StateModIndexIsActive = unsafe extern "C" fn(*mut XkbState, u32, c_int) -> c_int;
pub type StateUpdateKey = unsafe extern "C" fn(*mut XkbState, u32, c_int) -> c_int;
}
mod x11_prototype {
use super::{XkbContext, XkbKeymap, XkbState, c_int, c_void};
pub type SetupXkbExtension = unsafe extern "C" fn(
*mut c_void,
u16,
u16,
c_int,
*mut u16,
*mut u16,
*mut u8,
*mut u8,
) -> c_int;
pub type GetCoreKeyboardDeviceId = unsafe extern "C" fn(*mut c_void) -> i32;
pub type KeymapNewFromDevice =
unsafe extern "C" fn(*mut XkbContext, *mut c_void, i32, c_int) -> *mut XkbKeymap;
pub type StateNewFromDevice =
unsafe extern "C" fn(*mut XkbKeymap, *mut c_void, i32) -> *mut XkbState;
}
struct Lib {
context_new: prototype::ContextNew,
context_unref: prototype::ContextUnref,
keymap_new_from_buffer: prototype::KeymapNewFromBuffer,
keymap_unref: prototype::KeymapUnref,
keymap_mod_get_index: prototype::KeymapModGetIndex,
keymap_key_repeats: prototype::KeymapKeyRepeats,
state_new: prototype::StateNew,
state_unref: prototype::StateUnref,
state_update_mask: prototype::StateUpdateMask,
state_update_key: prototype::StateUpdateKey,
state_key_get_one_sym: prototype::StateKeyGetOneSym,
state_key_get_utf8: prototype::StateKeyGetUtf8,
state_mod_index_is_active: prototype::StateModIndexIsActive,
compose: Option<ComposeLib>,
}
struct X11Lib {
setup_xkb_extension: x11_prototype::SetupXkbExtension,
get_core_keyboard_device_id: x11_prototype::GetCoreKeyboardDeviceId,
keymap_new_from_device: x11_prototype::KeymapNewFromDevice,
state_new_from_device: x11_prototype::StateNewFromDevice,
}
const SONAMES: [&CStr; 2] = [c"libxkbcommon.so.0", c"libxkbcommon.so"];
const X11_SONAMES: [&CStr; 2] = [c"libxkbcommon-x11.so.0", c"libxkbcommon-x11.so"];
static LIB: OnceLock<Option<Lib>> = OnceLock::new();
static X11_LIB: OnceLock<Option<X11Lib>> = OnceLock::new();
fn load() -> Option<&'static Lib> {
LIB.get_or_init(load_uncached).as_ref()
}
fn load_x11() -> Option<&'static X11Lib> {
X11_LIB.get_or_init(load_x11_uncached).as_ref()
}
#[must_use]
pub fn x11_available() -> bool {
load().is_some() && load_x11().is_some()
}
#[must_use]
pub fn available() -> bool {
load().is_some()
}
fn load_uncached() -> Option<Lib> {
let mut handle = ptr::null_mut();
for soname in SONAMES {
handle = unsafe { dlopen(soname.as_ptr(), RTLD_NOW) };
if !handle.is_null() {
break;
}
}
if handle.is_null() {
crcbl_core::log::warn!(
"libxkbcommon is not available: keys still map to KeyCode, but there \
will be no keysyms and no text input"
);
return None;
}
macro_rules! symbol {
($name:literal, $ty:ty) => {{
let raw = unsafe { dlsym(handle, concat!($name, "\0").as_ptr().cast()) };
if raw.is_null() {
crcbl_core::log::warn!("libxkbcommon has no symbol {}; degrading", $name);
return None;
}
unsafe { core::mem::transmute::<*mut c_void, $ty>(raw) }
}};
}
Some(Lib {
context_new: symbol!("xkb_context_new", prototype::ContextNew),
context_unref: symbol!("xkb_context_unref", prototype::ContextUnref),
keymap_new_from_buffer: symbol!(
"xkb_keymap_new_from_buffer",
prototype::KeymapNewFromBuffer
),
keymap_unref: symbol!("xkb_keymap_unref", prototype::KeymapUnref),
keymap_mod_get_index: symbol!("xkb_keymap_mod_get_index", prototype::KeymapModGetIndex),
keymap_key_repeats: symbol!("xkb_keymap_key_repeats", prototype::KeymapKeyRepeats),
state_new: symbol!("xkb_state_new", prototype::StateNew),
state_unref: symbol!("xkb_state_unref", prototype::StateUnref),
state_update_mask: symbol!("xkb_state_update_mask", prototype::StateUpdateMask),
state_update_key: symbol!("xkb_state_update_key", prototype::StateUpdateKey),
state_key_get_one_sym: symbol!("xkb_state_key_get_one_sym", prototype::StateKeyGetOneSym),
state_key_get_utf8: symbol!("xkb_state_key_get_utf8", prototype::StateKeyGetUtf8),
state_mod_index_is_active: symbol!(
"xkb_state_mod_index_is_active",
prototype::StateModIndexIsActive
),
compose: load_compose(handle),
})
}
fn load_x11_uncached() -> Option<X11Lib> {
let mut handle = ptr::null_mut();
for soname in X11_SONAMES {
handle = unsafe { dlopen(soname.as_ptr(), RTLD_NOW) };
if !handle.is_null() {
break;
}
}
if handle.is_null() {
crcbl_core::log::warn!(
"libxkbcommon-x11 is not available: keys still map to KeyCode, but there \
will be no keysyms and no text input on X11"
);
return None;
}
macro_rules! symbol {
($name:literal, $ty:ty) => {{
let raw = unsafe { dlsym(handle, concat!($name, "\0").as_ptr().cast()) };
if raw.is_null() {
crcbl_core::log::warn!("libxkbcommon-x11 has no symbol {}; degrading", $name);
return None;
}
unsafe { core::mem::transmute::<*mut c_void, $ty>(raw) }
}};
}
Some(X11Lib {
setup_xkb_extension: symbol!(
"xkb_x11_setup_xkb_extension",
x11_prototype::SetupXkbExtension
),
get_core_keyboard_device_id: symbol!(
"xkb_x11_get_core_keyboard_device_id",
x11_prototype::GetCoreKeyboardDeviceId
),
keymap_new_from_device: symbol!(
"xkb_x11_keymap_new_from_device",
x11_prototype::KeymapNewFromDevice
),
state_new_from_device: symbol!(
"xkb_x11_state_new_from_device",
x11_prototype::StateNewFromDevice
),
})
}
const MODIFIER_NAMES: [(&CStr, Modifiers); 7] = [
(c"Shift", Modifiers::SHIFT),
(c"Control", Modifiers::CTRL),
(c"Mod1", Modifiers::ALT),
(c"Mod4", Modifiers::SUPER),
(c"Lock", Modifiers::CAPS_LOCK),
(c"Mod2", Modifiers::NUM_LOCK),
(c"Mod5", Modifiers::ALT_GR),
];
pub struct Keymap {
lib: &'static Lib,
context: *mut XkbContext,
keymap: *mut XkbKeymap,
state: *mut XkbState,
modifiers: Vec<(u32, Modifiers)>,
compose: Option<Compose>,
}
impl core::fmt::Debug for Keymap {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Keymap")
.field("modifiers", &self.modifiers.len())
.field("compose", &self.compose.is_some())
.finish()
}
}
impl Keymap {
#[must_use]
pub fn from_fd(fd: i32, size: u32) -> Option<Self> {
if fd < 0 {
return None;
}
let file = unsafe { std::fs::File::from_raw_fd(fd) };
let declared = size as usize;
let actual = file
.metadata()
.ok()
.and_then(|metadata| usize::try_from(metadata.len()).ok())?;
if actual < declared {
crcbl_core::log::warn!(
"the compositor declared a {declared}-byte keymap but sent a \
{actual}-byte file; ignoring it"
);
return None;
}
Self::compile(file.as_raw_fd(), declared)
}
#[must_use]
pub unsafe fn from_x11_device(connection: *mut c_void) -> Option<Self> {
let lib = load()?;
let x11 = load_x11()?;
let ok = unsafe {
(x11.setup_xkb_extension)(
connection,
1,
0,
0,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
)
};
if ok == 0 {
crcbl_core::log::warn!(
"the X server has no usable XKB extension; no keysyms this session"
);
return None;
}
let device = unsafe { (x11.get_core_keyboard_device_id)(connection) };
if device == -1 {
crcbl_core::log::warn!("the X server reports no core keyboard device");
return None;
}
unsafe {
let context = (lib.context_new)(0);
if context.is_null() {
return None;
}
let keymap = (x11.keymap_new_from_device)(context, connection, device, 0);
if keymap.is_null() {
(lib.context_unref)(context);
crcbl_core::log::warn!(
"the X server's keymap did not compile; no keysyms this session"
);
return None;
}
let state = (x11.state_new_from_device)(keymap, connection, device);
if state.is_null() {
(lib.keymap_unref)(keymap);
(lib.context_unref)(context);
return None;
}
Some(Self {
lib,
context,
keymap,
state,
modifiers: Self::modifier_indices(lib, keymap),
compose: Compose::for_locale(lib, context),
})
}
}
unsafe fn modifier_indices(lib: &'static Lib, keymap: *mut XkbKeymap) -> Vec<(u32, Modifiers)> {
MODIFIER_NAMES
.iter()
.filter_map(|(name, bit)| {
let index = unsafe { (lib.keymap_mod_get_index)(keymap, name.as_ptr()) };
(index != MOD_INVALID).then_some((index, *bit))
})
.collect()
}
fn compile(fd: i32, size: usize) -> Option<Self> {
let lib = load()?;
if size == 0 {
return None;
}
let mapped = unsafe { mmap(ptr::null_mut(), size, PROT_READ, MAP_PRIVATE, fd, 0) };
if mapped == MAP_FAILED || mapped.is_null() {
crcbl_core::log::warn!("cannot mmap the compositor's keymap ({size} bytes)");
return None;
}
let text_len = if unsafe { *mapped.cast::<u8>().add(size - 1) } == 0 {
size - 1
} else {
size
};
let compiled = unsafe {
let context = (lib.context_new)(0);
if context.is_null() {
return Self::unmap(mapped, size, None);
}
let keymap = (lib.keymap_new_from_buffer)(
context,
mapped.cast::<c_char>(),
text_len,
KEYMAP_FORMAT_TEXT_V1,
0,
);
if keymap.is_null() {
(lib.context_unref)(context);
crcbl_core::log::warn!(
"the compositor's keymap did not compile; no keysyms this session"
);
return Self::unmap(mapped, size, None);
}
let state = (lib.state_new)(keymap);
if state.is_null() {
(lib.keymap_unref)(keymap);
(lib.context_unref)(context);
return Self::unmap(mapped, size, None);
}
Self {
lib,
context,
keymap,
state,
modifiers: Self::modifier_indices(lib, keymap),
compose: Compose::for_locale(lib, context),
}
};
Self::unmap(mapped, size, Some(compiled))
}
fn unmap(mapped: *mut c_void, size: usize, result: Option<Self>) -> Option<Self> {
unsafe { munmap(mapped, size) };
result
}
pub fn update(&mut self, depressed: u32, latched: u32, locked: u32, group: u32) {
unsafe {
(self.lib.state_update_mask)(self.state, depressed, latched, locked, 0, 0, group);
}
}
pub fn update_key(&mut self, keycode: u32, pressed: bool) {
const UP: c_int = 0;
const DOWN: c_int = 1;
unsafe {
(self.lib.state_update_key)(self.state, keycode, if pressed { DOWN } else { UP });
}
}
pub fn resync_from_x11_state(&mut self, state: u16) {
const LOCKED: u16 = (1 << 1) | (1 << 4);
const GROUP_SHIFT: u16 = 13;
let group = u32::from((state >> GROUP_SHIFT) & 0x3);
let depressed = u32::from(state & !LOCKED & 0xff);
let locked = u32::from(state & LOCKED);
self.update(depressed, 0, locked, group);
}
#[must_use]
pub fn modifiers(&self) -> Modifiers {
let mut active = Modifiers::empty();
for (index, bit) in &self.modifiers {
let is_active = unsafe {
(self.lib.state_mod_index_is_active)(self.state, *index, STATE_MODS_EFFECTIVE)
};
if is_active > 0 {
active |= *bit;
}
}
active
}
#[must_use]
pub fn keysym(&self, scancode: u32) -> Keysym {
Keysym(unsafe { (self.lib.state_key_get_one_sym)(self.state, scancode + EVDEV_OFFSET) })
}
#[must_use]
pub fn text(&mut self, scancode: u32) -> Option<String> {
let keysym = self.keysym(scancode);
match self.compose.as_mut().map(|compose| compose.feed(keysym)) {
Some(Composed::Swallowed) => return None,
Some(Composed::Text(text)) => return text,
Some(Composed::Passed) | None => {}
}
printable(|buffer, length| unsafe {
(self.lib.state_key_get_utf8)(self.state, scancode + EVDEV_OFFSET, buffer, length)
})
}
pub fn reset_compose(&mut self) {
if let Some(compose) = &mut self.compose {
compose.reset();
}
}
#[must_use]
pub fn repeats(&self, scancode: u32) -> bool {
unsafe { (self.lib.keymap_key_repeats)(self.keymap, scancode + EVDEV_OFFSET) > 0 }
}
}
impl Drop for Keymap {
fn drop(&mut self) {
unsafe {
(self.lib.state_unref)(self.state);
(self.lib.keymap_unref)(self.keymap);
(self.lib.context_unref)(self.context);
}
}
}
fn printable(read: impl FnOnce(*mut c_char, usize) -> c_int) -> Option<String> {
let mut buffer = [0u8; TEXT_BUFFER];
let written = usize::try_from(read(buffer.as_mut_ptr().cast(), buffer.len())).ok()?;
if written == 0 || written >= buffer.len() {
return None;
}
let text = core::str::from_utf8(&buffer[..written]).ok()?;
if text.chars().all(char::is_control) {
return None;
}
Some(text.to_string())
}
#[must_use]
pub fn modifiers_from_held(held: &[KeyCode]) -> Modifiers {
let mut active = Modifiers::empty();
for key in held {
active |= match key {
KeyCode::ShiftLeft | KeyCode::ShiftRight => Modifiers::SHIFT,
KeyCode::ControlLeft | KeyCode::ControlRight => Modifiers::CTRL,
KeyCode::AltLeft => Modifiers::ALT,
KeyCode::AltRight => Modifiers::ALT | Modifiers::ALT_GR,
KeyCode::SuperLeft | KeyCode::SuperRight => Modifiers::SUPER,
_ => Modifiers::empty(),
};
}
active
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_degraded_modifier_path_covers_every_chord_bit() {
assert_eq!(modifiers_from_held(&[]), Modifiers::empty());
assert_eq!(
modifiers_from_held(&[KeyCode::ControlLeft, KeyCode::ShiftRight]),
Modifiers::CTRL | Modifiers::SHIFT
);
assert_eq!(modifiers_from_held(&[KeyCode::SuperLeft]), Modifiers::SUPER);
assert_eq!(
modifiers_from_held(&[KeyCode::AltRight]),
Modifiers::ALT | Modifiers::ALT_GR
);
assert_eq!(modifiers_from_held(&[KeyCode::AltLeft]), Modifiers::ALT);
assert_eq!(
modifiers_from_held(&[KeyCode::KeyW, KeyCode::Space]),
Modifiers::empty()
);
assert_eq!(
modifiers_from_held(&[KeyCode::ControlLeft, KeyCode::KeyS]).chord(),
Modifiers::CTRL
);
}
#[test]
fn the_evdev_offset_is_the_x11_one() {
assert_eq!(EVDEV_OFFSET, 8);
assert_eq!(30 + EVDEV_OFFSET, 38);
}
#[test]
fn the_modifier_name_table_covers_the_engine_bits_that_are_layout_defined() {
let covered = MODIFIER_NAMES
.iter()
.fold(Modifiers::empty(), |set, (_, bit)| set | *bit);
assert_eq!(
covered,
Modifiers::all(),
"every Modifiers bit must have an XKB name to read it from"
);
let mut names: Vec<&CStr> = MODIFIER_NAMES.iter().map(|(name, _)| *name).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "two bits must not read the same index");
}
#[test]
fn loading_is_idempotent_and_never_panics() {
let first = available();
assert_eq!(first, available(), "the load is cached, not repeated");
}
}