use core::ffi::{c_char, c_int, c_void};
use crcbl_core::input::Keysym;
use super::{Lib, XkbContext, dlsym, printable};
#[repr(C)]
struct XkbComposeTable {
_opaque: [u8; 0],
}
#[repr(C)]
struct XkbComposeState {
_opaque: [u8; 0],
}
const COMPOSE_FEED_IGNORED: c_int = 0;
const COMPOSE_COMPOSING: c_int = 1;
const COMPOSE_COMPOSED: c_int = 2;
const COMPOSE_CANCELLED: c_int = 3;
mod compose_prototype {
use super::{XkbComposeState, XkbComposeTable, XkbContext, c_char, c_int};
pub type TableNewFromLocale =
unsafe extern "C" fn(*mut XkbContext, *const c_char, c_int) -> *mut XkbComposeTable;
#[cfg(test)]
pub type TableNewFromBuffer = unsafe extern "C" fn(
*mut XkbContext,
*const c_char,
usize,
*const c_char,
c_int,
c_int,
) -> *mut XkbComposeTable;
pub type TableUnref = unsafe extern "C" fn(*mut XkbComposeTable);
pub type StateNew = unsafe extern "C" fn(*mut XkbComposeTable, c_int) -> *mut XkbComposeState;
pub type StateUnref = unsafe extern "C" fn(*mut XkbComposeState);
pub type StateFeed = unsafe extern "C" fn(*mut XkbComposeState, u32) -> c_int;
pub type StateReset = unsafe extern "C" fn(*mut XkbComposeState);
pub type StateGetStatus = unsafe extern "C" fn(*mut XkbComposeState) -> c_int;
pub type StateGetUtf8 = unsafe extern "C" fn(*mut XkbComposeState, *mut c_char, usize) -> c_int;
}
pub(super) struct ComposeLib {
table_new_from_locale: compose_prototype::TableNewFromLocale,
#[cfg(test)]
table_new_from_buffer: compose_prototype::TableNewFromBuffer,
table_unref: compose_prototype::TableUnref,
state_new: compose_prototype::StateNew,
state_unref: compose_prototype::StateUnref,
state_feed: compose_prototype::StateFeed,
state_reset: compose_prototype::StateReset,
state_get_status: compose_prototype::StateGetStatus,
state_get_utf8: compose_prototype::StateGetUtf8,
}
pub(super) fn load_compose(handle: *mut c_void) -> Option<ComposeLib> {
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 {}; dead keys will type nothing",
$name
);
return None;
}
unsafe { core::mem::transmute::<*mut c_void, $ty>(raw) }
}};
}
Some(ComposeLib {
table_new_from_locale: symbol!(
"xkb_compose_table_new_from_locale",
compose_prototype::TableNewFromLocale
),
#[cfg(test)]
table_new_from_buffer: symbol!(
"xkb_compose_table_new_from_buffer",
compose_prototype::TableNewFromBuffer
),
table_unref: symbol!("xkb_compose_table_unref", compose_prototype::TableUnref),
state_new: symbol!("xkb_compose_state_new", compose_prototype::StateNew),
state_unref: symbol!("xkb_compose_state_unref", compose_prototype::StateUnref),
state_feed: symbol!("xkb_compose_state_feed", compose_prototype::StateFeed),
state_reset: symbol!("xkb_compose_state_reset", compose_prototype::StateReset),
state_get_status: symbol!(
"xkb_compose_state_get_status",
compose_prototype::StateGetStatus
),
state_get_utf8: symbol!(
"xkb_compose_state_get_utf8",
compose_prototype::StateGetUtf8
),
})
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Composed {
Passed,
Swallowed,
Text(Option<String>),
}
pub(super) struct Compose {
lib: &'static ComposeLib,
table: *mut XkbComposeTable,
state: *mut XkbComposeState,
}
impl Compose {
pub(super) unsafe fn for_locale(lib: &'static Lib, context: *mut XkbContext) -> Option<Self> {
let compose = lib.compose.as_ref()?;
let locale = locale(std::env::var_os);
let table = unsafe { (compose.table_new_from_locale)(context, locale.as_ptr(), 0) };
if table.is_null() {
crcbl_core::log::warn!(
"no compose table for the locale {locale:?}; dead keys will type nothing"
);
return None;
}
unsafe { Self::with_table(compose, table) }
}
unsafe fn with_table(lib: &'static ComposeLib, table: *mut XkbComposeTable) -> Option<Self> {
let state = unsafe { (lib.state_new)(table, 0) };
if state.is_null() {
unsafe { (lib.table_unref)(table) };
return None;
}
Some(Self { lib, table, state })
}
pub(super) fn feed(&mut self, keysym: Keysym) -> Composed {
let fed = unsafe { (self.lib.state_feed)(self.state, keysym.0) };
if fed == COMPOSE_FEED_IGNORED {
return Composed::Passed;
}
match unsafe { (self.lib.state_get_status)(self.state) } {
COMPOSE_COMPOSING | COMPOSE_CANCELLED => Composed::Swallowed,
COMPOSE_COMPOSED => {
let text = printable(|buffer, length| unsafe {
(self.lib.state_get_utf8)(self.state, buffer, length)
});
self.reset();
Composed::Text(text)
}
_ => Composed::Passed,
}
}
pub(super) fn reset(&mut self) {
unsafe { (self.lib.state_reset)(self.state) };
}
}
impl Drop for Compose {
fn drop(&mut self) {
unsafe {
(self.lib.state_unref)(self.state);
(self.lib.table_unref)(self.table);
}
}
}
fn locale(var: impl Fn(&'static str) -> Option<std::ffi::OsString>) -> std::ffi::CString {
use std::os::unix::ffi::OsStringExt;
["LC_ALL", "LC_CTYPE", "LANG"]
.into_iter()
.filter_map(var)
.find(|value| !value.is_empty())
.and_then(|value| std::ffi::CString::new(value.into_vec()).ok())
.unwrap_or_else(|| c"C".to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linux::xkb::load;
fn compose_from(text: &str) -> Compose {
let lib = load().expect("libxkbcommon, which CI installs for the unit tests");
let compose = lib.compose.as_ref().expect("libxkbcommon's compose module");
unsafe {
let context = (lib.context_new)(0);
assert!(!context.is_null());
const FORMAT_TEXT_V1: c_int = 1;
let table = (compose.table_new_from_buffer)(
context,
text.as_ptr().cast(),
text.len(),
c"en_US.UTF-8".as_ptr(),
FORMAT_TEXT_V1,
0,
);
(lib.context_unref)(context);
assert!(!table.is_null(), "the compose text parses");
Compose::with_table(compose, table).expect("a compose state")
}
}
#[test]
fn a_dead_key_composes_with_the_next_and_a_wrong_one_cancels() {
const DEAD_ACUTE: Keysym = Keysym(0xfe51);
const SHIFT_L: Keysym = Keysym(0xffe1);
const E: Keysym = Keysym(0x65);
const Q: Keysym = Keysym(0x71);
let mut compose = compose_from("<dead_acute> <e> : \"é\" eacute\n");
assert_eq!(compose.feed(E), Composed::Passed);
assert_eq!(compose.feed(DEAD_ACUTE), Composed::Swallowed);
assert_eq!(compose.feed(SHIFT_L), Composed::Passed);
assert_eq!(compose.feed(E), Composed::Text(Some("é".to_string())));
assert_eq!(
compose.feed(E),
Composed::Passed,
"the sequence starts over"
);
assert_eq!(compose.feed(DEAD_ACUTE), Composed::Swallowed);
assert_eq!(
compose.feed(Q),
Composed::Swallowed,
"no sequence: cancelled"
);
assert_eq!(compose.feed(E), Composed::Passed);
assert_eq!(compose.feed(DEAD_ACUTE), Composed::Swallowed);
compose.reset();
assert_eq!(
compose.feed(E),
Composed::Passed,
"a reset forgets the dead key"
);
}
#[test]
fn the_locale_is_the_first_variable_set_to_something() {
let from = |pairs: &'static [(&'static str, &'static str)]| {
locale(move |name| {
pairs
.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| std::ffi::OsString::from(value))
})
};
assert_eq!(from(&[]).as_c_str(), c"C");
assert_eq!(from(&[("LANG", "de_DE.UTF-8")]).as_c_str(), c"de_DE.UTF-8");
assert_eq!(
from(&[("LANG", "de_DE.UTF-8"), ("LC_CTYPE", "fr_FR.UTF-8")]).as_c_str(),
c"fr_FR.UTF-8"
);
assert_eq!(
from(&[("LC_ALL", ""), ("LC_CTYPE", "fr_FR.UTF-8")]).as_c_str(),
c"fr_FR.UTF-8",
"an empty variable is skipped"
);
assert_eq!(
from(&[("LC_ALL", "pt_BR.UTF-8"), ("LC_CTYPE", "fr_FR.UTF-8")]).as_c_str(),
c"pt_BR.UTF-8"
);
}
}