Mount nearby shortcut and theme rows, preserve gallery keyboard navigation, reset page scrolling, and reuse native themes during palette transitions. Prepare and deduplicate live config reloads on a worker and apply only changed runtime sections. Align pointer focus and overview selection on overlapping outputs, add regression coverage, and clean up workspace Clippy warnings.
416 lines
18 KiB
Rust
416 lines
18 KiB
Rust
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::os::fd::{AsFd, OwnedFd};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
use calloop::{EventLoop, LoopSignal, PostAction, channel};
|
|
use reis::calloop::{EisRequestSource, EisRequestSourceEvent};
|
|
use reis::eis;
|
|
use reis::enumflags2::BitFlags;
|
|
use reis::request::{Connection, Device, DeviceCapability, EisRequest};
|
|
use serde_json::Value;
|
|
|
|
use crate::backend::Cancel;
|
|
|
|
pub(crate) struct Worker {
|
|
sender: channel::SyncSender<Vec<Value>>,
|
|
signal: LoopSignal,
|
|
pub(crate) ready: Arc<AtomicBool>,
|
|
}
|
|
|
|
impl Worker {
|
|
pub(crate) fn start(capabilities: u32, keymap: &str, cancel: Arc<Cancel>) -> Result<(Self, OwnedFd), String> {
|
|
if keymap.is_empty() || keymap.len() > 256 * 1024 {
|
|
return Err("Invalid captured keyboard map".into());
|
|
}
|
|
let mut keymap_file = tempfile::tempfile().map_err(|e| e.to_string())?;
|
|
keymap_file
|
|
.write_all(keymap.as_bytes())
|
|
.and_then(|_| keymap_file.write_all(&[0]))
|
|
.map_err(|e| e.to_string())?;
|
|
let keymap_size = keymap.len() as u32 + 1;
|
|
let (server, client) = UnixStream::pair().map_err(|e| e.to_string())?;
|
|
let ready = Arc::new(AtomicBool::new(false));
|
|
let thread_ready = ready.clone();
|
|
let (startup, started) = std::sync::mpsc::sync_channel(1);
|
|
std::thread::Builder::new()
|
|
.name("ferese-input-eis".into())
|
|
.spawn(move || {
|
|
struct Finish(Arc<Cancel>);
|
|
impl Drop for Finish {
|
|
fn drop(&mut self) {
|
|
self.0.stop();
|
|
}
|
|
}
|
|
let _finish = Finish(cancel);
|
|
let run = || {
|
|
let context = eis::Context::new(server).map_err(|e| e.to_string())?;
|
|
let mut event_loop = EventLoop::<State>::try_new().map_err(|e| e.to_string())?;
|
|
let signal = event_loop.get_signal();
|
|
let (sender, receiver) = channel::sync_channel(4);
|
|
event_loop
|
|
.handle()
|
|
.insert_source(receiver, |event, _, state| match event {
|
|
channel::Event::Msg(events) => state.events(events),
|
|
channel::Event::Closed => state.signal.stop(),
|
|
})
|
|
.map_err(|e| e.to_string())?;
|
|
event_loop
|
|
.handle()
|
|
.insert_source(EisRequestSource::new(context, 1), |event, connection, state| {
|
|
let action = match event {
|
|
Ok(event) => state.request(connection, event),
|
|
Err(_) => {
|
|
state.signal.stop();
|
|
PostAction::Remove
|
|
}
|
|
};
|
|
if connection.flush().is_err() {
|
|
state.signal.stop();
|
|
}
|
|
Ok(action)
|
|
})
|
|
.map_err(|e| e.to_string())?;
|
|
let mut state = State {
|
|
signal: signal.clone(),
|
|
ready: thread_ready.clone(),
|
|
capabilities,
|
|
keymap: keymap_file,
|
|
keymap_size,
|
|
connection: None,
|
|
keyboard: None,
|
|
pointer: None,
|
|
activation: None,
|
|
keyboard_ready: false,
|
|
pointer_ready: false,
|
|
};
|
|
startup
|
|
.send(Ok((sender, signal)))
|
|
.map_err(|_| "Input transport startup cancelled".to_owned())?;
|
|
let _ = event_loop.run(None, &mut state, |_| {});
|
|
if let Some(connection) = state.connection.take() {
|
|
connection.disconnected(eis::connection::DisconnectReason::Disconnected, None);
|
|
}
|
|
state.ready.store(false, Ordering::SeqCst);
|
|
Ok::<(), String>(())
|
|
};
|
|
if let Err(error) = run() {
|
|
let _ = startup.send(Err(error));
|
|
}
|
|
})
|
|
.map_err(|e| e.to_string())?;
|
|
let (sender, signal) = started
|
|
.recv_timeout(std::time::Duration::from_secs(2))
|
|
.map_err(|_| "Input transport startup timed out".to_owned())??;
|
|
Ok((Self { sender, signal, ready }, client.into()))
|
|
}
|
|
|
|
pub(crate) fn send(&self, events: Vec<Value>) -> Result<(), String> {
|
|
self.sender
|
|
.try_send(events)
|
|
.map_err(|_| "Input transport is closed or stalled".into())
|
|
}
|
|
|
|
pub(crate) fn stop(&self) {
|
|
self.signal.stop();
|
|
self.signal.wakeup();
|
|
}
|
|
}
|
|
|
|
impl Drop for Worker {
|
|
fn drop(&mut self) {
|
|
self.stop();
|
|
}
|
|
}
|
|
|
|
struct State {
|
|
signal: LoopSignal,
|
|
ready: Arc<AtomicBool>,
|
|
capabilities: u32,
|
|
keymap: File,
|
|
keymap_size: u32,
|
|
connection: Option<Connection>,
|
|
keyboard: Option<Device>,
|
|
pointer: Option<Device>,
|
|
activation: Option<u32>,
|
|
keyboard_ready: bool,
|
|
pointer_ready: bool,
|
|
}
|
|
|
|
impl State {
|
|
fn request(&mut self, connection: &Connection, event: EisRequestSourceEvent) -> PostAction {
|
|
match event {
|
|
EisRequestSourceEvent::Connected => {
|
|
if connection.context_type() != eis::handshake::ContextType::Receiver {
|
|
connection.disconnected(
|
|
eis::connection::DisconnectReason::Mode,
|
|
Some("Input capture requires a receiver context"),
|
|
);
|
|
self.signal.stop();
|
|
return PostAction::Remove;
|
|
}
|
|
let mut capabilities = BitFlags::<DeviceCapability>::empty();
|
|
if self.capabilities & 1 != 0 {
|
|
capabilities |= DeviceCapability::Keyboard;
|
|
}
|
|
if self.capabilities & 2 != 0 {
|
|
capabilities |= DeviceCapability::Pointer | DeviceCapability::Button | DeviceCapability::Scroll;
|
|
}
|
|
let _ = connection.add_seat(Some("Ferese input capture"), capabilities);
|
|
self.connection = Some(connection.clone());
|
|
}
|
|
EisRequestSourceEvent::Request(EisRequest::Bind(request)) => {
|
|
let keyboard = request.capabilities.contains(DeviceCapability::Keyboard) && self.capabilities & 1 != 0;
|
|
let pointer = request.capabilities.contains(DeviceCapability::Pointer) && self.capabilities & 2 != 0;
|
|
if !keyboard && let Some(device) = self.keyboard.take() {
|
|
device.remove();
|
|
self.keyboard_ready = false;
|
|
}
|
|
if !pointer && let Some(device) = self.pointer.take() {
|
|
device.remove();
|
|
self.pointer_ready = false;
|
|
}
|
|
if keyboard && self.keyboard.is_none() {
|
|
let device = request.seat.add_device(
|
|
Some("Ferese keyboard"),
|
|
eis::device::DeviceType::Virtual,
|
|
DeviceCapability::Keyboard.into(),
|
|
|device| {
|
|
if let Some(keyboard) = device.interface::<eis::Keyboard>() {
|
|
keyboard.keymap(eis::keyboard::KeymapType::Xkb, self.keymap_size, self.keymap.as_fd());
|
|
}
|
|
},
|
|
);
|
|
self.keyboard_ready = device.device().version() < 3;
|
|
device.resumed();
|
|
self.keyboard = Some(device);
|
|
}
|
|
if pointer && self.pointer.is_none() {
|
|
let caps = request.capabilities
|
|
& (DeviceCapability::Pointer | DeviceCapability::Button | DeviceCapability::Scroll);
|
|
let device =
|
|
request
|
|
.seat
|
|
.add_device(Some("Ferese pointer"), eis::device::DeviceType::Virtual, caps, |_| {});
|
|
self.pointer_ready = device.device().version() < 3;
|
|
device.resumed();
|
|
self.pointer = Some(device);
|
|
}
|
|
let ready = self.keyboard.is_some() || self.pointer.is_some();
|
|
self.ready.store(
|
|
ready
|
|
&& (self.keyboard.is_none() || self.keyboard_ready)
|
|
&& (self.pointer.is_none() || self.pointer_ready),
|
|
Ordering::SeqCst,
|
|
);
|
|
if self.activation.is_some() || !ready {
|
|
self.signal.stop();
|
|
}
|
|
}
|
|
EisRequestSourceEvent::Request(EisRequest::Ready(request)) => {
|
|
if self
|
|
.keyboard
|
|
.as_ref()
|
|
.is_some_and(|device| device.device() == request.device.device())
|
|
{
|
|
self.keyboard_ready = true;
|
|
} else if self
|
|
.pointer
|
|
.as_ref()
|
|
.is_some_and(|device| device.device() == request.device.device())
|
|
{
|
|
self.pointer_ready = true;
|
|
} else {
|
|
self.signal.stop();
|
|
return PostAction::Remove;
|
|
}
|
|
self.ready.store(
|
|
(self.keyboard.is_none() || self.keyboard_ready) && (self.pointer.is_none() || self.pointer_ready),
|
|
Ordering::SeqCst,
|
|
);
|
|
}
|
|
EisRequestSourceEvent::Request(EisRequest::Disconnect | EisRequest::DeviceClosed(_)) => {
|
|
self.signal.stop();
|
|
return PostAction::Remove;
|
|
}
|
|
EisRequestSourceEvent::Request(_) => {
|
|
connection.disconnected(
|
|
eis::connection::DisconnectReason::Mode,
|
|
Some("Input capture does not accept injected input"),
|
|
);
|
|
self.signal.stop();
|
|
return PostAction::Remove;
|
|
}
|
|
}
|
|
PostAction::Continue
|
|
}
|
|
|
|
fn events(&mut self, events: Vec<Value>) {
|
|
for event in events {
|
|
match event["type"].as_str() {
|
|
Some("activated") => {
|
|
let activation = event["activation_id"].as_u64().unwrap_or(0) as u32;
|
|
self.activation = Some(activation);
|
|
for device in [&self.keyboard, &self.pointer].into_iter().flatten() {
|
|
device.start_emulating(activation);
|
|
}
|
|
}
|
|
Some("deactivated" | "disabled" | "closed") if self.activation.take().is_some() => {
|
|
for device in [&self.keyboard, &self.pointer].into_iter().flatten() {
|
|
device.stop_emulating();
|
|
}
|
|
}
|
|
Some("keys") if self.activation.is_some() => {
|
|
if let Some(device) = &self.keyboard
|
|
&& let Some(keyboard) = device.interface::<eis::Keyboard>()
|
|
{
|
|
for key in event["keys"].as_array().into_iter().flatten().filter_map(Value::as_u64) {
|
|
keyboard.key(key as u32, eis::keyboard::KeyState::Press);
|
|
}
|
|
device.frame(now());
|
|
}
|
|
}
|
|
Some("key") if self.activation.is_some() => {
|
|
if let Some(device) = &self.keyboard
|
|
&& let Some(keyboard) = device.interface::<eis::Keyboard>()
|
|
{
|
|
keyboard.key(
|
|
event["key"].as_u64().unwrap_or(0) as u32,
|
|
if event["pressed"] == true {
|
|
eis::keyboard::KeyState::Press
|
|
} else {
|
|
eis::keyboard::KeyState::Released
|
|
},
|
|
);
|
|
device.frame(now());
|
|
}
|
|
}
|
|
Some("motion" | "button" | "scroll") if self.activation.is_some() => {
|
|
if let Some(device) = &self.pointer {
|
|
match event["type"].as_str() {
|
|
Some("motion") => {
|
|
if let Some(pointer) = device.interface::<eis::Pointer>() {
|
|
pointer.motion_relative(
|
|
event["x"].as_f64().unwrap_or(0.0) as f32,
|
|
event["y"].as_f64().unwrap_or(0.0) as f32,
|
|
);
|
|
}
|
|
}
|
|
Some("button") => {
|
|
if let Some(button) = device.interface::<eis::Button>() {
|
|
button.button(
|
|
event["button"].as_u64().unwrap_or(0) as u32,
|
|
if event["pressed"] == true {
|
|
eis::button::ButtonState::Press
|
|
} else {
|
|
eis::button::ButtonState::Released
|
|
},
|
|
);
|
|
}
|
|
}
|
|
Some("scroll") => {
|
|
if let Some(scroll) = device.interface::<eis::Scroll>() {
|
|
let x = event["x"].as_f64().unwrap_or(0.0) as f32;
|
|
let y = event["y"].as_f64().unwrap_or(0.0) as f32;
|
|
if !event["v120_x"].is_null() || !event["v120_y"].is_null() {
|
|
scroll.scroll_discrete(
|
|
event["v120_x"].as_f64().unwrap_or(0.0) as i32,
|
|
event["v120_y"].as_f64().unwrap_or(0.0) as i32,
|
|
);
|
|
} else if x != 0.0 || y != 0.0 {
|
|
scroll.scroll(x, y);
|
|
}
|
|
let stop_x = event["stop_x"] == true;
|
|
let stop_y = event["stop_y"] == true;
|
|
if stop_x || stop_y {
|
|
scroll.scroll_stop(stop_x as u32, stop_y as u32, 0);
|
|
}
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
device.frame(now());
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
if let Some(connection) = &self.connection
|
|
&& connection.flush().is_err()
|
|
{
|
|
self.signal.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn now() -> u64 {
|
|
let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 };
|
|
// SAFETY: the initialized output pointer covers a timespec allocation.
|
|
if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } == 0 {
|
|
time.tv_sec as u64 * 1_000_000 + time.tv_nsec as u64 / 1_000
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::process::{Command, Stdio};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use serde_json::json;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
#[ignore = "requires Python and the system libei receiver library"]
|
|
fn libei_receives_keymap_motion_keyboard_and_explicit_axis_stop() {
|
|
let cancel = Arc::new(Cancel::default());
|
|
let map = "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; xkb_types { include \"complete\" }; xkb_compatibility { include \"complete\" }; xkb_symbols { include \"pc+us+inet(evdev)\" }; };";
|
|
let (worker, fd) = Worker::start(3, map, cancel.clone()).unwrap();
|
|
let fixture =
|
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/tests/fixtures/eis-receiver.py");
|
|
let mut client = Command::new("python3")
|
|
.arg(fixture)
|
|
.stdin(Stdio::from(fd))
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while !worker.ready.load(Ordering::SeqCst) && Instant::now() < deadline {
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
if !worker.ready.load(Ordering::SeqCst) {
|
|
let _ = client.kill();
|
|
let output = client.wait_with_output().unwrap();
|
|
panic!("Receiver did not bind: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
worker
|
|
.send(vec![
|
|
json!({"type":"activated", "activation_id":1}),
|
|
json!({"type":"motion", "x":10.0, "y":-5.0}),
|
|
json!({"type":"motion", "x":10.0, "y":-5.0}),
|
|
json!({"type":"key", "key":30, "pressed":true}),
|
|
json!({"type":"key", "key":30, "pressed":false}),
|
|
json!({"type":"scroll", "x":0.0, "y":4.0}),
|
|
json!({"type":"scroll", "x":0.0, "y":0.0, "stop_x":false, "stop_y":true}),
|
|
json!({"type":"deactivated"}),
|
|
])
|
|
.unwrap();
|
|
let output = client.wait_with_output().unwrap();
|
|
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
|
assert!(String::from_utf8_lossy(&output.stdout).contains("\"keymap\": true"));
|
|
let deadline = Instant::now() + Duration::from_secs(2);
|
|
while !cancel.stopped.load(Ordering::SeqCst) && Instant::now() < deadline {
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
assert!(
|
|
cancel.stopped.load(Ordering::SeqCst),
|
|
"Receiver disconnect must revoke capture"
|
|
);
|
|
}
|
|
}
|