use std::collections::HashMap;
use std::io::ErrorKind;
use std::os::unix::net::UnixStream;
use std::time::{Duration, Instant};
use anyhow::{Result, anyhow, bail};
use pixelactions_core::flow::Axis;
use pixelactions_core::stream::{Placement, Region};
use reis::{Interface, ei};
use xkbcommon::xkb;
const BTN_LEFT: u32 = 0x110;
const XKB_TO_EVDEV: u32 = 8;
const SCROLL_DETENT: i32 = 120;
const SETTLE: Duration = Duration::from_secs(10);
const KEY_HOLD: Duration = Duration::from_millis(25);
pub struct Sender {
context: ei::Context,
devices: Vec<Slot>,
keymap: Option<xkb::Keymap>,
seat_capabilities: HashMap<String, u64>,
serial: u32,
sequence: u32,
pending_mapping_id: Option<String>,
}
struct Slot {
device: ei::Device,
interfaces: HashMap<String, reis::Object>,
regions: Vec<Region>,
resumed: bool,
emulating: bool,
}
impl Slot {
fn interface<T: reis::Interface>(&self) -> Option<T> {
self.interfaces.get(T::NAME)?.clone().downcast()
}
fn has<T: reis::Interface>(&self) -> bool {
self.interfaces.contains_key(T::NAME)
}
}
impl Sender {
pub fn connect(socket: UnixStream) -> Result<Self> {
socket
.set_nonblocking(true)
.map_err(|e| anyhow!("cannot configure the EIS socket: {e}"))?;
let context =
ei::Context::new(socket).map_err(|e| anyhow!("cannot open the EIS connection: {e}"))?;
let handshake = reis::handshake::ei_handshake_blocking(
&context,
"pixelactions",
ei::handshake::ContextType::Sender,
)
.map_err(|e| anyhow!("the EIS handshake failed: {e}"))?;
let mut sender = Self {
context,
devices: Vec::new(),
keymap: None,
seat_capabilities: HashMap::new(),
serial: handshake.serial,
sequence: 0,
pending_mapping_id: None,
};
sender.settle()?;
Ok(sender)
}
pub fn regions(&self) -> &[Region] {
self.pointer_slot()
.map_or(&[], |slot| slot.regions.as_slice())
}
pub fn can_type(&self) -> bool {
self.keymap.is_some() && self.keyboard().is_some()
}
fn pointer_slot(&self) -> Option<&Slot> {
self.devices
.iter()
.find(|slot| slot.has::<ei::PointerAbsolute>())
}
fn keyboard(&self) -> Option<(ei::Keyboard, &Slot)> {
let slot = self
.devices
.iter()
.find(|slot| slot.has::<ei::Keyboard>())?;
Some((slot.interface::<ei::Keyboard>()?, slot))
}
fn pump(&mut self, budget: Duration) -> Result<()> {
let deadline = Instant::now() + budget;
loop {
match self.context.read() {
Ok(_) => {}
Err(error) if error.kind() == ErrorKind::WouldBlock => {}
Err(error) => bail!("the EIS connection broke: {error}"),
}
while let Some(pending) = self.context.pending_event() {
match pending {
reis::PendingRequestResult::Request(event) => self.dispatch(event),
reis::PendingRequestResult::ParseError(error) => {
bail!("the compositor sent an EI message this build cannot read: {error}")
}
reis::PendingRequestResult::InvalidObject(object) => {
bail!("the compositor referred to an unknown EI object {object}")
}
}
}
let _ = self.context.flush();
if Instant::now() >= deadline {
return Ok(());
}
std::thread::sleep(Duration::from_millis(5));
}
}
fn dispatch(&mut self, event: ei::Event) {
match event {
ei::Event::Connection(_, event) => Self::on_connection(event),
ei::Event::Seat(seat, event) => self.on_seat(&seat, event),
ei::Event::Device(device, event) => self.on_device(&device, event),
ei::Event::Keyboard(_, ei::keyboard::Event::Keymap { size, keymap, .. }) => {
self.on_keymap(size, keymap);
}
_ => {}
}
}
fn on_connection(event: ei::connection::Event) {
if let ei::connection::Event::Ping { ping } = event {
ping.done(0);
}
}
fn on_seat(&mut self, seat: &ei::Seat, event: ei::seat::Event) {
match event {
ei::seat::Event::Capability { mask, interface } => {
self.seat_capabilities.insert(interface, mask);
}
ei::seat::Event::Done => {
let mask = [
ei::PointerAbsolute::NAME,
ei::Button::NAME,
ei::Scroll::NAME,
ei::Keyboard::NAME,
]
.iter()
.filter_map(|name| self.seat_capabilities.get(*name))
.fold(0u64, |all, one| all | one);
if mask != 0 {
seat.bind(mask);
}
}
_ => {}
}
}
fn on_device(&mut self, device: &ei::Device, event: ei::device::Event) {
let known = self.devices.iter().position(|slot| &slot.device == device);
let index = known.unwrap_or_else(|| {
self.devices.push(Slot {
device: device.clone(),
interfaces: HashMap::new(),
regions: Vec::new(),
resumed: false,
emulating: false,
});
self.devices.len() - 1
});
match event {
ei::device::Event::Interface { object } => {
self.devices[index]
.interfaces
.insert(object.interface().to_owned(), object);
}
ei::device::Event::RegionMappingId { mapping_id } => {
self.pending_mapping_id = Some(mapping_id);
}
ei::device::Event::Region {
offset_x,
offset_y,
width,
hight,
scale,
} => {
self.devices[index].regions.push(Region {
offset_x: offset_x as i32,
offset_y: offset_y as i32,
width: width as i32,
height: hight as i32,
scale: f64::from(scale),
mapping_id: self.pending_mapping_id.take(),
});
}
ei::device::Event::Resumed { serial } => {
self.serial = serial;
self.devices[index].resumed = true;
}
ei::device::Event::Paused { serial } => {
self.serial = serial;
self.devices[index].resumed = false;
self.devices[index].emulating = false;
}
ei::device::Event::Destroyed { serial } => {
self.serial = serial;
self.devices.remove(index);
}
_ => {}
}
}
fn on_keymap(&mut self, size: u32, keymap: std::os::fd::OwnedFd) {
let context = xkb::Context::new(0);
let loaded = unsafe {
xkb::Keymap::new_from_fd(
&context,
keymap,
size as usize,
xkb::KEYMAP_FORMAT_TEXT_V1,
xkb::KEYMAP_COMPILE_NO_FLAGS,
)
};
if let Ok(Some(keymap)) = loaded {
self.keymap = Some(keymap);
}
}
fn settle(&mut self) -> Result<()> {
let deadline = Instant::now() + SETTLE;
while Instant::now() < deadline {
self.pump(Duration::from_millis(100))?;
let pointer_ready = self
.pointer_slot()
.is_some_and(|slot| slot.resumed && !slot.regions.is_empty());
let keyboard_promised = self.seat_capabilities.contains_key(ei::Keyboard::NAME);
let keyboard_settled = !keyboard_promised || self.can_type();
if pointer_ready && keyboard_settled {
return Ok(());
}
}
if self
.pointer_slot()
.is_some_and(|slot| slot.resumed && !slot.regions.is_empty())
{
return Ok(());
}
let Some(slot) = self.pointer_slot() else {
bail!(
"the compositor granted a session but never offered a pointer that can be \
placed at a coordinate. On GNOME and KDE this is what \
org.freedesktop.portal.RemoteDesktop provides; other compositors do not \
implement it yet"
);
};
if slot.regions.is_empty() {
bail!(
"the pointer the compositor offered has no region, so there is no coordinate \
space to aim in. This is what the linked screen share provides — re-run and \
share a screen rather than cancelling that part"
);
}
bail!("the compositor never allowed the pointer to emit (it stayed paused)")
}
fn begin(&mut self, index: usize) -> Result<()> {
if !self.devices[index].resumed {
bail!(
"the compositor has paused input emulation, which it does when something \
else takes over the pointer. Nothing was sent"
);
}
if self.devices[index].emulating {
return Ok(());
}
let serial = self.serial;
let sequence = self.sequence;
self.sequence += 1;
self.devices[index].device.start_emulating(serial, sequence);
self.devices[index].emulating = true;
Ok(())
}
fn frame(&mut self, index: usize) -> Result<()> {
let serial = self.serial;
self.devices[index].device.frame(serial, now_micros());
self.context
.flush()
.map_err(|e| anyhow!("cannot send to the compositor: {e}"))
}
fn pointer_index(&self) -> Result<usize> {
self.devices
.iter()
.position(Slot::has::<ei::PointerAbsolute>)
.ok_or_else(|| anyhow!("the pointer device went away mid-run"))
}
fn keyboard_index(&self) -> Result<usize> {
self.devices
.iter()
.position(Slot::has::<ei::Keyboard>)
.ok_or_else(|| anyhow!("the compositor offered no keyboard, so this step cannot run"))
}
pub fn move_to(&mut self, placement: Placement) -> Result<()> {
let index = self.pointer_index()?;
self.begin(index)?;
let absolute = self.devices[index]
.interface::<ei::PointerAbsolute>()
.ok_or_else(|| anyhow!("the pointer device lost its absolute-motion interface"))?;
absolute.motion_absolute(placement.x as f32, placement.y as f32);
self.frame(index)
}
pub fn button(&mut self, press: bool) -> Result<()> {
let index = self.pointer_index()?;
self.begin(index)?;
let button = self.devices[index]
.interface::<ei::Button>()
.ok_or_else(|| anyhow!("the compositor offered no mouse buttons"))?;
let state = if press {
ei::button::ButtonState::Press
} else {
ei::button::ButtonState::Released
};
button.button(BTN_LEFT, state);
self.frame(index)
}
pub fn scroll(&mut self, clicks: i32, axis: Axis) -> Result<()> {
let index = self.pointer_index()?;
self.begin(index)?;
let scroll = self.devices[index]
.interface::<ei::Scroll>()
.ok_or_else(|| anyhow!("the compositor offered no scroll capability"))?;
let amount = clicks.saturating_mul(SCROLL_DETENT);
match axis {
Axis::Vertical => scroll.scroll_discrete(0, amount),
Axis::Horizontal => scroll.scroll_discrete(amount, 0),
}
self.frame(index)
}
fn tap(&mut self, keycode: u32, modifiers: &[u32]) -> Result<()> {
let index = self.keyboard_index()?;
self.begin(index)?;
let keyboard = self.devices[index]
.interface::<ei::Keyboard>()
.ok_or_else(|| anyhow!("the keyboard device lost its interface"))?;
for modifier in modifiers {
keyboard.key(*modifier, ei::keyboard::KeyState::Press);
}
keyboard.key(keycode, ei::keyboard::KeyState::Press);
self.frame(index)?;
std::thread::sleep(KEY_HOLD);
let keyboard = self.devices[index]
.interface::<ei::Keyboard>()
.ok_or_else(|| anyhow!("the keyboard device lost its interface"))?;
keyboard.key(keycode, ei::keyboard::KeyState::Released);
for modifier in modifiers.iter().rev() {
keyboard.key(*modifier, ei::keyboard::KeyState::Released);
}
self.frame(index)
}
pub fn text(&mut self, text: &str) -> Result<()> {
let keymap = self
.keymap
.clone()
.ok_or_else(|| anyhow!("the compositor sent no keymap, so text cannot be typed"))?;
for character in text.chars() {
let Some((keycode, level)) = key_for_char(&keymap, character) else {
bail!(
"the active keyboard layout has no key for {character:?}, and Wayland \
gives no way to remap one for a moment the way X11 does. Type it with \
a layout that has it, or paste instead"
);
};
let modifiers = modifiers_for_level(&keymap, level).ok_or_else(|| {
anyhow!(
"{character:?} needs modifier level {level} on this layout, which this \
build cannot reproduce"
)
})?;
self.tap(keycode, &modifiers)?;
}
Ok(())
}
pub fn chord(&mut self, chord: &str) -> Result<()> {
let keymap = self
.keymap
.clone()
.ok_or_else(|| anyhow!("the compositor sent no keymap, so chords cannot be sent"))?;
let (modifier_names, key) = pixelactions_core::chord::split(chord)?;
let mut modifiers = Vec::with_capacity(modifier_names.len());
for name in &modifier_names {
let keysym = keysym_for_token(name)
.ok_or_else(|| anyhow!("{name:?} is not a modifier this build knows"))?;
modifiers.push(keycode_for_keysym(&keymap, keysym).ok_or_else(|| {
anyhow!("the active layout has no {name:?} key to hold for {chord:?}")
})?);
}
let keysym = keysym_for_token(key)
.ok_or_else(|| anyhow!("{key:?} is not a key name this build knows"))?;
let keycode = keycode_for_keysym(&keymap, keysym)
.ok_or_else(|| anyhow!("the active layout has no {key:?} key for {chord:?}"))?;
self.tap(keycode, &modifiers)
}
}
impl Drop for Sender {
fn drop(&mut self) {
let serial = self.serial;
for slot in &mut self.devices {
if slot.emulating {
slot.device.stop_emulating(serial);
slot.emulating = false;
}
}
let _ = self.context.flush();
}
}
fn now_micros() -> u64 {
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
let start = START.get_or_init(Instant::now);
start.elapsed().as_micros() as u64
}
fn key_for_char(keymap: &xkb::Keymap, character: char) -> Option<(u32, u32)> {
let wanted = xkb::Keysym::from_char(character);
let layout = 0;
for raw in keymap.min_keycode().raw()..=keymap.max_keycode().raw() {
let keycode = xkb::Keycode::new(raw);
for level in 0..keymap.num_levels_for_key(keycode, layout) {
if keymap
.key_get_syms_by_level(keycode, layout, level)
.contains(&wanted)
{
return Some((raw - XKB_TO_EVDEV, level));
}
}
}
None
}
fn keycode_for_keysym(keymap: &xkb::Keymap, keysym: xkb::Keysym) -> Option<u32> {
let layout = 0;
for raw in keymap.min_keycode().raw()..=keymap.max_keycode().raw() {
let keycode = xkb::Keycode::new(raw);
for level in 0..keymap.num_levels_for_key(keycode, layout) {
if keymap
.key_get_syms_by_level(keycode, layout, level)
.contains(&keysym)
{
return Some(raw - XKB_TO_EVDEV);
}
}
}
None
}
fn modifiers_for_level(keymap: &xkb::Keymap, level: u32) -> Option<Vec<u32>> {
let shift = || keycode_for_keysym(keymap, xkb::keysyms::KEY_Shift_L.into());
let altgr = || keycode_for_keysym(keymap, xkb::keysyms::KEY_ISO_Level3_Shift.into());
match level {
0 => Some(Vec::new()),
1 => Some(vec![shift()?]),
2 => Some(vec![altgr()?]),
3 => Some(vec![shift()?, altgr()?]),
_ => None,
}
}
fn keysym_for_token(token: &str) -> Option<xkb::Keysym> {
let keysym = match token.to_ascii_lowercase().as_str() {
"cmd" | "command" | "meta" | "super" => xkb::keysyms::KEY_Super_L,
"ctrl" | "control" => xkb::keysyms::KEY_Control_L,
"alt" | "option" | "opt" => xkb::keysyms::KEY_Alt_L,
"shift" => xkb::keysyms::KEY_Shift_L,
"tab" => xkb::keysyms::KEY_Tab,
"enter" | "return" => xkb::keysyms::KEY_Return,
"esc" | "escape" => xkb::keysyms::KEY_Escape,
"space" => xkb::keysyms::KEY_space,
"backspace" | "delete" => xkb::keysyms::KEY_BackSpace,
"up" => xkb::keysyms::KEY_Up,
"down" => xkb::keysyms::KEY_Down,
"left" => xkb::keysyms::KEY_Left,
"right" => xkb::keysyms::KEY_Right,
other => {
let mut characters = other.chars();
let first = characters.next()?;
if characters.next().is_some() {
return None;
}
return Some(xkb::Keysym::from_char(first));
}
};
Some(keysym.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modifier_names_map_the_way_a_human_writes_them() {
for name in ["cmd", "command", "meta", "super", "SUPER"] {
assert_eq!(
keysym_for_token(name),
Some(xkb::keysyms::KEY_Super_L.into()),
"{name}"
);
}
for name in ["ctrl", "control"] {
assert_eq!(
keysym_for_token(name),
Some(xkb::keysyms::KEY_Control_L.into())
);
}
for name in ["alt", "option", "opt"] {
assert_eq!(keysym_for_token(name), Some(xkb::keysyms::KEY_Alt_L.into()));
}
}
#[test]
fn named_keys_resolve_and_single_characters_fall_through() {
assert_eq!(
keysym_for_token("enter"),
Some(xkb::keysyms::KEY_Return.into())
);
assert_eq!(
keysym_for_token("esc"),
Some(xkb::keysyms::KEY_Escape.into())
);
assert_eq!(
keysym_for_token("space"),
Some(xkb::keysyms::KEY_space.into())
);
assert_eq!(keysym_for_token("s"), Some(xkb::Keysym::from_char('s')));
assert_eq!(keysym_for_token("7"), Some(xkb::Keysym::from_char('7')));
}
#[test]
fn an_unknown_multi_character_key_is_refused() {
assert_eq!(keysym_for_token("fnord"), None);
assert_eq!(keysym_for_token(""), None);
}
#[test]
fn levels_beyond_altgr_are_refused_rather_than_approximated() {
let keymap = compiled_us_keymap();
assert!(modifiers_for_level(&keymap, 4).is_none());
assert!(modifiers_for_level(&keymap, 99).is_none());
assert_eq!(modifiers_for_level(&keymap, 0), Some(Vec::new()));
}
#[test]
fn characters_resolve_to_keys_on_a_real_keymap() {
let keymap = compiled_us_keymap();
let (a_code, a_level) = key_for_char(&keymap, 'a').expect("'a' is on a US layout");
assert_eq!(a_level, 0, "lowercase needs no modifier");
assert_eq!(a_code, 30);
let (upper_code, upper_level) = key_for_char(&keymap, 'A').expect("'A' is shift+a");
assert_eq!(upper_code, a_code, "same key, different level");
assert_eq!(upper_level, 1);
assert_eq!(
modifiers_for_level(&keymap, upper_level).expect("shift resolves"),
vec![keycode_for_keysym(&keymap, xkb::keysyms::KEY_Shift_L.into()).expect("shift")]
);
}
#[test]
fn a_character_no_layout_has_is_refused_not_approximated() {
let keymap = compiled_us_keymap();
assert!(key_for_char(&keymap, '☃').is_none());
}
#[test]
fn named_chord_keys_exist_on_a_real_keymap() {
let keymap = compiled_us_keymap();
for name in [
"ctrl", "shift", "alt", "super", "enter", "esc", "tab", "space",
] {
let keysym = keysym_for_token(name).expect(name);
assert!(
keycode_for_keysym(&keymap, keysym).is_some(),
"no keycode for {name}"
);
}
}
fn compiled_us_keymap() -> xkb::Keymap {
let context = xkb::Context::new(0);
xkb::Keymap::new_from_names(&context, "", "", "us", "", None, xkb::COMPILE_NO_FLAGS)
.expect("xkbcommon can compile a US layout")
}
}