Skip to content

fix(clipboard): read the Wayland clipboard on Hyprland - #482

Merged
eval-exec merged 1 commit into
eval-exec:mainfrom
thanosapollo:fix/wayland-clipboard-hyprland-data-control
Oct 5, 2026
Merged

eval-exec merged 1 commit into
eval-exec:mainfrom
thanosapollo:fix/wayland-clipboard-hyprland-data-control

Conversation

@thanosapollo

Copy link
Copy Markdown
Contributor

On Hyprland, neomacs-clipboard-get returns nil while Neomacs has keyboard focus, even when another client (or Neomacs itself) owns a text CLIPBOARD selection. Copying out of Neomacs works; reading never does.

Why

Neomacs has two wl_data_device objects for the seat on its one Wayland connection: winit's drag-and-drop device, which is created first, and smithay-clipboard's. wlroots sends selection/data_offer to every data device of the focused client. Hyprland's CWLDataDeviceProtocol::dataDeviceForClient returns only the first device it finds for the client (src/protocols/core/DataDevice.cpp in Hyprland 0.56.2), so the offer goes to winit's device and smithay-clipboard's device never gets one. Clipboard::load() then fails with selection is empty, which Neomacs reports as nil.

WAYLAND_DEBUG=client traces from nested Hyprland 0.56.2 show every wl_data_device.selection going to winit's device and none to smithay-clipboard's. The same client under a wlroots compositor (labwc 0.20.2) gets the selection on both devices. An input method (fcitx5) makes no difference.

