use std::collections::HashSet;
use std::io;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use evdev::uinput::VirtualDevice;
use evdev::{
AttributeSet, Device, EventType, InputEvent, KeyCode as Key,
RelativeAxisCode as RelativeAxisType,
};
use crate::ipc::Shared;
use crate::pointer::{accel_pointer, Pointer};
use crate::wheel::{scroll, Axis};
pub const VIRT_PREFIX: &str = "wayland-mouse";
const REL_WHEEL_HI: RelativeAxisType = RelativeAxisType(0x0b);
const REL_HWHEEL_HI: RelativeAxisType = RelativeAxisType(0x0c);
pub fn run(shared: Arc<Shared>) {
let handled: Arc<Mutex<HashSet<PathBuf>>> = Arc::new(Mutex::new(HashSet::new()));
let cfg = shared.current();
if !cfg.button.is_empty() {
shared.ensure_keyboard();
}
{
let shared = shared.clone();
thread::spawn(move || crate::ipc::serve(shared));
}
let name_filter = cfg.name_filter.clone();
eprintln!(
"wayland-mouse started (preset={}, name_filter={:?}, buttons={}, debug={})",
cfg.preset,
name_filter,
cfg.button.len(),
cfg.debug
);
loop {
for (path, dev) in evdev::enumerate() {
{
let s = handled.lock().unwrap();
if s.contains(&path) {
continue;
}
}
if is_target_mouse(&dev, &name_filter) {
drop(dev);
handled.lock().unwrap().insert(path.clone());
let shared = shared.clone();
let handled = handled.clone();
let p = path.clone();
thread::spawn(move || {
if let Err(e) = run_device(p.clone(), shared, handled.clone()) {
eprintln!("device {p:?} error: {e}");
if let Ok(mut s) = handled.lock() {
s.remove(&p);
}
}
});
}
}
thread::sleep(Duration::from_secs(3));
}
}
fn is_target_mouse(dev: &Device, filter: &str) -> bool {
let name = dev.name().unwrap_or("");
if name.contains(VIRT_PREFIX) {
return false;
}
let has_wheel = dev
.supported_relative_axes()
.is_some_and(|s| s.contains(RelativeAxisType::REL_WHEEL) || s.contains(REL_WHEEL_HI));
let has_extra_buttons = dev.supported_keys().is_some_and(|k| {
k.contains(Key::BTN_SIDE)
|| k.contains(Key::BTN_EXTRA)
|| k.contains(Key::BTN_FORWARD)
|| k.contains(Key::BTN_BACK)
});
if !has_wheel && !has_extra_buttons {
return false;
}
filter.is_empty() || name.to_lowercase().contains(&filter.to_lowercase())
}
fn run_device(
path: PathBuf,
shared: Arc<Shared>,
handled: Arc<Mutex<HashSet<PathBuf>>>,
) -> io::Result<()> {
let mut dev = Device::open(&path)?;
let name = dev.name().unwrap_or("mouse").to_string();
let mut settings = shared.current().resolve(&name);
let mut version = shared.version();
let has_hires = dev
.supported_relative_axes()
.is_some_and(|s| s.contains(REL_WHEEL_HI));
let mut axes = AttributeSet::<RelativeAxisType>::new();
if let Some(s) = dev.supported_relative_axes() {
for a in s.iter() {
axes.insert(a);
}
}
for a in [
RelativeAxisType::REL_X,
RelativeAxisType::REL_Y,
RelativeAxisType::REL_WHEEL,
RelativeAxisType::REL_HWHEEL,
REL_WHEEL_HI,
REL_HWHEEL_HI,
] {
axes.insert(a);
}
let mut keys = AttributeSet::<Key>::new();
if let Some(s) = dev.supported_keys() {
for k in s.iter() {
keys.insert(k);
}
}
let vname = format!("{VIRT_PREFIX} {name}");
let mut vdev = VirtualDevice::builder()?
.name(&vname)
.with_relative_axes(&axes)?
.with_keys(&keys)?
.build()?;
dev.grab()?;
eprintln!(
"handling {path:?} {name:?} hi-res={has_hires} wheel={} pointer={}",
settings.wheel_enabled, settings.pointer_accel
);
let mut vy = Axis::new();
let mut hx = Axis::new();
let mut ptr = Pointer::new();
let mut fdx = 0i32; let mut fdy = 0i32;
let mut pa = settings.pointer_accel;
let mut we = settings.wheel_enabled;
let mut out: Vec<InputEvent> = Vec::with_capacity(64);
loop {
let events = match dev.fetch_events() {
Ok(e) => e,
Err(_) => break,
};
let v = shared.version();
if v != version {
settings = shared.current().resolve(&name);
version = v;
pa = settings.pointer_accel;
we = settings.wheel_enabled;
}
out.clear();
for ev in events {
let et = ev.event_type();
if et == EventType::RELATIVE {
let code = ev.code();
if code == REL_WHEEL_HI.0 {
if we {
scroll(
&settings,
&mut vy,
ev.value(),
ev.timestamp(),
REL_WHEEL_HI,
&mut out,
'V',
);
} else {
out.push(InputEvent::new(et.0, code, ev.value()));
}
} else if code == REL_HWHEEL_HI.0 {
if we {
scroll(
&settings,
&mut hx,
ev.value(),
ev.timestamp(),
REL_HWHEEL_HI,
&mut out,
'H',
);
} else {
out.push(InputEvent::new(et.0, code, ev.value()));
}
} else if code == RelativeAxisType::REL_WHEEL.0 {
if !we {
out.push(InputEvent::new(et.0, code, ev.value()));
} else if !has_hires {
scroll(
&settings,
&mut vy,
ev.value() * 120,
ev.timestamp(),
REL_WHEEL_HI,
&mut out,
'V',
);
}
} else if code == RelativeAxisType::REL_HWHEEL.0 {
if !we {
out.push(InputEvent::new(et.0, code, ev.value()));
} else if !has_hires {
scroll(
&settings,
&mut hx,
ev.value() * 120,
ev.timestamp(),
REL_HWHEEL_HI,
&mut out,
'H',
);
}
} else if code == RelativeAxisType::REL_X.0 {
if pa {
fdx += ev.value();
} else {
out.push(InputEvent::new(EventType::RELATIVE.0, code, ev.value()));
}
} else if code == RelativeAxisType::REL_Y.0 {
if pa {
fdy += ev.value();
} else {
out.push(InputEvent::new(EventType::RELATIVE.0, code, ev.value()));
}
} else {
out.push(InputEvent::new(EventType::RELATIVE.0, code, ev.value()));
}
} else if et == EventType::KEY {
let code = ev.code();
if ev.value() == 1 {
shared.telemetry.set_button(code);
}
let remap = shared.remap();
match remap.get(code) {
Some(action) => match shared.ensure_keyboard() {
Some(kb) => kb.apply(action, ev.value()),
None => out.push(InputEvent::new(et.0, code, ev.value())),
},
None => out.push(InputEvent::new(et.0, code, ev.value())),
}
} else if et == EventType::SYNCHRONIZATION && ev.code() == 0 {
if pa && (fdx != 0 || fdy != 0) {
accel_pointer(&settings, &mut ptr, fdx, fdy, ev.timestamp(), &mut out);
fdx = 0;
fdy = 0;
}
out.push(InputEvent::new(et.0, ev.code(), ev.value()));
} else {
out.push(InputEvent::new(et.0, ev.code(), ev.value()));
}
}
if !out.is_empty() && vdev.emit(&out).is_err() {
break;
}
shared.telemetry.set_pointer(ptr.speed(), ptr.gain());
shared.telemetry.set_wheel(vy.dps(), vy.mult());
}
let _ = dev.ungrab();
if let Ok(mut s) = handled.lock() {
s.remove(&path);
}
eprintln!("released {path:?}");
Ok(())
}
pub fn watch_buttons() -> i32 {
let mut threads = Vec::new();
let mut count = 0usize;
for (_path, dev) in evdev::enumerate() {
if !is_mouse_like(&dev) {
continue;
}
let name = dev.name().unwrap_or("mouse").to_string();
count += 1;
let mut dev = dev;
threads.push(thread::spawn(move || {
while let Ok(evs) = dev.fetch_events() {
for ev in evs {
if ev.event_type() == EventType::KEY && ev.value() == 1 {
println!("{name}: {:?} (code {})", Key(ev.code()), ev.code());
}
}
}
}));
}
if count == 0 {
eprintln!("no mouse-like devices found — are you root? (sudo wayland-mouse buttons)");
return 1;
}
eprintln!("watching {count} device(s) — press your mouse buttons (Ctrl-C to stop)");
for t in threads {
let _ = t.join();
}
0
}
fn is_mouse_like(dev: &Device) -> bool {
let name = dev.name().unwrap_or("");
if name.contains(VIRT_PREFIX) {
return false;
}
let has_btn = dev
.supported_keys()
.is_some_and(|k| (0x110..=0x117).any(|c| k.contains(Key(c))));
let has_wheel = dev
.supported_relative_axes()
.is_some_and(|s| s.contains(RelativeAxisType::REL_WHEEL) || s.contains(REL_WHEEL_HI));
has_btn || has_wheel
}