use std::ops::{ControlFlow, Range};
use std::thread;
use std::time::Duration;
use anyhow::Context;
use evdev::{uinput::VirtualDevice, AttributeSet, EventType, InputEvent, Key};
#[cfg(feature = "logging")]
use log::{debug, warn};
#[cfg(not(feature = "logging"))]
macro_rules! debug {
($($arg:tt)*) => {};
}
#[cfg(not(feature = "logging"))]
macro_rules! warn {
($($arg:tt)*) => {};
}
use crate::keymap::{KeyboardLayout, XkbKeymap};
use crate::{default_clipboard, run_gated, ClipboardBackend, KeyMapping, KeyTap, Typed};
const DEVICE_SETTLE_DELAY: Duration = Duration::from_millis(200);
const INITIAL_WARMUP_DELAY: Duration = Duration::from_secs(1);
const UINPUT_MIN_DELAY: Duration = Duration::from_millis(1);
enum Sink {
Uinput(VirtualDevice),
#[cfg(feature = "xtest")]
Xtest(Box<crate::xtest::XtestSink>),
}
impl Sink {
fn key(&mut self, code: u16, pressed: bool) -> anyhow::Result<()> {
match self {
Sink::Uinput(device) => {
device.emit(&[InputEvent::new(EventType::KEY, code, i32::from(pressed))])?;
}
#[cfg(feature = "xtest")]
Sink::Xtest(xtest) => xtest.key(code, pressed)?,
}
Ok(())
}
fn flush(&mut self) -> anyhow::Result<()> {
match self {
Sink::Uinput(_) => Ok(()),
#[cfg(feature = "xtest")]
Sink::Xtest(xtest) => xtest.flush(),
}
}
fn min_delay(&self) -> Duration {
match self {
Sink::Uinput(_) => UINPUT_MIN_DELAY,
#[cfg(feature = "xtest")]
Sink::Xtest(_) => Duration::ZERO,
}
}
fn sync(&mut self) -> anyhow::Result<()> {
match self {
Sink::Uinput(_) => Ok(()),
#[cfg(feature = "xtest")]
Sink::Xtest(xtest) => xtest.sync(),
}
}
}
#[derive(Debug, Clone)]
enum Step {
Key(KeyMapping),
Paste(Range<usize>),
}
fn plan_steps(
text: &str,
mut lookup: impl FnMut(char) -> Option<KeyMapping>,
) -> Vec<(usize, Step)> {
let mut steps = Vec::new();
let mut paste_start = None;
for (at, ch) in text.char_indices() {
match lookup(ch) {
Some(mapping) => {
if let Some(start) = paste_start.take() {
steps.push((start, Step::Paste(start..at)));
}
steps.push((at, Step::Key(mapping)));
}
None => {
paste_start.get_or_insert(at);
}
}
}
if let Some(start) = paste_start {
steps.push((start, Step::Paste(start..text.len())));
}
steps
}
pub struct Keyboard {
sink: Sink,
keymap: XkbKeymap,
clipboard: Box<dyn ClipboardBackend>,
key_delay: Duration,
}
impl Keyboard {
pub fn new(key_delay: Duration) -> anyhow::Result<Self> {
let layout = KeyboardLayout::detect();
let keymap = XkbKeymap::from_layout(&layout)?;
Self::with_components(keymap, default_clipboard(), key_delay)
}
pub fn new_prewarm(key_delay: Duration) -> anyhow::Result<Self> {
let layout = KeyboardLayout::detect();
let keymap = XkbKeymap::from_layout(&layout)?;
Self::with_components_settle(keymap, default_clipboard(), key_delay, INITIAL_WARMUP_DELAY)
}
pub fn with_layout(
layout: &str,
variant: Option<&str>,
key_delay: Duration,
) -> anyhow::Result<Self> {
let detected = KeyboardLayout {
layout: layout.to_string(),
variant: variant.unwrap_or("").to_string(),
};
let keymap = XkbKeymap::from_layout(&detected)?;
Self::with_components(keymap, default_clipboard(), key_delay)
}
pub fn with_components(
keymap: XkbKeymap,
clipboard: Box<dyn ClipboardBackend>,
key_delay: Duration,
) -> anyhow::Result<Self> {
Self::with_components_settle(keymap, clipboard, key_delay, DEVICE_SETTLE_DELAY)
}
fn with_components_settle(
keymap: XkbKeymap,
clipboard: Box<dyn ClipboardBackend>,
key_delay: Duration,
settle: Duration,
) -> anyhow::Result<Self> {
let mut keys = AttributeSet::<Key>::new();
for code in 1..=247 {
keys.insert(Key::new(code));
}
let device = evdev::uinput::VirtualDeviceBuilder::new()
.context("failed to create VirtualDeviceBuilder")?
.name("whisrs virtual keyboard")
.with_keys(&keys)
.context("failed to register key events")?
.build()
.context("failed to build uinput virtual device — check /dev/uinput permissions")?;
thread::sleep(settle);
debug!("uinput virtual keyboard created");
Ok(Self {
sink: Sink::Uinput(device),
keymap,
clipboard,
key_delay,
})
}
#[cfg(feature = "xtest")]
pub fn new_xtest(key_delay: Duration) -> anyhow::Result<Self> {
let layout = KeyboardLayout::detect();
let mut keymap = XkbKeymap::from_layout(&layout)?;
let sink = Sink::Xtest(Box::new(crate::xtest::XtestSink::connect(&mut keymap)?));
debug!("XTEST keyboard connected");
Ok(Self {
sink,
keymap,
clipboard: default_clipboard(),
key_delay,
})
}
pub fn set_key_delay(&mut self, key_delay: Duration) {
self.key_delay = key_delay;
}
pub fn type_text(&mut self, text: &str) -> anyhow::Result<()> {
self.type_text_gated(text, &mut |_| ControlFlow::Continue(()))
.map(|_| ())
}
pub fn type_text_gated(
&mut self,
text: &str,
gate: &mut dyn FnMut(usize) -> ControlFlow<()>,
) -> anyhow::Result<Typed> {
self.release_all_modifiers()?;
let keymap = &self.keymap;
let steps = plan_steps(text, |ch| keymap.lookup(ch).copied());
let typed = run_gated(steps, gate, |step| match step {
Step::Key(mapping) => {
self.tap_key(&mapping.main)?;
if let Some(follow) = mapping.follow.as_ref() {
self.tap_key(follow)?;
}
Ok(())
}
Step::Paste(range) => self.paste_text(&text[range]),
})?;
self.sink.sync()?;
Ok(typed)
}
pub fn backspace(&mut self, count: usize) -> anyhow::Result<()> {
self.release_all_modifiers()?;
for _ in 0..count {
self.tap_key(&KeyTap {
keycode: Key::KEY_BACKSPACE.code(),
shift: false,
altgr: false,
})?;
}
self.sink.sync()
}
pub fn send_combo(&mut self, keys: &[Key]) -> anyhow::Result<()> {
self.release_all_modifiers()?;
for key in keys {
self.sink.key(key.code(), true)?;
self.pause()?;
}
for key in keys.iter().rev() {
self.sink.key(key.code(), false)?;
self.pause()?;
}
self.sink.sync()
}
fn pause(&mut self) -> anyhow::Result<()> {
self.sink.flush()?;
thread::sleep(self.key_delay.max(self.sink.min_delay()));
Ok(())
}
fn set_modifier(&mut self, modifier: Key, pressed: bool) -> anyhow::Result<()> {
self.sink.key(modifier.code(), pressed)?;
self.pause()
}
fn tap_key(&mut self, tap: &KeyTap) -> anyhow::Result<()> {
let level3_key = Key::new(self.keymap.level3_keycode());
if tap.shift {
self.set_modifier(Key::KEY_LEFTSHIFT, true)?;
}
if tap.altgr {
self.set_modifier(level3_key, true)?;
}
self.sink.key(tap.keycode, true)?;
self.pause()?;
self.sink.key(tap.keycode, false)?;
self.pause()?;
if tap.altgr {
self.set_modifier(level3_key, false)?;
}
if tap.shift {
self.set_modifier(Key::KEY_LEFTSHIFT, false)?;
}
Ok(())
}
fn release_all_modifiers(&mut self) -> anyhow::Result<()> {
let level3_key = Key::new(self.keymap.level3_keycode());
let modifiers = [
Key::KEY_LEFTSHIFT,
Key::KEY_RIGHTSHIFT,
Key::KEY_LEFTCTRL,
Key::KEY_RIGHTCTRL,
Key::KEY_LEFTALT,
Key::KEY_RIGHTALT,
level3_key,
Key::KEY_LEFTMETA,
Key::KEY_RIGHTMETA,
];
for modifier in &modifiers {
self.sink.key(modifier.code(), false)?;
}
self.pause()
}
fn inject_ctrl_v(&mut self) -> anyhow::Result<()> {
self.set_modifier(Key::KEY_LEFTCTRL, true)?;
self.sink.key(Key::KEY_V.code(), true)?;
self.pause()?;
self.sink.key(Key::KEY_V.code(), false)?;
self.pause()?;
self.set_modifier(Key::KEY_LEFTCTRL, false)?;
self.sink.sync()
}
fn paste_text(&mut self, text: &str) -> anyhow::Result<()> {
let saved = self.clipboard.get_text().ok();
self.clipboard
.set_text(text)
.context("failed to set clipboard for paste")?;
thread::sleep(Duration::from_millis(10));
self.release_all_modifiers()?;
self.inject_ctrl_v()?;
thread::sleep(Duration::from_millis(50));
if let Some(previous) = saved {
if let Err(_e) = self.clipboard.set_text(&previous) {
warn!("failed to restore clipboard: {_e}");
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tap(keycode: u16) -> KeyTap {
KeyTap {
keycode,
shift: false,
altgr: false,
}
}
fn lookup(ch: char) -> Option<KeyMapping> {
match ch {
'a'..='z' | ' ' => Some(KeyMapping {
main: tap(ch as u16),
follow: None,
}),
'ã' => Some(KeyMapping {
main: tap(100),
follow: Some(tap('a' as u16)),
}),
_ => None,
}
}
fn plan(text: &str) -> Vec<(usize, String)> {
plan_steps(text, lookup)
.into_iter()
.map(|(at, step)| {
let what = match step {
Step::Key(m) => match m.follow {
Some(f) => format!("key:{}+{}", m.main.keycode, f.keycode),
None => format!("key:{}", m.main.keycode),
},
Step::Paste(range) => {
assert_eq!(range.start, at, "a paste is asked about at its start");
format!("paste:{}", &text[range])
}
};
(at, what)
})
.collect()
}
#[test]
fn mapped_characters_are_one_step_each_at_their_byte_offset() {
assert_eq!(
plan("ab c"),
vec![
(0, "key:97".to_string()),
(1, "key:98".to_string()),
(2, "key:32".to_string()),
(3, "key:99".to_string()),
]
);
assert!(plan("").is_empty());
}
#[test]
fn a_dead_key_pair_is_one_step() {
assert_eq!(
plan("ãb"),
vec![(0, "key:100+97".to_string()), (2, "key:98".to_string())]
);
}
#[test]
fn a_run_of_unmapped_characters_is_one_paste_at_its_start() {
assert_eq!(
plan("a😀😀b€€"),
vec![
(0, "key:97".to_string()),
(1, "paste:😀😀".to_string()),
(9, "key:98".to_string()),
(10, "paste:€€".to_string()),
]
);
assert_eq!(
plan("😀a"),
vec![(0, "paste:😀".to_string()), (4, "key:97".to_string())]
);
}
#[test]
fn the_steps_cover_the_text_in_order() {
let text = "grüße ã 😀 x€y";
let mut covered = String::new();
let mut last = None;
for (at, step) in plan_steps(text, lookup) {
assert!(text.is_char_boundary(at), "offset {at} splits a character");
if let Some(last) = last {
assert!(last < at, "offsets go up");
}
last = Some(at);
match step {
Step::Key(_) => {
let ch = text[at..].chars().next().unwrap();
covered.push(ch);
}
Step::Paste(range) => covered.push_str(&text[range]),
}
}
assert_eq!(covered, text);
}
#[test]
fn a_break_leaves_the_rest_from_a_step_boundary() {
let text = "aã😀😀b";
let steps = plan_steps(text, lookup);
let offsets: Vec<usize> = steps.iter().map(|(at, _)| *at).collect();
assert_eq!(offsets, vec![0, 1, 3, 11]);
for (n, &stop) in offsets.iter().enumerate() {
let mut gate = |at: usize| {
if at == stop {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
};
let mut sent = 0;
let typed = run_gated(plan_steps(text, lookup), &mut gate, |_| {
sent += 1;
Ok(())
})
.unwrap();
assert_eq!(typed, Typed::StoppedAt(stop));
assert_eq!(sent, n, "steps before the break");
}
}
}