diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 301454d..bf4d0cf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,11 +19,24 @@ jobs: - run: sudo apt-get -qq update - run: sudo apt-get install -y libxkbcommon-dev - run: cargo fmt --all -- --check + + # Make sure there are no broken intra-doc links with and without features + - run: RUSTDOCFLAGS="--deny warnings" cargo doc --no-deps --all-features + - run: RUSTDOCFLAGS="--deny warnings" cargo doc --no-deps + - run: cargo clippy --all-features -- -Dwarnings + if: matrix.rust_version != 'stable' + - run: cargo clippy --all-features --all-targets -- -Dwarnings + if: matrix.rust_version == 'stable' + + # Sanity check: generic code doesn't depend on features + - run: cargo check + - run: cargo build --all-features if: matrix.rust_version != 'stable' - run: cargo build --all-features --examples if: matrix.rust_version == 'stable' + - uses: actions/upload-artifact@v4 with: name: reis-demo-server diff --git a/Cargo.toml b/Cargo.toml index a4fe527..8aba4b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ calloop = { version = "0.14.0", optional = true } rustix = { version = "0.38.3", features = ["event", "fs", "net", "time"] } futures = { version = "0.3.28", optional = true } tokio = { version = "1.31.0", features = ["net"], optional = true } +enumflags2 = "0.7.12" [dev-dependencies] ashpd = "0.11.0" @@ -23,6 +24,20 @@ tokio = { version = "1.31.0", features = ["rt", "macros"] } signal-hook = "0.3.17" pollster = "0.4.0" +[lints.rust] +missing_docs = "warn" + +[lints.clippy] +pedantic = "warn" +# Blocked by MSRV +#allow_attributes = "warn" +#allow_attributes_without_reason = "warn" +#should_panic_without_expect = "warn" +str_to_string = "warn" +string_to_string = "warn" +cast_possible_wrap = { level = "allow", priority = 1 } +cast_possible_truncation = { level = "allow", priority = 1 } + [features] tokio = ["dep:tokio", "futures"] # Experimental and somewhat incomplete diff --git a/examples/list-devices.rs b/examples/list-devices.rs index e473312..c56f852 100644 --- a/examples/list-devices.rs +++ b/examples/list-devices.rs @@ -1,3 +1,5 @@ +//! List devices with sender context type. + use ashpd::desktop::remote_desktop::{DeviceType, RemoteDesktop}; use futures::stream::StreamExt; use reis::{ei, tokio::EiEventStream, PendingRequestResult}; @@ -20,6 +22,7 @@ struct DeviceData { } impl DeviceData { + #[allow(dead_code)] fn interface(&self) -> Option { self.interfaces.get(T::NAME)?.clone().downcast() } @@ -37,10 +40,10 @@ struct State { impl State { fn handle_event(&mut self, event: ei::Event) { match event { - ei::Event::Handshake(handshake, request) => panic!(), - ei::Event::Connection(connection, request) => match request { + ei::Event::Handshake(_handshake, _request) => panic!(), + ei::Event::Connection(_connection, request) => match request { ei::connection::Event::Seat { seat } => { - self.seats.insert(seat, Default::default()); + self.seats.insert(seat, SeatData::default()); } ei::connection::Event::Ping { ping } => { ping.done(0); @@ -71,7 +74,7 @@ impl State { } ei::seat::Event::Device { device } => { data.devices.push(device.clone()); - self.devices.insert(device, Default::default()); + self.devices.insert(device, DeviceData::default()); } _ => {} } @@ -87,7 +90,7 @@ impl State { } ei::device::Event::Interface { object } => { data.interfaces - .insert(object.interface().to_string(), object); + .insert(object.interface().to_owned(), object); } ei::device::Event::Done => { data.done = true; @@ -100,23 +103,20 @@ impl State { data.interfaces.keys().collect::>() ); } - ei::device::Event::Resumed { serial } => {} _ => {} } } - ei::Event::Callback(callback, request) => match request { - ei::callback::Event::Done { callback_data: _ } => { - // TODO: Callback being called after first device, but not later ones? - // self.print_and_exit_if_done(); - } - _ => {} - }, + ei::Event::Callback(_callback, ei::callback::Event::Done { .. }) => { + // TODO: Callback being called after first device, but not later ones? + // self.print_and_exit_if_done(); + } _ => {} } let _ = self.context.flush(); } + #[allow(dead_code)] fn print_and_exit_if_done(&self) { if !(self.seats.values().all(|x| x.done) && self.devices.values().all(|x| x.done)) { return; @@ -171,10 +171,10 @@ async fn main() { while let Some(result) = events.next().await { let event = match result.unwrap() { PendingRequestResult::Request(event) => event, - PendingRequestResult::ParseError(msg) => { + PendingRequestResult::ParseError(_msg) => { todo!() } - PendingRequestResult::InvalidObject(object_id) => { + PendingRequestResult::InvalidObject(_object_id) => { // TODO continue; } diff --git a/examples/receive-synchronous.rs b/examples/receive-synchronous.rs index 0970caf..0c1ff66 100644 --- a/examples/receive-synchronous.rs +++ b/examples/receive-synchronous.rs @@ -1,3 +1,5 @@ +//! Capturing input synchronously. + use ashpd::desktop::input_capture::{Barrier, Capabilities, InputCapture}; use pollster::FutureExt as _; use reis::{ei, event::DeviceCapability}; @@ -34,7 +36,7 @@ async fn open_connection() -> ei::Context { let x = region.x_offset(); let y = region.y_offset(); let w = region.width() as i32; - let h = region.height() as i32; + let _h = region.height() as i32; Barrier::new(NonZero::new(n as u32 + 1).unwrap(), (x, y, x + w - 1, y)) }) .collect::>(); @@ -64,25 +66,25 @@ fn main() { match &event { reis::event::EiEvent::SeatAdded(evt) => { // println!(" capabilities: {:?}", evt.seat); - evt.seat.bind_capabilities(&[ - DeviceCapability::Pointer, - DeviceCapability::PointerAbsolute, - DeviceCapability::Keyboard, - DeviceCapability::Touch, - DeviceCapability::Scroll, - DeviceCapability::Button, - ]); - context.flush(); + evt.seat.bind_capabilities( + DeviceCapability::Pointer + | DeviceCapability::PointerAbsolute + | DeviceCapability::Keyboard + | DeviceCapability::Touch + | DeviceCapability::Scroll + | DeviceCapability::Button, + ); + let _ = context.flush(); } reis::event::EiEvent::DeviceAdded(evt) => { println!(" seat: {:?}", evt.device.seat().name()); println!(" type: {:?}", evt.device.device_type()); if let Some(dimensions) = evt.device.dimensions() { - println!(" dimensions: {:?}", dimensions); + println!(" dimensions: {dimensions:?}"); } println!(" regions: {:?}", evt.device.regions()); if let Some(keymap) = evt.device.keymap() { - println!(" keymap: {:?}", keymap); + println!(" keymap: {keymap:?}"); } // Interfaces? } diff --git a/examples/receive.rs b/examples/receive.rs index e33c435..fb09c31 100644 --- a/examples/receive.rs +++ b/examples/receive.rs @@ -1,3 +1,5 @@ +//! Capturing input asynchronously. + use ashpd::desktop::input_capture::{Barrier, Capabilities, InputCapture}; use futures::stream::StreamExt; use reis::{ei, event::DeviceCapability}; @@ -12,7 +14,7 @@ async fn open_connection() -> ei::Context { let session = input_capture .create_session( None, - (Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen).into(), + Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, ) .await .unwrap() @@ -34,7 +36,7 @@ async fn open_connection() -> ei::Context { let x = region.x_offset(); let y = region.y_offset(); let w = region.width() as i32; - let h = region.height() as i32; + let _h = region.height() as i32; Barrier::new(NonZero::new(n as u32 + 1).unwrap(), (x, y, x + w - 1, y)) }) .collect::>(); @@ -65,25 +67,25 @@ async fn main() { match &event { reis::event::EiEvent::SeatAdded(evt) => { // println!(" capabilities: {:?}", evt.seat); - evt.seat.bind_capabilities(&[ - DeviceCapability::Pointer, - DeviceCapability::PointerAbsolute, - DeviceCapability::Keyboard, - DeviceCapability::Touch, - DeviceCapability::Scroll, - DeviceCapability::Button, - ]); - context.flush(); + evt.seat.bind_capabilities( + DeviceCapability::Pointer + | DeviceCapability::PointerAbsolute + | DeviceCapability::Keyboard + | DeviceCapability::Touch + | DeviceCapability::Scroll + | DeviceCapability::Button, + ); + let _ = context.flush(); } reis::event::EiEvent::DeviceAdded(evt) => { println!(" seat: {:?}", evt.device.seat().name()); println!(" type: {:?}", evt.device.device_type()); if let Some(dimensions) = evt.device.dimensions() { - println!(" dimensions: {:?}", dimensions); + println!(" dimensions: {dimensions:?}"); } println!(" regions: {:?}", evt.device.regions()); if let Some(keymap) = evt.device.keymap() { - println!(" keymap: {:?}", keymap); + println!(" keymap: {keymap:?}"); } // Interfaces? } diff --git a/examples/reis-demo-server.rs b/examples/reis-demo-server.rs index d5e9291..cdaba34 100644 --- a/examples/reis-demo-server.rs +++ b/examples/reis-demo-server.rs @@ -1,3 +1,4 @@ +//! Demo server. // TODO: Require context_type use reis::{ @@ -20,18 +21,17 @@ struct ContextState { impl ContextState { fn disconnected( - &self, connection: &Connection, reason: eis::connection::DisconnectReason, explaination: &str, ) -> calloop::PostAction { connection.disconnected(reason, explaination); - connection.flush(); + let _ = connection.flush(); calloop::PostAction::Remove } - fn protocol_error(&self, connection: &Connection, explanation: &str) -> calloop::PostAction { - self.disconnected( + fn protocol_error(connection: &Connection, explanation: &str) -> calloop::PostAction { + Self::disconnected( connection, eis::connection::DisconnectReason::Protocol, explanation, @@ -50,58 +50,49 @@ impl ContextState { EisRequest::Bind(request) => { let capabilities = request.capabilities; - // TODO Handle in converter - if capabilities & 0x7e != capabilities { - return self.disconnected( - connection, - eis::connection::DisconnectReason::Value, - "Invalid capabilities", - ); - } - let seat = self.seat.as_ref().unwrap(); if connection.has_interface("ei_keyboard") - && capabilities & 2 << DeviceCapability::Keyboard as u64 != 0 + && capabilities.contains(DeviceCapability::Keyboard) { seat.add_device( Some("keyboard"), DeviceType::Virtual, - &[DeviceCapability::Keyboard], + DeviceCapability::Keyboard.into(), |_| {}, ); } // XXX button/etc should be on same object if connection.has_interface("ei_pointer") - && capabilities & 2 << DeviceCapability::Pointer as u64 != 0 + && capabilities.contains(DeviceCapability::Pointer) { seat.add_device( Some("pointer"), DeviceType::Virtual, - &[DeviceCapability::Pointer], + DeviceCapability::Pointer.into(), |_| {}, ); } if connection.has_interface("ei_touchscreen") - && capabilities & 2 << DeviceCapability::Touch as u64 != 0 + && capabilities.contains(DeviceCapability::Touch) { seat.add_device( Some("touch"), DeviceType::Virtual, - &[DeviceCapability::Touch], + DeviceCapability::Touch.into(), |_| {}, ); } if connection.has_interface("ei_pointer_absolute") - && capabilities & 2 << DeviceCapability::PointerAbsolute as u64 != 0 + && capabilities.contains(DeviceCapability::PointerAbsolute) { seat.add_device( Some("pointer-abs"), DeviceType::Virtual, - &[DeviceCapability::PointerAbsolute], + DeviceCapability::PointerAbsolute.into(), |_| {}, ); } @@ -120,49 +111,42 @@ struct State { } impl State { + #![allow(clippy::unnecessary_wraps)] fn handle_new_connection(&mut self, context: eis::Context) -> io::Result { - println!("New connection: {:?}", context); + println!("New connection: {context:?}"); let source = EisRequestSource::new(context, 1); let mut context_state = ContextState { seat: None }; self.handle - .insert_source(source, move |event, connected_state, state| match event { - Ok(event) => Ok(state.handle_request_source_event( + .insert_source(source, move |event, connected_state, _state| match event { + Ok(event) => Ok(Self::handle_request_source_event( &mut context_state, connected_state, event, )), - Err(err) => Ok(context_state.protocol_error(connected_state, &err.to_string())), + Err(err) => { + if let reis::Error::Request(reis::request::RequestError::InvalidCapabilities) = + err + { + Ok(ContextState::disconnected( + connected_state, + eis::connection::DisconnectReason::Value, + &err.to_string(), + )) + } else { + Ok(ContextState::protocol_error( + connected_state, + &err.to_string(), + )) + } + } }) .unwrap(); Ok(calloop::PostAction::Continue) } - fn connected(&mut self, connection: &Connection) { - if !connection.has_interface("ei_seat") || !connection.has_interface("ei_device") { - connection.disconnected( - eis::connection::DisconnectReason::Protocol, - "Need `ei_seat` and `ei_device`", - ); - connection.flush(); - } - - let seat = connection.add_seat( - Some("default"), - &[ - DeviceCapability::Pointer, - DeviceCapability::PointerAbsolute, - DeviceCapability::Keyboard, - DeviceCapability::Touch, - DeviceCapability::Scroll, - DeviceCapability::Button, - ], - ); - } - fn handle_request_source_event( - &mut self, context_state: &mut ContextState, connection: &Connection, event: EisRequestSourceEvent, @@ -174,19 +158,17 @@ impl State { eis::connection::DisconnectReason::Protocol, "Need `ei_seat` and `ei_device`", ); - connection.flush(); + let _ = connection.flush(); } let seat = connection.add_seat( Some("default"), - &[ - DeviceCapability::Pointer, - DeviceCapability::PointerAbsolute, - DeviceCapability::Keyboard, - DeviceCapability::Touch, - DeviceCapability::Scroll, - DeviceCapability::Button, - ], + DeviceCapability::Pointer + | DeviceCapability::PointerAbsolute + | DeviceCapability::Keyboard + | DeviceCapability::Touch + | DeviceCapability::Scroll + | DeviceCapability::Button, ); context_state.seat = Some(seat); @@ -205,7 +187,7 @@ impl State { } } - connection.flush(); + let _ = connection.flush(); calloop::PostAction::Continue } @@ -216,7 +198,7 @@ fn main() { let handle = event_loop.handle(); let path = reis::default_socket_path().unwrap(); - std::fs::remove_file(&path); // XXX in use? + let _ = std::fs::remove_file(&path); // XXX in use? let listener = eis::Listener::bind(&path).unwrap(); let listener_source = EisListenerSource::new(listener); handle diff --git a/examples/type-text.rs b/examples/type-text.rs index f328f41..f4eaed8 100644 --- a/examples/type-text.rs +++ b/examples/type-text.rs @@ -1,3 +1,5 @@ +//! Typing text. + use ashpd::desktop::{ remote_desktop::{DeviceType, RemoteDesktop}, PersistMode, @@ -50,21 +52,22 @@ struct State { } impl State { + #![allow(clippy::unnecessary_wraps, clippy::too_many_lines)] fn handle_listener_readable( &mut self, context: &mut ei::Context, ) -> io::Result { - if let Err(_) = context.read() { + if context.read().is_err() { return Ok(calloop::PostAction::Remove); } while let Some(result) = context.pending_event() { let request = match result { PendingRequestResult::Request(request) => request, - PendingRequestResult::ParseError(msg) => { + PendingRequestResult::ParseError(_msg) => { todo!() } - PendingRequestResult::InvalidObject(object_id) => { + PendingRequestResult::InvalidObject(_object_id) => { // TODO continue; } @@ -80,10 +83,6 @@ impl State { } handshake.finish(); } - ei::handshake::Event::InterfaceVersion { - name: _, - version: _, - } => {} ei::handshake::Event::Connection { connection: _, serial, @@ -92,9 +91,9 @@ impl State { } _ => {} }, - ei::Event::Connection(connection, request) => match request { + ei::Event::Connection(_connection, request) => match request { ei::connection::Event::Seat { seat } => { - self.seats.insert(seat, Default::default()); + self.seats.insert(seat, SeatData::default()); } ei::connection::Event::Ping { ping } => { ping.done(0); @@ -115,7 +114,7 @@ impl State { // XXX } ei::seat::Event::Device { device } => { - self.devices.insert(device, Default::default()); + self.devices.insert(device, DeviceData::default()); } _ => {} } @@ -131,7 +130,7 @@ impl State { } ei::device::Event::Interface { object } => { data.interfaces - .insert(object.interface().to_string(), object); + .insert(object.interface().to_owned(), object); } ei::device::Event::Done => { if let Some(keyboard) = data.interface::() { @@ -192,39 +191,37 @@ impl State { _ => {} } } - ei::Event::Keyboard(keyboard, request) => { - match request { - ei::keyboard::Event::Keymap { - keymap_type, - size, - keymap, - } => { - // XXX format - // flags? - // handle multiple keyboard? - let context = xkb::Context::new(0); - self.keymap = Some( - unsafe { - xkb::Keymap::new_from_fd( - &context, - keymap, - size as _, - xkb::KEYMAP_FORMAT_TEXT_V1, - 0, - ) - } - .unwrap() - .unwrap(), - ); + ei::Event::Keyboard( + _keyboard, + ei::keyboard::Event::Keymap { + keymap_type: _, + size, + keymap, + }, + ) => { + // XXX format + // flags? + // handle multiple keyboard? + let context = xkb::Context::new(0); + self.keymap = Some( + unsafe { + xkb::Keymap::new_from_fd( + &context, + keymap, + size as _, + xkb::KEYMAP_FORMAT_TEXT_V1, + 0, + ) } - _ => {} - } + .unwrap() + .unwrap(), + ); } _ => {} } } - context.flush(); + let _ = context.flush(); Ok(calloop::PostAction::Continue) } @@ -259,8 +256,8 @@ fn main() { let context = futures_executor::block_on(open_connection()); // XXX wait for server version? - let handshake = context.handshake(); - context.flush(); + let _handshake = context.handshake(); + let _ = context.flush(); let context_source = Generic::new(context, calloop::Interest::READ, calloop::Mode::Level); handle .insert_source(context_source, |_event, context, state: &mut State| { diff --git a/src/calloop.rs b/src/calloop.rs index e6dee91..297cc87 100644 --- a/src/calloop.rs +++ b/src/calloop.rs @@ -1,3 +1,5 @@ +//! Module containing [`calloop`] sources. + // TODO Define an event source that reads socket and produces eis::Event // - is it easy to compose and wrap with handshaker, event handler? // produce an event of some kind on disconnect/eof? @@ -11,12 +13,15 @@ use crate::{ Error, PendingRequestResult, }; +/// [`calloop`] source that receives EI connections by listening on a socket. #[derive(Debug)] pub struct EisListenerSource { source: Generic, } impl EisListenerSource { + /// Creates a new EIS listener source. + #[must_use] pub fn new(listener: eis::Listener) -> Self { Self { source: Generic::new(listener, Interest::READ, Mode::Level), @@ -159,6 +164,7 @@ enum State { Connected(ConnectedContextState), } +/// [`calloop`] source that receives EI protocol requests. #[derive(Debug)] pub struct EisRequestSource { source: Generic, @@ -166,6 +172,8 @@ pub struct EisRequestSource { } impl EisRequestSource { + /// Creates a new EIS request source. + #[must_use] pub fn new(context: eis::Context, initial_serial: u32) -> Self { let handshaker = crate::handshake::EisHandshaker::new(&context, initial_serial); Self { @@ -241,9 +249,13 @@ impl calloop::EventSource for EisRequestSource { } // TODO +/// Event returned by [`EisRequestSource`]. #[derive(Debug)] pub enum EisRequestSourceEvent { + /// Handshake has finished. Connected, + /// High-level request to EIS. Request(request::EisRequest), + /// Invalid object ID. InvalidObject(u64), } diff --git a/src/ei.rs b/src/ei.rs index 991238a..2025a19 100644 --- a/src/ei.rs +++ b/src/ei.rs @@ -44,11 +44,15 @@ impl Context { /// use std::os::unix::net::UnixStream; /// use reis::ei::Context; /// - /// let socket = UnixStream::connect("/example/path"); + /// let socket = UnixStream::connect("/example/path").unwrap(); /// // Or receive from, for example, the RemoteDesktop XDG desktop protal. /// /// let context = Context::new(socket).unwrap(); /// ``` + /// + /// # Errors + /// + /// Will return `Err` if setting the socket to non-blocking mode fails. pub fn new(socket: UnixStream) -> io::Result { Ok(Self(Backend::new(socket, true)?)) } @@ -63,6 +67,11 @@ impl Context { /// /// let context = Context::connect_to_env().expect("Shouldn't error").unwrap(); /// ``` + /// + /// # Errors + /// + /// Will return `Err` if the resolved socket path cannot be connected to or if + /// [`Context::new`] fails. pub fn connect_to_env() -> io::Result> { let Some(path) = env::var_os("LIBEI_SOCKET") else { // XXX return error type @@ -87,6 +96,10 @@ impl Context { /// Reads any pending data on the socket into the internal buffer. /// /// Returns `UnexpectedEof` if end-of-file is reached. + /// + /// # Errors + /// + /// Will return `Err` if there is an I/O error. pub fn read(&self) -> io::Result { self.0.read() } @@ -98,11 +111,17 @@ impl Context { } /// Returns the interface proxy for the `ei_handshake` object. + #[must_use] + #[allow(clippy::missing_panics_doc)] // infallible; Backend always creates ei_handshake object at 0 pub fn handshake(&self) -> handshake::Handshake { self.0.object_for_id(0).unwrap().downcast_unchecked() } /// Sends buffered messages. Call after you're finished with sending requests. + /// + /// # Errors + /// + /// An error will be returned if sending the buffered messages fails. pub fn flush(&self) -> rustix::io::Result<()> { self.0.flush() } diff --git a/src/eiproto.rs.jinja b/src/eiproto.rs.jinja index c455690..88c066b 100644 --- a/src/eiproto.rs.jinja +++ b/src/eiproto.rs.jinja @@ -1,5 +1,24 @@ {# ei-scanner jinja template, for `` #} -#![allow(unknown_lints, unused_imports, unused_parens, clippy::useless_conversion, clippy::double_parens, clippy::match_single_binding, clippy::unused_unit, clippy::empty_docs, clippy::doc_lazy_continuation)] +#![allow( + unknown_lints, + unused_imports, + unused_parens, + clippy::useless_conversion, + clippy::double_parens, + clippy::match_single_binding, + clippy::unused_unit, + clippy::empty_docs, + clippy::doc_lazy_continuation, + + // Explicitly set to warn + clippy::doc_markdown, + clippy::must_use_candidate, + clippy::semicolon_if_nothing_returned, + clippy::used_underscore_binding, + clippy::match_same_arms, + clippy::str_to_string, + missing_docs, +)] // GENERATED FILE diff --git a/src/eiproto_ei.rs b/src/eiproto_ei.rs index 898248e..d27b748 100644 --- a/src/eiproto_ei.rs +++ b/src/eiproto_ei.rs @@ -7,7 +7,16 @@ clippy::match_single_binding, clippy::unused_unit, clippy::empty_docs, - clippy::doc_lazy_continuation + clippy::doc_lazy_continuation, + + // Explicitly set to warn + clippy::doc_markdown, + clippy::must_use_candidate, + clippy::semicolon_if_nothing_returned, + clippy::used_underscore_binding, + clippy::match_same_arms, + clippy::str_to_string, + missing_docs, )] // GENERATED FILE diff --git a/src/eiproto_eis.rs b/src/eiproto_eis.rs index 8ab402d..73eba02 100644 --- a/src/eiproto_eis.rs +++ b/src/eiproto_eis.rs @@ -7,7 +7,16 @@ clippy::match_single_binding, clippy::unused_unit, clippy::empty_docs, - clippy::doc_lazy_continuation + clippy::doc_lazy_continuation, + + // Explicitly set to warn + clippy::doc_markdown, + clippy::must_use_candidate, + clippy::semicolon_if_nothing_returned, + clippy::used_underscore_binding, + clippy::match_same_arms, + clippy::str_to_string, + missing_docs, )] // GENERATED FILE diff --git a/src/eis.rs b/src/eis.rs index 8cadd01..228fb77 100644 --- a/src/eis.rs +++ b/src/eis.rs @@ -29,6 +29,11 @@ pub struct Listener { impl Listener { // TODO Use a lock here /// Listens on a specific path. + /// + /// # Errors + /// + /// Will return `Err` if binding to the given path or setting the socket to + /// non-blocking mode fails. pub fn bind(path: &Path) -> io::Result { Self::bind_inner(PathBuf::from(path), None) } @@ -36,6 +41,11 @@ impl Listener { // XXX result type? // Error if XDG_RUNTIME_DIR not set? /// Listens on a file in `XDG_RUNTIME_DIR`. + /// + /// # Errors + /// + /// Will return `Err` if a lock file cannot be locked, binding to the generated path + /// fails or setting the socket to non-blocking mode fails. pub fn bind_auto() -> io::Result> { let xdg_dir = if let Some(var) = env::var_os("XDG_RUNTIME_DIR") { PathBuf::from(var) @@ -65,6 +75,10 @@ impl Listener { } /// Accepts a connection from a client. Returns `Ok(Some(_)` if an incoming connection is ready, and `Ok(None)` if there is no connection ready (would block). + /// + /// # Errors + /// + /// Will return `Err` if [`Context::new`] fails. pub fn accept(&self) -> io::Result> { match self.listener.accept() { Ok((socket, _)) => Ok(Some(Context::new(socket)?)), @@ -116,6 +130,10 @@ impl Context { /// /// // Pass the `b` file descriptor to implement the RemoteDesktop XDG desktop portal /// ``` + /// + /// # Errors + /// + /// Will return `Err` if setting the socket to non-blocking mode fails. pub fn new(socket: UnixStream) -> io::Result { Ok(Self(Backend::new(socket, false)?)) } @@ -123,6 +141,10 @@ impl Context { /// Reads any pending data on the socket into the internal buffer. /// /// Returns `UnexpectedEof` if end-of-file is reached. + /// + /// # Errors + /// + /// Will return `Err` if there is an I/O error. pub fn read(&self) -> io::Result { self.0.read() } @@ -134,11 +156,17 @@ impl Context { } /// Returns the interface proxy for the `ei_handshake` object. + #[must_use] + #[allow(clippy::missing_panics_doc)] // infallible; Backend always creates ei_handshake object at 0 pub fn handshake(&self) -> handshake::Handshake { self.0.object_for_id(0).unwrap().downcast_unchecked() } /// Sends buffered messages. Call after you're finished with sending events. + /// + /// # Errors + /// + /// An error will be returned if sending the buffered messages fails. pub fn flush(&self) -> rustix::io::Result<()> { self.0.flush() } diff --git a/src/error.rs b/src/error.rs index 55c6107..599f57e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,15 +1,23 @@ -use crate::{event::EventError, handshake::HandshakeError, ParseError}; +use crate::{event::EventError, handshake::HandshakeError, request::RequestError, ParseError}; use std::{fmt, io}; /// An error coming from the `reis` crate #[derive(Debug)] pub enum Error { + /// Handshake event after handshake. UnexpectedHandshakeEvent, + /// Invalid interface version InvalidInterfaceVersion(&'static str, u32), // TODO better error type here? + /// Protocol error caused by server. Event(EventError), + /// Protocol error caused by client. + Request(RequestError), + /// Wire format parse error. Parse(ParseError), + /// Handshake error. Handshake(HandshakeError), + /// I/O error. Io(io::Error), } @@ -21,6 +29,7 @@ impl fmt::Display for Error { write!(f, "invalid version {version} for interface '{interface}'") } Self::Event(err) => write!(f, "event error: {err}"), + Self::Request(err) => write!(f, "request error: {err}"), Self::Io(err) => write!(f, "IO error: {err}"), Self::Handshake(err) => write!(f, "handshake error: {err}"), Self::Parse(err) => write!(f, "parse error: {err}"), @@ -34,6 +43,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: RequestError) -> Self { + Self::Request(err) + } +} + impl From for Error { fn from(err: ParseError) -> Self { Self::Parse(err) diff --git a/src/event.rs b/src/event.rs index 8239d99..b157208 100644 --- a/src/event.rs +++ b/src/event.rs @@ -16,6 +16,8 @@ #![allow(clippy::derive_partial_eq_without_eq)] +use enumflags2::BitFlags; + use crate::{ei, handshake::HandshakeResp, util, Error, Interface, Object, PendingRequestResult}; use std::{ collections::{HashMap, VecDeque}, @@ -27,14 +29,23 @@ use std::{ }, }; +/// Protocol errors of the server. #[derive(Debug)] pub enum EventError { + /// Non-setup device event before `done`. DeviceEventBeforeDone, + /// Device setup event after `done`. DeviceSetupEventAfterDone, + /// Seat setup event after `done`. SeatSetupEventAfterDone, + /// Non-setup seat event before `done`. SeatEventBeforeDone, + /// `ei_device.device_type` was not sent before `done`. NoDeviceType, + /// Handshake event after handshake. UnexpectedHandshakeEvent, + /// Non-negotiated interface advertised in `ei_device.capability`. + UnknownCapabilityInterface, } impl fmt::Display for EventError { @@ -46,6 +57,9 @@ impl fmt::Display for EventError { Self::SeatEventBeforeDone => write!(f, "seat event before done"), Self::NoDeviceType => write!(f, "no device"), Self::UnexpectedHandshakeEvent => write!(f, "unexpected handshake event"), + Self::UnknownCapabilityInterface => { + write!(f, "unknown interface in ei_seat.capability") + } } } } @@ -66,11 +80,16 @@ pub struct Connection(Arc); impl Connection { /// Returns the interface proxy for the underlying `ei_connection` object. + #[must_use] pub fn connection(&self) -> &ei::Connection { &self.0.handshake_resp.connection } /// Sends buffered messages. Call after you're finished with sending requests. + /// + /// # Errors + /// + /// An error will be returned if sending the buffered messages fails. pub fn flush(&self) -> rustix::io::Result<()> { self.0.context.flush() } @@ -82,6 +101,7 @@ impl Connection { // TODO(axka, 2025-07-08): specify in the function name that this is the last serial from // the server, and not the client, and create a function for the other way around. /// Returns the last serial number used in an event by the server. + #[must_use] pub fn serial(&self) -> u32 { self.0.serial.load(Ordering::Relaxed) } @@ -102,6 +122,8 @@ pub struct EiEventConverter { } impl EiEventConverter { + /// Creates a new converter. + #[must_use] pub fn new(context: &ei::Context, handshake_resp: HandshakeResp) -> Self { Self { pending_seats: HashMap::new(), @@ -120,6 +142,8 @@ impl EiEventConverter { } } + /// Returns a handle to the connection used by this converer. + #[must_use] pub fn connection(&self) -> &Connection { &self.connection } @@ -143,6 +167,13 @@ impl EiEventConverter { } } + /// Handles a low-level protocol-level [`ei::Event`], possibly converting it into a high-level + /// [`EiEvent`]. + /// + /// # Errors + /// + /// The errors returned are protocol violations. + #[allow(clippy::too_many_lines)] // Handler is allowed to be big pub fn handle_event(&mut self, event: ei::Event) -> Result<(), EventError> { match event { ei::Event::Handshake(_handshake, _event) => { @@ -153,9 +184,9 @@ impl EiEventConverter { self.pending_seats.insert( seat.clone(), SeatInner { - seat, + proto_seat: seat, name: None, - capabilities: HashMap::new(), + capability_map: CapabilityMap::default(), }, ); } @@ -205,7 +236,11 @@ impl EiEventConverter { .pending_seats .get_mut(&seat) .ok_or(EventError::SeatSetupEventAfterDone)?; - seat.capabilities.insert(interface, mask); + seat.capability_map.set( + DeviceCapability::from_interface_name(&interface) + .ok_or(EventError::UnknownCapabilityInterface)?, + mask, + ); } ei::seat::Event::Done => { let seat = self @@ -213,7 +248,7 @@ impl EiEventConverter { .remove(&seat) .ok_or(EventError::SeatSetupEventAfterDone)?; let seat = Seat(Arc::new(seat)); - self.seats.insert(seat.0.seat.clone(), seat.clone()); + self.seats.insert(seat.0.proto_seat.clone(), seat.clone()); self.queue_event(EiEvent::SeatAdded(SeatAdded { seat })); } ei::seat::Event::Device { device } => { @@ -266,7 +301,7 @@ impl EiEventConverter { .ok_or(EventError::DeviceSetupEventAfterDone)?; device .interfaces - .insert(object.interface().to_string(), object); + .insert(object.interface().to_owned(), object); } ei::device::Event::Dimensions { width, height } => { let device = self @@ -596,10 +631,12 @@ impl EiEventConverter { Ok(()) } + /// Returns the next queued request if one exists. pub fn next_event(&mut self) -> Option { self.events.pop_front() } + /// Adds a function to execute when the server informs that the the associated request is done. pub fn add_callback_handler( &mut self, callback: ei::Callback, @@ -637,23 +674,30 @@ pub struct Keymap { pub type_: ei::keyboard::KeymapType, } -/// A capability of a seat used when advertising seats and binding to capabilities in seats. -// TODO: bitflags? -// TODO(axka, 2025-07-08): rename to SeatCapability? -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +/// Capabilities of devices used when advertising seats and devices, and binding to capabilities in seats. +#[enumflags2::bitflags] #[repr(u64)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub enum DeviceCapability { - Pointer, - PointerAbsolute, - Keyboard, - Touch, - Scroll, - Button, + /// Capability for relative pointer motion. + Pointer = 1 << 0, + /// Capability for absolute pointer motion. + PointerAbsolute = 1 << 1, + /// Capability for keyboard input events. + Keyboard = 1 << 2, + /// Capability for touch input events. + Touch = 1 << 3, + /// Capability for scroll input events. + Scroll = 1 << 4, + /// Capability for mouse button input events. + Button = 1 << 5, } impl DeviceCapability { - /// Returns the name of the interface for this capability. - pub(crate) fn name(self) -> &'static str { + /// Returns the name of the interface. + /// + /// `None` is returned if none of the flags match + pub(crate) fn interface_name(self) -> &'static str { match self { DeviceCapability::Pointer => ei::Pointer::NAME, DeviceCapability::PointerAbsolute => ei::PointerAbsolute::NAME, @@ -663,12 +707,49 @@ impl DeviceCapability { DeviceCapability::Button => ei::Button::NAME, } } + + /// Returns the capability for the interface. + /// + /// `None` is returned if there is no match + pub(crate) fn from_interface_name(interface_name: &str) -> Option { + match interface_name { + ei::Pointer::NAME => Some(DeviceCapability::Pointer), + ei::PointerAbsolute::NAME => Some(DeviceCapability::PointerAbsolute), + ei::Keyboard::NAME => Some(DeviceCapability::Keyboard), + ei::Touchscreen::NAME => Some(DeviceCapability::Touch), + ei::Scroll::NAME => Some(DeviceCapability::Scroll), + ei::Button::NAME => Some(DeviceCapability::Button), + _ => None, + } + } + + /// Returns the binary logarithm of the capability's bitwise value, useful for indexing + /// lookup tables. + fn index(self) -> usize { + (self as u64).trailing_zeros() as usize + } +} + +/// Lookup table from [`DeviceCapability`] to a protocol capability. +#[derive(Clone, Copy, PartialEq, Eq, Default)] +struct CapabilityMap([u64; BitFlags::::ALL.bits_c().count_ones() as usize]); + +impl CapabilityMap { + /// Returns the matching protocol capability for the library capability. Defaults to 0. + fn get(&self, capability: DeviceCapability) -> u64 { + self.0[capability.index()] + } + + /// Sets the protocol capability for the library capability. + fn set(&mut self, capability: DeviceCapability, proto_capability: u64) { + self.0[capability.index()] = proto_capability; + } } struct SeatInner { - seat: ei::Seat, + proto_seat: ei::Seat, name: Option, - capabilities: HashMap, + capability_map: CapabilityMap, } /// High-level client-side wrapper for `ei_seat`. @@ -695,26 +776,27 @@ impl Eq for Seat {} impl std::hash::Hash for Seat { fn hash(&self, state: &mut H) { - self.0.seat.0.id().hash(state); + self.0.proto_seat.0.id().hash(state); } } impl Seat { /// Returns the name of the seat, as provided by the server. + #[must_use] pub fn name(&self) -> Option<&str> { self.0.name.as_deref() } // TODO has_capability - pub fn bind_capabilities(&self, capabilities: &[DeviceCapability]) { - let mut caps = 0; - for i in capabilities { - if let Some(value) = self.0.capabilities.get(i.name()) { - caps |= value; - } + /// Binds to a selection of the advertised capabilities received through + /// [`EiEvent::SeatAdded`]. + pub fn bind_capabilities(&self, capabilities: BitFlags) { + let mut proto_caps = 0; + for cap in capabilities { + proto_caps |= self.0.capability_map.get(cap); } - self.0.seat.bind(caps); + self.0.proto_seat.bind(proto_caps); } // TODO: mirror C API more? @@ -751,36 +833,44 @@ impl fmt::Debug for Device { impl Device { /// Returns the high-level [`Seat`] wrapper for the device. + #[must_use] pub fn seat(&self) -> &Seat { &self.0.seat } /// Returns the interface proxy for the underlying `ei_device` object. + #[must_use] pub fn device(&self) -> &ei::Device { &self.0.device } /// Returns the name of the device, as provided by the server. + #[must_use] pub fn name(&self) -> Option<&str> { self.0.name.as_deref() } /// Returns the device's type. + #[must_use] + #[allow(clippy::missing_panics_doc)] // EiEventConverter makes sure to not return Device if device_type is None pub fn device_type(&self) -> ei::device::DeviceType { self.0.device_type.unwrap() } /// Returns the device's dimensions, if applicable. + #[must_use] pub fn dimensions(&self) -> Option<(u32, u32)> { self.0.dimensions } /// Returns the device's regions. + #[must_use] pub fn regions(&self) -> &[Region] { &self.0.regions } /// Returns the device's keymap, if applicable. + #[must_use] pub fn keymap(&self) -> Option<&Keymap> { self.0.keymap.as_ref() } @@ -788,13 +878,15 @@ impl Device { /// Returns an interface proxy if it is implemented for this device. /// /// Interfaces of devices are implemented, such that there is one `ei_device` object and other objects (for example `ei_keyboard`) denoting capabilities. + #[must_use] pub fn interface(&self) -> Option { self.0.interfaces.get(T::NAME)?.clone().downcast() } /// Returns `true` if this device has an interface matching the provided capability. + #[must_use] pub fn has_capability(&self, capability: DeviceCapability) -> bool { - self.0.interfaces.contains_key(capability.name()) + self.0.interfaces.contains_key(capability.interface_name()) } } @@ -814,6 +906,7 @@ impl std::hash::Hash for Device { /// Enum containing all possible events the high-level utilities will give for a client implementation to handle. #[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] // Inner types have docs pub enum EiEvent { // Connected, Disconnected(Disconnected), @@ -878,14 +971,18 @@ impl EiEvent { /// High-level translation of [`ei_connection.disconnected`](ei::connection::Event::Disconnected). #[derive(Clone, Debug, PartialEq)] pub struct Disconnected { + /// Last serial sent by the EIS implementation. pub last_serial: u32, + /// Reason for disconnection. pub reason: ei::connection::DisconnectReason, + /// Explanation for debugging purposes. pub explanation: String, } /// High-level translation of the seat description events ending with [`ei_seat.done`](ei::seat::Event::Done). #[derive(Clone, Debug, PartialEq)] pub struct SeatAdded { + /// High-level [`Seat`] wrapper for the seat that was added. pub seat: Seat, } @@ -895,6 +992,7 @@ pub struct SeatAdded { /// in an event. #[derive(Clone, Debug, PartialEq)] pub struct SeatRemoved { + /// High-level [`Seat`] wrapper for the seat that was removed. pub seat: Seat, } @@ -920,6 +1018,7 @@ pub struct DeviceRemoved { pub struct DevicePaused { /// High-level [`Device`] wrapper. pub device: Device, + /// The event's serial number. pub serial: u32, } @@ -928,6 +1027,7 @@ pub struct DevicePaused { pub struct DeviceResumed { /// High-level [`Device`] wrapper. pub device: Device, + /// The event's serial number. pub serial: u32, } @@ -936,10 +1036,15 @@ pub struct DeviceResumed { pub struct KeyboardModifiers { /// High-level [`Device`] wrapper. pub device: Device, + /// The event's serial number. pub serial: u32, + /// Mask of modifiers in the depressed state. pub depressed: u32, + /// Mask of modifiers in the latched state. pub latched: u32, + /// Mask of modifiers in the locked state. pub locked: u32, + /// The current group or layout index in the keymap. pub group: u32, } @@ -948,6 +1053,7 @@ pub struct KeyboardModifiers { pub struct Frame { /// High-level [`Device`] wrapper. pub device: Device, + /// The event's serial number. pub serial: u32, /// Timestamp in microseconds. pub time: u64, @@ -958,7 +1064,9 @@ pub struct Frame { pub struct DeviceStartEmulating { /// High-level [`Device`] wrapper. pub device: Device, + /// The event's serial number. pub serial: u32, + /// The event's sequence number. pub sequence: u32, } @@ -967,6 +1075,7 @@ pub struct DeviceStartEmulating { pub struct DeviceStopEmulating { /// High-level [`Device`] wrapper. pub device: Device, + /// The event's serial number. pub serial: u32, } @@ -977,7 +1086,9 @@ pub struct PointerMotion { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Relative motion on the X axis. pub dx: f32, + /// Relative motion on the Y axis. pub dy: f32, } @@ -988,7 +1099,9 @@ pub struct PointerMotionAbsolute { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Absolute position on the X axis. pub dx_absolute: f32, + /// Absolute position on the Y axis. pub dy_absolute: f32, } @@ -999,7 +1112,9 @@ pub struct Button { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Button code, as in Linux's `input-event-codes.h`. pub button: u32, + /// State of the button. pub state: ei::button::ButtonState, } @@ -1010,7 +1125,9 @@ pub struct ScrollDelta { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Motion on the X axis. pub dx: f32, + /// Motion on the Y axis. pub dy: f32, } @@ -1021,7 +1138,9 @@ pub struct ScrollStop { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Whether motion on the X axis stopped. pub x: bool, + /// Whether motion on the Y axis stopped. pub y: bool, } @@ -1032,7 +1151,9 @@ pub struct ScrollCancel { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Whether motion on the X axis was canceled. pub x: bool, + /// Whether motion on the Y axis was canceled. pub y: bool, } @@ -1043,7 +1164,9 @@ pub struct ScrollDiscrete { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Discrete motion on the X axis. pub discrete_dx: i32, + /// Discrete motion on the Y axis. pub discrete_dy: i32, } @@ -1069,7 +1192,9 @@ pub struct TouchDown { pub time: u64, /// Unique touch ID, defined in this event. pub touch_id: u32, + /// Absolute position on the X axis. pub x: f32, + /// Absolute position on the Y axis. pub y: f32, } @@ -1082,7 +1207,9 @@ pub struct TouchMotion { pub time: u64, /// Unique touch ID, defined in [`TouchDown`]. pub touch_id: u32, + /// Absolute position on the X axis. pub x: f32, + /// Absolute position on the Y axis. pub y: f32, } @@ -1217,7 +1344,7 @@ impl Iterator for EiConvertEventIterator { Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => return None, Err(err) => return Some(Err(err.into())), Ok(_) => {} - }; + } while let Some(result) = self.context.pending_event() { let request = match result { PendingRequestResult::Request(request) => request, @@ -1239,6 +1366,11 @@ impl Iterator for EiConvertEventIterator { } impl ei::Context { + /// Executes the handshake in blocking mode. + /// + /// # Errors + /// + /// Will return `Err` if there is an I/O error or a protocol violation. pub fn handshake_blocking( &self, name: &str, diff --git a/src/handshake.rs b/src/handshake.rs index 876f7be..9f1973a 100644 --- a/src/handshake.rs +++ b/src/handshake.rs @@ -1,4 +1,6 @@ -// Generic `EiHandshaker` can be used in async or sync code +//! Module implementing the EI protocol handshake. +//! +//! The generic [`EiHandshaker`] can be used in async and sync code. use crate::{ei, eis, util, Error, PendingRequestResult}; use std::{collections::HashMap, error, fmt, mem, sync::OnceLock}; @@ -27,19 +29,29 @@ fn interfaces() -> &'static HashMap<&'static str, u32> { }) } +/// Handshake response. #[derive(Clone, Debug)] pub struct HandshakeResp { + /// Global `ei_connection` singleton object. pub connection: ei::Connection, + /// Serial number of `ei_handshake.connection`. pub serial: u32, + /// Interfaces along with their versions negotiated in the handshake. pub negotiated_interfaces: HashMap, } +/// Error during handshake. #[derive(Debug)] pub enum HandshakeError { + /// Invalid object ID. InvalidObject(u64), + /// Non-handshake event. NonHandshakeEvent, + /// Missing required interface. MissingInterface, + /// Duplicate event. DuplicateEvent, + /// No [`ContextType`](ei::handshake::ContextType) sent in handshake. NoContextType, } @@ -57,6 +69,7 @@ impl fmt::Display for HandshakeError { impl error::Error for HandshakeError {} +/// Implementation of the EI protocol handshake on the client side. pub struct EiHandshaker<'a> { name: &'a str, context_type: ei::handshake::ContextType, @@ -64,6 +77,8 @@ pub struct EiHandshaker<'a> { } impl<'a> EiHandshaker<'a> { + /// Creates a client-side handshaker. + #[must_use] pub fn new(name: &'a str, context_type: ei::handshake::ContextType) -> Self { Self { name, @@ -72,6 +87,11 @@ impl<'a> EiHandshaker<'a> { } } + /// Handles the given event, possibly returning a filled handshake response. + /// + /// # Errors + /// + /// The errors returned are protocol violations. pub fn handle_event( &mut self, event: ei::Event, @@ -84,7 +104,7 @@ impl<'a> EiHandshaker<'a> { handshake.handshake_version(1); handshake.name(self.name); handshake.context_type(self.context_type); - for (interface, version) in interfaces().iter() { + for (interface, version) in interfaces() { handshake.interface_version(interface, *version); } handshake.finish(); @@ -119,6 +139,11 @@ pub(crate) fn request_result(result: PendingRequestResult) -> Result, + /// Context type of connection. pub context_type: eis::handshake::ContextType, + /// Interfaces along with their versions negotiated in the handshake. pub negotiated_interfaces: HashMap, } +/// Implementation of the EI protocol handshake on the server side. #[derive(Debug)] pub struct EisHandshaker { name: Option, @@ -154,6 +185,8 @@ pub struct EisHandshaker { } impl EisHandshaker { + /// Creates a server-side handshaker. + #[must_use] pub fn new(context: &eis::Context, initial_serial: u32) -> Self { let handshake = context.handshake(); handshake.handshake_version(1); @@ -168,6 +201,11 @@ impl EisHandshaker { } } + /// Handles the given request, possibly returning a filled handshake response. + /// + /// # Errors + /// + /// The errors returned are protocol violations. pub fn handle_request( &mut self, request: eis::Request, @@ -193,11 +231,11 @@ impl EisHandshaker { if let Some((interface, server_version)) = interfaces().get_key_value(name.as_str()) { self.negotiated_interfaces - .insert(interface.to_string(), version.min(*server_version)); + .insert((*interface).to_owned(), version.min(*server_version)); } } eis::handshake::Request::Finish => { - for (interface, version) in self.negotiated_interfaces.iter() { + for (interface, version) in &self.negotiated_interfaces { handshake.interface_version(interface, *version); } diff --git a/src/lib.rs b/src/lib.rs index 1dddb75..51fe37b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,19 @@ +//! Reis 🍚 provides a Rust version of EI 🥚 and EIS 🍨 for emulated input on Wayland. +//! +//! See the upstream project [libei](https://gitlab.freedesktop.org/libinput/libei) for more information. +//! +//! This library is currently **incomplete** and subject to change. It should probably do more to provide a more high level API that handles the things a client/server needs to deal with. +//! +//! Setting the env var `REIS_DEBUG` will make the library print ei messages it sends and receives. +//! +//! # Features +//! +//! `reis` has the following Cargo features: +//! +//! - `tokio`: Enables tokio support for clients. +//! - `calloop`: Enables calloop sources for EIS implementations. Somewhat experimental and +//! incomplete. + #![forbid(unsafe_code)] // TODO split up @@ -38,6 +54,8 @@ mod private { // XXX // Want to fallback to higher number if exists, on server? // create on server, not client. +/// Returns the default socket path for EIS. +#[must_use] pub fn default_socket_path() -> Option { let mut path = PathBuf::from(env::var_os("XDG_RUNTIME_DIR")?); path.push("eis-0"); diff --git a/src/object.rs b/src/object.rs index 3646570..9fdbde0 100644 --- a/src/object.rs +++ b/src/object.rs @@ -30,7 +30,7 @@ impl Eq for Object {} impl hash::Hash for Object { fn hash(&self, hasher: &mut H) { - self.0.id.hash(hasher) + self.0.id.hash(hasher); } } @@ -52,8 +52,8 @@ impl Object { ) -> Self { Self(Arc::new(ObjectInner { backend, - id, client_side, + id, interface, version, })) @@ -62,6 +62,7 @@ impl Object { /// Returns a handle to the backend. /// /// Returns `None` if the backend has been destroyed. + #[must_use] pub fn backend(&self) -> Option { self.0.backend.upgrade() } @@ -74,6 +75,7 @@ impl Object { /// Returns `true` if the backend has this object, and `false` otherwise or if the backend /// has been destroyed. + #[must_use] pub fn is_alive(&self) -> bool { if let Some(backend) = self.backend() { backend.has_object_for_id(self.id()) @@ -84,6 +86,7 @@ impl Object { /// Returns the object's /// [ID](https://libinput.pages.freedesktop.org/libei/doc/types/index.html#object-ids). + #[must_use] pub fn id(&self) -> u64 { self.0.id } @@ -92,11 +95,13 @@ impl Object { /// /// Interface names for new objects aren't usually transmitted, but rather come from /// the protocol definition. + #[must_use] pub fn interface(&self) -> &str { &self.0.interface } /// Returns the version of the interface of this object. + #[must_use] pub fn version(&self) -> u32 { self.0.version } @@ -139,6 +144,7 @@ impl Object { /// keyboard.key(0x41, KeyState::Press); /// ``` // TODO(axka, 2025-07-02): return Result + #[must_use] pub fn downcast(self) -> Option { if (self.0.client_side, self.interface()) == (T::CLIENT_SIDE, T::NAME) { Some(self.downcast_unchecked()) diff --git a/src/request.rs b/src/request.rs index d624eb2..fb0e90c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -4,6 +4,8 @@ // TODO: rename/reorganize things; doc comments on public types/methods +use enumflags2::{BitFlag, BitFlags}; + use crate::{ ei::connection::DisconnectReason, eis, handshake::EisHandshakeResp, wire::Interface, Error, Object, @@ -16,6 +18,20 @@ use std::{ pub use crate::event::DeviceCapability; +/// Protocol errors of the client. +#[derive(Debug)] +pub enum RequestError { + /// Invalid capabilities in `ei_seat.bind`. + InvalidCapabilities, +} +impl fmt::Display for RequestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::InvalidCapabilities => write!(f, "Invalid capabilities"), + } + } +} + #[derive(Debug)] struct ConnectionInner { context: eis::Context, @@ -32,6 +48,7 @@ pub struct Connection(Arc); impl Connection { /// Returns the interface proxy for the underlying `ei_connection` object. + #[must_use] pub fn connection(&self) -> &eis::Connection { &self.0.handshake_resp.connection } @@ -44,6 +61,10 @@ impl Connection { /// /// When a client is disconnected on purpose, for example after a user interaction, /// `reason` must be [`DisconnectReason::Disconnected`], and `explanation` must be `None`. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. // TODO(axka, 2025-07-08): rename to something imperative like `notify_disconnection` // TODO(axka, 2025-07-08): `explanation` must support NULL: https://gitlab.freedesktop.org/libinput/libei/-/commit/267716a7609730914b24adf5860ec8d2cf2e7636 pub fn disconnected(&self, reason: DisconnectReason, explanation: &str) { @@ -63,6 +84,10 @@ impl Connection { } /// Sends buffered messages. Call after you're finished with sending events. + /// + /// # Errors + /// + /// An error will be returned if sending the buffered messages fails. pub fn flush(&self) -> rustix::io::Result<()> { self.0.context.flush() } @@ -71,17 +96,20 @@ impl Connection { /// /// That is — whether the client emulates input events via requests or receives /// input events. + #[must_use] pub fn context_type(&self) -> eis::handshake::ContextType { self.0.handshake_resp.context_type } /// Returns the human-readable name of the client. + #[must_use] pub fn name(&self) -> Option<&str> { self.0.handshake_resp.name.as_deref() } // Use type instead of string? - /// Returns `true` if the connection supports the named interface. + /// Returns `true` if the connection has negotiated support for the named interface. + #[must_use] pub fn has_interface(&self, interface: &str) -> bool { self.0 .handshake_resp @@ -91,6 +119,7 @@ impl Connection { /// Returns the version of the named interface if it's supported on this /// connection. Otherwise returns `None`. + #[must_use] pub fn interface_version(&self, interface: &str) -> Option { self.0 .handshake_resp @@ -102,11 +131,20 @@ impl Connection { // TODO(axka, 2025-07-08): specify in the function name that this is the last serial from // the server, and not the client, and create a function for the other way around. /// Returns the last serial used in an event sent by the server. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. + #[must_use] pub fn last_serial(&self) -> u32 { *self.0.last_serial.lock().unwrap() } /// Increments the current serial and runs the provided callback with it. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. pub fn with_next_serial T>(&self, cb: F) -> T { let mut last_serial = self.0.last_serial.lock().unwrap(); let serial = last_serial.wrapping_add(1); @@ -125,20 +163,35 @@ impl Connection { } /// Adds a seat to the connection. - pub fn add_seat(&self, name: Option<&str>, capabilities: &[DeviceCapability]) -> Seat { + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. + #[must_use] + pub fn add_seat(&self, name: Option<&str>, capabilities: BitFlags) -> Seat { let seat = self.connection().seat(1); if let Some(name) = name { seat.name(name); } + for capability in capabilities { - // TODO only send negotiated interfaces - seat.capability(2 << *capability as u64, capability.name()); + let interface_name = capability.interface_name(); + + if !self.has_interface(interface_name) { + // Not negotiated + continue; + } + + // Using bitflag value because as the server we control its meaning + seat.capability(capability as u64, interface_name); } + seat.done(); let seat = Seat(Arc::new(SeatInner { seat, - name: name.map(|x| x.to_owned()), + name: name.map(std::borrow::ToOwned::to_owned), handle: Arc::downgrade(&self.0), + advertised_capabilities: capabilities, })); self.0 .seats @@ -151,6 +204,7 @@ impl Connection { // TODO libei has a `eis_clock_set_now_func` // Return time in us +#[allow(clippy::cast_sign_loss)] // Monotonic clock never returns negatives fn eis_now() -> u64 { let time = rustix::time::clock_gettime(rustix::time::ClockId::Monotonic); time.tv_sec as u64 * 1_000_000 + time.tv_nsec as u64 / 1_000 @@ -163,11 +217,12 @@ fn eis_now() -> u64 { pub struct EisRequestConverter { requests: VecDeque, pending_requests: VecDeque, - handle: Connection, + connection: Connection, } impl EisRequestConverter { /// Creates a new converter. + #[must_use] pub fn new( context: &eis::Context, handshake_resp: EisHandshakeResp, @@ -176,27 +231,29 @@ impl EisRequestConverter { Self { requests: VecDeque::new(), pending_requests: VecDeque::new(), - handle: Connection(Arc::new(ConnectionInner { + connection: Connection(Arc::new(ConnectionInner { context: context.clone(), handshake_resp, - seats: Default::default(), - devices: Default::default(), - device_for_interface: Default::default(), + seats: Mutex::default(), + devices: Mutex::default(), + device_for_interface: Mutex::default(), last_serial: Mutex::new(initial_serial), })), } } + /// Returns a handle to the connection used by this converer. + #[must_use] pub fn handle(&self) -> &Connection { - &self.handle + &self.connection } fn queue_frame_event(&mut self, device: &Device) { self.queue_request(EisRequest::Frame(Frame { time: eis_now(), device: device.clone(), - last_serial: self.handle.last_serial(), - })) + last_serial: self.connection.last_serial(), + })); } // Based on behavior of `eis_queue_request` in libeis @@ -222,90 +279,39 @@ impl EisRequestConverter { } } + /// Returns the next queued request if one exists. pub fn next_request(&mut self) -> Option { self.requests.pop_front() } + /// Handles a low-level protocol-level [`eis::Request`], possibly converting it into + /// a high-level [`EisRequest`]. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. + /// + /// # Errors + /// + /// The errors returned are protocol violations. pub fn handle_request(&mut self, request: eis::Request) -> Result<(), Error> { match request { eis::Request::Handshake(_handshake, _request) => { return Err(Error::UnexpectedHandshakeEvent); } - eis::Request::Connection(_connection, request) => match request { - eis::connection::Request::Sync { callback } => { - if callback.version() != 1 { - return Err(Error::InvalidInterfaceVersion( - "ei_callback", - callback.version(), - )); - } - callback.done(0); - if let Some(backend) = callback.0.backend() { - // XXX Error? - let _ = backend.flush(); - } - } - eis::connection::Request::Disconnect => { - self.queue_request(EisRequest::Disconnect); - } - }, + eis::Request::Connection(_connection, request) => { + self.handle_connection_request(request)?; + } eis::Request::Callback(_callback, request) => match request {}, eis::Request::Pingpong(_ping_pong, request) => match request { eis::pingpong::Request::Done { callback_data: _ } => { // TODO } }, - eis::Request::Seat(seat, request) => match request { - eis::seat::Request::Release => { - self.handle - .with_next_serial(|serial| seat.destroyed(serial)); - } - eis::seat::Request::Bind { capabilities } => { - let Some(seat) = self.handle.0.seats.lock().unwrap().get(&seat).cloned() else { - return Ok(()); - }; - self.queue_request(EisRequest::Bind(Bind { seat, capabilities })); - } - }, - eis::Request::Device(device, request) => { - let Some(device) = self.handle.0.devices.lock().unwrap().get(&device).cloned() - else { - return Ok(()); - }; - match request { - eis::device::Request::Release => {} - eis::device::Request::StartEmulating { - last_serial, - sequence, - } => { - self.queue_request(EisRequest::DeviceStartEmulating( - DeviceStartEmulating { - device, - last_serial, - sequence, - }, - )); - } - eis::device::Request::StopEmulating { last_serial } => { - self.queue_request(EisRequest::DeviceStopEmulating(DeviceStopEmulating { - device, - last_serial, - })); - } - eis::device::Request::Frame { - last_serial, - timestamp, - } => { - self.queue_request(EisRequest::Frame(Frame { - device, - last_serial, - time: timestamp, - })); - } - } - } + eis::Request::Seat(seat, request) => self.handle_seat_request(&seat, &request)?, + eis::Request::Device(device, request) => self.handle_device_request(device, request), eis::Request::Keyboard(keyboard, request) => { - let Some(device) = self.handle.device_for_interface(&keyboard) else { + let Some(device) = self.connection.device_for_interface(&keyboard) else { return Ok(()); }; match request { @@ -321,7 +327,7 @@ impl EisRequestConverter { } } eis::Request::Pointer(pointer, request) => { - let Some(device) = self.handle.device_for_interface(&pointer) else { + let Some(device) = self.connection.device_for_interface(&pointer) else { return Ok(()); }; match request { @@ -337,7 +343,7 @@ impl EisRequestConverter { } } eis::Request::PointerAbsolute(pointer_absolute, request) => { - let Some(device) = self.handle.device_for_interface(&pointer_absolute) else { + let Some(device) = self.connection.device_for_interface(&pointer_absolute) else { return Ok(()); }; match request { @@ -355,48 +361,10 @@ impl EisRequestConverter { } } eis::Request::Scroll(scroll, request) => { - let Some(device) = self.handle.device_for_interface(&scroll) else { - return Ok(()); - }; - match request { - eis::scroll::Request::Release => {} - eis::scroll::Request::Scroll { x, y } => { - self.queue_request(EisRequest::ScrollDelta(ScrollDelta { - device, - dx: x, - dy: y, - time: 0, - })); - } - eis::scroll::Request::ScrollDiscrete { x, y } => { - self.queue_request(EisRequest::ScrollDiscrete(ScrollDiscrete { - device, - discrete_dx: x, - discrete_dy: y, - time: 0, - })); - } - eis::scroll::Request::ScrollStop { x, y, is_cancel } => { - if is_cancel != 0 { - self.queue_request(EisRequest::ScrollCancel(ScrollCancel { - device, - time: 0, - x: x != 0, - y: y != 0, - })); - } else { - self.queue_request(EisRequest::ScrollStop(ScrollStop { - device, - time: 0, - x: x != 0, - y: y != 0, - })); - } - } - } + self.handle_scroll_request(scroll, request); } eis::Request::Button(button, request) => { - let Some(device) = self.handle.device_for_interface(&button) else { + let Some(device) = self.connection.device_for_interface(&button) else { return Ok(()); }; match request { @@ -412,50 +380,200 @@ impl EisRequestConverter { } } eis::Request::Touchscreen(touchscreen, request) => { - let Some(device) = self.handle.device_for_interface(&touchscreen) else { + self.handle_touchscreen_request(touchscreen, request)?; + } + } + Ok(()) + } + + fn handle_connection_request( + &mut self, + request: eis::connection::Request, + ) -> Result<(), Error> { + match request { + eis::connection::Request::Sync { callback } => { + if callback.version() != 1 { + return Err(Error::InvalidInterfaceVersion( + "ei_callback", + callback.version(), + )); + } + callback.done(0); + if let Some(backend) = callback.0.backend() { + // XXX Error? + let _ = backend.flush(); + } + } + eis::connection::Request::Disconnect => { + self.queue_request(EisRequest::Disconnect); + } + } + Ok(()) + } + + fn handle_seat_request( + &mut self, + seat: &eis::Seat, + request: &eis::seat::Request, + ) -> Result<(), Error> { + match request { + eis::seat::Request::Release => { + self.connection + .with_next_serial(|serial| seat.destroyed(serial)); + } + eis::seat::Request::Bind { capabilities } => { + let Some(seat) = self.connection.0.seats.lock().unwrap().get(seat).cloned() else { return Ok(()); }; - match request { - eis::touchscreen::Request::Release => {} - eis::touchscreen::Request::Down { touchid, x, y } => { - self.queue_request(EisRequest::TouchDown(TouchDown { - device, - touch_id: touchid, - x, - y, - time: 0, - })); - } - eis::touchscreen::Request::Motion { touchid, x, y } => { - self.queue_request(EisRequest::TouchMotion(TouchMotion { - device, - touch_id: touchid, - x, - y, - time: 0, - })); - } - eis::touchscreen::Request::Up { touchid } => { - self.queue_request(EisRequest::TouchUp(TouchUp { - device, - touch_id: touchid, - time: 0, - })); - } - eis::touchscreen::Request::Cancel { touchid } => { - if touchscreen.version() < 2 { - return Err(Error::InvalidInterfaceVersion( - "ei_touchscreen", - touchscreen.version(), - )); - } - self.queue_request(EisRequest::TouchCancel(TouchCancel { - device, - touch_id: touchid, - time: 0, - })); - } + + let capabilities = DeviceCapability::from_bits(*capabilities) + .map_err(|_err| RequestError::InvalidCapabilities)?; + if !seat.0.advertised_capabilities.contains(capabilities) { + return Err(RequestError::InvalidCapabilities.into()); } + + self.queue_request(EisRequest::Bind(Bind { seat, capabilities })); + return Ok(()); + } + } + Ok(()) + } + + #[allow(clippy::needless_pass_by_value)] // Arguably better code when we don't have to dereference data + fn handle_device_request(&mut self, device: eis::Device, request: eis::device::Request) { + let Some(device) = self + .connection + .0 + .devices + .lock() + .unwrap() + .get(&device) + .cloned() + else { + return; + }; + match request { + eis::device::Request::Release => {} + eis::device::Request::StartEmulating { + last_serial, + sequence, + } => { + self.queue_request(EisRequest::DeviceStartEmulating(DeviceStartEmulating { + device, + last_serial, + sequence, + })); + } + eis::device::Request::StopEmulating { last_serial } => { + self.queue_request(EisRequest::DeviceStopEmulating(DeviceStopEmulating { + device, + last_serial, + })); + } + eis::device::Request::Frame { + last_serial, + timestamp, + } => { + self.queue_request(EisRequest::Frame(Frame { + device, + last_serial, + time: timestamp, + })); + } + } + } + + #[allow(clippy::needless_pass_by_value)] // Arguably better code when we don't have to dereference data + fn handle_scroll_request(&mut self, scroll: eis::Scroll, request: eis::scroll::Request) { + let Some(device) = self.connection.device_for_interface(&scroll) else { + return; + }; + match request { + eis::scroll::Request::Release => {} + eis::scroll::Request::Scroll { x, y } => { + self.queue_request(EisRequest::ScrollDelta(ScrollDelta { + device, + dx: x, + dy: y, + time: 0, + })); + } + eis::scroll::Request::ScrollDiscrete { x, y } => { + self.queue_request(EisRequest::ScrollDiscrete(ScrollDiscrete { + device, + discrete_dx: x, + discrete_dy: y, + time: 0, + })); + } + eis::scroll::Request::ScrollStop { x, y, is_cancel } => { + if is_cancel != 0 { + self.queue_request(EisRequest::ScrollCancel(ScrollCancel { + device, + time: 0, + x: x != 0, + y: y != 0, + })); + } else { + self.queue_request(EisRequest::ScrollStop(ScrollStop { + device, + time: 0, + x: x != 0, + y: y != 0, + })); + } + } + } + } + + #[allow(clippy::needless_pass_by_value)] // Arguably better code when we don't have to dereference data + fn handle_touchscreen_request( + &mut self, + touchscreen: eis::Touchscreen, + request: eis::touchscreen::Request, + ) -> Result<(), Error> { + let Some(device) = self.connection.device_for_interface(&touchscreen) else { + return Ok(()); + }; + match request { + eis::touchscreen::Request::Release => {} + eis::touchscreen::Request::Down { touchid, x, y } => { + self.queue_request(EisRequest::TouchDown(TouchDown { + device, + touch_id: touchid, + x, + y, + time: 0, + })); + } + eis::touchscreen::Request::Motion { touchid, x, y } => { + self.queue_request(EisRequest::TouchMotion(TouchMotion { + device, + touch_id: touchid, + x, + y, + time: 0, + })); + } + eis::touchscreen::Request::Up { touchid } => { + self.queue_request(EisRequest::TouchUp(TouchUp { + device, + touch_id: touchid, + time: 0, + })); + } + eis::touchscreen::Request::Cancel { touchid } => { + if touchscreen.version() < 2 { + return Err(Error::InvalidInterfaceVersion( + "ei_touchscreen", + touchscreen.version(), + )); + } + self.queue_request(EisRequest::TouchCancel(TouchCancel { + device, + touch_id: touchid, + time: 0, + })); } } Ok(()) @@ -465,8 +583,8 @@ impl EisRequestConverter { struct SeatInner { seat: eis::Seat, name: Option, - //capabilities: HashMap, handle: Weak, + advertised_capabilities: BitFlags, } /// High-level server-side wrapper for `ei_seat`. @@ -487,17 +605,22 @@ fn add_interface( impl Seat { /// Returns the interface proxy for the underlying `ei_seat` object. + #[must_use] pub fn eis_seat(&self) -> &eis::Seat { &self.0.seat } // builder pattern? /// Adds a device to the connection. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. pub fn add_device( &self, name: Option<&str>, device_type: eis::device::DeviceType, - capabilities: &[DeviceCapability], + capabilities: BitFlags, // TODO: better solution; keymap, etc. before_done_cb: impl for<'a> FnOnce(&'a Device), ) -> Device { @@ -535,13 +658,13 @@ impl Seat { add_interface::(&device, connection.as_ref()) } }; - interfaces.insert(object.interface().to_string(), object); + interfaces.insert(object.interface().to_owned(), object); } let device = Device(Arc::new(DeviceInner { device, seat: self.clone(), - name: name.map(|x| x.to_string()), + name: name.map(std::string::ToString::to_string), interfaces, handle: self.0.handle.clone(), })); @@ -569,6 +692,10 @@ impl Seat { } /// Removes this seat and associated devices from the connection. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. pub fn remove(&self) { if let Some(handle) = self.0.handle.upgrade().map(Connection) { let devices = handle @@ -614,6 +741,7 @@ impl std::hash::Hash for Seat { } } +/// Trait marking interfaces that can be on devices. pub trait DeviceInterface: eis::Interface {} macro_rules! impl_device_interface { @@ -672,16 +800,19 @@ impl fmt::Debug for Device { impl Device { /// Returns the high-level [`Seat`] wrapper for this device. + #[must_use] pub fn seat(&self) -> &Seat { &self.0.seat } /// Returns the interface proxy for the underlying `ei_device` object. + #[must_use] pub fn device(&self) -> &eis::Device { &self.0.device } /// Returns the name of the device. + #[must_use] pub fn name(&self) -> Option<&str> { self.0.name.as_deref() } @@ -689,16 +820,22 @@ impl Device { /// Returns an interface proxy if it is implemented for this device. /// /// Interfaces of devices are implemented, such that there is one `ei_device` object and other objects (for example `ei_keyboard`) denoting capabilities. + #[must_use] pub fn interface(&self) -> Option { self.0.interfaces.get(T::NAME)?.clone().downcast() } /// Returns `true` if this device has an interface matching the provided capability. + #[must_use] pub fn has_capability(&self, capability: DeviceCapability) -> bool { - self.0.interfaces.contains_key(capability.name()) + self.0.interfaces.contains_key(capability.interface_name()) } /// Removes this device and associated interfaces from the connection. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. pub fn remove(&self) { if let Some(handle) = self.0.handle.upgrade().map(Connection) { for interface in self.0.interfaces.values() { @@ -721,7 +858,7 @@ impl Device { /// See [`eis::Device::resumed`] for documentation from the protocol specification. pub fn resumed(&self) { if let Some(handle) = self.0.handle.upgrade().map(Connection) { - handle.with_next_serial(|serial| self.device().resumed(serial)) + handle.with_next_serial(|serial| self.device().resumed(serial)); } } @@ -731,7 +868,7 @@ impl Device { /// See [`eis::Device::paused`] for documentation from the protocol specification. pub fn paused(&self) { if let Some(handle) = self.0.handle.upgrade().map(Connection) { - handle.with_next_serial(|serial| self.device().paused(serial)) + handle.with_next_serial(|serial| self.device().paused(serial)); } } @@ -744,7 +881,7 @@ impl Device { /// See [`eis::Device::start_emulating`] for documentation from the protocol specification. pub fn start_emulating(&self, sequence: u32) { if let Some(handle) = self.0.handle.upgrade().map(Connection) { - handle.with_next_serial(|serial| self.device().start_emulating(serial, sequence)) + handle.with_next_serial(|serial| self.device().start_emulating(serial, sequence)); } } @@ -755,7 +892,7 @@ impl Device { /// See [`eis::Device::stop_emulating`] for documentation from the protocol specification. pub fn stop_emulating(&self) { if let Some(handle) = self.0.handle.upgrade().map(Connection) { - handle.with_next_serial(|serial| self.device().stop_emulating(serial)) + handle.with_next_serial(|serial| self.device().stop_emulating(serial)); } } @@ -767,7 +904,7 @@ impl Device { /// See [`eis::Device::frame`] for documentation from the protocol specification. pub fn frame(&self, time: u64) { if let Some(handle) = self.0.handle.upgrade().map(Connection) { - handle.with_next_serial(|serial| self.device().frame(serial, time)) + handle.with_next_serial(|serial| self.device().frame(serial, time)); } } } @@ -788,6 +925,7 @@ impl std::hash::Hash for Device { /// Enum containing all possible requests the high-level utilities will give for a server implementation to handle. #[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] // Inner types have docs pub enum EisRequest { // TODO connect, disconnect, device closed Disconnect, @@ -837,6 +975,7 @@ impl EisRequest { } /// Returns the high-level [`Device`] wrapper for this request, if applicable. + #[must_use] pub fn device(&self) -> Option<&Device> { match self { Self::Frame(evt) => Some(&evt.device), @@ -864,7 +1003,8 @@ impl EisRequest { pub struct Bind { /// High-level [`Seat`] wrapper. pub seat: Seat, - pub capabilities: u64, + /// Capabilities requested by the client. + pub capabilities: BitFlags, } /// High-level translation of [`ei_device.frame`](eis::device::Request::Frame). @@ -872,6 +1012,7 @@ pub struct Bind { pub struct Frame { /// High-level [`Device`] wrapper. pub device: Device, + /// Last serial sent by the EIS implementation. pub last_serial: u32, /// Timestamp in microseconds. pub time: u64, @@ -882,7 +1023,9 @@ pub struct Frame { pub struct DeviceStartEmulating { /// High-level [`Device`] wrapper. pub device: Device, + /// Last serial sent by the EIS implementation. pub last_serial: u32, + /// The event's sequence number. pub sequence: u32, } @@ -891,6 +1034,7 @@ pub struct DeviceStartEmulating { pub struct DeviceStopEmulating { /// High-level [`Device`] wrapper. pub device: Device, + /// Last serial sent by the EIS implementation. pub last_serial: u32, } @@ -901,7 +1045,9 @@ pub struct PointerMotion { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Relative motion on the X axis. pub dx: f32, + /// Relative motion on the Y axis. pub dy: f32, } @@ -912,7 +1058,9 @@ pub struct PointerMotionAbsolute { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Absolute position on the X axis. pub dx_absolute: f32, + /// Absolute position on the Y axis. pub dy_absolute: f32, } @@ -923,7 +1071,9 @@ pub struct Button { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Button code, as in Linux's `input-event-codes.h`. pub button: u32, + /// State of the button. pub state: eis::button::ButtonState, } @@ -934,7 +1084,9 @@ pub struct ScrollDelta { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Motion on the X axis. pub dx: f32, + /// Motion on the Y axis. pub dy: f32, } @@ -945,7 +1097,9 @@ pub struct ScrollStop { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Whether motion on the X axis stopped. pub x: bool, + /// Whether motion on the Y axis stopped. pub y: bool, } @@ -956,7 +1110,9 @@ pub struct ScrollCancel { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Whether motion on the X axis was canceled. pub x: bool, + /// Whether motion on the Y axis was canceled. pub y: bool, } @@ -967,7 +1123,9 @@ pub struct ScrollDiscrete { pub device: Device, /// Timestamp in microseconds. pub time: u64, + /// Discrete motion on the X axis. pub discrete_dx: i32, + /// Discrete motion on the Y axis. pub discrete_dy: i32, } @@ -993,7 +1151,9 @@ pub struct TouchDown { pub time: u64, /// Unique touch ID, defined in this request. pub touch_id: u32, + /// Absolute position on the X axis. pub x: f32, + /// Absolute position on the Y axis. pub y: f32, } @@ -1006,7 +1166,9 @@ pub struct TouchMotion { pub time: u64, /// Unique touch ID, defined in [`TouchDown`]. pub touch_id: u32, + /// Absolute position on the X axis. pub x: f32, + /// Absolute position on the Y axis. pub y: f32, } diff --git a/src/tokio.rs b/src/tokio.rs index 42b2739..1a48f87 100644 --- a/src/tokio.rs +++ b/src/tokio.rs @@ -1,3 +1,5 @@ +//! Module containing [`tokio`] event streams. + // TODO: Handle writable fd too? use futures::stream::{Stream, StreamExt}; @@ -12,9 +14,15 @@ pub use crate::handshake::{HandshakeError, HandshakeResp}; use crate::{ei, handshake::EiHandshaker, Error, PendingRequestResult}; // XXX make this ei::EventStream? +/// Stream of `ei::Event`s. pub struct EiEventStream(AsyncFd); impl EiEventStream { + /// Creates a new event stream. + /// + /// # Errors + /// + /// Will return `Err` if the underlying async file descriptor registration fails. pub fn new(context: ei::Context) -> io::Result { AsyncFd::with_interest(context, tokio::io::Interest::READABLE).map(Self) } @@ -60,6 +68,7 @@ impl Stream for EiEventStream { } // TODO rename EiProtoEventStream +/// EI convert event stream. pub struct EiConvertEventStream { inner: EiEventStream, converter: crate::event::EiEventConverter, @@ -113,6 +122,11 @@ impl Stream for EiConvertEventStream { } } +/// Executes the handshake in async mode. +/// +/// # Errors +/// +/// Will return `Err` if there is an I/O error or a protocol violation. pub async fn ei_handshake( events: &mut EiEventStream, name: &str, @@ -133,6 +147,11 @@ pub async fn ei_handshake( } impl ei::Context { + /// Executes the handshake in async mode. + /// + /// # Errors + /// + /// Will return `Err` if there is an I/O error or a protocol violation. pub async fn handshake_tokio( &self, name: &str, diff --git a/src/util.rs b/src/util.rs index c65b07e..0e8d2d0 100644 --- a/src/util.rs +++ b/src/util.rs @@ -19,7 +19,7 @@ pub fn array_from_iterator_unchecked, c mut iter: I, ) -> [T; N] { let mut arr = [T::default(); N]; - for i in arr.iter_mut() { + for i in &mut arr { *i = iter.next().unwrap(); } arr diff --git a/src/wire/arg.rs b/src/wire/arg.rs index dd7e336..064a3d1 100644 --- a/src/wire/arg.rs +++ b/src/wire/arg.rs @@ -49,7 +49,9 @@ impl Arg<'_> { match self { Arg::Uint32(value) => buf.extend(value.to_ne_bytes()), Arg::Int32(value) => buf.extend(value.to_ne_bytes()), - Arg::Uint64(value) => buf.extend(value.to_ne_bytes()), + Arg::Uint64(value) | Arg::NewId(value) | Arg::Id(value) => { + buf.extend(value.to_ne_bytes()); + } Arg::Int64(value) => buf.extend(value.to_ne_bytes()), Arg::Float(value) => buf.extend(value.to_ne_bytes()), // XXX unwrap? @@ -67,8 +69,6 @@ impl Arg<'_> { buf.extend((0..4 - (len % 4)).map(|_| b'\0')); } } - Arg::NewId(value) => buf.extend(value.to_ne_bytes()), - Arg::Id(value) => buf.extend(value.to_ne_bytes()), } } } diff --git a/src/wire/backend.rs b/src/wire/backend.rs index c074142..c94c39c 100644 --- a/src/wire/backend.rs +++ b/src/wire/backend.rs @@ -93,18 +93,28 @@ impl AsFd for Backend { } } +/// Pending message result. #[derive(Debug)] pub enum PendingRequestResult { + /// The message. Either an event or a request. Request(T), + /// Wire format parse error. ParseError(ParseError), + /// Invalid object ID. InvalidObject(u64), } impl Backend { + /// Creates a [`Backend`] based on the given `socket`, and whether this is the `client` + /// side or not. + /// + /// # Errors + /// + /// Will return `Err` if setting the socket to non-blocking mode fails. pub fn new(socket: UnixStream, client: bool) -> io::Result { socket.set_nonblocking(true)?; - let next_id = if client { 1 } else { 0xff00000000000000 }; - let next_peer_id = if client { 0xff00000000000000 } else { 1 }; + let next_id = if client { 1 } else { 0xff00_0000_0000_0000 }; + let next_peer_id = if client { 0xff00_0000_0000_0000 } else { 1 }; let backend = Self(Arc::new(BackendInner { socket, client, @@ -117,13 +127,8 @@ impl Backend { write: Mutex::new(Buffer::default()), debug: is_reis_debug(), })); - let handshake = Object::for_new_id( - backend.downgrade(), - 0, - client, - "ei_handshake".to_string(), - 1, - ); + let handshake = + Object::for_new_id(backend.downgrade(), 0, client, "ei_handshake".to_owned(), 1); backend.0.state.lock().unwrap().objects.insert(0, handshake); Ok(backend) } @@ -149,19 +154,16 @@ impl Backend { "unexpected EOF reading ei socket", )); } - Ok(0) => { - return Ok(total_count); - } Ok(count) => { read.buf.extend(&buf[0..count]); total_count += count; } #[allow(unreachable_patterns)] // `WOULDBLOCK` and `AGAIN` typically equal - Err(Errno::WOULDBLOCK | Errno::AGAIN) => { + Ok(0) | Err(Errno::WOULDBLOCK | Errno::AGAIN) => { return Ok(total_count); } Err(err) => return Err(err.into()), - }; + } } } @@ -235,7 +237,7 @@ impl Backend { ) -> Result { let mut state = self.0.state.lock().unwrap(); - if id < state.next_peer_id || (!self.0.client && id >= 0xff00000000000000) { + if id < state.next_peer_id || (!self.0.client && id >= 0xff00_0000_0000_0000) { return Err(ParseError::InvalidId(id)); } state.next_peer_id = id + 1; @@ -252,7 +254,7 @@ impl Backend { version: u32, ) -> Result { Ok(self - .new_peer_object(id, T::NAME.to_string(), version)? + .new_peer_object(id, T::NAME.to_owned(), version)? .downcast_unchecked()) } @@ -271,10 +273,10 @@ impl Backend { fn print_msg(&self, object_id: u64, opcode: u32, args: &[Arg], incoming: bool) { let object = self.object_for_id(object_id); let interface = object.as_ref().map_or("UNKNOWN", |x| x.interface()); - let op_name = if self.0.client != incoming { - eis::Request::op_name(interface, opcode) - } else { + let op_name = if self.0.client == incoming { ei::Event::op_name(interface, opcode) + } else { + eis::Request::op_name(interface, opcode) } .unwrap_or("UNKNOWN"); if incoming { diff --git a/src/wire/mod.rs b/src/wire/mod.rs index 64ecf52..89d533a 100644 --- a/src/wire/mod.rs +++ b/src/wire/mod.rs @@ -101,16 +101,26 @@ impl<'a> ByteStream<'a> { } } +/// Wire format parse error. #[derive(Debug)] pub enum ParseError { + /// End of message while parsing argument. EndOfMessage, + /// Invalid UTF-8 string in message. Utf8(FromUtf8Error), + /// Invalid object ID. InvalidId(u64), + /// Expected file descriptor. NoFd, + /// Invalid opcode for interface. InvalidOpcode(&'static str, u32), + /// Invalid variant for enum. InvalidVariant(&'static str, u32), + /// Unknown interface. InvalidInterface(String), + /// Message header is too short. HeaderLength(u32), + /// Message length didn't match header. MessageLength(u32, u32), } @@ -122,10 +132,10 @@ impl fmt::Display for ParseError { Self::InvalidId(id) => write!(f, "new object id '{id}' invalid"), Self::NoFd => write!(f, "expected fd"), Self::InvalidOpcode(intr, op) => { - write!(f, "opcode '{op}' invallid for interface '{intr}'") + write!(f, "opcode '{op}' invalid for interface '{intr}'") } Self::InvalidVariant(enum_, var) => { - write!(f, "variant '{var}' invallid for enum '{enum_}'") + write!(f, "variant '{var}' invalid for enum '{enum_}'") } Self::InvalidInterface(intr) => write!(f, "unknown interface '{intr}'"), Self::HeaderLength(len) => write!(f, "header length {len} < 16"),