diff --git a/Cargo.lock b/Cargo.lock index 002b7ae..e4b602d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1742,9 +1742,9 @@ dependencies = [ "bitflags 2.6.0", "log", "memmap2 0.9.4", - "once_cell", "raw-window-handle", "smithay-client-toolkit 0.18.1", + "smol_str", "tempfile", "thiserror", "wayland-backend", diff --git a/layershellev/Cargo.toml b/layershellev/Cargo.toml index 2f56bae..d9a9b06 100644 --- a/layershellev/Cargo.toml +++ b/layershellev/Cargo.toml @@ -35,5 +35,5 @@ sctk.workspace = true log.workspace = true memmap2 = "0.9.4" -once_cell = "1.19.0" xkbcommon-dl = "0.4.2" +smol_str = "0.2.2" diff --git a/layershellev/src/keyboard.rs b/layershellev/src/keyboard.rs index 3d1d87c..77756ff 100644 --- a/layershellev/src/keyboard.rs +++ b/layershellev/src/keyboard.rs @@ -1,16 +1,141 @@ -use std::{ffi::c_char, ops::Deref, os::fd::OwnedFd, ptr::NonNull}; - use memmap2::MmapOptions; -use once_cell::sync::Lazy; +use smol_str::SmolStr; +use std::sync::LazyLock; +use std::{ + env, + ffi::{c_char, CString}, + ops::Deref, + os::{fd::OwnedFd, unix::ffi::OsStringExt}, + ptr::{self, NonNull}, + time::Duration, +}; +use wayland_client::{protocol::wl_keyboard::WlKeyboard, Proxy}; -use xkbcommon_dl::{self as xkb, xkbcommon_handle, XkbCommon}; +use xkbcommon_dl::{ + self as xkb, xkb_compose_compile_flags, xkb_compose_feed_result, xkb_compose_state, + xkb_compose_state_flags, xkb_compose_status, xkb_compose_table, xkb_keysym_t, + xkbcommon_compose_handle, xkbcommon_handle, XkbCommon, XkbCommonCompose, +}; use xkb::{ xkb_context, xkb_context_flags, xkb_keymap, xkb_keymap_compile_flags, xkb_state, xkb_state_component, }; -static XKBH: Lazy<&'static XkbCommon> = Lazy::new(xkbcommon_handle); +static XKBH: LazyLock<&'static XkbCommon> = LazyLock::new(xkbcommon_handle); +static XKBCH: LazyLock<&'static XkbCommonCompose> = LazyLock::new(xkbcommon_compose_handle); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RepeatInfo { + /// Keys will be repeated at the specified rate and delay. + Repeat { + /// The time between the key repeats. + gap: Duration, + + /// Delay (in milliseconds) between a key press and the start of repetition. + delay: Duration, + }, + + /// Keys should not be repeated. + Disable, +} + +impl Default for RepeatInfo { + /// The default repeat rate is 25 keys per second with the delay of 200ms. + /// + /// The values are picked based on the default in various compositors and Xorg. + fn default() -> Self { + Self::Repeat { + gap: Duration::from_millis(40), + delay: Duration::from_millis(200), + } + } +} + +#[derive(Debug)] +pub struct KeyboardState { + pub keyboard: WlKeyboard, + + pub xkb_context: Context, + pub repeat_info: RepeatInfo, + pub current_repeat: Option, +} + +impl KeyboardState { + pub fn new(keyboard: WlKeyboard) -> Self { + Self { + keyboard, + xkb_context: Context::new().unwrap(), + repeat_info: RepeatInfo::default(), + current_repeat: None, + } + } +} + +impl Drop for KeyboardState { + fn drop(&mut self) { + if self.keyboard.version() >= 3 { + self.keyboard.release(); + } + } +} + +#[derive(Debug)] +pub enum Error { + /// libxkbcommon is not available + XKBNotFound, +} + +#[derive(Debug)] +pub struct Context { + // NOTE: field order matters. + state: Option, + keymap: Option, + compose_state1: Option, + compose_state2: Option, + _compose_table: Option, + context: XkbContext, + scratch_buffer: Vec, +} + +impl Context { + pub fn new() -> Result { + if xkb::xkbcommon_option().is_none() { + return Err(Error::XKBNotFound); + } + + let context = XkbContext::new(); + let mut compose_table = XkbComposeTable::new(&context); + let mut compose_state1 = compose_table.as_ref().and_then(|table| table.new_state()); + let mut compose_state2 = compose_table.as_ref().and_then(|table| table.new_state()); + + // Disable compose if anything compose related failed to initialize. + if compose_table.is_none() || compose_state1.is_none() || compose_state2.is_none() { + compose_state2 = None; + compose_state1 = None; + compose_table = None; + } + + Ok(Self { + state: None, + keymap: None, + compose_state1, + compose_state2, + _compose_table: compose_table, + context, + scratch_buffer: Vec::with_capacity(8), + }) + } + pub fn set_keymap_from_fd(&mut self, fd: OwnedFd, size: usize) { + let keymap = XkbKeymap::from_fd(&self.context, fd, size); + let state = keymap.as_ref().and_then(XkbState::new_wayland); + if keymap.is_none() || state.is_none() { + log::warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } +} #[derive(Debug)] pub struct XkbKeymap { @@ -107,7 +232,6 @@ impl XkbState { self.modifiers.alt = self.mod_name_is_active(xkb::XKB_MOD_NAME_ALT); self.modifiers.shift = self.mod_name_is_active(xkb::XKB_MOD_NAME_SHIFT); self.modifiers.caps_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_CAPS); - println!("caps: {}", self.modifiers.caps_lock); self.modifiers.logo = self.mod_name_is_active(xkb::XKB_MOD_NAME_LOGO); self.modifiers.num_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_NUM); } @@ -122,3 +246,155 @@ pub struct ModifiersState { logo: bool, num_lock: bool, } + +#[derive(Debug)] +pub struct XkbComposeTable { + table: NonNull, +} + +impl XkbComposeTable { + pub fn new(context: &XkbContext) -> Option { + let locale = env::var_os("LC_ALL") + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LC_CTYPE")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LANG")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .unwrap_or_else(|| "C".into()); + let locale = CString::new(locale.into_vec()).unwrap(); + + let table = unsafe { + (XKBCH.xkb_compose_table_new_from_locale)( + context.as_ptr(), + locale.as_ptr(), + xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS, + ) + }; + + let table = NonNull::new(table)?; + Some(Self { table }) + } + + /// Create new state with the given compose table. + pub fn new_state(&self) -> Option { + let state = unsafe { + (XKBCH.xkb_compose_state_new)( + self.table.as_ptr(), + xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, + ) + }; + + let state = NonNull::new(state)?; + Some(XkbComposeState { state }) + } +} + +impl Deref for XkbComposeTable { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.table + } +} + +impl Drop for XkbComposeTable { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_table_unref)(self.table.as_ptr()); + } + } +} + +#[derive(Debug)] +pub struct XkbComposeState { + state: NonNull, +} + +// NOTE: This is track_caller so we can have more informative line numbers when logging +#[track_caller] +fn byte_slice_to_smol_str(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes) + .map(SmolStr::new) + .map_err(|e| { + log::warn!( + "UTF-8 received from libxkbcommon ({:?}) was invalid: {e}", + bytes + ) + }) + .ok() +} + +/// Shared logic for constructing a string with `xkb_compose_state_get_utf8` and +/// `xkb_state_key_get_utf8`. +fn make_string_with(scratch_buffer: &mut Vec, mut f: F) -> Option +where + F: FnMut(*mut c_char, usize) -> i32, +{ + let size = f(ptr::null_mut(), 0); + if size == 0 { + return None; + } + let size = usize::try_from(size).unwrap(); + scratch_buffer.clear(); + // The allocated buffer must include space for the null-terminator. + scratch_buffer.reserve(size + 1); + unsafe { + let written = f( + scratch_buffer.as_mut_ptr().cast(), + scratch_buffer.capacity(), + ); + if usize::try_from(written).unwrap() != size { + // This will likely never happen. + return None; + } + scratch_buffer.set_len(size); + }; + + byte_slice_to_smol_str(scratch_buffer) +} + +impl XkbComposeState { + pub fn get_string(&mut self, scratch_buffer: &mut Vec) -> Option { + make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBCH.xkb_compose_state_get_utf8)(self.state.as_ptr(), ptr, len) + }) + } + + #[inline] + pub fn feed(&mut self, keysym: xkb_keysym_t) -> ComposeStatus { + let feed_result = unsafe { (XKBCH.xkb_compose_state_feed)(self.state.as_ptr(), keysym) }; + match feed_result { + xkb_compose_feed_result::XKB_COMPOSE_FEED_IGNORED => ComposeStatus::Ignored, + xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED => { + ComposeStatus::Accepted(self.status()) + } + } + } + + #[inline] + pub fn reset(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_reset)(self.state.as_ptr()); + } + } + + #[inline] + pub fn status(&mut self) -> xkb_compose_status { + unsafe { (XKBCH.xkb_compose_state_get_status)(self.state.as_ptr()) } + } +} + +impl Drop for XkbComposeState { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_unref)(self.state.as_ptr()); + }; + } +} + +#[derive(Copy, Clone, Debug)] +pub enum ComposeStatus { + Accepted(xkb_compose_status), + Ignored, + None, +} diff --git a/layershellev/src/lib.rs b/layershellev/src/lib.rs index d739d3a..573d285 100644 --- a/layershellev/src/lib.rs +++ b/layershellev/src/lib.rs @@ -173,6 +173,7 @@ pub mod key; pub use events::{AxisScroll, DispatchMessage, LayerEvent, ReturnData, XdgInfoChangedType}; use key::KeyModifierType; +use keyboard::KeyboardState; use strtoshape::str_to_shape; use wayland_client::{ delegate_noop, @@ -503,7 +504,8 @@ pub struct WindowState { // base managers seat: Option, - keyboard: Option, + keyboard_state: Option, + pointer: Option, touch: Option, @@ -560,7 +562,7 @@ impl WindowState { /// get the keyboard pub fn get_keyboard(&self) -> Option<&WlKeyboard> { - self.keyboard.as_ref() + Some(&self.keyboard_state.as_ref()?.keyboard) } /// get the pointer @@ -709,7 +711,7 @@ impl Default for WindowState { fractional_scale_manager: None, seat: None, - keyboard: None, + keyboard_state: None, pointer: None, touch: None, @@ -804,7 +806,7 @@ impl Dispatch for WindowState { } = event { if capabilities.contains(wl_seat::Capability::Keyboard) { - state.keyboard = Some(seat.get_keyboard(qh, ())); + state.keyboard_state = Some(KeyboardState::new(seat.get_keyboard(qh, ()))); } if capabilities.contains(wl_seat::Capability::Pointer) { state.pointer = Some(seat.get_pointer(qh, ())); @@ -825,17 +827,23 @@ impl Dispatch for WindowState { _conn: &Connection, _qhandle: &QueueHandle, ) { - use keyboard::*; + let keyboard_state = state.keyboard_state.as_mut().unwrap(); match event { - wl_keyboard::Event::Keymap { format, fd, size } => { - if !matches!(format, WEnum::Value(KeymapFormat::XkbV1)) { - return; + wl_keyboard::Event::Keymap { format, fd, size } => match format { + WEnum::Value(KeymapFormat::XkbV1) => { + let context = &mut keyboard_state.xkb_context; + context.set_keymap_from_fd(fd, size as usize) + } + WEnum::Value(KeymapFormat::NoKeymap) => { + log::warn!("non-xkb compatible keymap") } - println!("it is {format:?}, {fd:?}, {size}"); - let context = XkbContext::new(); - let keymap = XkbKeymap::from_fd(&context, fd, size as usize).unwrap(); - let state = XkbState::new_wayland(&keymap).unwrap(); - println!("{state:?}"); + _ => unreachable!(), + }, + wl_keyboard::Event::Leave { serial, surface } => { + // NOTE: clear modifier + } + wl_keyboard::Event::RepeatInfo { rate, delay } => { + // NOTE: RepeatInfo } wl_keyboard::Event::Key { state: keystate,