Skip to content

fix(cua-driver): make GNOME/KDE Wayland input actually land via libei (#2105, #1982)#2112

Open
ai-ag2026 wants to merge 7 commits into
trycua:mainfrom
ai-ag2026:fix/linux-portal-runtime-backend
Open

fix(cua-driver): make GNOME/KDE Wayland input actually land via libei (#2105, #1982)#2112
ai-ag2026 wants to merge 7 commits into
trycua:mainfrom
ai-ag2026:fix/linux-portal-runtime-backend

Conversation

@ai-ag2026

@ai-ag2026 ai-ag2026 commented Jul 6, 2026

Copy link
Copy Markdown

Summary

Makes synthetic input actually land on GNOME/Mutter (and KDE/KWin) Wayland.
The first revision of this PR fixed doctor reporting + libei readiness but not
dispatch — input still silently no-oped on the compositors it claimed to
support. This revision fixes the whole chain, verified end-to-end on a real
Ubuntu 26.04 GNOME/Wayland session (click lands in ~3 ms; type_text lands
in a focused entry).

What was broken (root causes, in order)

  1. Routing gate — input tools gated the Wayland branch on is_wayland(),
    which requires DISPLAY unset. GNOME/KDE always run XWayland → DISPLAY
    set → is_wayland() false → input fell to X11 XTEST (dropped by Mutter),
    while doctor reported wayland_backend: pass. Silent false-success.
  2. EIS handshake rejected — the client advertised ei_device v2, omitted
    ei_pingpong, and never answered Ping. Mutter closed the EIS connection
    immediately after our finish() (confirmed: one Handshake event, then
    read() errors).
  3. Portal session died early (GNOME/Wayland: no input-injection backend available, no way to get the doctor-recommended build #2105)open_eis_context dropped the
    ashpd proxy/session + runtime before the EIS loop ran.
  4. Command/handshake race — commands ran before Resumed.
  5. Wrong device — Mutter announces multiple devices; the code took the first.
  6. No ei_text on Muttertype_text via ei_text fails.

Changes

  • wayland_input_enabled() predicate + 9 input call sites (capture / list_windows
    keep is_wayland() — native enum needs wlr-foreign-toplevel, absent on Mutter).
  • Handshake: ei_device v1, advertise ei_pingpong, answer Ping — matches
    reis 0.7 examples/type-text.rs.
  • Hold the portal RemoteDesktop session + runtime for the worker lifetime
    (multi-thread(1) runtime so the zbus connection stays driven).
  • Queue input commands until a device is negotiated (input_ready + timeout).
  • device_with_interface(...) selects the device carrying the needed interface.
  • Keycode typing fallback via ei_keyboard when ei_text is absent, mapped
    through the compositor's actual xkb keymap (read from the ei_keyboard
    Keymap event via xkbcommon) so typing is layout-correct; a US-layout table
    is the fallback when no keymap arrives.
  • Single-shot drag (press → interpolated motion → release) via ei_button
    Press/Released — text selection / drag-and-drop / sliders.

Validation (Ubuntu 26.04, Mutter, Wayland; --features portal-libei)

Action Result
EIS handshake Handshake → Seat → Device → Resumed (was: disconnect after finish())
click / scroll / move ✅ lands (click OK ~3 ms)
press_key
type_text (incl. layout-specific chars) ✅ lands layout-correctly — verified on a German QWERTZ keymap (test_YZ_yz_123 typed verbatim)
drag (select / drag-and-drop) ✅ drag-select verified — dragging over a filled entry then typing replaced the whole selection (XXXXXXXXXXZ)
doctor wayland_backend pass and input lands (mismatch gone)
first call one-time xdg-desktop-portal consent dialog

Honest deferrals (documented, not silently broken)

  • ei_text: used when the compositor advertises it (libei 1.6+); Mutter
    doesn't, hence the keycode + xkb-keymap fallback.
  • Button held across separate tool calls (mouse_button_down / mouse_button_up,
    the persistent-cursor mouse_drag) and parallel_mouse_drag (multi-pointer /
    MPX) stay on the X11 path — they need cross-call emulation state / multiple
    pointers the single-shot libei command model doesn't provide, so on native
    Wayland windows they no-op (they still work on XWayland windows). Single-shot
    drag (above) does work via libei.
  • Modifier hotkeys: a bare-key hotkey now routes via libei; true modifier
    chords (Ctrl+C, …) aren't wired into the libei keyboard yet and error loudly
    rather than silently mis-firing.
  • Per-window background input is impossible on Wayland by design; the
    substitute is delivery_mode:"foreground".

Refs

Closes the runtime half of #1982 / #2105. Pairs with #2111 (publishes the
portal-libei artifact users need). Builds on #1966 / #2008 (libei machinery).

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@ai-ag2026 is attempting to deploy a commit to the Cua Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f9fba30-139a-4133-bd9b-97cd070fcf17

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a portal/libei RemoteDesktop reachability probe integrated into Wayland health-check classification, and introduces a libei-based input fallback path (pointer clicks, scroll, drag, move, typing, hotkeys) used when the wlroots virtual-pointer manager is unavailable, gated by a new NO_VPTR_MARKER and feature flag.

Changes

Wayland libei/portal fallback

Layer / File(s) Summary
Portal RemoteDesktop probe and health classification
libs/cua-driver/rust/crates/platform-linux/src/health_report.rs
Adds probe_portal_remote_desktop() using ashpd/Tokio, wires reachability into classify_wayland_backend for pass/fail messaging, updates partial-pass criteria, and adds unit tests.
Virtual-pointer session detection and fallback marker
libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs
Adds NO_VPTR_MARKER constant and with_libei_fallback/is_no_vptr helpers; reorders open_vptr_session to detect missing virtual-pointer manager and defer foreign-toplevel requirement.
Pointer operations routed through libei fallback
libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs
Adds output_dimensions()/normalize_click_xy(); refactors click, scroll, move_cursor_absolute, drag to route through with_libei_fallback.
Typing and key operations with libei adapter implementations
libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs
Refactors type_text, press_key, hotkey to fall back to libei on wtype failure; implements libei adapters (click, scroll, move, type, press_key, key_to_evdev) and drag-unsupported error.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HealthReport as check_wayland_backend
  participant Probe as probe_portal_remote_desktop
  participant Ashpd as ashpd RemoteDesktop
  participant Classifier as classify_wayland_backend

  HealthReport->>Probe: run probe (portal-libei feature)
  Probe->>Ashpd: RemoteDesktop::new()
  Ashpd-->>Probe: reachable / not-found error
  Probe-->>HealthReport: portal_libei_backend bool
  HealthReport->>Classifier: classify(virtual_pointer, portal_libei_backend, globals)
  Classifier-->>HealthReport: pass / fail with hint
Loading
sequenceDiagram
  participant Caller
  participant WithLibeiFallback as with_libei_fallback
  participant VptrImpl as click_vptr/scroll_vptr/...
  participant LibeiImpl as libei_click/libei_scroll/...

  Caller->>WithLibeiFallback: click/scroll/move/drag
  WithLibeiFallback->>VptrImpl: try wlroots vptr operation
  VptrImpl-->>WithLibeiFallback: NO_VPTR_MARKER error (if missing)
  WithLibeiFallback->>LibeiImpl: fallback (portal-libei enabled)
  LibeiImpl-->>Caller: result
Loading

Possibly related issues

Possibly related PRs

  • trycua/cua#1966: Updates the same health_report.rs classification and wayland/mod.rs virtual-pointer fallback logic for portal-libei integration.
  • trycua/cua#1992: Modifies the same missing zwlr_virtual_pointer handling path based on portal-libei compile flag.

Suggested reviewers: r33drichards

Poem

A rabbit hopped where wlroots failed,
Found libei's door, and portals trailed,
With markers set and fallbacks true,
Clicks and keys now find their way through 🐰
No missing pointer stops the play!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: routing GNOME/KDE Wayland input through libei fallback.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/cua-driver/rust/crates/platform-linux/src/health_report.rs`:
- Around line 522-524: The readiness check in RemoteDesktop::new() is too
shallow because it only confirms the portal proxy exists, not that the libei
input path is usable. Update the health probe around the existing rt.block_on
match so it exercises the full RemoteDesktop handshake by continuing through
create_session, select_devices, start, and connect_to_eis, or else change the
reported verdict/message to indicate only “RemoteDesktop portal reachable”
rather than full input backend readiness. Use the RemoteDesktop::new flow and
the surrounding health_report logic to locate the fix.

In `@libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs`:
- Around line 1413-1495: The libei-backed input helpers are still treating
missing EIS capabilities as success, so requests can disappear silently. Update
the capability checks in the libei path so the relevant operations fail when the
negotiated device lacks PointerAbsolute, Button, Scroll, Text, or Keyboard, and
make sure the callers such as libei_click, libei_scroll, libei_move_absolute,
libei_type_text, and libei_press_key propagate that error instead of returning
Ok(()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0177a824-f19a-47fb-bd81-b1ecd81b68cc

📥 Commits

Reviewing files that changed from the base of the PR and between ce2da2d and c214a29.

📒 Files selected for processing (2)
  • libs/cua-driver/rust/crates/platform-linux/src/health_report.rs
  • libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs

Comment thread libs/cua-driver/rust/crates/platform-linux/src/health_report.rs
Comment thread libs/cua-driver/rust/crates/platform-linux/src/wayland/mod.rs
@ai-ag2026

Copy link
Copy Markdown
Author

Addressed the two CodeRabbit findings in 60bb3015:

  • health_report.rs: the Wayland health message now reports only RemoteDesktop portal reachability and says command execution will attempt the full portal/EIS handshake, instead of implying the whole input backend is already proven ready by RemoteDesktop::new().
  • wayland/libei.rs: libei command execution now fails loudly when the negotiated EIS device lacks the required interface (PointerAbsolute, Button, Scroll, Text, or Keyboard) instead of silently returning Ok(()).

Verification run in a clean rust:1.90-bookworm container:

cargo test -q -p platform-linux --features portal-libei health_report::tests -- --nocapture
cargo check -q -p cua-driver --features portal-libei
cargo check -q -p cua-driver
git diff --check

All passed. Existing warnings are pre-existing in adjacent code. Vercel is still blocked by deploy authorization, not by this code path.

ai-ag2026 added a commit to ai-ag2026/cua that referenced this pull request Jul 8, 2026
trycua#2112 as filed only fixed doctor reporting + libei readiness; input still
never reached libei on GNOME/Mutter. This makes click/scroll/move/keys
actually land. Chain of fixes, each verified on a real Ubuntu 26.04
GNOME/Wayland session:

1. Routing gate: input tools gated the Wayland branch on `is_wayland()`,
   which requires DISPLAY unset. GNOME/KDE always run XWayland -> DISPLAY is
   always set -> libei path was dead, input fell to X11 XTEST (dropped by
   Mutter). Add `wayland_input_enabled()` (opted in + WAYLAND_DISPLAY, no
   DISPLAY-unset clause) and route the 9 input tool sites through it. Capture
   / list_windows keep `is_wayland()` (native enum needs wlr-foreign-toplevel,
   absent on Mutter).

2. EIS handshake: the client advertised ei_device v2 and omitted ei_pingpong
   and Ping handling, so Mutter closed the EIS connection immediately after
   our handshake finish() (one Handshake event, then read() error). Match
   reis 0.7's own examples: ei_device v1, advertise ei_pingpong, answer
   ei_connection Ping with done(). The handshake now negotiates devices
   through to Resumed.

3. Portal session lifetime (trycua#2105): open_eis_context dropped the ashpd
   proxy/session + tokio runtime before the calloop loop started, so
   GNOME/KDE tore the RemoteDesktop+EIS session (and the fd) down at once.
   Hold them for the libei worker lifetime; use a multi-thread(1) runtime so
   the zbus connection stays driven.

4. Command readiness: commands could run before seat->device->Resumed
   completed and fail "no EIS device negotiated yet". Queue commands and run
   them once a device is negotiated (READY_TIMEOUT bound so callers never
   hang).

5. Device selection: Mutter/KWin announce one device per capability group
   (relative pointer, absolute pointer, keyboard). Pick the device that
   carries the interface a command needs (device_with_interface) instead of
   "first virtual device".

6. Keyboard typing: Mutter doesn't advertise ei_text, so type_text via
   ei_text fails. Fall back to keycode typing via ei_keyboard (US-layout
   char->evdev + shift). Layout-correct typing (compositor xkb keymap) is a
   follow-up; non-US layouts mistype layout-specific punctuation.

Verified: click lands (OK in ~3ms), type_text lands ("CUA_FIX_OK_5566" typed
into a focused entry; letters/digits correct, '_' mistyped on a de keymap per
the US-layout limitation above).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ai-ag2026 ai-ag2026 changed the title fix(cua-driver): route non-wlroots Wayland input via libei fix(cua-driver): make GNOME/KDE Wayland input actually land via libei (#2105, #1982) Jul 8, 2026
@ai-ag2026

Copy link
Copy Markdown
Author

@f-trycua — this PR (and #2111) are fork PRs, so their workflow runs (incl. the blocking nix-wayland.yml gate) sit at action_required until a maintainer approves them. Could you kick them off?

This one now makes GNOME/KDE Wayland input actually land via libei — it fixes the EIS handshake Mutter was rejecting, the DISPLAY-gated dispatch, the #2105 session lifetime, device selection, and adds a keycode typing fallback since Mutter has no ei_text. Verified end-to-end on Ubuntu 26.04 GNOME (click OK ~3 ms; type_text landed in a focused entry). #2111 ships the portal-libei artifact users need. Happy to iterate on the gnome nix-wayland scenarios once CI can run.

ai-ag2026 and others added 7 commits July 9, 2026 04:34
Fallback from wlroots virtual-pointer to the portal/libei backend when a compositor does not advertise zwlr_virtual_pointer, and teach doctor to treat a reachable RemoteDesktop portal as the GNOME/KDE input backend instead of reporting a wlroots-only failure.
trycua#2112 as filed only fixed doctor reporting + libei readiness; input still
never reached libei on GNOME/Mutter. This makes click/scroll/move/keys
actually land. Chain of fixes, each verified on a real Ubuntu 26.04
GNOME/Wayland session:

1. Routing gate: input tools gated the Wayland branch on `is_wayland()`,
   which requires DISPLAY unset. GNOME/KDE always run XWayland -> DISPLAY is
   always set -> libei path was dead, input fell to X11 XTEST (dropped by
   Mutter). Add `wayland_input_enabled()` (opted in + WAYLAND_DISPLAY, no
   DISPLAY-unset clause) and route the 9 input tool sites through it. Capture
   / list_windows keep `is_wayland()` (native enum needs wlr-foreign-toplevel,
   absent on Mutter).

2. EIS handshake: the client advertised ei_device v2 and omitted ei_pingpong
   and Ping handling, so Mutter closed the EIS connection immediately after
   our handshake finish() (one Handshake event, then read() error). Match
   reis 0.7's own examples: ei_device v1, advertise ei_pingpong, answer
   ei_connection Ping with done(). The handshake now negotiates devices
   through to Resumed.

3. Portal session lifetime (trycua#2105): open_eis_context dropped the ashpd
   proxy/session + tokio runtime before the calloop loop started, so
   GNOME/KDE tore the RemoteDesktop+EIS session (and the fd) down at once.
   Hold them for the libei worker lifetime; use a multi-thread(1) runtime so
   the zbus connection stays driven.

4. Command readiness: commands could run before seat->device->Resumed
   completed and fail "no EIS device negotiated yet". Queue commands and run
   them once a device is negotiated (READY_TIMEOUT bound so callers never
   hang).

5. Device selection: Mutter/KWin announce one device per capability group
   (relative pointer, absolute pointer, keyboard). Pick the device that
   carries the interface a command needs (device_with_interface) instead of
   "first virtual device".

6. Keyboard typing: Mutter doesn't advertise ei_text, so type_text via
   ei_text fails. Fall back to keycode typing via ei_keyboard (US-layout
   char->evdev + shift). Layout-correct typing (compositor xkb keymap) is a
   follow-up; non-US layouts mistype layout-specific punctuation.

Verified: click lands (OK in ~3ms), type_text lands ("CUA_FIX_OK_5566" typed
into a focused entry; letters/digits correct, '_' mistyped on a de keymap per
the US-layout limitation above).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The keycode typing fallback (for compositors without ei_text, e.g. Mutter)
assumed a US layout, so layout-specific characters mistyped on other keymaps —
e.g. '_' -> '?' and swapped Y/Z on a German QWERTZ.

Read the xkb keymap the ei_keyboard device delivers (Keymap event) and build a
char -> (evdev keycode, shift) table from it via xkbcommon, so typing follows
the user's actual layout. Falls back to the US-layout table when no keymap
arrives.

Verified on a real German (QWERTZ) GNOME/Wayland session: "test_YZ_yz_123"
types verbatim (previously '_' and Y/Z were wrong).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The libei fallback previously hard-errored on drag, claiming no button-hold
primitive. But ei_button exposes separate Press/Released states (already used
for click), so a genuine press -> interpolated motion -> release drag is
possible.

Add a Drag command to the libei worker (motion_absolute to start, button
Press, interpolate to end, button Release), a libei::drag() entry point, and
route wayland::drag's libei fallback + the drag tool site through it on
XWayland-co-present sessions.

Verified on Ubuntu 26.04 GNOME/Wayland: dragging across a pre-filled entry
selects the text (typing a replacement char then replaces the whole selection
-- field went from "XXXXXXXXXX" to "Z").

mouse_button_down/up (button held across separate tool calls) and
parallel_mouse_drag (MPX) remain deferred -- those need cross-call emulation
state / multiple pointers, which single-shot drag doesn't.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review of the libei input path before maintainer review:

- libei drag: resolve the drag END into the start device's region (was:
  reuse the start region offset for both, mis-mapping a cross-monitor drag).
  Clamp interpolation steps (<=500) so a huge count can't block the worker
  loop / starve EIS Ping handling. Clamp endpoints without normalize_click_xy's
  (0,0)->centre convention (a drag corner target is valid).
- Command readiness is now per-command (cmd_ready): a keyboard-only session no
  longer makes keyboard commands wait 20s for an absolute-pointer device that
  never arrives (was one global ei_pointer_absolute gate).
- Route hotkey, the cursor-glide targets, and move_cursor's real-warp through
  wayland_input_enabled() too -- they were left on is_wayland() and so silently
  took the (Mutter-ignored) X11 path / desynced the cursor overlay on GNOME/KDE.
- Cap the xkb keymap size read from the ei_keyboard Keymap event (16 MiB) to
  avoid a multi-GB alloc on a buggy size.
- Log (not silently drop) characters with no keycode in the active keymap
  (e.g. non-Latin/emoji) in the keycode typing fallback.

Verified drag-select still lands after the changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ty only

Address CodeRabbit: the `pass` verdict is driven by a shallow
`RemoteDesktop::new()` proxy-reachability probe, not the full
create_session → select_devices → start → connect_to_eis handshake (which is
deliberately not run in `doctor`, to avoid a consent prompt on every health
check). Spell that out in the message so a green `wayland_backend` isn't read
as a guarantee that injection will succeed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ai-ag2026 ai-ag2026 force-pushed the fix/linux-portal-runtime-backend branch from 6a8aa04 to 46c529e Compare July 9, 2026 02:38
@ai-ag2026

Copy link
Copy Markdown
Author

Rebased this PR onto current origin/main; no conflict resolution or code changes beyond replaying the existing commits were needed.

Local verification after rebase:

git diff --check origin/main...HEAD
cargo test -q -p platform-linux --features portal-libei health_report::tests -- --nocapture
cargo check -q -p cua-driver --features portal-libei
cargo check -q -p cua-driver

All passed. The Rust commands still emit the existing adjacent warnings, but no errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant