diff --git a/Cargo.lock b/Cargo.lock index 2254718..1a1bc3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1194,8 +1194,8 @@ dependencies = [ name = "input-capture" version = "0.1.0" dependencies = [ - "anyhow", "ashpd", + "async-trait", "core-graphics", "futures", "futures-core", @@ -1207,6 +1207,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-util", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", @@ -1244,6 +1245,7 @@ dependencies = [ "futures-core", "log", "num_enum", + "reis", "serde", "thiserror", ] @@ -1327,6 +1329,7 @@ dependencies = [ "slab", "thiserror", "tokio", + "tokio-util", "toml", ] @@ -2048,6 +2051,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.14" diff --git a/Cargo.toml b/Cargo.toml index 814c2bb..d5d4391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,5 @@ [workspace] -members = [ - "input-capture", - "input-emulation", - "input-event", -] +members = ["input-capture", "input-emulation", "input-event"] [package] name = "lan-mouse" @@ -29,16 +25,30 @@ anyhow = "1.0.71" log = "0.4.20" env_logger = "0.11.3" serde_json = "1.0.107" -tokio = {version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "process", "rt", "sync", "signal"] } +tokio = { version = "1.32.0", features = [ + "io-util", + "io-std", + "macros", + "net", + "process", + "rt", + "sync", + "signal", +] } futures = "0.3.28" -clap = { version="4.4.11", features = ["derive"] } -gtk = { package = "gtk4", version = "0.8.1", features = ["v4_2"], optional = true } -adw = { package = "libadwaita", version = "0.6.0", features = ["v1_1"], optional = true } +clap = { version = "4.4.11", features = ["derive"] } +gtk = { package = "gtk4", version = "0.8.1", features = [ + "v4_2", +], optional = true } +adw = { package = "libadwaita", version = "0.6.0", features = [ + "v1_1", +], optional = true } async-channel = { version = "2.1.1", optional = true } hostname = "0.4.0" slab = "0.4.9" endi = "1.1.0" thiserror = "1.0.61" +tokio-util = "0.7.11" [target.'cfg(unix)'.dependencies] libc = "0.2.148" @@ -47,9 +57,9 @@ libc = "0.2.148" glib-build-tools = { version = "0.19.0", optional = true } [features] -default = [ "wayland", "x11", "xdg_desktop_portal", "libei", "gtk" ] -wayland = [ "input-capture/wayland", "input-emulation/wayland" ] -x11 = [ "input-capture/x11", "input-emulation/x11" ] -xdg_desktop_portal = [ "input-emulation/xdg_desktop_portal" ] -libei = [ "input-capture/libei", "input-emulation/libei" ] +default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"] +wayland = ["input-capture/wayland", "input-emulation/wayland"] +x11 = ["input-capture/x11", "input-emulation/x11"] +xdg_desktop_portal = ["input-emulation/xdg_desktop_portal"] +libei = ["input-event/libei", "input-capture/libei", "input-emulation/libei"] gtk = ["dep:gtk", "dep:adw", "dep:async-channel", "dep:glib-build-tools"] diff --git a/input-capture/Cargo.toml b/input-capture/Cargo.toml index eadb004..aedfae5 100644 --- a/input-capture/Cargo.toml +++ b/input-capture/Cargo.toml @@ -7,7 +7,6 @@ license = "GPL-3.0-or-later" repository = "https://github.com/ferdinandschober/lan-mouse" [dependencies] -anyhow = "1.0.86" futures = "0.3.28" futures-core = "0.3.30" log = "0.4.22" @@ -15,23 +14,42 @@ input-event = { path = "../input-event", version = "0.1.0" } memmap = "0.7" tempfile = "3.8" thiserror = "1.0.61" -tokio = { version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "process", "rt", "sync", "signal"] } +tokio = { version = "1.32.0", features = [ + "io-util", + "io-std", + "macros", + "net", + "process", + "rt", + "sync", + "signal", +] } once_cell = "1.19.0" +async-trait = "0.1.81" +tokio-util = "0.7.11" [target.'cfg(all(unix, not(target_os="macos")))'.dependencies] -wayland-client = { version="0.31.1", optional = true } -wayland-protocols = { version="0.32.1", features=["client", "staging", "unstable"], optional = true } -wayland-protocols-wlr = { version="0.3.1", features=["client"], optional = true } +wayland-client = { version = "0.31.1", optional = true } +wayland-protocols = { version = "0.32.1", features = [ + "client", + "staging", + "unstable", +], optional = true } +wayland-protocols-wlr = { version = "0.3.1", features = [ + "client", +], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } -ashpd = { version = "0.8", default-features = false, features = ["tokio"], optional = true } -reis = { version = "0.2", features = [ "tokio" ], optional = true } +ashpd = { version = "0.8", default-features = false, features = [ + "tokio", +], optional = true } +reis = { version = "0.2", features = ["tokio"], optional = true } [target.'cfg(target_os="macos")'.dependencies] core-graphics = { version = "0.23", features = ["highsierra"] } [target.'cfg(windows)'.dependencies] -windows = { version = "0.57.0", features = [ +windows = { version = "0.57.0", features = [ "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Foundation", @@ -43,6 +61,10 @@ windows = { version = "0.57.0", features = [ [features] default = ["wayland", "x11", "libei"] -wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr" ] +wayland = [ + "dep:wayland-client", + "dep:wayland-protocols", + "dep:wayland-protocols-wlr", +] x11 = ["dep:x11"] libei = ["dep:reis", "dep:ashpd"] diff --git a/input-capture/src/dummy.rs b/input-capture/src/dummy.rs index 21808db..8bd2f65 100644 --- a/input-capture/src/dummy.rs +++ b/input-capture/src/dummy.rs @@ -1,11 +1,13 @@ -use std::io; use std::pin::Pin; use std::task::{Context, Poll}; +use async_trait::async_trait; use futures_core::Stream; use input_event::Event; +use crate::CaptureError; + use super::{CaptureHandle, InputCapture, Position}; pub struct DummyInputCapture {} @@ -22,22 +24,27 @@ impl Default for DummyInputCapture { } } +#[async_trait] impl InputCapture for DummyInputCapture { - fn create(&mut self, _handle: CaptureHandle, _pos: Position) -> io::Result<()> { + async fn create(&mut self, _handle: CaptureHandle, _pos: Position) -> Result<(), CaptureError> { Ok(()) } - fn destroy(&mut self, _handle: CaptureHandle) -> io::Result<()> { + async fn destroy(&mut self, _handle: CaptureHandle) -> Result<(), CaptureError> { Ok(()) } - fn release(&mut self) -> io::Result<()> { + async fn release(&mut self) -> Result<(), CaptureError> { + Ok(()) + } + + async fn terminate(&mut self) -> Result<(), CaptureError> { Ok(()) } } impl Stream for DummyInputCapture { - type Item = io::Result<(CaptureHandle, Event)>; + type Item = Result<(CaptureHandle, Event), CaptureError>; fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { Poll::Pending diff --git a/input-capture/src/error.rs b/input-capture/src/error.rs index 35b7839..2866c82 100644 --- a/input-capture/src/error.rs +++ b/input-capture/src/error.rs @@ -1,5 +1,13 @@ use thiserror::Error; +#[derive(Debug, Error)] +pub enum InputCaptureError { + #[error("error creating input-capture: `{0}`")] + Create(#[from] CaptureCreationError), + #[error("error while capturing input: `{0}`")] + Capture(#[from] CaptureError), +} + #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] use std::io; #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] @@ -9,6 +17,8 @@ use wayland_client::{ ConnectError, DispatchError, }; +#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] +use ashpd::desktop::ResponseError; #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] use reis::tokio::{EiConvertEventStreamError, HandshakeError}; @@ -43,6 +53,9 @@ pub enum CaptureError { #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[error(transparent)] Portal(#[from] ashpd::Error), + #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] + #[error("libei disconnected - reason: `{0}`")] + Disconnected(String), } #[derive(Debug, Error)] @@ -66,6 +79,23 @@ pub enum CaptureCreationError { Windows, } +impl CaptureCreationError { + /// request was intentionally denied by the user + #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] + pub(crate) fn cancelled_by_user(&self) -> bool { + matches!( + self, + CaptureCreationError::Libei(LibeiCaptureCreationError::Ashpd(ashpd::Error::Response( + ResponseError::Cancelled + ))) + ) + } + #[cfg(not(all(unix, feature = "libei", not(target_os = "macos"))))] + pub(crate) fn cancelled_by_user(&self) -> bool { + false + } +} + #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[derive(Debug, Error)] pub enum LibeiCaptureCreationError { diff --git a/input-capture/src/lib.rs b/input-capture/src/lib.rs index 90ed483..53505aa 100644 --- a/input-capture/src/lib.rs +++ b/input-capture/src/lib.rs @@ -1,10 +1,11 @@ -use std::{fmt::Display, io}; +use std::fmt::Display; +use async_trait::async_trait; use futures_core::Stream; use input_event::Event; -pub use error::{CaptureCreationError, CaptureError}; +pub use error::{CaptureCreationError, CaptureError, InputCaptureError}; pub mod error; @@ -92,21 +93,29 @@ impl Display for Backend { } } -pub trait InputCapture: Stream> + Unpin { +#[async_trait] +pub trait InputCapture: + Stream> + Unpin +{ /// create a new client with the given id - fn create(&mut self, id: CaptureHandle, pos: Position) -> io::Result<()>; + async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError>; /// destroy the client with the given id, if it exists - fn destroy(&mut self, id: CaptureHandle) -> io::Result<()>; + async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError>; /// release mouse - fn release(&mut self) -> io::Result<()>; + async fn release(&mut self) -> Result<(), CaptureError>; + + /// destroy the input capture + async fn terminate(&mut self) -> Result<(), CaptureError>; } pub async fn create_backend( backend: Backend, -) -> Result>>, CaptureCreationError> -{ +) -> Result< + Box>>, + CaptureCreationError, +> { match backend { #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)), @@ -124,8 +133,10 @@ pub async fn create_backend( pub async fn create( backend: Option, -) -> Result>>, CaptureCreationError> -{ +) -> Result< + Box>>, + CaptureCreationError, +> { if let Some(backend) = backend { let b = create_backend(backend).await; if b.is_ok() { @@ -145,13 +156,13 @@ pub async fn create( Backend::Windows, #[cfg(target_os = "macos")] Backend::MacOs, - Backend::Dummy, ] { match create_backend(backend).await { Ok(b) => { log::info!("using capture backend: {backend}"); return Ok(b); } + Err(e) if e.cancelled_by_user() => return Err(e), Err(e) => log::warn!("{backend} input capture backend unavailable: {e}"), } } diff --git a/input-capture/src/libei.rs b/input-capture/src/libei.rs index 0b19203..5c9c5a8 100644 --- a/input-capture/src/libei.rs +++ b/input-capture/src/libei.rs @@ -1,14 +1,14 @@ use ashpd::{ desktop::{ input_capture::{Activated, Barrier, BarrierID, Capabilities, InputCapture, Region, Zones}, - ResponseError, Session, + Session, }, enumflags2::BitFlags, }; -use futures::StreamExt; +use async_trait::async_trait; +use futures::{FutureExt, StreamExt}; use reis::{ - ei::{self, keyboard::KeyState}, - eis::button::ButtonState, + ei, event::{DeviceCapability, EiEvent}, tokio::{EiConvertEventStream, EiEventStream}, }; @@ -19,27 +19,35 @@ use std::{ os::unix::net::UnixStream, pin::Pin, rc::Rc, - task::{ready, Context, Poll}, + sync::Arc, + task::{Context, Poll}, }; use tokio::{ - sync::mpsc::{Receiver, Sender}, + sync::{ + mpsc::{self, Receiver, Sender}, + Notify, + }, task::JoinHandle, }; +use tokio_util::sync::CancellationToken; use futures_core::Stream; use once_cell::sync::Lazy; -use input_event::{Event, KeyboardEvent, PointerEvent}; - -use crate::error::{CaptureError, ReisConvertEventStreamError}; +use input_event::Event; use super::{ - error::LibeiCaptureCreationError, CaptureHandle, InputCapture as LanMouseInputCapture, Position, + error::{CaptureError, LibeiCaptureCreationError, ReisConvertEventStreamError}, + CaptureHandle, InputCapture as LanMouseInputCapture, Position, }; -#[derive(Debug)] -enum ProducerEvent { - Release, +/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that + * prevents receiving further events after a session has been disabled once. + * Therefore the session needs to be recreated when the barriers are updated */ + +/// events that necessitate restarting the capture session +#[derive(Clone, Copy, Debug)] +enum LibeiNotifyEvent { Create(CaptureHandle, Position), Destroy(CaptureHandle), } @@ -47,9 +55,12 @@ enum ProducerEvent { #[allow(dead_code)] pub struct LibeiInputCapture<'a> { input_capture: Pin>>, - libei_task: JoinHandle>, - event_rx: tokio::sync::mpsc::Receiver<(CaptureHandle, Event)>, - notify_tx: tokio::sync::mpsc::Sender, + capture_task: JoinHandle>, + event_rx: Receiver<(CaptureHandle, Event)>, + notify_capture: Sender, + notify_release: Arc, + cancellation_token: CancellationToken, + terminated: bool, } static INTERFACES: Lazy> = Lazy::new(|| { @@ -68,14 +79,15 @@ static INTERFACES: Lazy> = Lazy::new(|| { m }); +/// returns (start pos, end pos), inclusive fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) { let (x, y) = (r.x_offset(), r.y_offset()); - let (width, height) = (r.width() as i32, r.height() as i32); + let (w, h) = (r.width() as i32, r.height() as i32); match pos { - Position::Left => (x, y, x, y + height - 1), // start pos, end pos, inclusive - Position::Right => (x + width, y, x + width, y + height - 1), - Position::Top => (x, y, x + width - 1, y), - Position::Bottom => (x, y + height, x + width - 1, y + height), + Position::Left => (x, y, x, y + h - 1), + Position::Right => (x + w, y, x + w, y + h - 1), + Position::Top => (x, y, x + w - 1, y), + Position::Bottom => (x, y + h, x + w - 1, y + h), } } @@ -125,31 +137,16 @@ async fn update_barriers( Ok(id_map) } -impl<'a> Drop for LibeiInputCapture<'a> { - fn drop(&mut self) { - self.libei_task.abort(); - } -} - async fn create_session<'a>( input_capture: &'a InputCapture<'a>, ) -> std::result::Result<(Session<'a>, BitFlags), ashpd::Error> { log::debug!("creating input capture session"); - let (session, capabilities) = loop { - match input_capture - .create_session( - &ashpd::WindowIdentifier::default(), - Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, - ) - .await - { - Ok(s) => break s, - Err(ashpd::Error::Response(ResponseError::Cancelled)) => continue, - o => o?, - }; - }; - log::debug!("capabilities: {capabilities:?}"); - Ok((session, capabilities)) + input_capture + .create_session( + &ashpd::WindowIdentifier::default(), + Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, + ) + .await } async fn connect_to_eis( @@ -182,6 +179,7 @@ async fn libei_event_handler( mut ei_event_stream: EiConvertEventStream, context: ei::Context, event_tx: Sender<(CaptureHandle, Event)>, + release_session: Arc, current_client: Rc>>, ) -> Result<(), CaptureError> { loop { @@ -192,20 +190,7 @@ async fn libei_event_handler( .map_err(ReisConvertEventStreamError::from)?; log::trace!("from ei: {ei_event:?}"); let client = current_client.get(); - handle_ei_event(ei_event, client, &context, &event_tx).await; - } -} - -async fn wait_for_active_client( - notify_rx: &mut Receiver, - active_clients: &mut Vec<(CaptureHandle, Position)>, -) { - // wait for a client update - while let Some(producer_event) = notify_rx.recv().await { - if let ProducerEvent::Create(c, p) = producer_event { - handle_producer_event(ProducerEvent::Create(c, p), active_clients); - break; - } + handle_ei_event(ei_event, client, &context, &event_tx, &release_session).await?; } } @@ -215,16 +200,30 @@ impl<'a> LibeiInputCapture<'a> { let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>; let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?); - let (event_tx, event_rx) = tokio::sync::mpsc::channel(32); - let (notify_tx, notify_rx) = tokio::sync::mpsc::channel(32); - let capture = do_capture(input_capture_ptr, notify_rx, first_session, event_tx); - let libei_task = tokio::task::spawn_local(capture); + let (event_tx, event_rx) = mpsc::channel(1); + let (notify_capture, notify_rx) = mpsc::channel(1); + let notify_release = Arc::new(Notify::new()); + + let cancellation_token = CancellationToken::new(); + + let capture = do_capture( + input_capture_ptr, + notify_rx, + notify_release.clone(), + first_session, + event_tx, + cancellation_token.clone(), + ); + let capture_task = tokio::task::spawn_local(capture); let producer = Self { input_capture, event_rx, - libei_task, - notify_tx, + capture_task, + notify_capture, + notify_release, + cancellation_token, + terminated: false, }; Ok(producer) @@ -232,67 +231,157 @@ impl<'a> LibeiInputCapture<'a> { } async fn do_capture<'a>( - input_capture_ptr: *const InputCapture<'static>, - mut notify_rx: Receiver, - mut first_session: Option<(Session<'a>, BitFlags)>, + input_capture: *const InputCapture<'a>, + mut capture_event: Receiver, + notify_release: Arc, + session: Option<(Session<'a>, BitFlags)>, event_tx: Sender<(CaptureHandle, Event)>, + cancellation_token: CancellationToken, ) -> Result<(), CaptureError> { - /* safety: libei_task does not outlive Self */ - let input_capture = unsafe { &*input_capture_ptr }; + let mut session = session.map(|s| s.0); + /* safety: libei_task does not outlive Self */ + let input_capture = unsafe { &*input_capture }; let mut active_clients: Vec<(CaptureHandle, Position)> = vec![]; let mut next_barrier_id = 1u32; - /* there is a bug in xdg-remote-desktop-portal-gnome / mutter that - * prevents receiving further events after a session has been disabled once. - * Therefore the session needs to recreated when the barriers are updated */ + let mut zones_changed = input_capture.receive_zones_changed().await?; loop { - // otherwise it asks to capture input even with no active clients - if active_clients.is_empty() { - wait_for_active_client(&mut notify_rx, &mut active_clients).await; - if notify_rx.is_closed() { - break Ok(()); - } else { - continue; + // do capture session + let cancel_session = CancellationToken::new(); + let cancel_update = CancellationToken::new(); + + let mut capture_event_occured: Option = None; + let mut zones_have_changed = false; + + // kill session if clients need to be updated + let handle_session_update_request = async { + tokio::select! { + _ = cancellation_token.cancelled() => { + log::debug!("cancelled") + }, /* exit requested */ + _ = cancel_update.cancelled() => { + log::debug!("update task cancelled"); + }, /* session exited */ + _ = zones_changed.next() => { + log::debug!("zones changed!"); + zones_have_changed = true + }, /* zones have changed */ + e = capture_event.recv() => if let Some(e) = e { /* clients changed */ + log::debug!("capture event: {e:?}"); + capture_event_occured.replace(e); + }, + } + // kill session (might already be dead!) + log::debug!("=> cancelling session"); + cancel_session.cancel(); + }; + + if !active_clients.is_empty() { + // create session + let mut session = match session.take() { + Some(s) => s, + None => create_session(input_capture).await?.0, + }; + + let capture_session = do_capture_session( + input_capture, + &mut session, + &event_tx, + &mut active_clients, + &mut next_barrier_id, + ¬ify_release, + (cancel_session.clone(), cancel_update.clone()), + ); + + let (capture_result, ()) = tokio::join!(capture_session, handle_session_update_request); + log::debug!("capture session + session_update task done!"); + + // disable capture + log::debug!("disabling input capture"); + if let Err(e) = input_capture.disable(&session).await { + log::warn!("input_capture.disable(&session) {e}"); + } + if let Err(e) = session.close().await { + log::warn!("session.close(): {e}"); + } + + // propagate error from capture session + capture_result?; + } else { + handle_session_update_request.await; + } + + // update clients if requested + if let Some(event) = capture_event_occured.take() { + match event { + LibeiNotifyEvent::Create(c, p) => active_clients.push((c, p)), + LibeiNotifyEvent::Destroy(c) => active_clients.retain(|(h, _)| *h != c), } } - let current_client = Rc::new(Cell::new(None)); + // break + if cancellation_token.is_cancelled() { + break Ok(()); + } + } +} - // create session - let (session, _) = match first_session.take() { - Some(s) => s, - _ => create_session(input_capture).await?, - }; +async fn do_capture_session( + input_capture: &InputCapture<'_>, + session: &mut Session<'_>, + event_tx: &Sender<(CaptureHandle, Event)>, + active_clients: &mut Vec<(CaptureHandle, Position)>, + next_barrier_id: &mut u32, + notify_release: &Notify, + cancel: (CancellationToken, CancellationToken), +) -> Result<(), CaptureError> { + let (cancel_session, cancel_update) = cancel; + // current client + let current_client = Rc::new(Cell::new(None)); - // connect to eis server - let (context, ei_event_stream) = connect_to_eis(input_capture, &session).await?; + // connect to eis server + let (context, ei_event_stream) = connect_to_eis(input_capture, session).await?; - // async event task - let mut ei_task: JoinHandle> = - tokio::task::spawn_local(libei_event_handler( + // set barriers + let client_for_barrier_id = + update_barriers(input_capture, session, active_clients, next_barrier_id).await?; + + log::debug!("enabling session"); + input_capture.enable(session).await?; + + // cancellation token to release session + let release_session = Arc::new(Notify::new()); + + // async event task + let cancel_ei_handler = CancellationToken::new(); + let event_chan = event_tx.clone(); + let client = current_client.clone(); + let cancel_session_clone = cancel_session.clone(); + let release_session_clone = release_session.clone(); + let cancel_ei_handler_clone = cancel_ei_handler.clone(); + let ei_task = async move { + tokio::select! { + r = libei_event_handler( ei_event_stream, context, - event_tx.clone(), - current_client.clone(), - )); + event_chan, + release_session_clone, + client, + ) => { + log::debug!("libei exited: {r:?} cancelling session task"); + cancel_session_clone.cancel(); + } + _ = cancel_ei_handler_clone.cancelled() => {}, + } + Ok::<(), CaptureError>(()) + }; + let capture_session_task = async { + // receiver for activation tokens let mut activated = input_capture.receive_activated().await?; - let mut zones_changed = input_capture.receive_zones_changed().await?; - - // set barriers - let client_for_barrier_id = update_barriers( - input_capture, - &session, - &active_clients, - &mut next_barrier_id, - ) - .await?; - - log::debug!("enabling session"); - input_capture.enable(&session).await?; - + let mut ei_devices_changed = false; loop { tokio::select! { activated = activated.next() => { @@ -304,57 +393,60 @@ async fn do_capture<'a>( .expect("invalid barrier id"); current_client.replace(Some(client)); - if event_tx.send((client, Event::Enter())).await.is_err() { - break; - }; + // client entered => send event + event_tx.send((client, Event::Enter())).await.expect("no channel"); tokio::select! { - producer_event = notify_rx.recv() => { - let producer_event = producer_event.expect("channel closed"); - if handle_producer_event(producer_event, &mut active_clients) { - break; /* clients updated */ - } - } - zones_changed = zones_changed.next() => { - log::debug!("zones changed: {zones_changed:?}"); - break; - } - res = &mut ei_task => { - if let Err(e) = res.expect("ei task paniced") { - log::warn!("libei task exited: {e}"); - } - break; - } + _ = notify_release.notified() => { /* capture release */ + log::debug!("release session requested"); + }, + _ = release_session.notified() => { /* release session */ + log::debug!("ei devices changed"); + ei_devices_changed = true; + }, + _ = cancel_session.cancelled() => { /* kill session notify */ + log::debug!("session cancel requested"); + break + }, } - release_capture( - input_capture, - &session, - activated, - client, - &active_clients, - ).await?; + + release_capture(input_capture, session, activated, client, active_clients).await?; + } - producer_event = notify_rx.recv() => { - let producer_event = producer_event.expect("channel closed"); - if handle_producer_event(producer_event, &mut active_clients) { - /* clients updated */ - break; - } + _ = notify_release.notified() => { /* capture release -> we are not capturing anyway, so ignore */ + log::debug!("release session requested"); }, - res = &mut ei_task => { - if let Err(e) = res.expect("ei task paniced") { - log::warn!("libei task exited: {e}"); - } - break; - } + _ = release_session.notified() => { /* release session */ + log::debug!("ei devices changed"); + ei_devices_changed = true; + }, + _ = cancel_session.cancelled() => { /* kill session notify */ + log::debug!("session cancel requested"); + break + }, + } + if ei_devices_changed { + /* for whatever reason, GNOME seems to kill the session + * as soon as devices are added or removed, so we need + * to cancel */ + break; } } - ei_task.abort(); - input_capture.disable(&session).await?; - if event_tx.is_closed() { - break Ok(()); - } - } + // cancel libei task + log::debug!("session exited: killing libei task"); + cancel_ei_handler.cancel(); + Ok::<(), CaptureError>(()) + }; + + let (a, b) = tokio::join!(ei_task, capture_session_task); + + cancel_update.cancel(); + + log::debug!("both session and ei task finished!"); + a?; + b?; + + Ok(()) } async fn release_capture( @@ -387,209 +479,101 @@ async fn release_capture( Ok(()) } -fn handle_producer_event( - producer_event: ProducerEvent, - active_clients: &mut Vec<(CaptureHandle, Position)>, -) -> bool { - log::debug!("handling event: {producer_event:?}"); - match producer_event { - ProducerEvent::Release => false, - ProducerEvent::Create(c, p) => { - active_clients.push((c, p)); - true - } - ProducerEvent::Destroy(c) => { - active_clients.retain(|(h, _)| *h != c); - true - } - } -} +static ALL_CAPABILITIES: &[DeviceCapability] = &[ + DeviceCapability::Pointer, + DeviceCapability::PointerAbsolute, + DeviceCapability::Keyboard, + DeviceCapability::Touch, + DeviceCapability::Scroll, + DeviceCapability::Button, +]; async fn handle_ei_event( ei_event: EiEvent, current_client: Option, context: &ei::Context, event_tx: &Sender<(CaptureHandle, Event)>, -) { + release_session: &Notify, +) -> Result<(), CaptureError> { match ei_event { EiEvent::SeatAdded(s) => { - s.seat.bind_capabilities(&[ - DeviceCapability::Pointer, - DeviceCapability::PointerAbsolute, - DeviceCapability::Keyboard, - DeviceCapability::Touch, - DeviceCapability::Scroll, - DeviceCapability::Button, - ]); - context.flush().unwrap(); + s.seat.bind_capabilities(ALL_CAPABILITIES); + context.flush().map_err(|e| io::Error::new(e.kind(), e))?; } - EiEvent::SeatRemoved(_) => {} - EiEvent::DeviceAdded(_) => {} - EiEvent::DeviceRemoved(_) => {} - EiEvent::DevicePaused(_) => {} - EiEvent::DeviceResumed(_) => {} - EiEvent::KeyboardModifiers(mods) => { - let modifier_event = KeyboardEvent::Modifiers { - mods_depressed: mods.depressed, - mods_latched: mods.latched, - mods_locked: mods.locked, - group: mods.group, - }; - if let Some(current_client) = current_client { - event_tx - .send((current_client, Event::Keyboard(modifier_event))) - .await - .unwrap(); - } + EiEvent::SeatRemoved(_) | /* EiEvent::DeviceAdded(_) | */ EiEvent::DeviceRemoved(_) => { + log::debug!("releasing session: {ei_event:?}"); + release_session.notify_waiters(); } - EiEvent::Frame(_) => {} - EiEvent::DeviceStartEmulating(_) => { - log::debug!("START EMULATING =============>"); - } - EiEvent::DeviceStopEmulating(_) => { - log::debug!("==================> STOP EMULATING"); - } - EiEvent::PointerMotion(motion) => { - let motion_event = PointerEvent::Motion { - time: motion.time as u32, - dx: motion.dx as f64, - dy: motion.dy as f64, - }; - if let Some(current_client) = current_client { - event_tx - .send((current_client, Event::Pointer(motion_event))) - .await - .unwrap(); - } - } - EiEvent::PointerMotionAbsolute(_) => {} - EiEvent::Button(button) => { - let button_event = PointerEvent::Button { - time: button.time as u32, - button: button.button, - state: match button.state { - ButtonState::Released => 0, - ButtonState::Press => 1, - }, - }; - if let Some(current_client) = current_client { - event_tx - .send((current_client, Event::Pointer(button_event))) - .await - .unwrap(); - } - } - EiEvent::ScrollDelta(delta) => { - if let Some(handle) = current_client { - let mut events = vec![]; - if delta.dy != 0. { - events.push(PointerEvent::Axis { - time: 0, - axis: 0, - value: delta.dy as f64, - }); - } - if delta.dx != 0. { - events.push(PointerEvent::Axis { - time: 0, - axis: 1, - value: delta.dx as f64, - }); - } - for event in events { - event_tx - .send((handle, Event::Pointer(event))) - .await - .unwrap(); - } - } - } - EiEvent::ScrollStop(_) => {} - EiEvent::ScrollCancel(_) => {} - EiEvent::ScrollDiscrete(scroll) => { - if scroll.discrete_dy != 0 { - let event = PointerEvent::AxisDiscrete120 { - axis: 0, - value: scroll.discrete_dy, - }; - if let Some(current_client) = current_client { - event_tx - .send((current_client, Event::Pointer(event))) - .await - .unwrap(); - } - } - if scroll.discrete_dx != 0 { - let event = PointerEvent::AxisDiscrete120 { - axis: 1, - value: scroll.discrete_dx, - }; - if let Some(current_client) = current_client { - event_tx - .send((current_client, Event::Pointer(event))) - .await - .unwrap(); - } - }; - } - EiEvent::KeyboardKey(key) => { - let key_event = KeyboardEvent::Key { - key: key.key, - state: match key.state { - KeyState::Press => 1, - KeyState::Released => 0, - }, - time: key.time as u32, - }; - if let Some(current_client) = current_client { - event_tx - .send((current_client, Event::Keyboard(key_event))) - .await - .unwrap(); - } - } - EiEvent::TouchDown(_) => {} - EiEvent::TouchUp(_) => {} - EiEvent::TouchMotion(_) => {} + EiEvent::DevicePaused(_) | EiEvent::DeviceResumed(_) => {} + EiEvent::DeviceStartEmulating(_) => log::debug!("START EMULATING"), + EiEvent::DeviceStopEmulating(_) => log::debug!("STOP EMULATING"), EiEvent::Disconnected(d) => { - log::error!("disconnect: {d:?}"); + return Err(CaptureError::Disconnected(format!("{:?}", d.reason))) } + _ => { + if let Some(handle) = current_client { + for event in Event::from_ei_event(ei_event) { + event_tx.send((handle, event)).await.expect("no channel"); + } + } + } + } + Ok(()) +} + +#[async_trait] +impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> { + async fn create(&mut self, handle: CaptureHandle, pos: Position) -> Result<(), CaptureError> { + let _ = self + .notify_capture + .send(LibeiNotifyEvent::Create(handle, pos)) + .await; + Ok(()) + } + + async fn destroy(&mut self, handle: CaptureHandle) -> Result<(), CaptureError> { + let _ = self + .notify_capture + .send(LibeiNotifyEvent::Destroy(handle)) + .await; + Ok(()) + } + + async fn release(&mut self) -> Result<(), CaptureError> { + self.notify_release.notify_waiters(); + Ok(()) + } + + async fn terminate(&mut self) -> Result<(), CaptureError> { + self.cancellation_token.cancel(); + let task = &mut self.capture_task; + log::debug!("waiting for capture to terminate..."); + let res = task.await.expect("libei task panic"); + log::debug!("done!"); + self.terminated = true; + res } } -impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> { - fn create(&mut self, handle: super::CaptureHandle, pos: super::Position) -> io::Result<()> { - let notify_tx = self.notify_tx.clone(); - tokio::task::spawn_local(async move { - let _ = notify_tx.send(ProducerEvent::Create(handle, pos)).await; - }); - Ok(()) - } - - fn destroy(&mut self, handle: super::CaptureHandle) -> io::Result<()> { - let notify_tx = self.notify_tx.clone(); - tokio::task::spawn_local(async move { - let _ = notify_tx.send(ProducerEvent::Destroy(handle)).await; - }); - Ok(()) - } - - fn release(&mut self) -> io::Result<()> { - let notify_tx = self.notify_tx.clone(); - tokio::task::spawn_local(async move { - let _ = notify_tx.send(ProducerEvent::Release).await; - }); - Ok(()) +impl<'a> Drop for LibeiInputCapture<'a> { + fn drop(&mut self) { + if !self.terminated { + /* this workaround is needed until async drop is stabilized */ + panic!("LibeiInputCapture dropped without being terminated!"); + } } } impl<'a> Stream for LibeiInputCapture<'a> { - type Item = io::Result<(CaptureHandle, Event)>; + type Item = Result<(CaptureHandle, Event), CaptureError>; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match ready!(self.event_rx.poll_recv(cx)) { - None => Poll::Ready(None), - Some(e) => Poll::Ready(Some(Ok(e))), + match self.capture_task.poll_unpin(cx) { + Poll::Ready(r) => match r.expect("failed to join") { + Ok(()) => Poll::Ready(None), + Err(e) => Poll::Ready(Some(Err(e))), + }, + Poll::Pending => self.event_rx.poll_recv(cx).map(|e| e.map(Result::Ok)), } } } diff --git a/input-capture/src/macos.rs b/input-capture/src/macos.rs index a2383e1..fb899a2 100644 --- a/input-capture/src/macos.rs +++ b/input-capture/src/macos.rs @@ -1,8 +1,11 @@ -use crate::{error::MacOSInputCaptureCreationError, CaptureHandle, InputCapture, Position}; +use crate::{ + error::MacOSInputCaptureCreationError, CaptureError, CaptureHandle, InputCapture, Position, +}; +use async_trait::async_trait; use futures_core::Stream; use input_event::Event; +use std::pin::Pin; use std::task::{Context, Poll}; -use std::{io, pin::Pin}; pub struct MacOSInputCapture; @@ -13,23 +16,28 @@ impl MacOSInputCapture { } impl Stream for MacOSInputCapture { - type Item = io::Result<(CaptureHandle, Event)>; + type Item = Result<(CaptureHandle, Event), CaptureError>; fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { Poll::Pending } } +#[async_trait] impl InputCapture for MacOSInputCapture { - fn create(&mut self, _id: CaptureHandle, _pos: Position) -> io::Result<()> { + async fn create(&mut self, _id: CaptureHandle, _pos: Position) -> Result<(), CaptureError> { Ok(()) } - fn destroy(&mut self, _id: CaptureHandle) -> io::Result<()> { + async fn destroy(&mut self, _id: CaptureHandle) -> Result<(), CaptureError> { Ok(()) } - fn release(&mut self) -> io::Result<()> { + async fn release(&mut self) -> Result<(), CaptureError> { + Ok(()) + } + + async fn terminate(&mut self) -> Result<(), CaptureError> { Ok(()) } } diff --git a/input-capture/src/wayland.rs b/input-capture/src/wayland.rs index 22368cf..90974ee 100644 --- a/input-capture/src/wayland.rs +++ b/input-capture/src/wayland.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use futures_core::Stream; use memmap::MmapOptions; use std::{ @@ -14,7 +15,7 @@ use std::{ fs::File, io::{BufWriter, Write}, os::unix::prelude::{AsRawFd, FromRawFd}, - rc::Rc, + sync::Arc, }; use wayland_protocols::{ @@ -62,6 +63,8 @@ use tempfile; use input_event::{Event, KeyboardEvent, PointerEvent}; +use crate::CaptureError; + use super::{ error::{LayerShellCaptureCreationError, WaylandBindError}, CaptureHandle, InputCapture, Position, @@ -102,8 +105,8 @@ struct State { pointer_lock: Option, rel_pointer: Option, shortcut_inhibitor: Option, - client_for_window: Vec<(Rc, CaptureHandle)>, - focused: Option<(Rc, CaptureHandle)>, + client_for_window: Vec<(Arc, CaptureHandle)>, + focused: Option<(Arc, CaptureHandle)>, g: Globals, wayland_fd: OwnedFd, read_guard: Option, @@ -475,7 +478,7 @@ impl State { log::debug!("outputs: {outputs:?}"); outputs.iter().for_each(|(o, i)| { let window = Window::new(self, &self.qh, o, pos, i.size); - let window = Rc::new(window); + let window = Arc::new(window); self.client_for_window.push((window, client)); }); } @@ -561,28 +564,34 @@ impl Inner { } } +#[async_trait] impl InputCapture for WaylandInputCapture { - fn create(&mut self, handle: CaptureHandle, pos: Position) -> io::Result<()> { + async fn create(&mut self, handle: CaptureHandle, pos: Position) -> Result<(), CaptureError> { self.add_client(handle, pos); let inner = self.0.get_mut(); - inner.flush_events() - } - fn destroy(&mut self, handle: CaptureHandle) -> io::Result<()> { - self.delete_client(handle); - let inner = self.0.get_mut(); - inner.flush_events() + Ok(inner.flush_events()?) } - fn release(&mut self) -> io::Result<()> { + async fn destroy(&mut self, handle: CaptureHandle) -> Result<(), CaptureError> { + self.delete_client(handle); + let inner = self.0.get_mut(); + Ok(inner.flush_events()?) + } + + async fn release(&mut self) -> Result<(), CaptureError> { log::debug!("releasing pointer"); let inner = self.0.get_mut(); inner.state.ungrab(); - inner.flush_events() + Ok(inner.flush_events()?) + } + + async fn terminate(&mut self) -> Result<(), CaptureError> { + Ok(()) } } impl Stream for WaylandInputCapture { - type Item = io::Result<(CaptureHandle, Event)>; + type Item = Result<(CaptureHandle, Event), CaptureError>; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if let Some(event) = self.0.get_mut().state.pending_events.pop_front() { @@ -600,7 +609,7 @@ impl Stream for WaylandInputCapture { // prepare next read match inner.prepare_read() { Ok(_) => {} - Err(e) => return Poll::Ready(Some(Err(e))), + Err(e) => return Poll::Ready(Some(Err(e.into()))), } } @@ -610,14 +619,14 @@ impl Stream for WaylandInputCapture { // flush outgoing events if let Err(e) = inner.flush_events() { if e.kind() != ErrorKind::WouldBlock { - return Poll::Ready(Some(Err(e))); + return Poll::Ready(Some(Err(e.into()))); } } // prepare for the next read match inner.prepare_read() { Ok(_) => {} - Err(e) => return Poll::Ready(Some(Err(e))), + Err(e) => return Poll::Ready(Some(Err(e.into()))), } } diff --git a/input-capture/src/windows.rs b/input-capture/src/windows.rs index bfbf6c3..96a142a 100644 --- a/input-capture/src/windows.rs +++ b/input-capture/src/windows.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use core::task::{Context, Poll}; use futures::Stream; use once_cell::unsync::Lazy; @@ -10,7 +11,7 @@ use std::default::Default; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{mpsc, Mutex}; use std::task::ready; -use std::{io, pin::Pin, thread}; +use std::{pin::Pin, thread}; use tokio::sync::mpsc::{channel, Receiver, Sender}; use windows::core::{w, PCWSTR}; use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM}; @@ -36,7 +37,7 @@ use input_event::{ Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, }; -use super::{CaptureHandle, InputCapture, Position}; +use super::{CaptureError, CaptureHandle, InputCapture, Position}; enum Request { Create(CaptureHandle, Position), @@ -62,8 +63,9 @@ unsafe fn signal_message_thread(event_type: EventType) { } } +#[async_trait] impl InputCapture for WindowsInputCapture { - fn create(&mut self, handle: CaptureHandle, pos: Position) -> io::Result<()> { + async fn create(&mut self, handle: CaptureHandle, pos: Position) -> Result<(), CaptureError> { unsafe { { let mut requests = REQUEST_BUFFER.lock().unwrap(); @@ -73,7 +75,8 @@ impl InputCapture for WindowsInputCapture { } Ok(()) } - fn destroy(&mut self, handle: CaptureHandle) -> io::Result<()> { + + async fn destroy(&mut self, handle: CaptureHandle) -> Result<(), CaptureError> { unsafe { { let mut requests = REQUEST_BUFFER.lock().unwrap(); @@ -84,10 +87,14 @@ impl InputCapture for WindowsInputCapture { Ok(()) } - fn release(&mut self) -> io::Result<()> { + async fn release(&mut self) -> Result<(), CaptureError> { unsafe { signal_message_thread(EventType::Release) }; Ok(()) } + + async fn terminate(&mut self) -> Result<(), CaptureError> { + Ok(()) + } } static mut REQUEST_BUFFER: Mutex> = Mutex::new(Vec::new()); @@ -609,7 +616,7 @@ impl WindowsInputCapture { } impl Stream for WindowsInputCapture { - type Item = io::Result<(CaptureHandle, Event)>; + type Item = Result<(CaptureHandle, Event), CaptureError>; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match ready!(self.event_rx.poll_recv(cx)) { None => Poll::Ready(None), diff --git a/input-capture/src/x11.rs b/input-capture/src/x11.rs index 2378c85..1473d0f 100644 --- a/input-capture/src/x11.rs +++ b/input-capture/src/x11.rs @@ -1,8 +1,10 @@ -use std::io; use std::task::Poll; +use async_trait::async_trait; use futures_core::Stream; +use crate::CaptureError; + use super::InputCapture; use input_event::Event; @@ -17,22 +19,27 @@ impl X11InputCapture { } } +#[async_trait] impl InputCapture for X11InputCapture { - fn create(&mut self, _id: CaptureHandle, _pos: Position) -> io::Result<()> { + async fn create(&mut self, _id: CaptureHandle, _pos: Position) -> Result<(), CaptureError> { Ok(()) } - fn destroy(&mut self, _id: CaptureHandle) -> io::Result<()> { + async fn destroy(&mut self, _id: CaptureHandle) -> Result<(), CaptureError> { Ok(()) } - fn release(&mut self) -> io::Result<()> { + async fn release(&mut self) -> Result<(), CaptureError> { + Ok(()) + } + + async fn terminate(&mut self) -> Result<(), CaptureError> { Ok(()) } } impl Stream for X11InputCapture { - type Item = io::Result<(CaptureHandle, Event)>; + type Item = Result<(CaptureHandle, Event), CaptureError>; fn poll_next( self: std::pin::Pin<&mut Self>, diff --git a/input-emulation/src/dummy.rs b/input-emulation/src/dummy.rs index 596b71d..6039000 100644 --- a/input-emulation/src/dummy.rs +++ b/input-emulation/src/dummy.rs @@ -26,4 +26,7 @@ impl InputEmulation for DummyEmulation { } async fn create(&mut self, _: EmulationHandle) {} async fn destroy(&mut self, _: EmulationHandle) {} + async fn terminate(&mut self) { + /* nothing to do */ + } } diff --git a/input-emulation/src/error.rs b/input-emulation/src/error.rs index 8695777..bb54b90 100644 --- a/input-emulation/src/error.rs +++ b/input-emulation/src/error.rs @@ -1,3 +1,13 @@ +#[derive(Debug, Error)] +pub enum InputEmulationError { + #[error("error creating input-emulation: `{0}`")] + Create(#[from] EmulationCreationError), + #[error("error emulating input: `{0}`")] + Emulate(#[from] EmulationError), +} + +#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] +use ashpd::{desktop::ResponseError, Error::Response}; #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] use reis::tokio::EiConvertEventStreamError; use std::io; @@ -75,6 +85,25 @@ pub enum EmulationCreationError { NoAvailableBackend, } +impl EmulationCreationError { + /// request was intentionally denied by the user + #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] + pub(crate) fn cancelled_by_user(&self) -> bool { + matches!( + self, + EmulationCreationError::Libei(LibeiEmulationCreationError::Ashpd(Response( + ResponseError::Cancelled, + ))) | EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response( + ResponseError::Cancelled, + ))) + ) + } + #[cfg(not(all(unix, feature = "libei", not(target_os = "macos"))))] + pub(crate) fn cancelled_by_user(&self) -> bool { + false + } +} + #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[derive(Debug, Error)] pub enum WlrootsEmulationCreationError { diff --git a/input-emulation/src/lib.rs b/input-emulation/src/lib.rs index d05aff8..f9adf3e 100644 --- a/input-emulation/src/lib.rs +++ b/input-emulation/src/lib.rs @@ -1,10 +1,9 @@ use async_trait::async_trait; -use error::EmulationError; use std::fmt::Display; use input_event::Event; -use self::error::EmulationCreationError; +pub use self::error::{EmulationCreationError, EmulationError, InputEmulationError}; #[cfg(windows)] pub mod windows; @@ -76,6 +75,7 @@ pub trait InputEmulation: Send { ) -> Result<(), EmulationError>; async fn create(&mut self, handle: EmulationHandle); async fn destroy(&mut self, handle: EmulationHandle); + async fn terminate(&mut self); } pub async fn create_backend( @@ -131,6 +131,7 @@ pub async fn create( log::info!("using emulation backend: {backend}"); return Ok(b); } + Err(e) if e.cancelled_by_user() => return Err(e), Err(e) => log::warn!("{e}"), } } diff --git a/input-emulation/src/libei.rs b/input-emulation/src/libei.rs index 824fe4b..06510f0 100644 --- a/input-emulation/src/libei.rs +++ b/input-emulation/src/libei.rs @@ -1,12 +1,12 @@ -use futures::StreamExt; +use futures::{future, StreamExt}; use once_cell::sync::Lazy; use std::{ collections::HashMap, io, os::{fd::OwnedFd, unix::net::UnixStream}, sync::{ - atomic::{AtomicU32, Ordering}, - Arc, RwLock, + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, Mutex, RwLock, }, time::{SystemTime, UNIX_EPOCH}, }; @@ -15,7 +15,7 @@ use tokio::task::JoinHandle; use ashpd::{ desktop::{ remote_desktop::{DeviceType, RemoteDesktop}, - ResponseError, + Session, }, WindowIdentifier, }; @@ -60,51 +60,44 @@ struct Devices { keyboard: Arc>>, } -pub struct LibeiEmulation { +pub struct LibeiEmulation<'a> { context: ei::Context, devices: Devices, + ei_task: JoinHandle<()>, + error: Arc>>, + libei_error: Arc, serial: AtomicU32, - ei_task: JoinHandle>, + _remote_desktop: RemoteDesktop<'a>, + session: Session<'a>, } -async fn get_ei_fd() -> Result { - let proxy = RemoteDesktop::new().await?; +async fn get_ei_fd<'a>() -> Result<(RemoteDesktop<'a>, Session<'a>, OwnedFd), ashpd::Error> { + let remote_desktop = RemoteDesktop::new().await?; - // retry when user presses the cancel button - let (session, _) = loop { - log::debug!("creating session ..."); - let session = proxy.create_session().await?; + log::debug!("creating session ..."); + let session = remote_desktop.create_session().await?; - log::debug!("selecting devices ..."); - proxy - .select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer) - .await?; + log::debug!("selecting devices ..."); + remote_desktop + .select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer) + .await?; - log::info!("requesting permission for input emulation"); - match proxy - .start(&session, &WindowIdentifier::default()) - .await? - .response() - { - Ok(d) => break (session, d), - Err(ashpd::Error::Response(ResponseError::Cancelled)) => { - log::warn!("request cancelled!"); - continue; - } - e => e?, - }; - }; + log::info!("requesting permission for input emulation"); + let _devices = remote_desktop + .start(&session, &WindowIdentifier::default()) + .await? + .response()?; - proxy.connect_to_eis(&session).await + let fd = remote_desktop.connect_to_eis(&session).await?; + Ok((remote_desktop, session, fd)) } -impl LibeiEmulation { +impl<'a> LibeiEmulation<'a> { pub async fn new() -> Result { - let eifd = get_ei_fd().await?; + let (_remote_desktop, session, eifd) = get_ei_fd().await?; let stream = UnixStream::from(eifd); stream.set_nonblocking(true)?; let context = ei::Context::new(stream)?; - context.flush().map_err(|e| io::Error::new(e.kind(), e))?; let mut events = EiEventStream::new(context.clone())?; let handshake = ei_handshake( &mut events, @@ -115,28 +108,40 @@ impl LibeiEmulation { .await?; let events = EiConvertEventStream::new(events, handshake.serial); let devices = Devices::default(); - let ei_handler = ei_event_handler(events, context.clone(), devices.clone()); + let libei_error = Arc::new(AtomicBool::default()); + let error = Arc::new(Mutex::new(None)); + let ei_handler = ei_task( + events, + context.clone(), + devices.clone(), + libei_error.clone(), + error.clone(), + ); let ei_task = tokio::task::spawn_local(ei_handler); let serial = AtomicU32::new(handshake.serial); Ok(Self { - serial, context, - ei_task, devices, + ei_task, + error, + libei_error, + serial, + _remote_desktop, + session, }) } } -impl Drop for LibeiEmulation { +impl<'a> Drop for LibeiEmulation<'a> { fn drop(&mut self) { self.ei_task.abort(); } } #[async_trait] -impl InputEmulation for LibeiEmulation { +impl<'a> InputEmulation for LibeiEmulation<'a> { async fn consume( &mut self, event: Event, @@ -146,6 +151,12 @@ impl InputEmulation for LibeiEmulation { .duration_since(UNIX_EPOCH) .unwrap() .as_micros() as u64; + if self.libei_error.load(Ordering::SeqCst) { + // don't break sending additional events but signal error + if let Some(e) = self.error.lock().unwrap().take() { + return Err(e); + } + } match event { Event::Pointer(p) => match p { PointerEvent::Motion { time: _, dx, dy } => { @@ -228,12 +239,37 @@ impl InputEmulation for LibeiEmulation { async fn create(&mut self, _: EmulationHandle) {} async fn destroy(&mut self, _: EmulationHandle) {} + + async fn terminate(&mut self) { + let _ = self.session.close().await; + self.ei_task.abort(); + } } -async fn ei_event_handler( +async fn ei_task( mut events: EiConvertEventStream, context: ei::Context, devices: Devices, + libei_error: Arc, + error: Arc>>, +) { + loop { + match ei_event_handler(&mut events, &context, &devices).await { + Ok(()) => {} + Err(e) => { + libei_error.store(true, Ordering::SeqCst); + error.lock().unwrap().replace(e); + // wait for termination -> otherwise we will loop forever + future::pending::<()>().await; + } + } + } +} + +async fn ei_event_handler( + events: &mut EiConvertEventStream, + context: &ei::Context, + devices: &Devices, ) -> Result<(), EmulationError> { loop { let event = events @@ -253,7 +289,7 @@ async fn ei_event_handler( match event { EiEvent::Disconnected(e) => { log::debug!("ei disconnected: {e:?}"); - break; + return Err(EmulationError::EndOfStream); } EiEvent::SeatAdded(e) => { e.seat().bind_capabilities(CAPABILITIES); @@ -327,5 +363,4 @@ async fn ei_event_handler( } context.flush().map_err(|e| io::Error::new(e.kind(), e))?; } - Ok(()) } diff --git a/input-emulation/src/macos.rs b/input-emulation/src/macos.rs index 392aec1..e59c4bb 100644 --- a/input-emulation/src/macos.rs +++ b/input-emulation/src/macos.rs @@ -295,4 +295,6 @@ impl InputEmulation for MacOSEmulation { async fn create(&mut self, _handle: EmulationHandle) {} async fn destroy(&mut self, _handle: EmulationHandle) {} + + async fn terminate(&mut self) {} } diff --git a/input-emulation/src/windows.rs b/input-emulation/src/windows.rs index 3276769..3bf86d8 100644 --- a/input-emulation/src/windows.rs +++ b/input-emulation/src/windows.rs @@ -80,6 +80,8 @@ impl InputEmulation for WindowsEmulation { async fn create(&mut self, _handle: EmulationHandle) {} async fn destroy(&mut self, _handle: EmulationHandle) {} + + async fn terminate(&mut self) {} } impl WindowsEmulation { diff --git a/input-emulation/src/wlroots.rs b/input-emulation/src/wlroots.rs index 7d545c8..be5f71b 100644 --- a/input-emulation/src/wlroots.rs +++ b/input-emulation/src/wlroots.rs @@ -165,6 +165,9 @@ impl InputEmulation for WlrootsEmulation { log::error!("{}", e); } } + async fn terminate(&mut self) { + /* nothing to do */ + } } struct VirtualInput { diff --git a/input-emulation/src/x11.rs b/input-emulation/src/x11.rs index 790466a..496025f 100644 --- a/input-emulation/src/x11.rs +++ b/input-emulation/src/x11.rs @@ -148,4 +148,8 @@ impl InputEmulation for X11Emulation { async fn destroy(&mut self, _: EmulationHandle) { // for our purposes it does not matter what client sent the event } + + async fn terminate(&mut self) { + /* nothing to do */ + } } diff --git a/input-emulation/src/xdg_desktop_portal.rs b/input-emulation/src/xdg_desktop_portal.rs index 8c6ff0c..fedd3e2 100644 --- a/input-emulation/src/xdg_desktop_portal.rs +++ b/input-emulation/src/xdg_desktop_portal.rs @@ -1,7 +1,7 @@ use ashpd::{ desktop::{ remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop}, - ResponseError, Session, + Session, }, zbus::AsyncDrop, WindowIdentifier, @@ -29,29 +29,19 @@ impl<'a> DesktopPortalEmulation<'a> { let proxy = RemoteDesktop::new().await?; // retry when user presses the cancel button - let (session, _) = loop { - log::debug!("creating session ..."); - let session = proxy.create_session().await?; + log::debug!("creating session ..."); + let session = proxy.create_session().await?; - log::debug!("selecting devices ..."); - proxy - .select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer) - .await?; + log::debug!("selecting devices ..."); + proxy + .select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer) + .await?; - log::info!("requesting permission for input emulation"); - match proxy - .start(&session, &WindowIdentifier::default()) - .await? - .response() - { - Ok(d) => break (session, d), - Err(ashpd::Error::Response(ResponseError::Cancelled)) => { - log::warn!("request cancelled!"); - continue; - } - e => e?, - }; - }; + log::info!("requesting permission for input emulation"); + let _devices = proxy + .start(&session, &WindowIdentifier::default()) + .await? + .response()?; log::debug!("started session"); let session = session; @@ -142,6 +132,14 @@ impl<'a> InputEmulation for DesktopPortalEmulation<'a> { async fn create(&mut self, _client: EmulationHandle) {} async fn destroy(&mut self, _client: EmulationHandle) {} + async fn terminate(&mut self) { + if let Err(e) = self.session.close().await { + log::warn!("session.close(): {e}"); + }; + if let Err(e) = self.session.receive_closed().await { + log::warn!("session.receive_closed(): {e}"); + }; + } } impl<'a> AsyncDrop for DesktopPortalEmulation<'a> { diff --git a/input-event/Cargo.toml b/input-event/Cargo.toml index 82d6f6b..c392c6a 100644 --- a/input-event/Cargo.toml +++ b/input-event/Cargo.toml @@ -12,3 +12,10 @@ log = "0.4.22" num_enum = "0.7.2" serde = { version = "1.0", features = ["derive"] } thiserror = "1.0.61" + +[target.'cfg(all(unix, not(target_os="macos")))'.dependencies] +reis = { version = "0.2.0", optional = true } + +[features] +default = ["libei"] +libei = ["dep:reis"] diff --git a/input-event/src/lib.rs b/input-event/src/lib.rs index c66c214..c5c5bf6 100644 --- a/input-event/src/lib.rs +++ b/input-event/src/lib.rs @@ -5,6 +5,9 @@ pub mod error; pub mod proto; pub mod scancode; +#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] +mod libei; + // FIXME pub const BTN_LEFT: u32 = 0x110; pub const BTN_RIGHT: u32 = 0x111; diff --git a/input-event/src/libei.rs b/input-event/src/libei.rs new file mode 100644 index 0000000..51e6368 --- /dev/null +++ b/input-event/src/libei.rs @@ -0,0 +1,146 @@ +use reis::{ + ei::{button::ButtonState, keyboard::KeyState}, + event::EiEvent, +}; + +use crate::{Event, KeyboardEvent, PointerEvent}; + +impl Event { + pub fn from_ei_event(ei_event: EiEvent) -> impl Iterator { + to_input_events(ei_event).into_iter() + } +} + +enum Events { + None, + One(Event), + Two(Event, Event), +} + +impl Events { + fn into_iter(self) -> impl Iterator { + EventIterator::new(self) + } +} + +struct EventIterator { + events: [Option; 2], + pos: usize, +} + +impl EventIterator { + fn new(events: Events) -> Self { + let events = match events { + Events::None => [None, None], + Events::One(e) => [Some(e), None], + Events::Two(e, f) => [Some(e), Some(f)], + }; + Self { events, pos: 0 } + } +} + +impl Iterator for EventIterator { + type Item = Event; + + fn next(&mut self) -> Option { + let res = if self.pos >= self.events.len() { + None + } else { + self.events[self.pos] + }; + self.pos += 1; + res + } +} + +fn to_input_events(ei_event: EiEvent) -> Events { + match ei_event { + EiEvent::KeyboardModifiers(mods) => { + let modifier_event = KeyboardEvent::Modifiers { + mods_depressed: mods.depressed, + mods_latched: mods.latched, + mods_locked: mods.locked, + group: mods.group, + }; + Events::One(Event::Keyboard(modifier_event)) + } + EiEvent::Frame(_) => Events::None, /* FIXME */ + EiEvent::PointerMotion(motion) => { + let motion_event = PointerEvent::Motion { + time: motion.time as u32, + dx: motion.dx as f64, + dy: motion.dy as f64, + }; + Events::One(Event::Pointer(motion_event)) + } + EiEvent::PointerMotionAbsolute(_) => Events::None, + EiEvent::Button(button) => { + let button_event = PointerEvent::Button { + time: button.time as u32, + button: button.button, + state: match button.state { + ButtonState::Released => 0, + ButtonState::Press => 1, + }, + }; + Events::One(Event::Pointer(button_event)) + } + EiEvent::ScrollDelta(delta) => { + let dy = Event::Pointer(PointerEvent::Axis { + time: 0, + axis: 0, + value: delta.dy as f64, + }); + let dx = Event::Pointer(PointerEvent::Axis { + time: 0, + axis: 1, + value: delta.dx as f64, + }); + if delta.dy != 0. && delta.dx != 0. { + Events::Two(dy, dx) + } else if delta.dy != 0. { + Events::One(dy) + } else if delta.dx != 0. { + Events::One(dx) + } else { + Events::None + } + } + EiEvent::ScrollStop(_) => Events::None, /* TODO */ + EiEvent::ScrollCancel(_) => Events::None, /* TODO */ + EiEvent::ScrollDiscrete(scroll) => { + let dy = Event::Pointer(PointerEvent::AxisDiscrete120 { + axis: 0, + value: scroll.discrete_dy, + }); + let dx = Event::Pointer(PointerEvent::AxisDiscrete120 { + axis: 1, + value: scroll.discrete_dx, + }); + if scroll.discrete_dy != 0 && scroll.discrete_dx != 0 { + Events::Two(dy, dx) + } else if scroll.discrete_dy != 0 { + Events::One(dy) + } else if scroll.discrete_dx != 0 { + Events::One(dx) + } else { + Events::None + } + } + EiEvent::KeyboardKey(key) => { + let key_event = KeyboardEvent::Key { + key: key.key, + state: match key.state { + KeyState::Press => 1, + KeyState::Released => 0, + }, + time: key.time as u32, + }; + Events::One(Event::Keyboard(key_event)) + } + EiEvent::TouchDown(_) => Events::None, /* TODO */ + EiEvent::TouchUp(_) => Events::None, /* TODO */ + EiEvent::TouchMotion(_) => Events::None, /* TODO */ + _ => Events::None, + } +} diff --git a/resources/client_row.ui b/resources/client_row.ui index 317006d..7ef7219 100644 --- a/resources/client_row.ui +++ b/resources/client_row.ui @@ -44,6 +44,7 @@ + 5 GTK_INPUT_PURPOSE_NUMBER 0.5 center diff --git a/resources/window.ui b/resources/window.ui index ea094b7..7c96036 100644 --- a/resources/window.ui +++ b/resources/window.ui @@ -1,167 +1,227 @@ - - - - - _Close window - window.close - - - + + + + + _Close window + window.close + + + diff --git a/src/capture_test.rs b/src/capture_test.rs index 68b94c5..4eafc05 100644 --- a/src/capture_test.rs +++ b/src/capture_test.rs @@ -1,11 +1,10 @@ use crate::config::Config; -use anyhow::{anyhow, Result}; use futures::StreamExt; -use input_capture::{self, Position}; +use input_capture::{self, CaptureError, InputCapture, InputCaptureError, Position}; use input_event::{Event, KeyboardEvent}; use tokio::task::LocalSet; -pub fn run() -> Result<()> { +pub fn run() -> anyhow::Result<()> { log::info!("running input capture test"); let runtime = tokio::runtime::Builder::new_current_thread() .enable_io() @@ -14,23 +13,32 @@ pub fn run() -> Result<()> { let config = Config::new()?; - runtime.block_on(LocalSet::new().run_until(input_capture_test(config))) + Ok(runtime.block_on(LocalSet::new().run_until(input_capture_test(config)))?) } -async fn input_capture_test(config: Config) -> Result<()> { +async fn input_capture_test(config: Config) -> Result<(), InputCaptureError> { log::info!("creating input capture"); let backend = config.capture_backend.map(|b| b.into()); - let mut input_capture = input_capture::create(backend).await?; - log::info!("creating clients"); - input_capture.create(0, Position::Left)?; - input_capture.create(1, Position::Right)?; - input_capture.create(2, Position::Top)?; - input_capture.create(3, Position::Bottom)?; + loop { + let mut input_capture = input_capture::create(backend).await?; + log::info!("creating clients"); + input_capture.create(0, Position::Left).await?; + input_capture.create(1, Position::Right).await?; + input_capture.create(2, Position::Top).await?; + input_capture.create(3, Position::Bottom).await?; + if let Err(e) = do_capture(&mut input_capture).await { + log::warn!("{e} - recreating capture"); + } + let _ = input_capture.terminate().await; + } +} + +async fn do_capture(input_capture: &mut Box) -> Result<(), CaptureError> { loop { let (client, event) = input_capture .next() .await - .ok_or(anyhow!("capture stream closed"))??; + .ok_or(CaptureError::EndOfStream)??; let pos = match client { 0 => Position::Left, 1 => Position::Right, @@ -39,7 +47,8 @@ async fn input_capture_test(config: Config) -> Result<()> { }; log::info!("position: {pos}, event: {event}"); if let Event::Keyboard(KeyboardEvent::Key { key: 1, .. }) = event { - input_capture.release()?; + input_capture.release().await?; + break Ok(()); } } } diff --git a/src/client.rs b/src/client.rs index 2753e24..2ce6d5c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,5 @@ use std::{ collections::HashSet, - error::Error, fmt::Display, net::{IpAddr, SocketAddr}, str::FromStr, @@ -8,24 +7,20 @@ use std::{ use serde::{Deserialize, Serialize}; use slab::Slab; +use thiserror::Error; use crate::config::DEFAULT_PORT; use input_capture; -#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] pub enum Position { + #[default] Left, Right, Top, Bottom, } -impl Default for Position { - fn default() -> Self { - Self::Left - } -} - impl From for input_capture::Position { fn from(position: Position) -> input_capture::Position { match position { @@ -37,19 +32,12 @@ impl From for input_capture::Position { } } -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("not a valid position: {pos}")] pub struct PositionParseError { - string: String, + pos: String, } -impl Display for PositionParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "not a valid position: {}", self.string) - } -} - -impl Error for PositionParseError {} - impl FromStr for Position { type Err = PositionParseError; @@ -59,7 +47,7 @@ impl FromStr for Position { "right" => Ok(Self::Right), "top" => Ok(Self::Top), "bottom" => Ok(Self::Bottom), - _ => Err(PositionParseError { string: s.into() }), + _ => Err(PositionParseError { pos: s.into() }), } } } @@ -131,6 +119,8 @@ pub struct ClientState { pub active_addr: Option, /// tracks whether or not the client is responding to pings pub alive: bool, + /// ips from dns + pub dns_ips: Vec, /// all ip addresses associated with a particular client /// e.g. Laptops usually have at least an ethernet and a wifi port /// which have different ip addresses @@ -141,27 +131,15 @@ pub struct ClientState { pub resolving: bool, } +#[derive(Default)] pub struct ClientManager { clients: Slab<(ClientConfig, ClientState)>, } -impl Default for ClientManager { - fn default() -> Self { - Self::new() - } -} - impl ClientManager { - pub fn new() -> Self { - let clients = Slab::new(); - Self { clients } - } - /// add a new client to this manager pub fn add_client(&mut self) -> ClientHandle { - let client_config = Default::default(); - let client_state = Default::default(); - self.clients.insert((client_config, client_state)) as ClientHandle + self.clients.insert(Default::default()) as ClientHandle } /// find a client by its address diff --git a/src/dns.rs b/src/dns.rs index f331bf1..bdbabee 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -1,23 +1,62 @@ -use anyhow::Result; -use std::{error::Error, net::IpAddr}; +use std::net::IpAddr; +use tokio::sync::mpsc::Receiver; -use hickory_resolver::TokioAsyncResolver; +use hickory_resolver::{error::ResolveError, TokioAsyncResolver}; -pub struct DnsResolver { +use crate::{client::ClientHandle, server::Server}; + +pub(crate) struct DnsResolver { resolver: TokioAsyncResolver, + dns_request: Receiver, } + impl DnsResolver { - pub(crate) async fn new() -> Result { + pub(crate) fn new(dns_request: Receiver) -> Result { let resolver = TokioAsyncResolver::tokio_from_system_conf()?; - Ok(Self { resolver }) + Ok(Self { + resolver, + dns_request, + }) } - pub(crate) async fn resolve(&self, host: &str) -> Result, Box> { - log::info!("resolving {host} ..."); + async fn resolve(&self, host: &str) -> Result, ResolveError> { let response = self.resolver.lookup_ip(host).await?; for ip in response.iter() { log::info!("{host}: adding ip {ip}"); } Ok(response.iter().collect()) } + + pub(crate) async fn run(mut self, server: Server) { + tokio::select! { + _ = server.cancelled() => {}, + _ = self.do_dns(&server) => {}, + } + } + + async fn do_dns(&mut self, server: &Server) { + loop { + let handle = self.dns_request.recv().await.expect("channel closed"); + + /* update resolving status */ + let hostname = match server.get_hostname(handle) { + Some(hostname) => hostname, + None => continue, + }; + + log::info!("resolving ({handle}) `{hostname}` ..."); + server.set_resolving(handle, true); + + let ips = match self.resolve(&hostname).await { + Ok(ips) => ips, + Err(e) => { + log::warn!("could not resolve host '{hostname}': {e}"); + vec![] + } + }; + + server.update_dns_ips(handle, ips); + server.set_resolving(handle, false); + } + } } diff --git a/src/frontend.rs b/src/frontend.rs index 2adb78b..05f83f2 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -97,8 +97,6 @@ pub enum FrontendRequest { Enumerate(), /// resolve dns ResolveDns(ClientHandle), - /// service shutdown - Terminate(), /// update hostname UpdateHostname(ClientHandle, Option), /// update port @@ -109,6 +107,26 @@ pub enum FrontendRequest { UpdateFixIps(ClientHandle, Vec), /// request the state of the given client GetState(ClientHandle), + /// request reenabling input capture + EnableCapture, + /// request reenabling input emulation + EnableEmulation, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub enum Status { + #[default] + Disabled, + Enabled, +} + +impl From for bool { + fn from(status: Status) -> Self { + match status { + Status::Enabled => true, + Status::Disabled => false, + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -127,6 +145,10 @@ pub enum FrontendEvent { Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>), /// an error occured Error(String), + /// capture status + CaptureStatus(Status), + /// emulation status + EmulationStatus(Status), } pub struct FrontendListener { @@ -232,7 +254,7 @@ impl FrontendListener { Ok(rx) } - pub(crate) async fn broadcast_event(&mut self, notify: FrontendEvent) -> Result<()> { + pub(crate) async fn broadcast(&mut self, notify: FrontendEvent) { // encode event let json = serde_json::to_string(¬ify).unwrap(); let payload = json.as_bytes(); @@ -256,7 +278,6 @@ impl FrontendListener { // could not find a better solution because async let mut keep = keep.into_iter(); self.tx_streams.retain(|_| keep.next().unwrap()); - Ok(()) } } diff --git a/src/frontend/cli.rs b/src/frontend/cli.rs index 89fc010..4224bb4 100644 --- a/src/frontend/cli.rs +++ b/src/frontend/cli.rs @@ -273,6 +273,12 @@ impl<'a> Cli<'a> { FrontendEvent::Error(e) => { eprintln!("ERROR: {e}"); } + FrontendEvent::CaptureStatus(s) => { + eprintln!("capture status: {s:?}") + } + FrontendEvent::EmulationStatus(s) => { + eprintln!("emulation status: {s:?}") + } } } diff --git a/src/frontend/gtk.rs b/src/frontend/gtk.rs index a3dad8c..a64dbfc 100644 --- a/src/frontend/gtk.rs +++ b/src/frontend/gtk.rs @@ -8,7 +8,7 @@ use std::{ process, str, }; -use crate::frontend::{gtk::window::Window, FrontendRequest}; +use crate::frontend::gtk::window::Window; use adw::Application; use endi::{Endian, ReadBytes}; @@ -113,7 +113,6 @@ fn build_ui(app: &Application) { }); let window = Window::new(app, tx); - window.request(FrontendRequest::Enumerate()); glib::spawn_future_local(clone!(@weak window => async move { loop { @@ -150,6 +149,12 @@ fn build_ui(app: &Application) { } window.imp().set_port(port); } + FrontendEvent::CaptureStatus(s) => { + window.set_capture(s.into()); + } + FrontendEvent::EmulationStatus(s) => { + window.set_emulation(s.into()); + } } } })); diff --git a/src/frontend/gtk/window.rs b/src/frontend/gtk/window.rs index e3726a6..7ba5ab7 100644 --- a/src/frontend/gtk/window.rs +++ b/src/frontend/gtk/window.rs @@ -207,29 +207,35 @@ impl Window { } pub fn request_port_change(&self) { - let port = self.imp().port_entry.get().text().to_string(); - if let Ok(port) = port.as_str().parse::() { - self.request(FrontendRequest::ChangePort(port)); - } else { - self.request(FrontendRequest::ChangePort(DEFAULT_PORT)); - } + let port = self + .imp() + .port_entry + .get() + .text() + .as_str() + .parse::() + .unwrap_or(DEFAULT_PORT); + self.request(FrontendRequest::ChangePort(port)); + } + + pub fn request_capture(&self) { + self.request(FrontendRequest::EnableCapture); + } + + pub fn request_emulation(&self) { + self.request(FrontendRequest::EnableEmulation); } pub fn request_client_state(&self, client: &ClientObject) { - let handle = client.handle(); - let event = FrontendRequest::GetState(handle); - self.request(event); + self.request(FrontendRequest::GetState(client.handle())); } pub fn request_client_create(&self) { - let event = FrontendRequest::Create; - self.request(event); + self.request(FrontendRequest::Create); } pub fn request_dns(&self, client: &ClientObject) { - let data = client.get_data(); - let event = FrontendRequest::ResolveDns(data.handle); - self.request(event); + self.request(FrontendRequest::ResolveDns(client.get_data().handle)); } pub fn request_client_update(&self, client: &ClientObject) { @@ -249,15 +255,11 @@ impl Window { } pub fn request_client_activate(&self, client: &ClientObject, active: bool) { - let handle = client.handle(); - let event = FrontendRequest::Activate(handle, active); - self.request(event); + self.request(FrontendRequest::Activate(client.handle(), active)); } pub fn request_client_delete(&self, client: &ClientObject) { - let handle = client.handle(); - let event = FrontendRequest::Delete(handle); - self.request(event); + self.request(FrontendRequest::Delete(client.handle())); } pub fn request(&self, event: FrontendRequest) { @@ -279,4 +281,24 @@ impl Window { let toast_overlay = &self.imp().toast_overlay; toast_overlay.add_toast(toast); } + + pub fn set_capture(&self, active: bool) { + self.imp().capture_active.replace(active); + self.update_capture_emulation_status(); + } + + pub fn set_emulation(&self, active: bool) { + self.imp().emulation_active.replace(active); + self.update_capture_emulation_status(); + } + + fn update_capture_emulation_status(&self) { + let capture = self.imp().capture_active.get(); + let emulation = self.imp().emulation_active.get(); + self.imp().capture_status_row.set_visible(!capture); + self.imp().emulation_status_row.set_visible(!emulation); + self.imp() + .capture_emulation_group + .set_visible(!capture || !emulation); + } } diff --git a/src/frontend/gtk/window/imp.rs b/src/frontend/gtk/window/imp.rs index f53b7f4..9b846ae 100644 --- a/src/frontend/gtk/window/imp.rs +++ b/src/frontend/gtk/window/imp.rs @@ -6,7 +6,7 @@ use std::net::TcpStream; use std::os::unix::net::UnixStream; use adw::subclass::prelude::*; -use adw::{prelude::*, ActionRow, ToastOverlay}; +use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay}; use glib::subclass::InitializingObject; use gtk::glib::clone; use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox}; @@ -30,12 +30,24 @@ pub struct Window { pub hostname_label: TemplateChild