What changed

  • When smithay-clipboard reports no selection offer or no text MIME type, the CLIPBOARD read falls back to ext-data-control-v1, using a private event queue on the same wl_display.
    • The manager and seat are bound lazily the first time the fallback is needed. Compositors without the global keep the previous behaviour (nil).
    • Each read creates a short-lived data-control device, takes the selection the compositor sends when the device is created, receives it through a pipe with a 3 s deadline, and destroys every offer and the device.
    • Text type choice and CR/CRLF normalization match smithay-clipboard: the first of text/plain;charset=utf-8 or UTF8_STRING in offer order, else text/plain. Both paths return the same text.
  • smithay-clipboard is still the first source. Its focus/seat errors (client doesn't have focus, no events received on any seat) still propagate. PRIMARY reads and all writes are unchanged.
  • The fallback runs only after smithay-clipboard has confirmed keyboard focus, so it never reads a selection Neomacs could not already read through wl_data_device. On compositors without the bug, an empty or non-text CLIPBOARD costs one extra short-lived data-control device and roundtrip per read; a failed or timed-out fallback logs a warning and reads as nil.
  • rustix gains the event feature for poll.

Testing

  • cargo nextest run -p neomacs-display-runtime -E 'test(/clipboard|wayland_data_control/)': 18 passed. This includes new tests for the fallback decision (native text wins, no-offer and no-MIME fall back, an empty transfer is text, focus/seat errors are not masked, a failed fallback stays nil) and for the transfer helpers (MIME choice, line-end normalization, lossy UTF-8, reading to EOF, timeout on a stalled owner).
  • Release build (cargo xtask fresh-build --release) run in a nested headless Hyprland 0.56.2, with and without fcitx5, driven through neomacsclient --eval:
    • Without this change: (neomacs-clipboard-get) returned nil after wl-copy while focused, and also nil for Neomacs's own selection.
    • With it: it returns the foreign text, the replacement text after a second wl-copy, and Neomacs's own selection. wl-paste still reads text set from Neomacs.
  • Same build under labwc 0.20.2 (wlroots): reads still return the foreign selection.

This PR is agent-assisted.

Hyprland delivers selection offers only to the first wl_data_device a
client creates. In Neomacs that is winit's drag-and-drop device, so
smithay-clipboard's device never sees an offer and every CLIPBOARD read
returns nil, even while Neomacs is focused. wlroots compositors send the
selection to every device, which is why they were unaffected.

When smithay-clipboard reports no selection offer or no text type, read
the CLIPBOARD through ext-data-control-v1 on a private event queue of the
same display. Each read creates a short-lived device, transfers the
current selection with smithay-clipboard's MIME preference and line-end
normalization, and destroys every object it received. Focus and seat
errors still propagate, and PRIMARY reads are unchanged.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 29271dad-05ba-4c54-a7b9-2e61a3f541d9
📥 Commits

Reviewing files that changed from the base of the PR and between 7764ef7 and 02806cc.

📒 Files selected for processing (5)
  • Cargo.toml
  • crates/neomacs-display-runtime/src/clipboard.rs
  • crates/neomacs-display-runtime/src/clipboard/tests/clipboard_test.rs
  • crates/neomacs-display-runtime/src/clipboard/tests/wayland_data_control_test.rs
  • crates/neomacs-display-runtime/src/clipboard/wayland_data_control.rs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The Wayland clipboard backend adds a data-control reader. CLIPBOARD reads use it when smithay-clipboard reports no usable selection offer. PRIMARY reads continue to use smithay-clipboard alone.

Changes

Wayland clipboard fallback

Layer / File(s) Summary
Data-control selection reader
Cargo.toml, crates/neomacs-display-runtime/src/clipboard/wayland_data_control.rs, crates/neomacs-display-runtime/src/clipboard/tests/wayland_data_control_test.rs
Adds a data-control reader that selects and decodes supported text MIME types and reads transfer data with a deadline. Tests cover MIME selection, decoding, transfer completion, and timeout behavior.
CLIPBOARD fallback integration
crates/neomacs-display-runtime/src/clipboard.rs, crates/neomacs-display-runtime/src/clipboard/tests/clipboard_test.rs
The Wayland backend lazily binds the reader and uses it for CLIPBOARD reads after specified smithay-clipboard errors. Tests cover fallback, preserved errors, and empty selections.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant WaylandClipboard
  participant SmithayClipboard
  participant DataControlReader
  participant SelectionOwner
  Caller->>WaylandClipboard: Read CLIPBOARD
  WaylandClipboard->>SmithayClipboard: Read selection
  SmithayClipboard-->>WaylandClipboard: No usable offer
  WaylandClipboard->>DataControlReader: Bind lazily if needed
  WaylandClipboard->>DataControlReader: Read text
  DataControlReader->>SelectionOwner: Request selected text MIME type
  SelectionOwner-->>DataControlReader: Transfer text over pipe
  DataControlReader-->>WaylandClipboard: Decoded text or no selection
  WaylandClipboard-->>Caller: Return selection
Loading

Suggested reviewers: eval-exec

Merge Risk: ⚪ Minimal · up to 02806

No actionable issue remains established for this change; it is mergeable after normal checks.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 02806

The fallback limits transfer time but not memory consumption. A hostile clipboard owner could supply enough data to exhaust the editor process when the fallback is used. Access remains on the existing desktop connection, but behavior across multiple seats is not established.

Retained concerns

  • Medium · security · inferred: The newly reachable fallback accepts clipboard-owner bytes without a size limit. A compositor-connected client that owns the selected CLIPBOARD can supply a high-volume stream when an application read reaches fallback, potentially exhausting memory in the editor process. The three-second timeout bounds transfer duration, not accumulated bytes or decoding allocations. At the PR base, the corresponding missing-offer branch returned no text without this transfer. This concern does not assert that ordinary smithay transfers had a size limit.
Security review details

Security Blast Radius

  • inferred — The supported attack scope is a clipboard owner reachable through the existing Wayland display and selected seat, acting when an application read invokes fallback. Resource exhaustion can affect the editor process despite worker-thread isolation. The inspected path does not establish remote reachability, cross-service authority, or cross-tenant access.

Security Findings and Attack Paths

  • inferred — A provider-controlled pipe feeds an uncapped byte vector, followed by text-decoding allocations. High-volume output can exceed available memory before the deadline. This is newly reachable in missing-offer cases that returned no text at the PR base; process exhaustion was not reproduced.

Trust Boundaries and Controls

  • observed — The fallback does not open another display connection. Its native display owner outlives the reader through field ordering. Explicit non-missing smithay errors remain errors, and helper tests confirm that constructed focus and seat errors do not invoke fallback. Those tests do not prove authorization equivalence across compositor policies or seats.

Resilience and Maintainability Implications

  • observed — The pre-existing service serializes backend commands and bounds its request queue to 32 entries. The new reader cleans up collected offers and its temporary device after returned errors. These controls contain concurrency and stalled-transfer effects, but neither bounds clipboard payload memory.

Hardening Proposals

  • proposed — Add a maximum accepted byte count before extending the transfer buffer, with rejection flowing through existing pipe and protocol cleanup. Exercise an oversized continuous stream to verify bounded memory independently of the time limit.
  • proposed — Establish the intended multi-seat contract and validate it against compositor behavior. If fallback must preserve the native active seat, supply or resolve that identity rather than choosing the first advertised seat.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.88% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 34 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing Wayland clipboard reads on Hyprland.
Description check ✅ Passed The description explains the clipboard issue, the data-control fallback, its behavior, and the reported tests. It is directly related to the changeset.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 55.88% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 34 functions across 4 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

It involves unsafe FFI on a cross-thread-shared foreign wl_display, manual Wayland protocol object lifecycle management, and correctness that ultimately depends on live-compositor behavior not verifiable in this environment.

Review effort: Balanced
Findings: None

What changed in this PR

On Hyprland, wlroots-style selection routing only delivers the CLIPBOARD offer to winit's first wl_data_device, so smithay-clipboard's device never sees it and neomacs-clipboard-get returns nil even while focused. This PR adds a lazily-bound ext-data-control-v1 fallback that reads the selection directly (bypassing data-device focus routing) only when smithay-clipboard reports no usable offer, leaving writes, PRIMARY reads, and non-buggy compositors behaving as before.

Changes:

  • New wayland_data_control module: a private event queue on the shared foreign wl_display that binds ext-data-control-v1, creates a short-lived device per read, transfers the selection over a pipe with a 3s deadline, and cleans up all offers/devices.
  • WaylandClipboard now holds a lazily-bound DataControl fallback; text() routes CLIPBOARD reads through smithay_text_or_fallback, consulting data-control only when smithay saw no offer while preserving focus/seat errors.
  • rustix gains the event feature (workspace) for poll; unit tests cover MIME selection, line-end normalization, lossy UTF-8, pipe read-to-EOF/timeout, and the fallback decision matrix.
File Description
crates/​neomacs-display-runtime/​src/​clipboard/​wayland_data_control.rs New ext-data-control-v1 reader: MIME choice, decode, per-read device lifecycle, poll-based pipe transfer with deadline.
crates/​neomacs-display-runtime/​src/​clipboard.rs Integrates the lazy DataControl fallback and smithay_text_or_fallback/smithay_saw_no_offer helpers into text().
crates/​neomacs-display-runtime/​src/​clipboard/​tests/​wayland_data_control_test.rs Tests TextMime selection/decoding and read_to_end_before EOF/timeout behavior.
crates/​neomacs-display-runtime/​src/​clipboard/​tests/​clipboard_test.rs Tests the fallback decision matrix (native wins, no-offer/no-MIME fall back, empty is text, errors not masked, failed fallback stays nil).
Cargo.toml Adds the event feature to the workspace rustix dependency for poll.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@eval-exec eval-exec left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Clean diagnosis and a well-scoped fix.

Verified against the vendored crate sources: the fallback only triggers when smithay-clipboard reports no selection offer (its focus and seat errors still propagate), and MIME choice, lossy UTF-8 decoding, and CRLF normalization match smithay-clipboard 0.7.3's behavior exactly — so both paths return the same text. The from_foreign_display call carries the same contract smithay-clipboard itself relies on, documented at the definition and the call site, and the field drop order holds (wayland-backend only disconnects a foreign display when it owns it).

Non-blocking follow-ups, fine for a later PR:

  • A byte cap on the transfer — the 3 s deadline bounds time, not memory.
  • A typed error at the backend boundary instead of matching smithay's "selection is empty" string; the arboard path already matches on a type.
  • An upstream Hyprland issue: the single-device-per-client behavior is still present on their main, so this fallback can eventually be deleted rather than maintained.

@eval-exec
eval-exec merged commit 9bb11e2 into eval-exec:main Oct 5, 2026
38 of 49 checks passed
eval-exec added a commit that referenced this pull request Oct 7, 2026
PR #482's data-control fallback re-implemented smithay-clipboard's MIME
preference and decode rules, and both the fallback trigger and the
arboard path classified native errors inline, including a string match
on smithay's "selection is empty" message.

Move the MIME vocabulary, the decode rules, the fallback decision, and
the two native-error classifiers into the clipboard text_policy module,
and give reads a typed answer: TextRead::{Text, NoSelection,
TargetUnavailable}.  "No owner" and "an owner with no readable text
type" are now distinct inside the runtime, and smithay's message strings
exist in exactly one adapter.

This is behaviour-preserving: execute_command maps the typed answer back
to the evaluator-facing Option<String>, with both absences reading as
nil, so batch semantics and the oracle pins are untouched.
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.

3 participants