Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 31 additions & 5 deletions input-capture/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::Display,
mem::swap,
sync::{Arc, Mutex},
task::{Poll, ready},
};

Expand Down Expand Up @@ -129,6 +130,24 @@ pub struct InputCapture {
pending: VecDeque<(CaptureHandle, CaptureEvent)>,
}

#[derive(Clone, Debug)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a type similar to this in ashpd, named WindowIdentifierType. Maybe we can make it public, currently it is behind a backend feature

pub enum WindowIdentifier {
Wayland(String),
X11(u32),
}

#[cfg(all(unix, feature = "libei"))]
impl Into<ashpd::WindowIdentifier> for WindowIdentifier {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then this impl can go into ashpd itself

fn into(self) -> ashpd::WindowIdentifier {
match self {
WindowIdentifier::Wayland(handle) => {
ashpd::WindowIdentifier::from_xdg_foreign_exported(handle)
}
WindowIdentifier::X11(_) => todo!(),
}
}
}

impl InputCapture {
/// create a new client with the given id
pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> {
Expand Down Expand Up @@ -190,8 +209,11 @@ impl InputCapture {
}

/// creates a new [`InputCapture`]
pub async fn new(backend: Option<Backend>) -> Result<Self, CaptureCreationError> {
let capture = create(backend).await?;
pub async fn new(
backend: Option<Backend>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<Self, CaptureCreationError> {
let capture = create(backend, window_identifier).await?;
Ok(Self {
capture,
id_map: Default::default(),
Expand Down Expand Up @@ -293,13 +315,16 @@ trait Capture: Stream<Item = Result<(Position, CaptureEvent), CaptureError>> + U

async fn create_backend(
backend: Backend,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError,
> {
match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)),
Backend::InputCapturePortal => Ok(Box::new(
libei::LibeiInputCapture::new(window_identifier).await?,
)),
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(layer_shell::LayerShellInputCapture::new()?)),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Expand All @@ -314,12 +339,13 @@ async fn create_backend(

async fn create(
backend: Option<Backend>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError,
> {
if let Some(backend) = backend {
let b = create_backend(backend).await;
let b = create_backend(backend, window_identifier).await;
if b.is_ok() {
log::info!("using capture backend: {backend}");
}
Expand All @@ -338,7 +364,7 @@ async fn create(
#[cfg(target_os = "macos")]
Backend::MacOs,
] {
match create_backend(backend).await {
match create_backend(backend, window_identifier.clone()).await {
Ok(b) => {
log::info!("using capture backend: {backend}");
return Ok(b);
Expand Down
29 changes: 22 additions & 7 deletions input-capture/src/libei.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::{
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
sync::Arc,
sync::{Arc, Mutex},
task::{Context, Poll},
};
use tokio::{
Expand All @@ -39,7 +39,7 @@ use futures_core::Stream;

use input_event::Event;

use crate::CaptureEvent;
use crate::{CaptureEvent, WindowIdentifier};

use super::{
Capture as LanMouseInputCapture, Position,
Expand Down Expand Up @@ -161,13 +161,17 @@ async fn update_barriers(

async fn create_session(
input_capture: &InputCapture,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> std::result::Result<(Session<InputCapture>, BitFlags<Capabilities>), ashpd::Error> {
log::debug!("creating input capture session");
log::debug!("creating input capture session: {window_identifier:?}");
let window_identifier = window_identifier.lock().unwrap().clone();
let create_session_options = CreateSessionOptions::default().set_capabilities(
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
);
let ashpd_window_identifier: Option<ashpd::WindowIdentifier> =
window_identifier.map(|i| i.into());
input_capture
.create_session(None, create_session_options)
.create_session(ashpd_window_identifier.as_ref(), create_session_options)
.await
}

Expand Down Expand Up @@ -212,10 +216,15 @@ async fn libei_event_handler(
}

impl LibeiInputCapture {
pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
/// creates a new libei input capture
/// `window_id` is a window identifier for user prompts
pub async fn new(
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> std::result::Result<Self, LibeiCaptureCreationError> {
let input_capture = Box::pin(InputCapture::new().await?);
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture;
let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?);
let first_session =
Some(create_session(unsafe { &*input_capture_ptr }, window_identifier.clone()).await?);

let (event_tx, event_rx) = mpsc::channel(1);
let (notify_capture, notify_rx) = mpsc::channel(1);
Expand All @@ -230,6 +239,7 @@ impl LibeiInputCapture {
first_session,
event_tx,
cancellation_token.clone(),
window_identifier,
);
let capture_task = tokio::task::spawn_local(capture);

Expand All @@ -254,6 +264,7 @@ async fn do_capture(
session: Option<(Session<InputCapture>, BitFlags<Capabilities>)>,
event_tx: Sender<(Position, CaptureEvent)>,
cancellation_token: CancellationToken,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<(), CaptureError> {
let mut session = session.map(|s| s.0);

Expand Down Expand Up @@ -299,7 +310,11 @@ async fn do_capture(
// create session
let mut session = match session.take() {
Some(s) => s,
None => create_session(input_capture).await?.0,
None => {
create_session(input_capture, window_identifier.clone())
.await?
.0
}
};

let capture_session = do_capture_session(
Expand Down
8 changes: 8 additions & 0 deletions lan-mouse-gtk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,13 @@ log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
thiserror = "2.0.0"

[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
gdk4_wayland = { package = "gdk4-wayland", version="0.9.6", optional=true }

[build-dependencies]
glib-build-tools = { version = "0.20.0" }


[features]
default = ["wayland_window_identifier"]
wayland_window_identifier = ["dep:gdk4_wayland"]
27 changes: 26 additions & 1 deletion lan-mouse-gtk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::{env, process, str};

use window::Window;

use lan_mouse_ipc::FrontendEvent;
use lan_mouse_ipc::{FrontendEvent, FrontendRequest, WindowIdentifier};

use adw::Application;
use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*};
Expand All @@ -23,6 +23,9 @@ use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use self::key_object::KeyObject;

#[cfg(all(unix, feature = "wayland_window_identifier", not(target_os = "macos")))]
use gdk4_wayland::WaylandToplevel;

use thiserror::Error;

#[derive(Error, Debug)]
Expand Down Expand Up @@ -227,6 +230,28 @@ fn build_ui(app: &Application) {
});
}

// export TopLevel handle and send it to the service so that it can put the InpuCapture / RemoteDesktop
// windows on top of it using xdg-foreign.
#[cfg(all(unix, feature = "wayland_window_identifier", not(target_os = "macos")))]
window.connect_show(|window| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a gtk feature for this, why not make use of it?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that possible? How do I get the handle from the GtkWindowIdentifier for serialization?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It implements Serialize/ToString but you will have to keep a ref to the ashpd type till the dialog is closed to not unexport the handle

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case I guess it would make sense to change bilelmoussaoui/ashpd#352 to take in the entire "wayland:{handle}" string?

@feschber feschber Feb 11, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to not unexport the handle

I should probably do that as well

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case I guess it would make sense to change bilelmoussaoui/ashpd#352 to take in the entire "wayland:{handle}" string?

Not if you make WindowIdentifierType public and you write a From trait impl for it.

// needs the surface so we have to present first!
if let Some(surface) = window.surface() {
if surface.display().backend().is_wayland() {
// let surface = surface.downcast::<WaylandSurface>();
let toplevel = surface.downcast::<WaylandToplevel>().expect("xdg-toplevel");
let window = window.clone();
toplevel.export_handle(move |_toplevel, handle| {
if let Ok(handle) = handle {
let handle = handle.to_string();
window.request(FrontendRequest::WindowIdentifier(
WindowIdentifier::Wayland(handle),
));
}
});
}
}
});

glib::spawn_future_local(clone!(
#[weak]
window,
Expand Down
2 changes: 1 addition & 1 deletion lan-mouse-gtk/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ impl Window {
self.request(FrontendRequest::RemoveAuthorizedKey(fp));
}

fn request(&self, request: FrontendRequest) {
pub(crate) fn request(&self, request: FrontendRequest) {
let mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap();
if let Err(e) = requester.request(request) {
Expand Down
8 changes: 8 additions & 0 deletions lan-mouse-ipc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,14 @@ pub enum FrontendRequest {
UpdateEnterHook(u64, Option<String>),
/// save config file
SaveConfiguration,
/// window identifier used to present input-capture / remote-desktop prompts
WindowIdentifier(WindowIdentifier),
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum WindowIdentifier {
Wayland(String),
X11(u32),
}

#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
Expand Down
8 changes: 7 additions & 1 deletion src/capture.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::{
cell::{Cell, RefCell},
rc::Rc,
sync::{Arc, Mutex},
time::{Duration, Instant},
};

use futures::StreamExt;
use input_capture::{
CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
WindowIdentifier,
};
use input_event::{Event, KeyboardEvent, scancode};
use lan_mouse_proto::ProtoEvent;
Expand Down Expand Up @@ -68,6 +70,7 @@ impl Capture {
backend: Option<input_capture::Backend>,
conn: LanMouseConnection,
release_bind: Vec<scancode::Linux>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Self {
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
Expand All @@ -82,6 +85,7 @@ impl Capture {
request_rx,
release_bind: Rc::new(RefCell::new(release_bind)),
state: Default::default(),
window_identifier,
};
let task = spawn_local(capture_task.run());
Self {
Expand Down Expand Up @@ -166,6 +170,7 @@ struct CaptureTask {
release_bind: Rc<RefCell<Vec<scancode::Linux>>>,
request_rx: Receiver<CaptureRequest>,
state: State,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
}

impl CaptureTask {
Expand Down Expand Up @@ -200,6 +205,7 @@ impl CaptureTask {
}

async fn run(mut self) {
tokio::time::sleep(Duration::from_secs(1)).await;
loop {
if let Err(e) = self.do_capture().await {
log::warn!("input capture exited: {e}");
Expand All @@ -224,7 +230,7 @@ impl CaptureTask {
async fn do_capture(&mut self) -> Result<(), InputCaptureError> {
/* allow cancelling capture request */
let mut capture = tokio::select! {
r = InputCapture::new(self.backend) => r?,
r = InputCapture::new(self.backend, self.window_identifier.clone()) => r?,
_ = self.cancellation_token.cancelled() => return Ok(()),
};

Expand Down
4 changes: 3 additions & 1 deletion src/capture_test.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::sync::{Arc, Mutex};

use crate::config::Config;
use clap::Args;
use futures::StreamExt;
Expand All @@ -12,7 +14,7 @@ pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCapt
log::info!("creating input capture");
let backend = config.capture_backend().map(|b| b.into());
loop {
let mut input_capture = InputCapture::new(backend).await?;
let mut input_capture = InputCapture::new(backend, Arc::new(Mutex::new(None))).await?;
log::info!("creating clients");
input_capture.create(0, Position::Left).await?;
input_capture.create(4, Position::Left).await?;
Expand Down
Loading
Loading