diff --git a/.github/workflows/cd-rust-cua-driver.yml b/.github/workflows/cd-rust-cua-driver.yml index c254b31621..afa837bf97 100644 --- a/.github/workflows/cd-rust-cua-driver.yml +++ b/.github/workflows/cd-rust-cua-driver.yml @@ -74,6 +74,13 @@ jobs: - name: Install base tooling (container is bare) run: | apt-get update + # The PipeWire / libei deps the native-Wayland portal path uses + # are intentionally NOT installed here: bullseye ships PipeWire + # 0.3.19 (too old for libspa-sys 0.8, which needs >= 0.3.40) and + # has no libei-dev at all. They sit behind the `portal-libei` + # Cargo feature (disabled by default) and are wired in via the + # Nix build. The wlroots screencopy + virtual-pointer paths and + # the X11 fallback work without them. apt-get install -y --no-install-recommends \ git ca-certificates curl build-essential pkg-config \ libx11-dev libxi-dev libxtst-dev libxext-dev libwayland-dev diff --git a/libs/cua-driver/rust/crates/cua-driver/Cargo.toml b/libs/cua-driver/rust/crates/cua-driver/Cargo.toml index fea806f76e..f836994f19 100644 --- a/libs/cua-driver/rust/crates/cua-driver/Cargo.toml +++ b/libs/cua-driver/rust/crates/cua-driver/Cargo.toml @@ -59,7 +59,13 @@ platform-windows = { path = "../platform-windows" } [target.'cfg(target_os = "linux")'.dependencies] platform-linux = { path = "../platform-linux" } -[build-dependencies] +[features] +default = [] +# Forwards `platform-linux/portal-libei` so the Nix build can flip the +# GNOME/KDE portal stack (PipeWire ScreenCast + libei RemoteDesktop) on +# from a single workspace-level flag while the cross-platform release CD +# leaves it off. No-op on macOS/Windows builds. +portal-libei = ["platform-linux/portal-libei"] [target.'cfg(target_os = "windows")'.build-dependencies] # Embed the Windows manifest for DPI awareness (fixes coordinate mismatches diff --git a/libs/cua-driver/rust/crates/platform-linux/Cargo.toml b/libs/cua-driver/rust/crates/platform-linux/Cargo.toml index 0138e3fca1..1cac27d175 100644 --- a/libs/cua-driver/rust/crates/platform-linux/Cargo.toml +++ b/libs/cua-driver/rust/crates/platform-linux/Cargo.toml @@ -50,7 +50,12 @@ wayland-protocols = { version = "0.32", features = ["client", "staging"] } # (org.freedesktop.portal.ScreenCast), and the libei input session # (org.freedesktop.portal.RemoteDesktop.ConnectToEIS). `tokio` shares the # driver's async runtime. -ashpd = { version = "0.13", features = ["tokio", "screencast", "remote_desktop", "screenshot"] } +# +# `screencast` + `remote_desktop` ashpd features pull pipewire-rs (libspa-sys), +# which needs `libpipewire-0.3 >= 0.3.40` headers at build time — newer than +# Debian bullseye ships. They're moved behind the `portal-libei` feature so +# the cross-platform CD (Debian 11, GLIBC_2.31 floor) can keep building. +ashpd = { version = "0.13", default-features = false, features = ["tokio", "screenshot"] } # zbus is pulled transitively via ashpd + atspi but declared directly so # the portal-probe path in wayland::portal_screenshot can talk to the # session bus without going through ashpd's higher-level builders. @@ -62,31 +67,32 @@ url = "2" # PipeWire client for the ScreenCast per-window capture path — dequeues # frames from the node id returned by `open_pipe_wire_remote`. Pinned to # 0.8 (last well-documented release; 0.10 had docs.rs build failures in -# mid-2026). Needs libpipewire-0.3 headers at build time. -pipewire = "0.8" +# mid-2026). Needs libpipewire-0.3 headers at build time — gated behind +# the `portal-libei` feature. +pipewire = { version = "0.8", optional = true } # Companion to pipewire 0.8 — needed to construct the SPA_PARAM_EnumFormat # pod (SPA_VIDEO_FORMAT_BGRx at announced width/height) we hand to -# `Stream::connect`. -libspa = "0.8" +# `Stream::connect`. Same `portal-libei` gating as `pipewire`. +libspa = { version = "0.8", optional = true } # Pure-Rust libei/libeis bindings for the GNOME/KDE input path (handshake # → seat → device → pointer_absolute/button/scroll/keyboard/text). API is # documented as "incomplete and subject to change" — budget for churn on # the next bump. Re-exports `enumflags2` we use for `BitFlags`. -reis = { version = "0.7", features = ["tokio"] } +reis = { version = "0.7", features = ["tokio"], optional = true } # Calloop event loop drives the EIS worker thread — reis's example uses # calloop::EventLoop + Generic source to multiplex EIS protocol reads # with command-channel polling. reis 0.7 also ships a calloop adapter # but we use the plain Generic source for clarity. -calloop = "0.14" +calloop = { version = "0.14", optional = true } # Used directly in the ashpd RemoteDesktop call to compose # `BitFlags::from(DeviceType::Keyboard) | DeviceType::Pointer`. reis also # re-exports this same major version; pinning explicitly prevents # duplicate-crate hazards. -enumflags2 = "0.7" +enumflags2 = { version = "0.7", optional = true } # Keymap fallback path for libei keyboard input when the EIS server does # NOT advertise `ei_text` (libei < 1.6, i.e. older GNOME / KDE Plasma 6.0). # Mirrors the reis `type-text.rs` example's keycode reverse-mapping. -xkbcommon = "0.9" +xkbcommon = { version = "0.9", optional = true } # Bounded MPSC command/reply channel for the persistent virtual-pointer # owner thread. EventQueue is !Send, so the persistent device must # live on its own thread; crossbeam gives us a Send sender and synchronous @@ -98,8 +104,38 @@ crossbeam-channel = "0.5" # dialog only once per install, not once per process. dirs = "5" +[features] +default = [] +# Opt-in stack that wires the portal ScreenCast per-window capture path +# (`wayland::portal_screencast`) and the libei input path +# (`wayland::libei`). Pulls in `pipewire`, `libspa` (need +# `libpipewire-0.3 >= 0.3.40` headers — newer than Debian bullseye) and +# `reis` (pure-Rust libei) plus the ashpd RemoteDesktop+ScreenCast +# features. Disabled in the cross-platform release CD (debian:11 +# container, GLIBC_2.31 floor) and enabled in the Nix build that +# already has the modern PipeWire + libei from nixpkgs. The wlroots +# screencopy + virtual-pointer paths and the X11 fallback work either +# way — only GNOME/KDE-portal-specific tiers go dark when this is off. +portal-libei = [ + "ashpd/screencast", + "ashpd/remote_desktop", + "dep:pipewire", + "dep:libspa", + "dep:reis", + "dep:calloop", + "dep:enumflags2", + "dep:xkbcommon", +] + [dev-dependencies] # Used by the `screenshot_cascade` example to surface the # `tracing::debug!` lines emitted by each tier of the dispatch as it # falls through. tracing-subscriber = { workspace = true } + +# Cargo example that exercises the libei input path directly. Lives under +# the `portal-libei` feature gate so a stock `cargo build --examples` +# doesn't try to compile against the (now-optional) reis/xkbcommon deps. +[[example]] +name = "libei_input" +required-features = ["portal-libei"] diff --git a/libs/cua-driver/rust/crates/platform-linux/examples/libei_input.rs b/libs/cua-driver/rust/crates/platform-linux/examples/libei_input.rs new file mode 100644 index 0000000000..5707047816 --- /dev/null +++ b/libs/cua-driver/rust/crates/platform-linux/examples/libei_input.rs @@ -0,0 +1,56 @@ +//! Standalone smoke for the libei input path on platform-linux. +//! +//! Drives `wayland::libei::move_absolute` + `click` and prints what happens. +//! Stress-tests the portal RemoteDesktop handshake plus the reis-based EIS +//! handshake on a real GNOME / KDE-Wayland host. +//! +//! Usage: +//! CUA_DRIVER_RS_ENABLE_WAYLAND=1 \ +//! WAYLAND_DISPLAY=wayland-0 \ +//! cargo run --example libei_input --release + +fn main() { + let _ = tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::DEBUG) + .with_writer(std::io::stderr) + .try_init(); + + eprintln!("== platform-linux libei input smoke =="); + eprintln!( + " WAYLAND_DISPLAY={:?} XDG_RUNTIME_DIR={:?} DBUS_SESSION_BUS_ADDRESS={:?}", + std::env::var("WAYLAND_DISPLAY").ok(), + std::env::var("XDG_RUNTIME_DIR").ok(), + std::env::var("DBUS_SESSION_BUS_ADDRESS").ok(), + ); + + // Try a no-op move first — opens the portal handshake + EIS context. + eprintln!("\n-- move_absolute(500, 400) --"); + let t = std::time::Instant::now(); + match platform_linux::wayland::libei::move_absolute(500.0, 400.0) { + Ok(()) => eprintln!(" OK in {:?}", t.elapsed()), + Err(e) => eprintln!(" ERR after {:?}: {}", t.elapsed(), e), + } + + eprintln!("\n-- click(500, 400, Left) --"); + let t = std::time::Instant::now(); + match platform_linux::wayland::libei::click( + 500.0, 400.0, + platform_linux::wayland::libei::Button::Left, + ) { + Ok(()) => eprintln!(" OK in {:?}", t.elapsed()), + Err(e) => eprintln!(" ERR after {:?}: {}", t.elapsed(), e), + } + + eprintln!("\n-- type_text('hello world') --"); + let t = std::time::Instant::now(); + match platform_linux::wayland::libei::type_text("hello world") { + Ok(()) => eprintln!(" OK in {:?}", t.elapsed()), + Err(e) => eprintln!(" ERR after {:?}: {}", t.elapsed(), e), + } + + eprintln!("\n-- shutdown --"); + platform_linux::wayland::libei::shutdown(); + // Give the worker a moment to drain. + std::thread::sleep(std::time::Duration::from_millis(500)); + eprintln!("done"); +} diff --git a/libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs b/libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs index da9cf1457d..3c38fbdb9e 100644 --- a/libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs +++ b/libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs @@ -14,7 +14,16 @@ pub mod persistent_vptr; pub mod portal_screenshot; pub mod overlay; pub mod ext_screencopy; +// `portal_screencast` (PipeWire per-window capture) and `libei` (GNOME/KDE +// input via xdg-desktop-portal RemoteDesktop) need libpipewire-0.3 and reis +// at build time, which the cross-platform release container (debian:11, +// GLIBC_2.31 floor) can't satisfy without bumping the floor. They're behind +// the `portal-libei` feature so the published binaries stay portable; the +// Nix build (which already has modern PipeWire + libei from nixpkgs) +// enables it. Wlroots screencopy + virtual-pointer remain unconditional. +#[cfg(feature = "portal-libei")] pub mod portal_screencast; +#[cfg(feature = "portal-libei")] pub mod libei; use std::collections::HashMap; diff --git a/nix/cua-driver/package.nix b/nix/cua-driver/package.nix index 409f163c86..4a59f798ac 100644 --- a/nix/cua-driver/package.nix +++ b/nix/cua-driver/package.nix @@ -38,8 +38,17 @@ pkgs.rustPlatform.buildRustPackage { # platform-macos, platform-windows, cua-driver-uia, and focus-monitor-win # which are gated behind cfg(target_os) and won't compile on Linux. # Using -p cua-driver ensures Cargo only resolves Linux dependencies. - cargoBuildFlags = [ "-p" "cua-driver" ]; - cargoTestFlags = [ "-p" "cua-driver" ]; + # + # Enable the `portal-libei` feature so the Nix build pulls the + # GNOME/KDE portal stack (PipeWire ScreenCast per-window capture + + # libei RemoteDesktop input). nixpkgs provides a recent enough + # PipeWire (>= 0.3.40 needed by libspa-sys) and libei, so this feature + # builds fine here. The cross-platform release CD leaves it off — its + # debian:11 container ships PipeWire 0.3.19 (too old for libspa-sys + # 0.8) and lacks libei entirely, but the wlroots screencopy + + # virtual-pointer paths still cover sway/Hyprland/labwc/wayfire/dwl. + cargoBuildFlags = [ "-p" "cua-driver" "--features" "portal-libei" ]; + cargoTestFlags = [ "-p" "cua-driver" "--features" "portal-libei" ]; # Mostly pure Rust: # x11rb -> RustConnection (no libxcb C binding)