feat(teipe): initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
24
.vscode/launch.json
vendored
Normal file
24
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "codelldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug teipe",
|
||||||
|
"program": "target/debug/teipe",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=teipe",
|
||||||
|
"--package=teipe"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "teipe",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1682
Cargo.lock
generated
Normal file
1682
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
Normal file
36
Cargo.toml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["libeirs", "libxkbcommonrs"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
tokio = { version = "1.39", features = [
|
||||||
|
"rt",
|
||||||
|
"macros",
|
||||||
|
"rt-multi-thread",
|
||||||
|
"net",
|
||||||
|
] }
|
||||||
|
tokio-stream = "0.1"
|
||||||
|
log = { version = "0.4", features = ["kv"] }
|
||||||
|
anyhow = "1.0"
|
||||||
|
bindgen = "0.70"
|
||||||
|
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "teipe"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libeirs = { path = "libeirs" }
|
||||||
|
libxkbcommonrs = { path = "libxkbcommonrs" }
|
||||||
|
ashpd = "0.9"
|
||||||
|
env_logger = { version = "0.11", features = ["unstable-kv"] }
|
||||||
|
tokio = { workspace = true, features = ["rt", "net"] }
|
||||||
|
tokio-stream = { workspace = true }
|
||||||
|
tokio-util = "0.7"
|
||||||
|
log = { workspace = true, features = ["kv"] }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
widestring = "1.1.0"
|
||||||
|
polling = "3.7"
|
||||||
|
clap = { version = "4.5.16", features = ["derive"] }
|
||||||
16
libeirs/Cargo.toml
Normal file
16
libeirs/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "libeirs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
bindgen.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { workspace = true, features = ["rt", "net"] }
|
||||||
|
tokio-stream = { workspace = true }
|
||||||
|
log = { workspace = true, features = ["kv"] }
|
||||||
|
memmap2 = "0.9.4"
|
||||||
|
nix = { version = "0.29", features = ["poll"] }
|
||||||
|
anyhow.workspace = true
|
||||||
25
libeirs/build.rs
Normal file
25
libeirs/build.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let bindings = bindgen::builder()
|
||||||
|
.header("/usr/include/libei-1.0/libei.h")
|
||||||
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||||
|
.allowlist_item("ei_.*")
|
||||||
|
.default_enum_style(bindgen::EnumVariation::NewType {
|
||||||
|
is_bitfield: false,
|
||||||
|
is_global: false,
|
||||||
|
})
|
||||||
|
.no_debug("ei_event_type") // Implemented manually to use Display
|
||||||
|
.bitfield_enum("ei_device_capability")
|
||||||
|
.generate()?;
|
||||||
|
|
||||||
|
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
let out_file = out_path.join("libei.rs");
|
||||||
|
bindings
|
||||||
|
.write_to_file(out_file)
|
||||||
|
.expect("Couldn't write bindings!");
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-lib=ei");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
129
libeirs/src/device.rs
Normal file
129
libeirs/src/device.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
use std::{borrow::Borrow, ffi::CStr, fmt::Display, ops::Deref, ptr::NonNull};
|
||||||
|
|
||||||
|
use crate::{eirc::Ei, ffi::ei_device};
|
||||||
|
|
||||||
|
use super::{ffi, keymap::Keymap};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub struct Device {
|
||||||
|
device: DeviceRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Device {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
log::trace!("Device dropped");
|
||||||
|
let device = self.as_raw();
|
||||||
|
unsafe {
|
||||||
|
ffi::ei_device_close(device);
|
||||||
|
ffi::ei_device_unref(device)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Device {
|
||||||
|
type Target = DeviceRef;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Device {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
self.device.to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Borrow<DeviceRef> for Device {
|
||||||
|
fn borrow(&self) -> &DeviceRef {
|
||||||
|
&self.device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<DeviceRef> for Device {
|
||||||
|
fn as_ref(&self) -> &DeviceRef {
|
||||||
|
&self.device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Device {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<DeviceRef> for Device {
|
||||||
|
fn eq(&self, other: &DeviceRef) -> bool {
|
||||||
|
self.device == *other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub struct DeviceRef {
|
||||||
|
device: NonNull<ffi::ei_device>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceRef {
|
||||||
|
pub(crate) fn new(device: *mut ei_device) -> Option<Self> {
|
||||||
|
NonNull::new(device).map(|device| Self { device })
|
||||||
|
}
|
||||||
|
pub fn start_emulating(&self, sequence: u32) {
|
||||||
|
unsafe { ffi::ei_device_start_emulating(self.as_raw(), sequence) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keyboard_key(&self, key: u32, pressed: bool) {
|
||||||
|
unsafe {
|
||||||
|
ffi::ei_device_keyboard_key(self.as_raw(), key, pressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frame(&self, time: Option<u64>) {
|
||||||
|
let time = time.unwrap_or_else(|| unsafe {
|
||||||
|
let ei = ffi::ei_device_get_context(self.as_raw());
|
||||||
|
ffi::ei_now(ei)
|
||||||
|
});
|
||||||
|
unsafe {
|
||||||
|
ffi::ei_device_frame(self.as_raw(), time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_raw(&self) -> *mut ffi::ei_device {
|
||||||
|
self.device.as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_keymap(&self) -> Option<Keymap> {
|
||||||
|
Keymap::from_device(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_context(&self) -> Ei {
|
||||||
|
unsafe { Ei::from_raw(ffi::ei_device_get_context(self.as_raw())) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DeviceRef {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = unsafe { CStr::from_ptr(ffi::ei_device_get_name(self.as_raw())) }
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
write!(f, "{str}",)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToOwned for DeviceRef {
|
||||||
|
type Owned = Device;
|
||||||
|
|
||||||
|
fn to_owned(&self) -> Self::Owned {
|
||||||
|
let ptr = unsafe { ffi::ei_device_ref(self.as_raw()) };
|
||||||
|
Device {
|
||||||
|
device: DeviceRef {
|
||||||
|
device: NonNull::new(ptr).unwrap(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<Device> for DeviceRef {
|
||||||
|
fn eq(&self, other: &Device) -> bool {
|
||||||
|
*other == *self
|
||||||
|
}
|
||||||
|
}
|
||||||
107
libeirs/src/eirc.rs
Normal file
107
libeirs/src/eirc.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use std::{
|
||||||
|
os::fd::{BorrowedFd, IntoRawFd, OwnedFd},
|
||||||
|
ptr::NonNull,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{events::Event, ffi};
|
||||||
|
|
||||||
|
const CLIENT_NAME: &std::ffi::CStr = c"teipe";
|
||||||
|
|
||||||
|
pub struct Ei {
|
||||||
|
ei: *mut ffi::ei,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn null_handler(
|
||||||
|
_ei: *mut ffi::ei,
|
||||||
|
_priority: ffi::ei_log_priority,
|
||||||
|
_message: *const ::std::os::raw::c_char,
|
||||||
|
_context: *mut ffi::ei_log_context,
|
||||||
|
) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn log_handler(
|
||||||
|
_ei: *mut ffi::ei,
|
||||||
|
priority: ffi::ei_log_priority,
|
||||||
|
message: *const ::std::os::raw::c_char,
|
||||||
|
ctx: *mut ffi::ei_log_context,
|
||||||
|
) {
|
||||||
|
let (file, func, line, msg) = unsafe {
|
||||||
|
(
|
||||||
|
std::ffi::CStr::from_ptr(ffi::ei_log_context_get_file(ctx)),
|
||||||
|
std::ffi::CStr::from_ptr(ffi::ei_log_context_get_func(ctx)),
|
||||||
|
ffi::ei_log_context_get_line(ctx),
|
||||||
|
std::ffi::CStr::from_ptr(message),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
log::log!(
|
||||||
|
target:"libei",
|
||||||
|
priority.into(),
|
||||||
|
file:?, func:?, line:?;
|
||||||
|
"{msg:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ei {
|
||||||
|
pub(crate) fn new_sender(fd: OwnedFd) -> Self {
|
||||||
|
// FIX: handle errors
|
||||||
|
log::info!("Creating ei");
|
||||||
|
let ei = unsafe { ffi::ei_new_sender(std::ptr::null_mut()) };
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
if let Some(level) = log::max_level().to_level() {
|
||||||
|
ffi::ei_log_set_handler(ei, Some(log_handler));
|
||||||
|
ffi::ei_log_set_priority(ei, level.into());
|
||||||
|
} else {
|
||||||
|
ffi::ei_log_set_handler(ei, Some(null_handler));
|
||||||
|
}
|
||||||
|
ffi::ei_configure_name(ei, CLIENT_NAME.as_ptr());
|
||||||
|
ffi::ei_setup_backend_fd(ei, fd.into_raw_fd());
|
||||||
|
}
|
||||||
|
Self { ei }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) unsafe fn from_raw(ei: *mut ffi::ei) -> Self {
|
||||||
|
unsafe {
|
||||||
|
ffi::ei_ref(ei);
|
||||||
|
}
|
||||||
|
Self { ei }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_fd(&self) -> BorrowedFd {
|
||||||
|
unsafe {
|
||||||
|
let fd = ffi::ei_get_fd(self.ei);
|
||||||
|
BorrowedFd::borrow_raw(fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch(&self) {
|
||||||
|
log::trace!("Dispatch");
|
||||||
|
unsafe {
|
||||||
|
ffi::ei_dispatch(self.ei);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_event(&self) -> Option<Event> {
|
||||||
|
let e = unsafe { ffi::ei_get_event(self.ei) };
|
||||||
|
NonNull::new(e).map(Event::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn now(&self) -> u64 {
|
||||||
|
unsafe { ffi::ei_now(self.ei) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Ei {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
ei: unsafe { ffi::ei_ref(self.ei) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Ei {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { ffi::ei_unref(self.ei) };
|
||||||
|
}
|
||||||
|
}
|
||||||
47
libeirs/src/events.rs
Normal file
47
libeirs/src/events.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use std::{fmt::Display, ptr::NonNull};
|
||||||
|
|
||||||
|
use crate::device::DeviceRef;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
ffi::{self, ei_event},
|
||||||
|
seat::Seat,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use ffi::ei_event_type as EventType;
|
||||||
|
pub struct Event(NonNull<ei_event>);
|
||||||
|
|
||||||
|
impl Drop for Event {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { ffi::ei_event_unref(self.as_raw()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
pub fn new(event: std::ptr::NonNull<ffi::ei_event>) -> Self {
|
||||||
|
Self(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_raw(&self) -> *mut ei_event {
|
||||||
|
self.0.as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_type(&self) -> EventType {
|
||||||
|
unsafe { ffi::ei_event_get_type(self.as_raw()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_seat(&self) -> Option<Seat> {
|
||||||
|
Seat::from_event(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_device(&self) -> Option<DeviceRef> {
|
||||||
|
DeviceRef::new(unsafe { ffi::ei_event_get_device(self.as_raw()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Event {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Event")
|
||||||
|
.field("Type", &self.get_type())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
49
libeirs/src/ffi.rs
Normal file
49
libeirs/src/ffi.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
#![allow(non_camel_case_types)]
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
ffi::CStr,
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
};
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/libei.rs"));
|
||||||
|
|
||||||
|
impl From<ei_log_priority> for log::Level {
|
||||||
|
fn from(value: ei_log_priority) -> Self {
|
||||||
|
match value {
|
||||||
|
ei_log_priority::EI_LOG_PRIORITY_DEBUG => log::Level::Debug,
|
||||||
|
ei_log_priority::EI_LOG_PRIORITY_INFO => log::Level::Info,
|
||||||
|
ei_log_priority::EI_LOG_PRIORITY_WARNING => log::Level::Warn,
|
||||||
|
ei_log_priority::EI_LOG_PRIORITY_ERROR => log::Level::Error,
|
||||||
|
_ => log::Level::Debug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<log::Level> for ei_log_priority {
|
||||||
|
fn from(value: log::Level) -> Self {
|
||||||
|
match value {
|
||||||
|
log::Level::Trace | log::Level::Debug => ei_log_priority::EI_LOG_PRIORITY_DEBUG,
|
||||||
|
log::Level::Info => ei_log_priority::EI_LOG_PRIORITY_INFO,
|
||||||
|
log::Level::Warn => ei_log_priority::EI_LOG_PRIORITY_WARNING,
|
||||||
|
log::Level::Error => ei_log_priority::EI_LOG_PRIORITY_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for ei_event_type {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ei_event_type {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = unsafe { CStr::from_ptr(ei_event_type_to_string(*self)) }
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
write!(f, "{}", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
libeirs/src/keymap.rs
Normal file
49
libeirs/src/keymap.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use std::{error::Error, fmt::Debug, ptr::NonNull};
|
||||||
|
|
||||||
|
use super::{device::DeviceRef, ffi};
|
||||||
|
use ffi::ei_keymap_type as KeymapType;
|
||||||
|
|
||||||
|
pub struct Keymap(NonNull<ffi::ei_keymap>);
|
||||||
|
|
||||||
|
impl Keymap {
|
||||||
|
pub(crate) fn from_device(device: &DeviceRef) -> Option<Self> {
|
||||||
|
let keymap = unsafe {
|
||||||
|
let keymap = ffi::ei_device_keyboard_get_keymap(device.as_raw());
|
||||||
|
ffi::ei_keymap_ref(keymap)
|
||||||
|
};
|
||||||
|
NonNull::new(keymap).map(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_raw(&self) -> *mut ffi::ei_keymap {
|
||||||
|
self.0.as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_type(&self) -> KeymapType {
|
||||||
|
unsafe { ffi::ei_keymap_get_type(self.as_raw()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mmap_keymap(&self) -> Result<impl AsRef<[u8]>, impl Error> {
|
||||||
|
unsafe {
|
||||||
|
let fd = ffi::ei_keymap_get_fd(self.as_raw());
|
||||||
|
memmap2::Mmap::map(fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Keymap {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_tuple("Keymap").field(&self.0).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Keymap {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { ffi::ei_keymap_unref(self.as_raw()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Keymap {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(unsafe { NonNull::new_unchecked(ffi::ei_keymap_ref(self.as_raw())) })
|
||||||
|
}
|
||||||
|
}
|
||||||
13
libeirs/src/lib.rs
Normal file
13
libeirs/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
mod device;
|
||||||
|
mod eirc;
|
||||||
|
pub mod events;
|
||||||
|
mod ffi;
|
||||||
|
mod keymap;
|
||||||
|
pub mod seat;
|
||||||
|
mod sender;
|
||||||
|
|
||||||
|
pub use device::*;
|
||||||
|
pub use events::*;
|
||||||
|
pub use keymap::Keymap;
|
||||||
|
pub use seat::Seat;
|
||||||
|
pub use sender::Sender;
|
||||||
41
libeirs/src/seat.rs
Normal file
41
libeirs/src/seat.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use std::{ffi::CStr, fmt::Display, ptr::NonNull};
|
||||||
|
|
||||||
|
use super::{events::Event, ffi};
|
||||||
|
|
||||||
|
pub use ffi::ei_device_capability as DeviceCapability;
|
||||||
|
|
||||||
|
pub struct Seat {
|
||||||
|
seat: NonNull<ffi::ei_seat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Seat {
|
||||||
|
pub fn from_event(event: &Event) -> Option<Self> {
|
||||||
|
let seat = unsafe {
|
||||||
|
let seat = ffi::ei_event_get_seat(event.as_raw());
|
||||||
|
ffi::ei_seat_ref(seat)
|
||||||
|
};
|
||||||
|
NonNull::new(seat).map(|seat| Self { seat })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind_capabilities(&self, capabilities: DeviceCapability) {
|
||||||
|
unsafe {
|
||||||
|
ffi::ei_seat_bind_capabilities(self.seat.as_ptr(), capabilities, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Seat {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = unsafe { CStr::from_ptr(ffi::ei_seat_get_name(self.seat.as_ptr())) }
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
write!(f, "{str}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Seat {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
log::trace!("Dropping seat {self}");
|
||||||
|
unsafe { ffi::ei_seat_unref(self.seat.as_ptr()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
23
libeirs/src/sender.rs
Normal file
23
libeirs/src/sender.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use std::{ops::Deref, os::fd::OwnedFd};
|
||||||
|
|
||||||
|
use super::eirc::Ei;
|
||||||
|
|
||||||
|
pub struct Sender {
|
||||||
|
ei: Ei,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sender {
|
||||||
|
pub fn new(fd: OwnedFd) -> Self {
|
||||||
|
Self {
|
||||||
|
ei: Ei::new_sender(fd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Sender {
|
||||||
|
type Target = Ei;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.ei
|
||||||
|
}
|
||||||
|
}
|
||||||
3
libxkbcommonrs/.clangd
Normal file
3
libxkbcommonrs/.clangd
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
CompileFlags:
|
||||||
|
Add: [-xc, -Wall, -Werror, -Wpedantic, -std=c17]
|
||||||
|
Compiler: clang
|
||||||
15
libxkbcommonrs/Cargo.toml
Normal file
15
libxkbcommonrs/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "libxkbcommonrs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
bindgen = { workspace = true, features = ["experimental"] }
|
||||||
|
anyhow.workspace = true
|
||||||
|
cc = "1.1"
|
||||||
|
convert_case = "0.6"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { workspace = true, features = ["rt", "net"] }
|
||||||
|
tokio-stream = { workspace = true }
|
||||||
|
log = { workspace = true, features = ["kv"] }
|
||||||
209
libxkbcommonrs/build.rs
Normal file
209
libxkbcommonrs/build.rs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
use bindgen::callbacks::{EnumVariantValue, ParseCallbacks};
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct RemovePrefixesCallback {}
|
||||||
|
|
||||||
|
impl RemovePrefixesCallback {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boxed() -> Box<dyn ParseCallbacks> {
|
||||||
|
Box::new(Self::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseCallbacks for RemovePrefixesCallback {
|
||||||
|
fn item_name(&self, original_item_name: &str) -> Option<String> {
|
||||||
|
const PREFIXES: &[&str] = &["XKB_", "xkb_"];
|
||||||
|
let mut ret = None;
|
||||||
|
for prefix in PREFIXES {
|
||||||
|
if let Some(name) = original_item_name.strip_prefix(prefix) {
|
||||||
|
ret = Some(name.to_owned());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enum_variant_name(
|
||||||
|
&self,
|
||||||
|
enum_name: Option<&str>,
|
||||||
|
original_variant_name: &str,
|
||||||
|
_variant_value: EnumVariantValue,
|
||||||
|
) -> Option<String> {
|
||||||
|
if enum_name.is_none() {
|
||||||
|
return self.item_name(original_variant_name);
|
||||||
|
}
|
||||||
|
// let original_variant_name = self
|
||||||
|
// .item_name(original_variant_name)
|
||||||
|
// .unwrap_or(original_variant_name.to_owned());
|
||||||
|
let enum_name = enum_name.unwrap();
|
||||||
|
let enum_name = enum_name.strip_prefix("enum ").unwrap_or(enum_name);
|
||||||
|
let enum_lower = original_variant_name.to_lowercase();
|
||||||
|
let diff_index = enum_lower
|
||||||
|
.chars()
|
||||||
|
.zip(enum_name.chars())
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_i, (v, e))| v != e)
|
||||||
|
.map(|it| it.0)
|
||||||
|
.unwrap_or(enum_name.len());
|
||||||
|
let name = original_variant_name[diff_index..]
|
||||||
|
.trim_start_matches('_')
|
||||||
|
.to_case(Case::Pascal);
|
||||||
|
|
||||||
|
Some(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TypedefCallback {}
|
||||||
|
|
||||||
|
impl TypedefCallback {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boxed() -> Box<dyn ParseCallbacks> {
|
||||||
|
Box::new(Self::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseCallbacks for TypedefCallback {
|
||||||
|
fn item_name(&self, original_item_name: &str) -> Option<String> {
|
||||||
|
if original_item_name == "xkb_keysym_t" {
|
||||||
|
Some(String::from("Keysym"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn int_macro(&self, name: &str, _value: i64) -> Option<IntKind> {
|
||||||
|
// if name.starts_with("XKB_KEY_") {
|
||||||
|
// Some(IntKind::Custom {
|
||||||
|
// name: "Keysym",
|
||||||
|
// is_signed: false,
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn add_derives(&self, info: &DeriveInfo<'_>) -> Vec<String> {
|
||||||
|
// if (info.name.contains("keysym") && !info.name.contains("flags"))
|
||||||
|
// || info.name == "Keysym"
|
||||||
|
// || info.name == "xkb_keysym_t"
|
||||||
|
// || info.name == "keysym_t"
|
||||||
|
// {
|
||||||
|
// panic!("{info:?}");
|
||||||
|
// vec![
|
||||||
|
// "PartialEq".to_string(),
|
||||||
|
// "PartialOrd".to_string(),
|
||||||
|
// "Ord".to_string(),
|
||||||
|
// "Eq".to_string(),
|
||||||
|
// ]
|
||||||
|
// } else {
|
||||||
|
// vec![]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// struct VariadicsCallback {}
|
||||||
|
//
|
||||||
|
// impl VariadicsCallback {
|
||||||
|
// fn new() -> Self {
|
||||||
|
// Self {}
|
||||||
|
// }
|
||||||
|
// fn boxed() -> Box<dyn ParseCallbacks> {
|
||||||
|
// Box::new(Self::new())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// impl ParseCallbacks for VariadicsCallback {
|
||||||
|
// fn wrap_as_variadic_fn(&self, name: &str) -> Option<String> {
|
||||||
|
// panic!("mdr {name}");
|
||||||
|
// Some(format!("{name}_var"))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn build_xkb_keysyms() -> anyhow::Result<()> {
|
||||||
|
let common_bindings = bindgen::builder()
|
||||||
|
.header("/usr/include/xkbcommon/xkbcommon-keysyms.h")
|
||||||
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||||
|
.parse_callbacks(RemovePrefixesCallback::boxed())
|
||||||
|
.parse_callbacks(TypedefCallback::boxed())
|
||||||
|
.allowlist_var("XKB_.*")
|
||||||
|
.generate()?;
|
||||||
|
|
||||||
|
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
let out_file = out_path.join("libxkbcommon-keysyms.rs");
|
||||||
|
common_bindings
|
||||||
|
.write_to_file(out_file)
|
||||||
|
.expect("Couldn't write bindings!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_xkbcommon() -> anyhow::Result<()> {
|
||||||
|
let common_bindings = bindgen::builder()
|
||||||
|
.header("ffi/logger.h")
|
||||||
|
.header("/usr/include/xkbcommon/xkbcommon.h")
|
||||||
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||||
|
.parse_callbacks(RemovePrefixesCallback::boxed())
|
||||||
|
.parse_callbacks(TypedefCallback::boxed())
|
||||||
|
// .parse_callbacks(VariadicsCallback::boxed())
|
||||||
|
.allowlist_item("xkb_.*")
|
||||||
|
.allowlist_item("log_.*") // Log wrapper
|
||||||
|
// .allowlist_item("XKB_.*")
|
||||||
|
.new_type_alias_deref("Keysym")
|
||||||
|
.no_debug("xkb_keysym_t")
|
||||||
|
.allowlist_var("XKB_KEYCODE_INVALID")
|
||||||
|
.allowlist_var("XKB_LAYOUT_INVALID")
|
||||||
|
.allowlist_var("XKB_LEVEL_INVALID")
|
||||||
|
.allowlist_var("XKB_MOD_INVALID")
|
||||||
|
.allowlist_var("XKB_LED_INVALID")
|
||||||
|
.allowlist_var("XKB_KEYCODE_MAX")
|
||||||
|
.allowlist_var("XKB_KEYSYM_MAX")
|
||||||
|
.default_enum_style(bindgen::EnumVariation::NewType {
|
||||||
|
is_bitfield: false,
|
||||||
|
is_global: false,
|
||||||
|
})
|
||||||
|
.derive_eq(true)
|
||||||
|
.derive_partialeq(true)
|
||||||
|
.derive_ord(true)
|
||||||
|
.derive_partialord(true)
|
||||||
|
.derive_hash(true)
|
||||||
|
.generate()?;
|
||||||
|
|
||||||
|
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
let out_file = out_path.join("libxkbcommon.rs");
|
||||||
|
common_bindings
|
||||||
|
.write_to_file(out_file)
|
||||||
|
.expect("Couldn't write bindings!");
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-lib=xkbcommon");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_logger() -> anyhow::Result<()> {
|
||||||
|
cc::Build::new()
|
||||||
|
.file("ffi/logger.c")
|
||||||
|
.std("c17")
|
||||||
|
.warnings(true)
|
||||||
|
.warnings_into_errors(true)
|
||||||
|
.extra_warnings(true)
|
||||||
|
.compile("logger");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
build_xkbcommon()?;
|
||||||
|
build_xkb_keysyms()?;
|
||||||
|
build_logger()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
17
libxkbcommonrs/ffi/logger.c
Normal file
17
libxkbcommonrs/ffi/logger.c
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define buf_size 256
|
||||||
|
static callback_type global_callback = NULL;
|
||||||
|
|
||||||
|
void log_set_callback(callback_type callback) { global_callback = callback; }
|
||||||
|
|
||||||
|
void log_logger(struct xkb_context *context, enum xkb_log_level level,
|
||||||
|
const char *format, va_list args) {
|
||||||
|
if (global_callback) {
|
||||||
|
char buf[buf_size] = {0};
|
||||||
|
vsnprintf(buf, buf_size, format, args);
|
||||||
|
global_callback(context, level, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
libxkbcommonrs/ffi/logger.h
Normal file
11
libxkbcommonrs/ffi/logger.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#include <stdarg.h>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
typedef void (*callback_type)(struct xkb_context *context,
|
||||||
|
enum xkb_log_level level, const char *message);
|
||||||
|
|
||||||
|
void log_set_callback(callback_type callback);
|
||||||
|
|
||||||
|
/// xkb log callback wrapper for printf formatting
|
||||||
|
void log_logger(struct xkb_context *context, enum xkb_log_level level,
|
||||||
|
const char *format, va_list args);
|
||||||
65
libxkbcommonrs/src/context.rs
Normal file
65
libxkbcommonrs/src/context.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use super::{
|
||||||
|
error,
|
||||||
|
ffi::{self, context},
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{ffi::CStr, ptr::NonNull};
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
ctx: NonNull<ffi::context>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new() -> Result<Self, error::Error> {
|
||||||
|
let ctx = NonNull::new(unsafe { ffi::context_new(ffi::context_flags::NoFlags) })
|
||||||
|
.ok_or(error::Error::ContextNewFailed)?;
|
||||||
|
unsafe {
|
||||||
|
let ctx = ctx.as_ptr();
|
||||||
|
if let Some(level) = log::max_level().to_level() {
|
||||||
|
ffi::context_set_log_level(ctx, level.into());
|
||||||
|
ffi::log_set_callback(Some(log_callback));
|
||||||
|
ffi::context_set_log_fn(ctx, Some(ffi::log_logger));
|
||||||
|
} else {
|
||||||
|
ffi::context_set_log_fn(ctx, Some(null_handler))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { ctx })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_raw(&self) -> *mut context {
|
||||||
|
self.ctx.as_ptr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Context {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { ffi::context_unref(self.as_raw()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn null_handler(
|
||||||
|
_ctx: *mut context,
|
||||||
|
_priority: ffi::log_level,
|
||||||
|
_format: *const ::std::os::raw::c_char,
|
||||||
|
_args: *mut ffi::__va_list_tag,
|
||||||
|
) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn log_callback(
|
||||||
|
_ctx: *mut context,
|
||||||
|
priority: ffi::log_level,
|
||||||
|
str: *const std::os::raw::c_char,
|
||||||
|
) {
|
||||||
|
match unsafe { CStr::from_ptr(str).to_str() } {
|
||||||
|
Ok(str) => {
|
||||||
|
log::log!(
|
||||||
|
target: "libxkbcommon",
|
||||||
|
priority.into(),
|
||||||
|
"{str}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Failed to log message with severity {priority:?}.\n\t{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
15
libxkbcommonrs/src/error.rs
Normal file
15
libxkbcommonrs/src/error.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
ContextNewFailed,
|
||||||
|
InvalidKeymap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Debug::fmt(&self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
61
libxkbcommonrs/src/ffi.rs
Normal file
61
libxkbcommonrs/src/ffi.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
#![allow(non_camel_case_types)]
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
pub mod syms {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/libxkbcommon-keysyms.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/libxkbcommon.rs"));
|
||||||
|
|
||||||
|
impl From<log_level> for log::Level {
|
||||||
|
fn from(value: log_level) -> Self {
|
||||||
|
match value {
|
||||||
|
log_level::Debug => log::Level::Debug,
|
||||||
|
log_level::Info => log::Level::Info,
|
||||||
|
log_level::Warning => log::Level::Warn,
|
||||||
|
log_level::Error => log::Level::Error,
|
||||||
|
_ => log::Level::Debug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<log::Level> for log_level {
|
||||||
|
fn from(value: log::Level) -> Self {
|
||||||
|
match value {
|
||||||
|
log::Level::Trace | log::Level::Debug => log_level::Debug,
|
||||||
|
log::Level::Info => log_level::Info,
|
||||||
|
log::Level::Warn => log_level::Warning,
|
||||||
|
log::Level::Error => log_level::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Keysym {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut buffer = [0_u8; 32];
|
||||||
|
let written =
|
||||||
|
unsafe { keysym_to_utf8(*self, buffer.as_mut_ptr() as *mut _, buffer.len()) } as usize;
|
||||||
|
let str = std::str::from_utf8(&buffer[0..written]).unwrap_or("<invalid>");
|
||||||
|
write!(f, "{str}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Keysym {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut buffer = [0_u8; 32];
|
||||||
|
let written =
|
||||||
|
unsafe { keysym_get_name(*self, buffer.as_mut_ptr() as *mut _, buffer.len()) } as usize;
|
||||||
|
let str = std::str::from_utf8(&buffer[0..written]).unwrap_or("<invalid>");
|
||||||
|
write!(f, "{str}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for Keysym {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
133
libxkbcommonrs/src/keymap.rs
Normal file
133
libxkbcommonrs/src/keymap.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use std::ffi::c_void;
|
||||||
|
use std::{ffi::CStr, ptr::NonNull};
|
||||||
|
|
||||||
|
use crate::{error, ffi, LayoutID, LayoutRef};
|
||||||
|
|
||||||
|
use super::context::Context;
|
||||||
|
|
||||||
|
pub use ffi::keycode_t as Key;
|
||||||
|
pub use ffi::layout_index_t as Level;
|
||||||
|
pub use ffi::Keysym;
|
||||||
|
|
||||||
|
pub type KeyIter<'a> = Box<dyn FnMut(Key) + 'a>;
|
||||||
|
|
||||||
|
pub struct Keymap {
|
||||||
|
keymap: NonNull<ffi::keymap>,
|
||||||
|
format: ffi::keymap_format,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keymap {
|
||||||
|
pub fn new(ctx: &Context, buffer: &[u8]) -> Result<Self, error::Error> {
|
||||||
|
let format = ffi::keymap_format::TextV1;
|
||||||
|
let keymap = unsafe {
|
||||||
|
ffi::keymap_new_from_buffer(
|
||||||
|
ctx.as_raw(),
|
||||||
|
buffer.as_ptr() as *const i8,
|
||||||
|
buffer.len(),
|
||||||
|
format,
|
||||||
|
ffi::keymap_compile_flags::NoFlags,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
keymap: NonNull::new(keymap).ok_or(error::Error::InvalidKeymap)?,
|
||||||
|
format,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_raw(&self) -> *mut ffi::keymap {
|
||||||
|
self.keymap.as_ptr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Keymap {
|
||||||
|
pub fn get_layouts(&'a self) -> Vec<LayoutRef<'a>> {
|
||||||
|
let num_layouts = unsafe { ffi::keymap_num_layouts(self.as_raw()) };
|
||||||
|
let mut v = Vec::with_capacity(num_layouts as usize);
|
||||||
|
|
||||||
|
for i in 0..num_layouts {
|
||||||
|
v.push(LayoutRef::new(self, i));
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sims_by_level(
|
||||||
|
&'a self,
|
||||||
|
key: Key,
|
||||||
|
layout: Option<LayoutID>,
|
||||||
|
level: Level,
|
||||||
|
) -> &'a [Keysym] {
|
||||||
|
let mut syms_out: *const Keysym = std::ptr::null_mut();
|
||||||
|
unsafe {
|
||||||
|
let ptr = &mut syms_out as *mut _;
|
||||||
|
let len = ffi::keymap_key_get_syms_by_level(
|
||||||
|
self.as_raw(),
|
||||||
|
key,
|
||||||
|
layout.unwrap_or_default(),
|
||||||
|
level,
|
||||||
|
ptr,
|
||||||
|
);
|
||||||
|
if syms_out.is_null() || !syms_out.is_aligned() {
|
||||||
|
&[]
|
||||||
|
} else {
|
||||||
|
std::slice::from_raw_parts(syms_out, len as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_levels_for_key(&self, key: Key, layout: Option<LayoutID>) -> u32 {
|
||||||
|
unsafe { ffi::keymap_num_levels_for_key(self.as_raw(), key, layout.unwrap_or_default()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_for_each(&'a self, visitor: KeyIter<'a>) {
|
||||||
|
let ptr = (&visitor) as *const _;
|
||||||
|
unsafe {
|
||||||
|
ffi::keymap_key_for_each(self.as_raw(), Some(key_iter), ptr as *mut c_void);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_get_name(&self, key: Key) -> Option<&str> {
|
||||||
|
let ptr = unsafe { ffi::keymap_key_get_name(self.as_raw(), key) };
|
||||||
|
if ptr.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
unsafe { CStr::from_ptr(ptr) }
|
||||||
|
.to_str()
|
||||||
|
.inspect_err(|e| log::warn!("Failed to get name for key {key}: {e}"))
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Keymap {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = unsafe { CStr::from_ptr(ffi::keymap_get_as_string(self.as_raw(), self.format)) }
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
write!(f, "{str}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn utf32_to_keysim(utf32: &[u32]) -> Vec<Keysym> {
|
||||||
|
let mut ret = Vec::with_capacity(utf32.len());
|
||||||
|
for c in utf32 {
|
||||||
|
let ks = unsafe { ffi::utf32_to_keysym(*c) };
|
||||||
|
ret.push(ks);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keycode_is_legal_ext(key: Key) -> bool {
|
||||||
|
key <= ffi::KEYCODE_MAX
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keycode_is_legal_x11(key: Key) -> bool {
|
||||||
|
(8..=255).contains(&key)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn key_iter(
|
||||||
|
_keymap: *mut ffi::keymap,
|
||||||
|
key: ffi::keycode_t,
|
||||||
|
data: *mut ::std::os::raw::c_void,
|
||||||
|
) {
|
||||||
|
let visitor = unsafe { &mut *(data as *mut KeyIter<'_>) };
|
||||||
|
visitor(key);
|
||||||
|
}
|
||||||
33
libxkbcommonrs/src/layout.rs
Normal file
33
libxkbcommonrs/src/layout.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::{ffi::CStr, fmt::Display};
|
||||||
|
|
||||||
|
use crate::{ffi, Keymap};
|
||||||
|
|
||||||
|
pub type LayoutID = ffi::layout_index_t;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct LayoutRef<'a> {
|
||||||
|
keymap: &'a Keymap,
|
||||||
|
id: ffi::layout_index_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LayoutRef<'a> {
|
||||||
|
pub fn new(keymap: &'a Keymap, id: ffi::layout_index_t) -> Self {
|
||||||
|
Self { keymap, id }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_name(&'a self) -> &'a str {
|
||||||
|
unsafe { CStr::from_ptr(ffi::keymap_layout_get_name(self.keymap.as_raw(), self.id)) }
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or("<invalid>")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_id(&self) -> LayoutID {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for LayoutRef<'a> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.get_name())
|
||||||
|
}
|
||||||
|
}
|
||||||
11
libxkbcommonrs/src/lib.rs
Normal file
11
libxkbcommonrs/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
mod context;
|
||||||
|
mod error;
|
||||||
|
mod ffi;
|
||||||
|
mod keymap;
|
||||||
|
mod layout;
|
||||||
|
|
||||||
|
pub use context::Context;
|
||||||
|
pub use keymap::*;
|
||||||
|
pub use layout::*;
|
||||||
|
|
||||||
|
pub use ffi::syms;
|
||||||
73
src/app.rs
Normal file
73
src/app.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::{ei, keyboard::Keyboard, portal::Portal};
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
KeyboardAdded(Keyboard),
|
||||||
|
KeyboardRemoved,
|
||||||
|
}
|
||||||
|
pub type EventSender = mpsc::Sender<Event>;
|
||||||
|
|
||||||
|
#[derive(clap::Parser, Debug)]
|
||||||
|
struct Config {
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||||
|
verbose: u8,
|
||||||
|
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run() -> anyhow::Result<()> {
|
||||||
|
let config = Config::parse();
|
||||||
|
|
||||||
|
setup_logger(config.verbose);
|
||||||
|
|
||||||
|
let portal = Portal::new_session().await?;
|
||||||
|
let eis_fd = portal.connect_to_eis().await?;
|
||||||
|
let (sndr, mut recv) = mpsc::channel(10);
|
||||||
|
let (ei_hdl, ei_sender) = ei::start_thread(eis_fd, sndr)?;
|
||||||
|
|
||||||
|
while let Some(event) = recv.recv().await {
|
||||||
|
match event {
|
||||||
|
Event::KeyboardAdded(k) => {
|
||||||
|
log::info!("Got keyboard");
|
||||||
|
let seq = k.string_to_sequence(&config.text);
|
||||||
|
ei_sender.send(seq).await.unwrap_or_default();
|
||||||
|
// Done
|
||||||
|
ei_sender.close().await;
|
||||||
|
}
|
||||||
|
Event::KeyboardRemoved => {
|
||||||
|
log::info!("Keyboard removed");
|
||||||
|
ei_sender.cancel().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match ei_hdl.await {
|
||||||
|
Err(error) => {
|
||||||
|
if let Ok(panic) = error.try_into_panic() {
|
||||||
|
std::panic::resume_unwind(panic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(res) => res?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_logger(verbose: u8) {
|
||||||
|
let (base, external) = match verbose {
|
||||||
|
0 => (LevelFilter::Warn, LevelFilter::Warn),
|
||||||
|
1 => (LevelFilter::Info, LevelFilter::Warn),
|
||||||
|
2 => (LevelFilter::Debug, LevelFilter::Info),
|
||||||
|
3 => (LevelFilter::Debug, LevelFilter::Debug),
|
||||||
|
4.. => (LevelFilter::Trace, LevelFilter::Trace),
|
||||||
|
};
|
||||||
|
|
||||||
|
env_logger::Builder::new()
|
||||||
|
.parse_env("RUST_LOG")
|
||||||
|
.filter_level(base)
|
||||||
|
.filter(Some("libei"), external)
|
||||||
|
.init();
|
||||||
|
log::info!("Hello, world!");
|
||||||
|
}
|
||||||
309
src/ei.rs
Normal file
309
src/ei.rs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
os::fd::{AsRawFd, OwnedFd},
|
||||||
|
sync::{Arc, Weak},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use libeirs as ei;
|
||||||
|
use libxkbcommonrs as xkb;
|
||||||
|
use tokio::{
|
||||||
|
sync::mpsc::{self, error::TryRecvError},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{self, EventSender},
|
||||||
|
keyboard::Keyboard,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KeyEvent(xkb::Key, bool);
|
||||||
|
pub type KeySequence = Vec<KeyEvent>;
|
||||||
|
|
||||||
|
impl KeyEvent {
|
||||||
|
pub fn new(k: xkb::Key, down: bool) -> Self {
|
||||||
|
Self(k, down)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_pressed(&self) -> bool {
|
||||||
|
self.1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> xkb::Key {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Message {
|
||||||
|
Keys(KeySequence),
|
||||||
|
Cancel,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around a sender to wake the EI thread
|
||||||
|
pub struct EIThreadSender {
|
||||||
|
send: mpsc::Sender<Message>,
|
||||||
|
poll: Weak<polling::Poller>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EIThreadSender {
|
||||||
|
pub async fn send(&self, keys: KeySequence) -> anyhow::Result<()> {
|
||||||
|
self.notify()?;
|
||||||
|
self.send.send(Message::Keys(keys)).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel(&self) {
|
||||||
|
if self.notify().is_ok() {
|
||||||
|
self.send.send(Message::Cancel).await.unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close(&self) {
|
||||||
|
if self.notify().is_ok() {
|
||||||
|
self.send.send(Message::Close).await.unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify(&self) -> anyhow::Result<()> {
|
||||||
|
Ok(self
|
||||||
|
.poll
|
||||||
|
.upgrade()
|
||||||
|
.ok_or(anyhow::anyhow!("Poller is dead?"))?
|
||||||
|
.notify()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_thread(
|
||||||
|
fd: OwnedFd,
|
||||||
|
event_sender: EventSender,
|
||||||
|
) -> anyhow::Result<(JoinHandle<anyhow::Result<()>>, EIThreadSender)> {
|
||||||
|
let (send, rcv) = mpsc::channel(10);
|
||||||
|
let poll = Arc::new(polling::Poller::new()?);
|
||||||
|
let weak = Arc::downgrade(&poll);
|
||||||
|
Ok((
|
||||||
|
tokio::task::spawn_blocking(move || run(fd, poll, rcv, event_sender)),
|
||||||
|
EIThreadSender { send, poll: weak },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
fd: OwnedFd,
|
||||||
|
poll: Arc<polling::Poller>,
|
||||||
|
mut rcv: mpsc::Receiver<Message>,
|
||||||
|
event_sender: mpsc::Sender<app::Event>,
|
||||||
|
) -> anyhow::Result<(), anyhow::Error> {
|
||||||
|
const EI_FD_KEY: usize = 1;
|
||||||
|
let raw_fd = fd.as_raw_fd();
|
||||||
|
let ei = ei::Sender::new(fd);
|
||||||
|
let xkb = xkb::Context::new()?;
|
||||||
|
let mut state = State::new(xkb);
|
||||||
|
|
||||||
|
let mut events = polling::Events::new();
|
||||||
|
let fd_event = polling::Event::readable(EI_FD_KEY).with_interrupt();
|
||||||
|
unsafe {
|
||||||
|
// This here is why we're not running in a normal tokio task. Tokio (Mio) only allow us to use the EdgeTrigger
|
||||||
|
// mode of epoll, so we need to reset the readiness of the file descriptor ourselves. But since we're not the
|
||||||
|
// ones reading from it, we'd have to reset the ready flag when we're sure libei got everything. Unfortunately,
|
||||||
|
// libei doesn't document how the buffer is read, and the user is expected to use the LevelTrigger mode of epoll
|
||||||
|
// to loop until libei is done reading.
|
||||||
|
poll.add_with_mode(raw_fd, fd_event, polling::PollMode::Level)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut done = false;
|
||||||
|
let mut timeout = None;
|
||||||
|
while poll.wait(&mut events, timeout).map(|_| true)? {
|
||||||
|
if done && state.to_send.is_empty() {
|
||||||
|
log::trace!("Done sending events");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
match rcv.try_recv() {
|
||||||
|
Ok(msg) => match msg {
|
||||||
|
Message::Keys(keys) => {
|
||||||
|
state.to_send.extend(keys.into_iter());
|
||||||
|
// libei asks us to not send event frames with timestamps into the future, so set up a timer using
|
||||||
|
// poll to time slice our events
|
||||||
|
timeout = Some(Duration::from_micros(10));
|
||||||
|
}
|
||||||
|
Message::Cancel => {
|
||||||
|
log::trace!("Thread cancelled");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Message::Close => {
|
||||||
|
log::trace!("Closing message channel, finishing up...");
|
||||||
|
done = true;
|
||||||
|
poll.notify().unwrap_or_default();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
if e == TryRecvError::Disconnected {
|
||||||
|
log::trace!("Lost message channel, finishing up...");
|
||||||
|
done = true;
|
||||||
|
poll.notify().unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ei.dispatch();
|
||||||
|
while let Some(event) = ei.get_event() {
|
||||||
|
if !handle_event(&mut state, event, &event_sender)? {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.to_send.is_empty() && state.ready {
|
||||||
|
send_keys(
|
||||||
|
&mut state,
|
||||||
|
timeout.expect("timeout is none but we have keys to send?"),
|
||||||
|
)?;
|
||||||
|
if state.to_send.is_empty() {
|
||||||
|
timeout = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("EI Thread over");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
xkb: xkb::Context,
|
||||||
|
keymap: Option<xkb::Keymap>,
|
||||||
|
seat: Option<ei::Seat>,
|
||||||
|
device: Option<ei::Device>,
|
||||||
|
to_send: VecDeque<KeyEvent>,
|
||||||
|
ready: bool,
|
||||||
|
sequence: u32,
|
||||||
|
elapsed: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn new(xkb: xkb::Context) -> Self {
|
||||||
|
Self {
|
||||||
|
xkb,
|
||||||
|
keymap: None,
|
||||||
|
seat: None,
|
||||||
|
device: None,
|
||||||
|
to_send: Default::default(),
|
||||||
|
ready: false,
|
||||||
|
sequence: 0,
|
||||||
|
elapsed: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(state: &mut State, event: ei::Event, send: &EventSender) -> anyhow::Result<bool> {
|
||||||
|
log::trace!("Got EI event {event}");
|
||||||
|
match event.get_type() {
|
||||||
|
ei::EventType::EI_EVENT_CONNECT => log::info!("Client connected"),
|
||||||
|
ei::EventType::EI_EVENT_DISCONNECT => {
|
||||||
|
log::info!("Client disconnected");
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
ei::EventType::EI_EVENT_SEAT_ADDED => {
|
||||||
|
if state.seat.is_none() {
|
||||||
|
state.seat = event.get_seat();
|
||||||
|
if let Some(seat) = state.seat.as_ref() {
|
||||||
|
seat.bind_capabilities(ei::seat::DeviceCapability::EI_DEVICE_CAP_KEYBOARD);
|
||||||
|
log::info!("Found seat \"{seat}\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ei::EventType::EI_EVENT_SEAT_REMOVED => {
|
||||||
|
if let Some(seat) = state.seat.take() {
|
||||||
|
log::info!("state.seat \"{seat}\" removed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ei::EventType::EI_EVENT_DEVICE_ADDED => {
|
||||||
|
if let Some(d) = event.get_device() {
|
||||||
|
log::trace!("Got device {d}");
|
||||||
|
if state.device.is_none() {
|
||||||
|
state.device = Some(d.to_owned());
|
||||||
|
let device = state.device.as_ref().unwrap();
|
||||||
|
let keymap = device
|
||||||
|
.get_keymap()
|
||||||
|
.ok_or(anyhow::anyhow!("Device {device} has no keymap"))?;
|
||||||
|
let keymap_buffer = keymap.mmap_keymap()?;
|
||||||
|
let xkb = libxkbcommonrs::Keymap::new(&state.xkb, keymap_buffer.as_ref())?;
|
||||||
|
let keyboard = Keyboard::new(&xkb)?;
|
||||||
|
state.keymap = Some(xkb);
|
||||||
|
send.blocking_send(app::Event::KeyboardAdded(keyboard))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ei::EventType::EI_EVENT_DEVICE_REMOVED => {
|
||||||
|
if let Some(d) = event.get_device() {
|
||||||
|
log::trace!("Removed device {d}");
|
||||||
|
if state.device.as_ref().is_some_and(|device| *device == d) {
|
||||||
|
state.device.take();
|
||||||
|
state.keymap.take();
|
||||||
|
state.ready = false;
|
||||||
|
send.blocking_send(app::Event::KeyboardRemoved).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ei::EventType::EI_EVENT_DEVICE_RESUMED => {
|
||||||
|
if event.get_device().as_ref() == state.device.as_ref().map(ei::Device::as_ref) {
|
||||||
|
log::debug!("device resumed");
|
||||||
|
state.sequence += 1;
|
||||||
|
state
|
||||||
|
.device
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.start_emulating(state.sequence);
|
||||||
|
state.ready = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ei::EventType::EI_EVENT_DEVICE_PAUSED => {
|
||||||
|
if event.get_device().as_ref() == state.device.as_ref().map(ei::Device::as_ref) {
|
||||||
|
log::debug!("device paused");
|
||||||
|
state.ready = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e => log::debug!("Unhandled event {e}"),
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_keys(state: &mut State, dt: Duration) -> anyhow::Result<()> {
|
||||||
|
log::trace!("Sending keys");
|
||||||
|
|
||||||
|
let first_pressed = if let Some(front) = state.to_send.front() {
|
||||||
|
front.is_pressed()
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let d = state.device.as_mut().ok_or(anyhow::anyhow!("No device"))?;
|
||||||
|
let mut count = 0;
|
||||||
|
let now = (state.elapsed + dt).as_micros() as u64;
|
||||||
|
for key in state
|
||||||
|
.to_send
|
||||||
|
.iter()
|
||||||
|
.take_while(|e| e.is_pressed() == first_pressed)
|
||||||
|
{
|
||||||
|
log::trace!(
|
||||||
|
"Sending event {}({}): {}",
|
||||||
|
state
|
||||||
|
.keymap
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.key_get_name(key.key())
|
||||||
|
.unwrap_or("<invalid>"),
|
||||||
|
key.key(),
|
||||||
|
key.is_pressed()
|
||||||
|
);
|
||||||
|
count += 1;
|
||||||
|
// XKB keys are offset by 8
|
||||||
|
let ei_key = key.key() - 8;
|
||||||
|
d.keyboard_key(ei_key, key.is_pressed());
|
||||||
|
}
|
||||||
|
d.frame(Some(now));
|
||||||
|
state.to_send.drain(0..count);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
142
src/keyboard.rs
Normal file
142
src/keyboard.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
use std::{collections::BTreeMap, fmt::Display};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use libxkbcommonrs::{self as xkb, Key, Keymap, Keysym, LayoutID, Level};
|
||||||
|
use widestring::Utf32String;
|
||||||
|
use xkb::syms;
|
||||||
|
|
||||||
|
use crate::ei::{KeyEvent, KeySequence};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Layout {
|
||||||
|
keysym_to_key_level: BTreeMap<Keysym, (Key, Level)>,
|
||||||
|
shift_mod: Key,
|
||||||
|
graph_mod: Key,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
fn from_xkb(keymap: &Keymap, l: &xkb::LayoutRef<'_>) -> anyhow::Result<Self> {
|
||||||
|
let id = l.get_id();
|
||||||
|
let keysym_to_key_level = build_keysym_key_map(keymap, Some(id));
|
||||||
|
let (shift_mod, shift_level) = keysym_to_key_level
|
||||||
|
.get(&syms::KEY_Shift_L.into())
|
||||||
|
.or_else(|| keysym_to_key_level.get(&syms::KEY_Shift_R.into()))
|
||||||
|
.copied()
|
||||||
|
.ok_or(anyhow::anyhow!("No shift key?"))?;
|
||||||
|
if shift_level > 0 {
|
||||||
|
Err(anyhow::anyhow!("Shift level need for shift key?"))?;
|
||||||
|
}
|
||||||
|
let (graph_mod, graph_level) = keysym_to_key_level
|
||||||
|
.get(&syms::KEY_ISO_Level3_Shift.into())
|
||||||
|
.copied()
|
||||||
|
.ok_or(anyhow!("No AltGr key?"))?;
|
||||||
|
if graph_level > 0 {
|
||||||
|
Err(anyhow::anyhow!("Graph level need for graph key?"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Layout {
|
||||||
|
keysym_to_key_level,
|
||||||
|
shift_mod,
|
||||||
|
graph_mod,
|
||||||
|
name: l.get_name().to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key_level_from_sym(&self, sym: Keysym) -> Option<(Key, Level)> {
|
||||||
|
self.keysym_to_key_level.get(&sym).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Layout {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Keyboard {
|
||||||
|
layouts: Vec<Layout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keyboard {
|
||||||
|
pub fn new(xkb: &libxkbcommonrs::Keymap) -> anyhow::Result<Self> {
|
||||||
|
let layouts = xkb.get_layouts();
|
||||||
|
log::debug!(
|
||||||
|
"Keymap has layouts {:?}",
|
||||||
|
layouts
|
||||||
|
.iter()
|
||||||
|
.map(|l| format!("{l}: {}", l.get_name()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
let layouts: Vec<Layout> = layouts
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|l| match Layout::from_xkb(xkb, &l) {
|
||||||
|
Ok(layout) => Some(layout),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Discarding invalid layout {l}: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if layouts.is_empty() {
|
||||||
|
return Err(anyhow!("Failed to get any working layouts"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { layouts })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_to_sequence(&self, string: &str) -> KeySequence {
|
||||||
|
let utf32 = Utf32String::from(string);
|
||||||
|
let keysyms = xkb::utf32_to_keysim(utf32.as_slice());
|
||||||
|
log::trace!("Syms: {string} => {keysyms:?}");
|
||||||
|
keysyms
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|sym| self.get_key_sequence(None, sym))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_key_sequence(&self, l: Option<LayoutID>, sym: Keysym) -> KeySequence {
|
||||||
|
let layout = self.layouts.get(l.unwrap_or_default() as usize).unwrap();
|
||||||
|
if let Some((key, level)) = layout.get_key_level_from_sym(sym) {
|
||||||
|
if level == 0 {
|
||||||
|
return vec![KeyEvent::new(key, true), KeyEvent::new(key, false)];
|
||||||
|
}
|
||||||
|
let shift_key = match level {
|
||||||
|
1 => layout.shift_mod,
|
||||||
|
2 => layout.graph_mod,
|
||||||
|
o => panic!("What: {o}"),
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
KeyEvent::new(shift_key, true),
|
||||||
|
KeyEvent::new(key, true),
|
||||||
|
KeyEvent::new(key, false),
|
||||||
|
KeyEvent::new(shift_key, false),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
log::warn!("No keys for keysym {sym} in layout {layout}");
|
||||||
|
KeySequence::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_keysym_key_map(keymap: &Keymap, l: Option<LayoutID>) -> BTreeMap<Keysym, (Key, Level)> {
|
||||||
|
let mut ret = BTreeMap::default();
|
||||||
|
keymap.key_for_each(Box::new(|k| {
|
||||||
|
for level in 0..keymap.num_levels_for_key(k, l) {
|
||||||
|
for sym in keymap.get_sims_by_level(k, l, level) {
|
||||||
|
ret.entry(*sym)
|
||||||
|
.and_modify(|(old_key, old_level)| {
|
||||||
|
if *old_level > level {
|
||||||
|
*old_key = k;
|
||||||
|
*old_level = level;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_insert((k, level));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
ret
|
||||||
|
}
|
||||||
11
src/main.rs
Normal file
11
src/main.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
mod app;
|
||||||
|
mod ei;
|
||||||
|
mod keyboard;
|
||||||
|
mod portal;
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
app::run().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
40
src/portal.rs
Normal file
40
src/portal.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use std::os::fd::OwnedFd;
|
||||||
|
|
||||||
|
use ashpd::{
|
||||||
|
desktop::{
|
||||||
|
remote_desktop::{DeviceType, RemoteDesktop},
|
||||||
|
PersistMode, Session,
|
||||||
|
},
|
||||||
|
WindowIdentifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Portal<'a> {
|
||||||
|
remote: RemoteDesktop<'a>,
|
||||||
|
session: Session<'a, RemoteDesktop<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Portal<'a> {
|
||||||
|
pub async fn new_session() -> anyhow::Result<Self> {
|
||||||
|
log::info!("Connect to portal");
|
||||||
|
let remote = RemoteDesktop::new().await?;
|
||||||
|
let session = remote.create_session().await?;
|
||||||
|
remote
|
||||||
|
.select_devices(
|
||||||
|
&session,
|
||||||
|
DeviceType::Keyboard.into(),
|
||||||
|
None,
|
||||||
|
PersistMode::ExplicitlyRevoked,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self { remote, session })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_to_eis(&self) -> anyhow::Result<OwnedFd> {
|
||||||
|
self.remote
|
||||||
|
.start(&self.session, &WindowIdentifier::None)
|
||||||
|
.await?;
|
||||||
|
let fd = self.remote.connect_to_eis(&self.session).await?;
|
||||||
|
Ok(fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user