use smallvec::SmallVec;
use wayland_backend::protocol::{Argument, Message};
use wayland_backend::server::ObjectId as SId;
use super::proxy::Conn;
const PTR_ENTER: u16 = 0;
const PTR_LEAVE: u16 = 1;
const PTR_MOTION: u16 = 2;
const PTR_BUTTON: u16 = 3;
const PTR_AXIS: u16 = 4;
const PTR_FRAME: u16 = 5;
const PTR_AXIS_DISCRETE: u16 = 8;
const KBD_ENTER: u16 = 1;
const KBD_LEAVE: u16 = 2;
const KBD_KEY: u16 = 3;
pub const BTN_LEFT: u32 = 0x110;
const KEY_RELEASED: u32 = 0;
const KEY_PRESSED: u32 = 1;
fn to_fixed(v: f64) -> i32 {
(v * 256.0).round() as i32
}
fn now_ms() -> u32 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u32)
.unwrap_or(0)
}
fn send(
conn: &Conn,
sender: &SId,
opcode: u16,
args: SmallVec<[Argument<SId, i32>; 4]>,
) -> Result<(), String> {
let msg = Message {
sender_id: sender.clone(),
opcode,
args,
};
conn.server_handle
.send_event(msg)
.map_err(|error| format!("could not deliver input event opcode {opcode}: {error}"))
}
fn next_serial(conn: &mut Conn) -> u32 {
let s = conn.next_serial;
conn.next_serial = conn.next_serial.wrapping_add(1);
s
}
fn target_surface(conn: &Conn) -> Option<SId> {
if let Some(s) = conn.focused_surface.clone() {
return Some(s);
}
conn.surfaces
.iter()
.max_by_key(|(_, st)| st.commit_count)
.map(|(id, _)| id.clone())
}
fn ensure_pointer_focus(conn: &mut Conn, surface: &SId, x: f64, y: f64) -> Result<(), String> {
let pointer = conn
.pointer_obj
.clone()
.ok_or_else(|| "the app has not created a wl_pointer yet".to_string())?;
if conn.pointer_focus.as_ref() == Some(surface) {
return Ok(());
}
if let Some(prev) = conn.pointer_focus.clone() {
let serial = next_serial(conn);
let mut args = SmallVec::new();
args.push(Argument::Uint(serial));
args.push(Argument::Object(prev));
send(conn, &pointer, PTR_LEAVE, args)?;
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
}
let serial = next_serial(conn);
let mut args = SmallVec::new();
args.push(Argument::Uint(serial));
args.push(Argument::Object(surface.clone()));
args.push(Argument::Fixed(to_fixed(x)));
args.push(Argument::Fixed(to_fixed(y)));
send(conn, &pointer, PTR_ENTER, args)?;
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
conn.pointer_focus = Some(surface.clone());
Ok(())
}
fn ensure_keyboard_focus(conn: &mut Conn, surface: &SId) -> Result<(), String> {
let keyboard = conn
.keyboard_obj
.clone()
.ok_or_else(|| "the app has not created a wl_keyboard yet".to_string())?;
if conn.keyboard_focus.as_ref() == Some(surface) {
return Ok(());
}
if let Some(previous) = conn.keyboard_focus.clone() {
let mut leave = SmallVec::new();
leave.push(Argument::Uint(next_serial(conn)));
leave.push(Argument::Object(previous));
send(conn, &keyboard, KBD_LEAVE, leave)?;
}
let mut enter = SmallVec::new();
enter.push(Argument::Uint(next_serial(conn)));
enter.push(Argument::Object(surface.clone()));
enter.push(Argument::Array(Box::<Vec<u8>>::default()));
send(conn, &keyboard, KBD_ENTER, enter)?;
conn.keyboard_focus = Some(surface.clone());
Ok(())
}
pub(crate) fn inject_click(
conn: &mut Conn,
x: f64,
y: f64,
button: u32,
count: u32,
) -> Result<(), String> {
let surface =
target_surface(conn).ok_or_else(|| "the app has no surface to click in yet".to_string())?;
let pointer = conn
.pointer_obj
.clone()
.ok_or_else(|| "the app has not created a wl_pointer yet".to_string())?;
ensure_pointer_focus(conn, &surface, x, y)?;
conn.focused_surface = Some(surface.clone());
let mut motion = SmallVec::new();
motion.push(Argument::Uint(now_ms()));
motion.push(Argument::Fixed(to_fixed(x)));
motion.push(Argument::Fixed(to_fixed(y)));
send(conn, &pointer, PTR_MOTION, motion)?;
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
for _ in 0..count.max(1) {
let serial = next_serial(conn);
let t = now_ms();
let mut press = SmallVec::new();
press.push(Argument::Uint(serial));
press.push(Argument::Uint(t));
press.push(Argument::Uint(button));
press.push(Argument::Uint(KEY_PRESSED));
send(conn, &pointer, PTR_BUTTON, press)?;
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
std::thread::sleep(std::time::Duration::from_millis(45));
let serial = next_serial(conn);
let t = now_ms();
let mut release = SmallVec::new();
release.push(Argument::Uint(serial));
release.push(Argument::Uint(t));
release.push(Argument::Uint(button));
release.push(Argument::Uint(KEY_RELEASED));
send(conn, &pointer, PTR_BUTTON, release)?;
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
std::thread::sleep(std::time::Duration::from_millis(30));
}
Ok(())
}
pub(crate) fn inject_click_with_modifiers(
conn: &mut Conn,
x: f64,
y: f64,
button: u32,
count: u32,
modifiers: Option<&str>,
) -> Result<(), String> {
let held = press_named_modifiers(conn, modifiers)?;
let click_result = inject_click(conn, x, y, button, count);
let release_result = release_named_modifiers(conn, &held);
click_result.and(release_result)
}
pub(crate) fn inject_scroll(
conn: &mut Conn,
x: f64,
y: f64,
discrete_x: i32,
discrete_y: i32,
) -> Result<(), String> {
let surface = target_surface(conn)
.ok_or_else(|| "the app has no surface to scroll in yet".to_string())?;
let pointer = conn
.pointer_obj
.clone()
.ok_or_else(|| "the app has not created a wl_pointer yet".to_string())?;
ensure_pointer_focus(conn, &surface, x, y)?;
let t = now_ms();
if discrete_y != 0 {
let mut axis = SmallVec::new();
axis.push(Argument::Uint(t));
axis.push(Argument::Uint(0)); axis.push(Argument::Fixed(to_fixed(f64::from(discrete_y) * 10.0)));
send(conn, &pointer, PTR_AXIS, axis)?;
let mut disc = SmallVec::new();
disc.push(Argument::Uint(0)); disc.push(Argument::Int(discrete_y));
send(conn, &pointer, PTR_AXIS_DISCRETE, disc)?;
}
if discrete_x != 0 {
let mut axis = SmallVec::new();
axis.push(Argument::Uint(t));
axis.push(Argument::Uint(1)); axis.push(Argument::Fixed(to_fixed(f64::from(discrete_x) * 10.0)));
send(conn, &pointer, PTR_AXIS, axis)?;
let mut disc = SmallVec::new();
disc.push(Argument::Uint(1)); disc.push(Argument::Int(discrete_x));
send(conn, &pointer, PTR_AXIS_DISCRETE, disc)?;
}
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
Ok(())
}
pub(crate) fn inject_drag(
conn: &mut Conn,
from_x: f64,
from_y: f64,
to_x: f64,
to_y: f64,
) -> Result<(), String> {
let surface =
target_surface(conn).ok_or_else(|| "the app has no surface to drag in yet".to_string())?;
let pointer = conn
.pointer_obj
.clone()
.ok_or_else(|| "the app has not created a wl_pointer yet".to_string())?;
ensure_pointer_focus(conn, &surface, from_x, from_y)?;
let mut start = SmallVec::new();
start.push(Argument::Uint(now_ms()));
start.push(Argument::Fixed(to_fixed(from_x)));
start.push(Argument::Fixed(to_fixed(from_y)));
send(conn, &pointer, PTR_MOTION, start)?;
let mut press = SmallVec::new();
press.push(Argument::Uint(next_serial(conn)));
press.push(Argument::Uint(now_ms()));
press.push(Argument::Uint(BTN_LEFT));
press.push(Argument::Uint(KEY_PRESSED));
send(conn, &pointer, PTR_BUTTON, press)?;
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
const STEPS: i32 = 24;
for step in 1..=STEPS {
let t = f64::from(step) / f64::from(STEPS);
let mut motion = SmallVec::new();
motion.push(Argument::Uint(now_ms()));
motion.push(Argument::Fixed(to_fixed(from_x + (to_x - from_x) * t)));
motion.push(Argument::Fixed(to_fixed(from_y + (to_y - from_y) * t)));
send(conn, &pointer, PTR_MOTION, motion)?;
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
std::thread::sleep(std::time::Duration::from_millis(8));
}
let mut release = SmallVec::new();
release.push(Argument::Uint(next_serial(conn)));
release.push(Argument::Uint(now_ms()));
release.push(Argument::Uint(BTN_LEFT));
release.push(Argument::Uint(KEY_RELEASED));
send(conn, &pointer, PTR_BUTTON, release)?;
send(conn, &pointer, PTR_FRAME, SmallVec::new())?;
Ok(())
}
pub(crate) fn inject_key_raw(conn: &mut Conn, keycode: u32, pressed: bool) -> Result<(), String> {
let keyboard = conn
.keyboard_obj
.clone()
.ok_or_else(|| "the app has not created a wl_keyboard yet".to_string())?;
let surface = target_surface(conn)
.ok_or_else(|| "the app has no surface to receive keyboard input".to_string())?;
ensure_keyboard_focus(conn, &surface)?;
let serial = next_serial(conn);
let evdev_keycode = keycode.saturating_sub(8);
let mut args = SmallVec::new();
args.push(Argument::Uint(serial));
args.push(Argument::Uint(now_ms()));
args.push(Argument::Uint(evdev_keycode));
args.push(Argument::Uint(if pressed {
KEY_PRESSED
} else {
KEY_RELEASED
}));
send(conn, &keyboard, KBD_KEY, args)
}
fn send_modifiers(conn: &mut Conn, mods_depressed: u32) -> Result<(), String> {
let keyboard = conn
.keyboard_obj
.clone()
.ok_or_else(|| "the app has not created a wl_keyboard yet".to_string())?;
let serial = next_serial(conn);
let mut args = SmallVec::new();
args.push(Argument::Uint(serial));
args.push(Argument::Uint(mods_depressed)); args.push(Argument::Uint(0)); args.push(Argument::Uint(0)); args.push(Argument::Uint(0)); send(conn, &keyboard, 4 , args)
}
#[derive(Clone, Copy)]
struct HeldModifier {
keycode: u32,
mask: u32,
}
fn modifier_definition(name: &str) -> Option<(&'static str, &'static str)> {
match name.trim().to_ascii_lowercase().as_str() {
"shift" => Some(("shift", "Shift")),
"ctrl" | "control" => Some(("ctrl", "Control")),
"alt" | "option" => Some(("alt", "Mod1")),
"super" | "cmd" | "command" | "meta" => Some(("super", "Mod4")),
_ => None,
}
}
fn modifier_mask(keymap: &xkbcommon::xkb::Keymap, xkb_name: &str) -> Result<u32, String> {
let index = keymap.mod_get_index(&xkb_name);
if index == u32::MAX {
return Err(format!("keyboard layout has no '{xkb_name}' modifier"));
}
1u32.checked_shl(index)
.ok_or_else(|| format!("invalid modifier index {index} for '{xkb_name}'"))
}
fn parse_keymap(keymap_text: &str) -> Result<xkbcommon::xkb::Keymap, String> {
let context = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
xkbcommon::xkb::Keymap::new_from_string(
&context,
keymap_text.to_string(),
xkbcommon::xkb::KEYMAP_FORMAT_TEXT_V1,
xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
)
.ok_or_else(|| "failed to parse the app's keymap".to_string())
}
fn press_named_modifiers(
conn: &mut Conn,
modifiers: Option<&str>,
) -> Result<Vec<HeldModifier>, String> {
let names: Vec<&str> = modifiers
.unwrap_or_default()
.split('+')
.map(str::trim)
.filter(|name| !name.is_empty())
.collect();
if names.is_empty() {
return Ok(Vec::new());
}
let keymap_text = read_keymap_text(conn)?;
let keymap = parse_keymap(&keymap_text)?;
let mut held = Vec::new();
let mut depressed = 0u32;
for name in names {
let Some((key_name, xkb_name)) = modifier_definition(name) else {
let _ = release_named_modifiers(conn, &held);
return Err(format!("unknown modifier '{name}'"));
};
let keysym = crate::keymap::keysym_for_key(key_name)
.ok_or_else(|| format!("unknown modifier key '{name}'"))?;
let Some(keycode) = find_keycode_for_keysym(&keymap_text, keysym)? else {
let _ = release_named_modifiers(conn, &held);
return Err(format!("keyboard layout has no keycode for '{name}'"));
};
let mask = modifier_mask(&keymap, xkb_name)?;
inject_key_raw(conn, keycode, true)?;
depressed |= mask;
send_modifiers(conn, depressed)?;
held.push(HeldModifier { keycode, mask });
}
Ok(held)
}
fn release_named_modifiers(conn: &mut Conn, held: &[HeldModifier]) -> Result<(), String> {
let mut depressed = held.iter().fold(0u32, |mask, item| mask | item.mask);
for item in held.iter().rev() {
inject_key_raw(conn, item.keycode, false)?;
depressed &= !item.mask;
send_modifiers(conn, depressed)?;
}
Ok(())
}
pub(crate) fn inject_key_combo(conn: &mut Conn, combination: &str) -> Result<(), String> {
let parts: Vec<&str> = combination
.split('+')
.map(str::trim)
.filter(|part| !part.is_empty())
.collect();
let Some((last, modifiers)) = parts.split_last() else {
return Err("empty key".to_string());
};
let modifier_text = modifiers.join("+");
let held = press_named_modifiers(conn, Some(&modifier_text))?;
let keymap_text = read_keymap_text(conn)?;
let key_result = (|| {
let keysym =
crate::keymap::keysym_for_key(last).ok_or_else(|| format!("unknown key '{last}'"))?;
let keycode = find_keycode_for_keysym(&keymap_text, keysym)?
.ok_or_else(|| format!("keyboard layout has no keycode for '{last}'"))?;
inject_key_raw(conn, keycode, true)?;
std::thread::sleep(std::time::Duration::from_millis(20));
inject_key_raw(conn, keycode, false)
})();
let release_result = release_named_modifiers(conn, &held);
key_result.and(release_result)
}
const KEYSYM_SHIFT_L: u32 = 0xffe1;
const KEYSYM_ENTER: u32 = 0xff0d;
const KEYSYM_TAB: u32 = 0xff09;
const KEYSYM_BACKSPACE: u32 = 0xff08;
const KEYSYM_ESCAPE: u32 = 0xff1b;
fn read_keymap_text(conn: &Conn) -> Result<String, String> {
let fd = conn.keymap_fd.as_ref().ok_or_else(|| {
"no keymap captured yet (app has not received wl_keyboard.keymap)".to_string()
})?;
let size = conn.keymap_size as usize;
use std::os::fd::AsRawFd;
let mapped = unsafe {
libc::mmap(
std::ptr::null_mut(),
size,
libc::PROT_READ,
libc::MAP_SHARED,
fd.as_raw_fd(),
0,
)
};
if mapped == libc::MAP_FAILED {
return Err(format!(
"mmap of the keymap fd failed: {}",
std::io::Error::last_os_error()
));
}
let bytes = unsafe { std::slice::from_raw_parts(mapped as *const u8, size) };
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
let text = String::from_utf8_lossy(&bytes[..end]).into_owned();
unsafe {
libc::munmap(mapped, size);
}
Ok(text)
}
struct CharResolution {
keycode: u32,
needs_shift: bool,
}
struct TextResolution {
shift_keycode: Option<u32>,
shift_mask: u32,
resolutions: Vec<Option<CharResolution>>,
}
fn resolve_text(keymap_text: &str, text: &str) -> Result<TextResolution, String> {
let ctx = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
let keymap = xkbcommon::xkb::Keymap::new_from_string(
&ctx,
keymap_text.to_string(),
xkbcommon::xkb::KEYMAP_FORMAT_TEXT_V1,
xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
)
.ok_or_else(|| "failed to parse the app's keymap".to_string())?;
let min = keymap.min_keycode().raw();
let max = keymap.max_keycode().raw();
let shift_idx = keymap.mod_get_index(&"Shift");
let shift_mask = if shift_idx == u32::MAX {
1u32
} else {
1u32 << shift_idx
};
let mut shift_keycode: Option<u32> = None;
'find_shift: for kc in min..=max {
let key = xkbcommon::xkb::Keycode::from(kc);
for sym in keymap.key_get_syms_by_level(key, 0, 0) {
if sym.raw() == KEYSYM_SHIFT_L {
shift_keycode = Some(kc);
break 'find_shift;
}
}
}
let mut resolutions = Vec::new();
for ch in text.chars() {
let target = xkbcommon::xkb::utf32_to_keysym(ch as u32);
let mut found: Option<CharResolution> = None;
'search: for kc in min..=max {
let key = xkbcommon::xkb::Keycode::from(kc);
for sym in keymap.key_get_syms_by_level(key, 0, 0) {
if *sym == target {
found = Some(CharResolution {
keycode: kc,
needs_shift: false,
});
break 'search;
}
}
for sym in keymap.key_get_syms_by_level(key, 0, 1) {
if *sym == target {
found = Some(CharResolution {
keycode: kc,
needs_shift: true,
});
break 'search;
}
}
}
resolutions.push(found);
}
if std::env::var("AGENT_SEAT_DEBUG").is_ok() {
eprintln!(
"seat KEYMAP min={min} max={max} shift={shift_keycode:?} mask={shift_mask:#x} text={text:?}"
);
for (ch, res) in text.chars().zip(resolutions.iter()) {
eprintln!(
"seat RESOLVE '{ch}' -> {:?}",
res.as_ref().map(|r| (r.keycode, r.needs_shift))
);
}
}
Ok(TextResolution {
shift_keycode,
shift_mask,
resolutions,
})
}
pub(crate) fn inject_text(conn: &mut Conn, text: &str) -> Result<(), String> {
if conn.text_input_enabled {
if let Some(text_input) = conn.text_input_obj.clone() {
let committed = std::ffi::CString::new(text)
.map_err(|_| "text input cannot contain a NUL character".to_string())?;
let mut commit = SmallVec::new();
commit.push(Argument::Str(Some(Box::new(committed))));
send(conn, &text_input, 3 , commit)?;
let mut done = SmallVec::new();
done.push(Argument::Uint(conn.text_input_commit_serial));
send(conn, &text_input, 5 , done)?;
return Ok(());
}
}
let keymap_text = read_keymap_text(conn)?;
let mut plain = String::new();
enum Item {
Plain(String),
Special(u32),
}
let mut items: Vec<Item> = Vec::new();
for ch in text.chars() {
let special = match ch {
'\n' => Some(KEYSYM_ENTER),
'\t' => Some(KEYSYM_TAB),
'\u{08}' => Some(KEYSYM_BACKSPACE),
'\u{1b}' => Some(KEYSYM_ESCAPE),
_ => None,
};
if let Some(sym) = special {
if !plain.is_empty() {
items.push(Item::Plain(std::mem::take(&mut plain)));
}
items.push(Item::Special(sym));
} else {
plain.push(ch);
}
}
if !plain.is_empty() {
items.push(Item::Plain(plain));
}
for item in items {
match item {
Item::Special(sym) => {
let keycode = find_keycode_for_keysym(&keymap_text, sym)?
.ok_or_else(|| format!("no keycode for special keysym {sym:#x}"))?;
inject_key_raw(conn, keycode, true)?;
inject_key_raw(conn, keycode, false)?;
}
Item::Plain(run) => {
let resolution = resolve_text(&keymap_text, &run)?;
let shift_mask = resolution.shift_mask;
for (ch, res) in run.chars().zip(resolution.resolutions) {
let Some(res) = res else {
return Err(format!(
"character '{ch}' has no keycode on this keyboard layout"
));
};
if res.needs_shift {
let shift = resolution.shift_keycode.ok_or_else(|| {
"layout needs Shift but no Shift keycode was found".to_string()
})?;
inject_key_raw(conn, shift, true)?;
send_modifiers(conn, shift_mask)?;
std::thread::sleep(std::time::Duration::from_millis(20));
}
inject_key_raw(conn, res.keycode, true)?;
std::thread::sleep(std::time::Duration::from_millis(20));
inject_key_raw(conn, res.keycode, false)?;
if res.needs_shift {
let shift = resolution.shift_keycode.unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
inject_key_raw(conn, shift, false)?;
send_modifiers(conn, 0)?;
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
}
}
Ok(())
}
fn find_keycode_for_keysym(keymap_text: &str, keysym: u32) -> Result<Option<u32>, String> {
let keymap = parse_keymap(keymap_text)?;
let target = xkbcommon::xkb::Keysym::from(keysym);
let min = keymap.min_keycode().raw();
let max = keymap.max_keycode().raw();
for kc in min..=max {
let key = xkbcommon::xkb::Keycode::from(kc);
for sym in keymap.key_get_syms_by_level(key, 0, 0) {
if *sym == target {
return Ok(Some(kc));
}
}
}
Ok(None)
}