Skip to main content

pixelflux/wayland/
ficlient.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! `org_kde_kwin_fake_input` client: relative pointer motion for a nested KWin
8//! session.
9//!
10//! A nested compositor learns the pointer from pixelflux's seat. wlroots sessions
11//! bind the seat's `zwp_relative_pointer_v1` and forward each delta to their
12//! own relative-pointer clients, so a game under labwc turns with the mouse.
13//! KWin's nested backend turns the host seat's motion into absolute-only events
14//! and hands its relative-pointer clients — Xwayland's relative device included,
15//! which SDL reads under a pointer grab — an empty delta; it binds the host's
16//! relative pointer only behind its Right-Ctrl debug lock, which also freezes
17//! absolute positioning. KWin has no `zwlr_virtual_pointer_v1` either. What it
18//! offers is `org_kde_kwin_fake_input`, whose `pointer_motion` enters KWin's
19//! delta path, so a relative move is delivered here as a client of the app
20//! compositor, followed by the seat's own absolute move to the same position:
21//! KWin sets its pointer from that rather than adding to it, which keeps the
22//! two aligned — and realigns a KWin that moved its pointer by itself, as it
23//! does when its screens change — while the composited cursor follows the seat
24//! as always. Buttons and scrolling keep riding the seat. A compositor that
25//! does not advertise the global keeps the seat path for deltas too.
26//!
27//! KWin restricts the global to trusted clients unless
28//! `KWIN_WAYLAND_NO_PERMISSION_CHECKS` is set in its environment; a session that
29//! withholds it simply never shows the global here. Authentication is requested
30//! once after binding, as the protocol requires before any event is honored.
31//!
32//! The connection is made on its own thread when selkies names the app
33//! compositor ([`arm`]), and re-made on demand, rate-limited, after a failure,
34//! so the compositor thread only ever performs a non-blocking write per delta.
35
36use std::os::unix::net::UnixStream;
37use std::sync::{Mutex, OnceLock};
38use std::time::{Duration, Instant};
39
40use wayland_client::backend::WaylandError;
41use wayland_client::protocol::wl_registry;
42use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, delegate_noop};
43use wayland_protocols_plasma::fake_input::client::org_kde_kwin_fake_input::OrgKdeKwinFakeInput;
44
45use crate::wayland::wlclient::{SyncState, bounded_roundtrip, impl_sync_callback};
46
47/// Highest `org_kde_kwin_fake_input` version the bindings describe; `authenticate`
48/// and `pointer_motion` are version 1.
49const FAKE_INPUT_VERSION: u32 = 5;
50/// A connection that failed (socket not there yet, KWin restarting) is tried again
51/// on a relative move this long after the last attempt.
52const RETRY_AFTER: Duration = Duration::from_secs(5);
53
54#[derive(Default)]
55struct Globals {
56    fake_input: Option<OrgKdeKwinFakeInput>,
57    sync_done: bool,
58}
59
60impl SyncState for Globals {
61    fn sync_done_mut(&mut self) -> &mut bool {
62        &mut self.sync_done
63    }
64}
65impl_sync_callback!(Globals);
66
67impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
68    fn event(
69        state: &mut Self,
70        registry: &wl_registry::WlRegistry,
71        event: wl_registry::Event,
72        _: &(),
73        _: &Connection,
74        qh: &QueueHandle<Self>,
75    ) {
76        if let wl_registry::Event::Global {
77            name,
78            interface,
79            version,
80        } = event
81            && interface == "org_kde_kwin_fake_input"
82            && state.fake_input.is_none()
83        {
84            state.fake_input = Some(registry.bind(name, version.min(FAKE_INPUT_VERSION), qh, ()));
85        }
86    }
87}
88
89delegate_noop!(Globals: OrgKdeKwinFakeInput);
90
91struct Device {
92    conn: Connection,
93    queue: EventQueue<Globals>,
94    state: Globals,
95    device: OrgKdeKwinFakeInput,
96}
97
98impl Device {
99    /// Connect to the app compositor and bind its fake-input device; `Ok(None)`
100    /// when the compositor answers but does not offer the global.
101    fn connect(socket_path: &str) -> Result<Option<Device>, String> {
102        let stream =
103            UnixStream::connect(socket_path).map_err(|e| format!("connect {socket_path}: {e}"))?;
104        let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
105        let mut queue = conn.new_event_queue();
106        let qh = queue.handle();
107        let _registry = conn.display().get_registry(&qh, ());
108        let mut state = Globals::default();
109        bounded_roundtrip(&conn, &mut queue, &mut state)?;
110        let Some(device) = state.fake_input.take() else {
111            return Ok(None);
112        };
113        device.authenticate("pixelflux".to_string(), "remote pointer motion".to_string());
114        // Surfaces a bind or authentication error before the first delta.
115        bounded_roundtrip(&conn, &mut queue, &mut state)?;
116        Ok(Some(Device {
117            conn,
118            queue,
119            state,
120            device,
121        }))
122    }
123
124    /// One delta as a non-blocking write. Whatever the compositor sent since the
125    /// last call (registry changes as screens come and go) is drained first, so
126    /// its side never backs up against an unread socket.
127    fn motion(&mut self, dx: f64, dy: f64) -> Result<(), String> {
128        if let Some(guard) = self.conn.prepare_read() {
129            match guard.read() {
130                Ok(_) => {}
131                Err(WaylandError::Io(e)) if e.kind() == std::io::ErrorKind::WouldBlock => {}
132                Err(e) => return Err(format!("read: {e}")),
133            }
134        }
135        self.queue
136            .dispatch_pending(&mut self.state)
137            .map_err(|e| format!("dispatch: {e}"))?;
138        self.device.pointer_motion(dx, dy);
139        match self.conn.flush() {
140            Ok(()) => Ok(()),
141            Err(WaylandError::Io(e)) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(()),
142            Err(e) => Err(format!("flush: {e}")),
143        }
144    }
145}
146
147#[derive(Default)]
148struct Slot {
149    /// The app compositor socket; None means nothing is nested.
150    socket: Option<String>,
151    device: Option<Device>,
152    /// The compositor answered without the global (a wlroots session): the seat
153    /// path serves it, and nothing is retried until the socket is re-armed.
154    absent: bool,
155    connecting: bool,
156    last_attempt: Option<Instant>,
157    failure_logged: bool,
158    /// Bumped per [`arm`] so a connection still in flight for an older socket
159    /// cannot land in the slot.
160    generation: u64,
161}
162
163fn slot() -> &'static Mutex<Slot> {
164    static SLOT: OnceLock<Mutex<Slot>> = OnceLock::new();
165    SLOT.get_or_init(Default::default)
166}
167
168fn lock_slot() -> std::sync::MutexGuard<'static, Slot> {
169    slot()
170        .lock()
171        .unwrap_or_else(|poisoned| poisoned.into_inner())
172}
173
174/// Point the client at the app compositor's socket path (None clears it), dropping
175/// any device held on the previous one; the new connection is made off the
176/// caller's thread.
177pub fn arm(socket_path: Option<String>) {
178    let mut s = lock_slot();
179    s.socket = socket_path;
180    s.device = None;
181    s.absent = false;
182    s.last_attempt = None;
183    s.failure_logged = false;
184    s.generation += 1;
185    spawn_connect(&mut s);
186}
187
188/// Start a connection attempt for the armed socket unless one is running, the
189/// compositor is known to lack the global, or the last attempt is too recent.
190fn spawn_connect(s: &mut Slot) {
191    let Some(sock) = s.socket.clone() else {
192        return;
193    };
194    if s.connecting || s.absent || s.last_attempt.is_some_and(|t| t.elapsed() < RETRY_AFTER) {
195        return;
196    }
197    s.connecting = true;
198    s.last_attempt = Some(Instant::now());
199    let generation = s.generation;
200    let spawned = std::thread::Builder::new()
201        .name("pixelflux-fake-input".into())
202        .spawn(move || {
203            let outcome = Device::connect(&sock);
204            let mut s = lock_slot();
205            s.connecting = false;
206            if s.generation != generation {
207                // Re-armed meanwhile: this result belongs to the old socket, and
208                // the new one was held back by the attempt in flight.
209                spawn_connect(&mut s);
210                return;
211            }
212            match outcome {
213                Ok(Some(device)) => {
214                    eprintln!("[FakeInput] relative pointer motion rides org_kde_kwin_fake_input on {sock}");
215                    s.device = Some(device);
216                    s.failure_logged = false;
217                }
218                Ok(None) => s.absent = true,
219                Err(e) => {
220                    if !s.failure_logged {
221                        eprintln!("[FakeInput] {sock}: {e}; the seat carries relative motion until it is reachable");
222                        s.failure_logged = true;
223                    }
224                }
225            }
226        });
227    if spawned.is_err() {
228        s.connecting = false;
229    }
230}
231
232/// Deliver one delta through the app compositor's fake-input device. False when
233/// no device serves it — nothing armed, a compositor without the global, or KWin
234/// not reachable right now — in which case the caller's own seat carries the move.
235pub fn pointer_motion_rel(dx: f64, dy: f64) -> bool {
236    let mut s = lock_slot();
237    if let Some(device) = s.device.as_mut() {
238        match device.motion(dx, dy) {
239            Ok(()) => return true,
240            Err(e) => {
241                eprintln!("[FakeInput] {e}; reconnecting");
242                s.device = None;
243                s.last_attempt = None;
244            }
245        }
246    }
247    spawn_connect(&mut s);
248    false
249}