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, ClipboardBackend, KeyTap};
const DEVICE_SETTLE_DELAY: Duration = Duration::from_millis(200);
const INITIAL_WARMUP_DELAY: Duration = Duration::from_secs(1);
pub struct Keyboard {
device: VirtualDevice,
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 {
device,
keymap,
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.release_all_modifiers()?;
let mut paste_buf = String::new();
for ch in text.chars() {
let mapping = self.keymap.lookup(ch).copied();
if let Some(mapping) = mapping {
if !paste_buf.is_empty() {
self.paste_text(&paste_buf)?;
paste_buf.clear();
}
self.tap_key(&mapping.main)?;
if let Some(follow) = mapping.follow.as_ref() {
self.tap_key(follow)?;
}
} else {
paste_buf.push(ch);
}
}
if !paste_buf.is_empty() {
self.paste_text(&paste_buf)?;
}
Ok(())
}
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,
})?;
}
Ok(())
}
pub fn send_combo(&mut self, keys: &[Key]) -> anyhow::Result<()> {
for key in keys {
self.device
.emit(&[InputEvent::new(EventType::KEY, key.code(), 1)])?;
thread::sleep(self.key_delay);
}
for key in keys.iter().rev() {
self.device
.emit(&[InputEvent::new(EventType::KEY, key.code(), 0)])?;
thread::sleep(self.key_delay);
}
Ok(())
}
fn set_modifier(&mut self, modifier: Key, pressed: bool) -> anyhow::Result<()> {
let value = if pressed { 1 } else { 0 };
self.device
.emit(&[InputEvent::new(EventType::KEY, modifier.code(), value)])?;
thread::sleep(self.key_delay);
Ok(())
}
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.device
.emit(&[InputEvent::new(EventType::KEY, tap.keycode, 1)])?;
thread::sleep(self.key_delay);
self.device
.emit(&[InputEvent::new(EventType::KEY, tap.keycode, 0)])?;
thread::sleep(self.key_delay);
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.device
.emit(&[InputEvent::new(EventType::KEY, modifier.code(), 0)])?;
}
thread::sleep(self.key_delay);
Ok(())
}
fn inject_ctrl_v(&mut self) -> anyhow::Result<()> {
self.set_modifier(Key::KEY_LEFTCTRL, true)?;
self.device
.emit(&[InputEvent::new(EventType::KEY, Key::KEY_V.code(), 1)])?;
thread::sleep(self.key_delay);
self.device
.emit(&[InputEvent::new(EventType::KEY, Key::KEY_V.code(), 0)])?;
thread::sleep(self.key_delay);
self.set_modifier(Key::KEY_LEFTCTRL, false)?;
Ok(())
}
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(())
}
}