Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .github/workflows/cd-rust-cua-driver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion libs/cua-driver/rust/crates/cua-driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 45 additions & 9 deletions libs/cua-driver/rust/crates/platform-linux/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<DeviceType>`.
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<State> is !Send, so the persistent device must
# live on its own thread; crossbeam gives us a Send sender and synchronous
Expand All @@ -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"]
Original file line number Diff line number Diff line change
@@ -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");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 11 additions & 2 deletions nix/cua-driver/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading