use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::driver::InputBackend;
use crate::driver::LinuxDriverSessionState;
use crate::error::{backend, invalid_input};
use crate::native::portal::{InputSession as PortalSession, PortalInput};
use auv_driver_common::KeyboardBackend;
use auv_driver_common::error::{DriverError, DriverResult};
use auv_driver_common::geometry::Point;
use auv_driver_common::input::{
Click, ClickModifiers, DisturbanceLevel, InputActionResult, InputAttempt, InputDeliveryPath, InputPolicy, KeyPressOptions,
PasteTextOptions, Scroll, TextSubmit, TypeTextOptions,
};
use crate::clipboard::{restore as restore_clipboard, set_text as set_clipboard_text, snapshot as snapshot_clipboard};
#[derive(Debug)]
pub(crate) enum InputSession {
Portal(PortalSession),
#[cfg(target_os = "linux")]
Uinput(crate::native::uinput::InputSession),
}
impl InputSession {
fn held_keys(&self, symbols: &[i32]) -> DriverResult<Vec<HeldKey>> {
match self {
Self::Portal(_) => Ok(symbols.iter().copied().map(HeldKey::Portal).collect()),
#[cfg(target_os = "linux")]
Self::Uinput(session) => {
let layout = crate::native::keymap::Keymap::load()?;
session.validate_keys(&layout, symbols)?;
let strokes = symbols.iter().map(|symbol| layout.stroke(*symbol)).collect::<DriverResult<Vec<_>>>()?;
let mut keys = strokes.iter().map(|stroke| HeldKey::Uinput(stroke.key)).collect::<Vec<_>>();
if strokes.iter().any(|stroke| stroke.shift) {
let shift = layout.stroke(keysym::SHIFT_L)?.key;
if !keys.contains(&HeldKey::Uinput(shift)) {
keys.insert(0, HeldKey::Uinput(shift));
}
}
Ok(keys)
}
}
}
fn key_transition(&mut self, key: &HeldKey, down: bool) -> DriverResult<()> {
match (self, key) {
(Self::Portal(session), HeldKey::Portal(symbol)) => session.key_transition(*symbol, down),
#[cfg(target_os = "linux")]
(Self::Uinput(session), HeldKey::Uinput(code)) => session.key_transition(*code, down),
#[cfg(target_os = "linux")]
_ => Err(backend("held keyboard backend changed")),
}
}
pub(crate) fn keyboard_layout(&self) -> DriverResult<KeyboardLayout> {
match self {
Self::Portal(_) => Ok(KeyboardLayout::Portal),
#[cfg(target_os = "linux")]
Self::Uinput(_) => crate::native::keymap::Keymap::load().map(KeyboardLayout::Uinput),
}
}
pub(crate) fn validate_keyboard(&self, layout: &KeyboardLayout, plan: &KeyboardPlan) -> DriverResult<()> {
match (self, layout) {
(Self::Portal(_), KeyboardLayout::Portal) => {
let _ = plan;
Ok(())
}
#[cfg(target_os = "linux")]
(Self::Uinput(session), KeyboardLayout::Uinput(layout)) => {
let keys = plan.steps.iter().flat_map(|(chord, _)| chord.modifiers.iter().copied().chain([chord.key])).collect::<Vec<_>>();
session.validate_keys(layout, &keys)
}
#[cfg(target_os = "linux")]
_ => Err(backend("keyboard backend changed after preparation")),
}
}
pub(crate) fn deliver_keyboard(&mut self, layout: &KeyboardLayout, plan: &KeyboardPlan) -> DriverResult<()> {
self.validate_keyboard(layout, plan)?;
for (chord, delay) in &plan.steps {
match (&mut *self, layout) {
(Self::Portal(session), KeyboardLayout::Portal) => session.key_chord(&chord.modifiers, chord.key)?,
#[cfg(target_os = "linux")]
(Self::Uinput(session), KeyboardLayout::Uinput(layout)) => session.key_chord(layout, &chord.modifiers, chord.key)?,
#[cfg(target_os = "linux")]
_ => return Err(backend("keyboard backend changed after preparation")),
}
sleep_if_nonzero(*delay);
}
Ok(())
}
fn button(&mut self, button: auv_driver_common::MouseButton, down: bool) -> DriverResult<()> {
match self {
Self::Portal(session) => session.button(button, down),
#[cfg(target_os = "linux")]
Self::Uinput(session) => session.button(button, down),
}
}
fn move_to(&mut self, point: Point) -> DriverResult<()> {
match self {
Self::Portal(session) => session.move_to(point),
#[cfg(target_os = "linux")]
Self::Uinput(session) => session.move_to(point),
}
}
fn click_at(&mut self, point: Point, button: auv_driver_common::MouseButton, click: Click, modifiers: &[i32]) -> DriverResult<()> {
match self {
Self::Portal(session) => session.click_at(point, button, click, modifiers),
#[cfg(target_os = "linux")]
Self::Uinput(session) => session.click_at(point, button, click, modifiers),
}
}
fn scroll_at(&mut self, point: Point, scroll: Scroll) -> DriverResult<()> {
match self {
Self::Portal(session) => session.scroll_at(point, scroll),
#[cfg(target_os = "linux")]
Self::Uinput(session) => session.scroll_at(point, scroll),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum HeldKey {
Portal(i32),
#[cfg(target_os = "linux")]
Uinput(evdev::KeyCode),
}
struct HeldKeyboardBackend {
session: Arc<Mutex<InputSession>>,
keys: Vec<HeldKey>,
}
impl KeyboardBackend for HeldKeyboardBackend {
fn key_count(&self) -> usize {
self.keys.len()
}
fn key(&self, index: usize, down: bool) -> DriverResult<()> {
self.session.lock().map_err(|_| backend("linux input session poisoned"))?.key_transition(&self.keys[index], down)
}
fn result(&self) -> InputActionResult {
keyboard_result()
}
}
pub(crate) fn held_keyboard_backend(state: &Arc<Mutex<LinuxDriverSessionState>>, symbols: &[i32]) -> DriverResult<Arc<dyn KeyboardBackend>> {
let session = input_session(state)?;
let keys = session.lock().map_err(|_| backend("linux input session poisoned"))?.held_keys(symbols)?;
Ok(Arc::new(HeldKeyboardBackend { session, keys }))
}
pub(crate) fn click_at(
state: &Arc<Mutex<LinuxDriverSessionState>>,
point: Point,
button: auv_driver_common::MouseButton,
click: Click,
modifiers: ClickModifiers,
) -> DriverResult<InputActionResult> {
let keys = click_modifier_keysyms(modifiers);
with_input_session(state, |session| session.click_at(point, button, click, &keys))?;
Ok(pointer_result())
}
fn click_modifier_keysyms(modifiers: ClickModifiers) -> Vec<i32> {
[
(modifiers.shift, keysym::SHIFT_L),
(modifiers.control, keysym::CONTROL_L),
(modifiers.alt, keysym::ALT_L),
(modifiers.meta, keysym::SUPER_L),
]
.into_iter()
.filter_map(|(enabled, key)| enabled.then_some(key))
.collect()
}
pub(crate) fn current_position() -> DriverResult<Point> {
Err(DriverError::unsupported("linux.input.current_position on Wayland"))
}
pub(crate) fn scroll_at(
state: &Arc<Mutex<LinuxDriverSessionState>>,
point: Point,
scroll: Scroll,
settle: Duration,
) -> DriverResult<InputActionResult> {
with_input_session(state, |session| session.scroll_at(point, scroll))?;
sleep_if_nonzero(settle);
Ok(pointer_result())
}
pub(crate) fn type_text(
state: &Arc<Mutex<LinuxDriverSessionState>>,
text: &str,
options: TypeTextOptions,
) -> DriverResult<InputActionResult> {
if matches!(options.policy, InputPolicy::BackgroundOnly) {
return Err(invalid_input("linux type_text cannot use background_only input policy"));
}
let plan = KeyboardPlan::type_text(text, options)?;
with_input_session(state, |session| {
let layout = session.keyboard_layout()?;
session.deliver_keyboard(&layout, &plan)
})?;
sleep_if_nonzero(options.settle);
Ok(keyboard_result())
}
pub(crate) fn press_key(state: &Arc<Mutex<LinuxDriverSessionState>>, options: KeyPressOptions) -> DriverResult<InputActionResult> {
let chord = parse_key_chord(&options.key)?;
let plan = KeyboardPlan {
steps: vec![(chord, Duration::ZERO)],
};
with_input_session(state, |session| {
let layout = session.keyboard_layout()?;
session.deliver_keyboard(&layout, &plan)
})?;
sleep_if_nonzero(options.settle);
Ok(keyboard_result())
}
pub(crate) fn copy(state: &Arc<Mutex<LinuxDriverSessionState>>) -> DriverResult<()> {
with_input_session(state, |session| {
let layout = session.keyboard_layout()?;
session.deliver_keyboard(&layout, &KeyboardPlan::chord(vec![keysym::CONTROL_L, 'c' as i32]))
})
}
pub(crate) fn paste(state: &Arc<Mutex<LinuxDriverSessionState>>) -> DriverResult<()> {
with_input_session(state, |session| {
let layout = session.keyboard_layout()?;
session.deliver_keyboard(&layout, &KeyboardPlan::chord(vec![keysym::CONTROL_L, 'v' as i32]))
})
}
pub(crate) fn paste_text(state: &Arc<Mutex<LinuxDriverSessionState>>, options: PasteTextOptions) -> DriverResult<InputActionResult> {
let plan = KeyboardPlan::paste_text(&options);
let layout = with_input_session(state, |session| {
let layout = session.keyboard_layout()?;
session.validate_keyboard(&layout, &plan)?;
Ok(layout)
})?;
paste_prepared(state, options, &layout, &plan)
}
pub(crate) fn paste_prepared(
state: &Arc<Mutex<LinuxDriverSessionState>>,
options: PasteTextOptions,
layout: &KeyboardLayout,
plan: &KeyboardPlan,
) -> DriverResult<InputActionResult> {
let snapshot = snapshot_clipboard(state)?;
let result = (|| {
set_clipboard_text(state, &options.text)?;
with_input_session(state, |session| session.deliver_keyboard(layout, plan))?;
sleep_if_nonzero(options.settle);
Ok(())
})();
let restore_result = restore_clipboard(state, &snapshot);
match (result, restore_result) {
(Ok(()), Ok(())) => Ok(InputActionResult {
selected_path: InputDeliveryPath::ClipboardPaste,
attempts: vec![InputAttempt::success(InputDeliveryPath::ClipboardPaste)],
verified: false,
mouse_disturbance: DisturbanceLevel::None,
focus_disturbance: DisturbanceLevel::Unknown,
clipboard_disturbance: DisturbanceLevel::Temporary,
}),
(Err(action_error), Ok(())) => Err(action_error),
(Ok(()), Err(restore_error)) => Err(crate::error::backend(format!("pasted text but failed to restore clipboard: {restore_error}"))),
(Err(action_error), Err(restore_error)) => {
Err(crate::error::backend(format!("{action_error}; additionally failed to restore clipboard: {restore_error}")))
}
}
}
pub fn reserved_input_result(reason: impl Into<String>) -> InputActionResult {
let reason = reason.into();
InputActionResult {
selected_path: InputDeliveryPath::Unsupported,
attempts: vec![InputAttempt::failure(
InputDeliveryPath::Unsupported,
reason.clone(),
)],
verified: false,
mouse_disturbance: DisturbanceLevel::None,
focus_disturbance: DisturbanceLevel::None,
clipboard_disturbance: DisturbanceLevel::None,
}
}
fn input_session(state: &Arc<Mutex<LinuxDriverSessionState>>) -> DriverResult<Arc<Mutex<InputSession>>> {
let mut state = state.lock().map_err(|_| backend("linux driver session state poisoned"))?;
if state.input_session.is_none() {
let restore_tokens = state.restore_tokens.clone();
let input = match state.input_backend {
InputBackend::Portal => InputSession::Portal(PortalInput::open(restore_tokens.as_ref(), state.portal_app_id.as_ref())?),
#[cfg(target_os = "linux")]
InputBackend::Uinput => InputSession::Uinput(crate::native::uinput::InputSession::open()?),
#[cfg(not(target_os = "linux"))]
InputBackend::Uinput => return Err(DriverError::unsupported("Linux uinput")),
};
state.input_session = Some(Arc::new(Mutex::new(input)));
}
Ok(state.input_session.as_ref().unwrap().clone())
}
pub(crate) fn with_input_session<T>(
state: &Arc<Mutex<LinuxDriverSessionState>>,
operation: impl FnOnce(&mut InputSession) -> DriverResult<T>,
) -> DriverResult<T> {
deliver_input(state, &input_session(state)?, operation)
}
fn deliver_input<T>(
state: &Arc<Mutex<LinuxDriverSessionState>>,
session: &Arc<Mutex<InputSession>>,
operation: impl FnOnce(&mut InputSession) -> DriverResult<T>,
) -> DriverResult<T> {
let result = operation(&mut *session.lock().map_err(|_| backend("linux input session poisoned"))?);
if matches!(&result, Err(DriverError::Backend { .. } | DriverError::PermissionDenied { .. })) {
let mut state = state.lock().map_err(|_| backend("linux driver session state poisoned"))?;
if state.input_session.as_ref().is_some_and(|current| Arc::ptr_eq(current, session)) {
state.input_session = None;
}
}
result
}
pub(crate) fn keyboard_result() -> InputActionResult {
InputActionResult {
selected_path: InputDeliveryPath::ForegroundSystemEvents,
attempts: vec![InputAttempt::success(
InputDeliveryPath::ForegroundSystemEvents,
)],
verified: false,
mouse_disturbance: DisturbanceLevel::None,
focus_disturbance: DisturbanceLevel::Unknown,
clipboard_disturbance: DisturbanceLevel::None,
}
}
fn pointer_result() -> InputActionResult {
InputActionResult {
selected_path: InputDeliveryPath::ForegroundSystemEvents,
attempts: vec![InputAttempt::success(
InputDeliveryPath::ForegroundSystemEvents,
)],
verified: false,
mouse_disturbance: DisturbanceLevel::Temporary,
focus_disturbance: DisturbanceLevel::Unknown,
clipboard_disturbance: DisturbanceLevel::None,
}
}
fn sleep_if_nonzero(duration: Duration) {
if !duration.is_zero() {
std::thread::sleep(duration);
}
}
pub(crate) enum KeyboardLayout {
Portal,
#[cfg(target_os = "linux")]
Uinput(crate::native::keymap::Keymap),
}
pub(crate) struct KeyboardPlan {
steps: Vec<(KeyChord, Duration)>,
}
impl KeyboardPlan {
pub(crate) fn chord(keys: Vec<i32>) -> Self {
let (key, modifiers) = keys.split_last().expect("validated nonempty combination");
Self {
steps: vec![(
KeyChord {
key: *key,
modifiers: modifiers.to_vec(),
},
Duration::ZERO,
)],
}
}
pub(crate) fn type_text(text: &str, options: TypeTextOptions) -> DriverResult<Self> {
let mut steps = Vec::new();
if options.replace_existing {
steps.extend(Self::chord(vec![keysym::CONTROL_L, 'a' as i32]).steps);
steps.extend(Self::chord(vec![keysym::BACKSPACE]).steps);
}
for character in text.chars() {
steps.push((
KeyChord {
modifiers: vec![],
key: keysym::for_char(character)?,
},
options.inter_char_delay,
));
}
if options.submit != TextSubmit::No {
steps.extend(Self::chord(vec![keysym::RETURN]).steps);
}
Ok(Self { steps })
}
pub(crate) fn paste_text(options: &PasteTextOptions) -> Self {
let mut steps = Vec::new();
if options.replace_existing {
steps.extend(Self::chord(vec![keysym::CONTROL_L, 'a' as i32]).steps);
}
steps.extend(Self::chord(vec![keysym::CONTROL_L, 'v' as i32]).steps);
if options.submit != TextSubmit::No {
steps.extend(Self::chord(vec![keysym::RETURN]).steps);
}
Self { steps }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct KeyChord {
modifiers: Vec<i32>,
key: i32,
}
fn parse_key_chord(input: &str) -> DriverResult<KeyChord> {
let options = auv_driver_common::PressKeysOptions::from(KeyPressOptions {
key: input.into(),
..Default::default()
});
let keys = crate::keyboard::combination(&options)?;
let (key, held) = keys.split_last().expect("validated nonempty keys");
Ok(KeyChord {
modifiers: held.to_vec(),
key: *key,
})
}
pub(crate) mod keysym {
use auv_driver_common::error::DriverResult;
use crate::error::invalid_input;
pub const BACKSPACE: i32 = auv_driver_common::Keysym::BackSpace.raw() as i32;
pub const TAB: i32 = auv_driver_common::Keysym::Tab.raw() as i32;
pub const RETURN: i32 = auv_driver_common::Keysym::Return.raw() as i32;
pub const SHIFT_L: i32 = auv_driver_common::Keysym::Shift_L.raw() as i32;
pub const CONTROL_L: i32 = auv_driver_common::Keysym::Control_L.raw() as i32;
pub const ALT_L: i32 = auv_driver_common::Keysym::Alt_L.raw() as i32;
pub const SUPER_L: i32 = auv_driver_common::Keysym::Super_L.raw() as i32;
pub fn modifier(raw: &str) -> Option<i32> {
use auv_driver_common::{Key, Modifier};
match raw.parse::<Key>().ok()? {
Key::Modifier(Modifier::Control) => Some(CONTROL_L),
Key::Modifier(Modifier::Shift) => Some(SHIFT_L),
Key::Modifier(Modifier::Alt) => Some(ALT_L),
Key::Modifier(Modifier::Meta) => Some(SUPER_L),
Key::Symbol(_) => None,
}
}
pub fn named_or_char(raw: &str) -> DriverResult<i32> {
if let Some(keysym) = named(raw) {
return Ok(keysym);
}
let mut chars = raw.chars();
let Some(ch) = chars.next() else {
return Err(invalid_input("key must not be empty"));
};
if chars.next().is_some() {
return Err(invalid_input(format!("invalid key {raw}; use a special key, shortcut, or type_text for multi-character text")));
}
for_char(ch)
}
pub fn for_char(ch: char) -> DriverResult<i32> {
if ch.is_ascii() && !ch.is_control() {
return Ok(ch as i32);
}
match ch {
'\n' | '\r' => Ok(RETURN),
'\t' => Ok(TAB),
_ => Err(invalid_input(format!("linux keyboard input only supports ASCII text in this slice; unsupported character {ch:?}"))),
}
}
fn named(raw: &str) -> Option<i32> {
use auv_driver_common::{Key, Keysym};
if let Some(modifier) = modifier(raw) {
return Some(modifier);
}
if matches!(raw.to_ascii_lowercase().as_str(), "forwarddelete" | "forward_delete") {
return None;
}
match raw.parse::<Key>().ok()? {
Key::Symbol(Keysym::KP_Enter) => Some(RETURN),
Key::Symbol(symbol)
if matches!(
symbol,
Keysym::Return
| Keysym::Tab
| Keysym::Escape
| Keysym::Home
| Keysym::Left
| Keysym::Up
| Keysym::Right
| Keysym::Down
| Keysym::Page_Up
| Keysym::Page_Down
| Keysym::End
| Keysym::Insert
| Keysym::space
| Keysym::Delete
| Keysym::BackSpace
) || (Keysym::F1.raw()..=Keysym::F12.raw()).contains(&symbol.raw()) =>
{
Some(symbol.raw() as i32)
}
_ => None,
}
}
}
#[cfg(test)]
#[path = "input_test.rs"]
mod tests;
pub(crate) fn with_held_keys<K: Copy>(
modifiers: &[K],
mut key_event: impl FnMut(K, bool) -> DriverResult<()>,
action: impl FnOnce() -> DriverResult<()>,
) -> DriverResult<()> {
let mut attempted = 0;
let mut result = Ok(());
for key in modifiers {
attempted += 1;
result = key_event(*key, true);
if result.is_err() {
break;
}
}
if result.is_ok() {
result = action();
}
for key in modifiers[..attempted].iter().rev() {
result = combine_release(result, key_event(*key, false));
}
result
}
pub(crate) fn combine_release(action: DriverResult<()>, release: DriverResult<()>) -> DriverResult<()> {
match (action, release) {
(Ok(()), result) | (result, Ok(())) => result,
(Err(action), Err(release)) => Err(backend(format!("{action}; additionally failed to release input: {release}"))),
}
}
pub(crate) struct MouseBackend {
state: Arc<Mutex<LinuxDriverSessionState>>,
pinned: Mutex<Option<Arc<Mutex<InputSession>>>>,
}
impl MouseBackend {
pub(crate) fn new(state: Arc<Mutex<LinuxDriverSessionState>>) -> Self {
Self {
state,
pinned: Mutex::new(None),
}
}
fn session(&self) -> DriverResult<Arc<Mutex<InputSession>>> {
let mut pinned = self.pinned.lock().map_err(|_| backend("mouse session pin poisoned"))?;
if pinned.is_none() {
*pinned = Some(input_session(&self.state)?);
}
Ok(pinned.as_ref().unwrap().clone())
}
}
impl auv_driver_common::mouse_input::MouseBackend for MouseBackend {
fn move_to(&self, point: Point, _held: Option<auv_driver_common::MouseButton>) -> DriverResult<InputActionResult> {
deliver_input(&self.state, &self.session()?, |session| session.move_to(point))?;
Ok(pointer_result())
}
fn button(&self, _point: Point, button: auv_driver_common::MouseButton, down: bool) -> DriverResult<InputActionResult> {
deliver_input(&self.state, &self.session()?, |session| session.button(button, down))?;
Ok(pointer_result())
}
}