Skip to content

native: restart a WSLg clipboard read when focus leaves its helper - #1849

Merged
simonklee merged 9 commits into
mainfrom
harden/385-wslg-read-fails-on-focus-churn
Oct 29, 2026
Merged

simonklee merged 9 commits into
mainfrom
harden/385-wslg-read-fails-on-focus-churn

Conversation

@simonklee

Copy link
Copy Markdown
Member

On WSLg the clipboard reads through the core Wayland data device, which only sends the selection to a focused surface, so OpenTUI maps a 1×1 helper surface for each read. When keyboard focus left that surface while the read waited, for example because another window took focus, the read failed with "Wayland clipboard protocol failed". The user sees an image paste that does nothing (part of anomalyco/opencode#49895).

The read now maps a new helper, which takes focus again and gets the current selection, and reads that offer. It does this at most twice per acquisition, the same bound as the stale-image retry, so a compositor that keeps taking focus still ends the read as failed. A seat that loses its keyboard still fails the connection; the next read reconnects.

Stacked on #1845, which retries an unsupported Wayland connection and tries the WSLg socket.

A native test replays focus losses on the connection and checks the fresh offer and the bound. On WSLg, with a second clipboard client taking focus during reads, failed reads went from 3–11 of 48 to 0 of 48. Source +7 lines, tests +51.

A Wayland connect failure, or a registry with no seat or no clipboard
protocol, left the connection `unsupported` for the life of the
service. Every later operation fell back to X11, or returned
`unsupported` without a display. Under WSLg the X11 fallback gives a
0-byte text for an image, so a socket that appeared after the first
paste never helped.

The idle drain now closes an unsupported connection like any other
idle one, so the next operation connects again. The retry costs at
most one connect per operation: an operation that sees `unsupported`
moves to X11 or settles, and the drain closes the connection only
when no pending operation still needs Wayland. On a nested compositor
whose socket appears after the first read, the next two reads return
`image/png` instead of `unsupported`.

Issue: 383
WSLg serves its compositor at `/mnt/wslg/runtime-dir/wayland-0`. When
`XDG_RUNTIME_DIR` points elsewhere (a systemd user runtime directory,
for example), libwayland cannot find the socket, the clipboard falls
back to the WSLg X11 bridge, and an image paste gives a 0-byte text.
Reporters worked around it with
`WAYLAND_DISPLAY=/mnt/wslg/runtime-dir/wayland-0`.

On WSL only, a failed connect now tries
`/mnt/wslg/runtime-dir/$WAYLAND_DISPLAY` before it reports
`unsupported`. Off WSL nothing changes. A connection made this way is
not the inherited `WAYLAND_SOCKET`, so the idle drain may close it.
The source grows by 13 lines for this branch, which the user asked
for. `allow_core_data_device` becomes `is_wsl`, because it now gates
both WSL paths.

Issue: 383
The setup table did not notice two plausible breaks. Trying the WSLg
socket before the default one on WSL passed, and so did forgetting a
failed connect so that every later drive connects again.

The table now counts connect calls and drives each connection three
more times. A failed connect must stay at one attempt (two on WSL)
until the host closes the connection. A new WSL row with a working
default socket must not touch the WSLg path.

Issue: 383
With `XDG_RUNTIME_DIR` unset and a relative `WAYLAND_DISPLAY` (the
anomalyco/opencode#57724 environment), libwayland refuses the connect
and logs "XDG_RUNTIME_DIR is invalid" to stderr, which is the
terminal. Since unsupported results are retried per operation, that
line appeared on every paste instead of once per process.

The default connect now runs only when libwayland can resolve it: an
inherited `WAYLAND_SOCKET`, an absolute `XDG_RUNTIME_DIR`, or an
absolute `WAYLAND_DISPLAY`. Otherwise it goes straight to the WSLg
socket on WSL, or reports unsupported. Five reads in that environment
print 0 lines, down from 5 (1 on the base). The source grows by five
lines net for the check and its comment.

Issue: 383
The WSLg fallback and the core data device both depend on the host
copying `libraries.is_wsl` into the connection, but no test went
through that assignment. Dropping it, or setting it to true, passed
the native suite while a real socket probe showed the wrong result
on and off WSL.

A host test now swaps in fake libwayland symbols, drives a connection
from the service for both flag values, and checks the flag the
connection received.

Issue: 383
A Wayland core data-device read needs a focused helper surface. When
keyboard focus left that surface while the read waited, for example
because another window took focus on WSLg, the read ended as `failed`
with "Wayland clipboard protocol failed".

The read now destroys the helper and maps a new one, which takes focus
again and receives the current selection. It does so at most twice per
acquisition, the bound the stale-image retry uses, so a compositor that
keeps taking focus still ends the read as `failed`. On WSLg with a
second clipboard client taking focus, failed reads went from 3-11 of
48 to 0 of 48.

The source grows by 8 lines: the restart, its counter and its comment.
The host test drives a core read through replayed compositor events
for the first time, so the tests grow by its table.

Issue: 385
Losing the bound seat's keyboard fails the connection in the same
branch, and both readers of `core_focus_lost` return on a failed
connection first, so setting the flag there had no effect. Now only a
`leave` for a live helper sets it.

Issue: 385
The restart lives in the connection, so a connection test that replays
focus losses shows it as well as a host read did: the restarted read
takes the fresh offer, and the third loss fails it. That replaces the
host test, and the listener functions are private again.

A host row now checks that a data-control read takes no helper and
stays on Wayland, which no test caught before.

Issue: 385
@simonklee
simonklee requested a review from kommander as a code owner October 29, 2026 08:28
Base automatically changed from harden/383-wayland-unsupported-cached-forever to main October 29, 2026 10:18
@simonklee
simonklee enabled auto-merge (squash) October 29, 2026 10:18
@simonklee
simonklee merged commit 7c34b99 into main Oct 29, 2026
26 checks passed
@simonklee
simonklee deleted the harden/385-wslg-read-fails-on-focus-churn branch October 29, 2026 10:40
Dicklesworthstone added a commit to Dicklesworthstone/opentui that referenced this pull request Oct 29, 2026
Local main 4c316d1 is 52 ahead and 9 behind origin/main. The merge-tree is one clean tree 592461c, with 0 path deletions. 38 files changed, 1416 insertions(+), 638 deletions(-).

Incoming:
738067f Release v0.8.0
25ab5fb native: drop the sync envelope for remote terminals that pay for it (anomalyco#1851)
366000f native: settle a running clipboard operation at its deadline (anomalyco#1847)
0718aa9 native: reconnect the X11 clipboard after a connection failure (anomalyco#1844)
4c12eed fix(core): restyle markdown lists on style changes in top-level block mode (anomalyco#1841)
7c34b99 native: restart a WSLg clipboard read when focus leaves its helper (anomalyco#1849)
87562e7 native: retry an unsupported Wayland clipboard connection (anomalyco#1845)
de63ba8 core: measure extmarks edits without moving the cursor (anomalyco#1848)
9a4e8cc native: marshal Wayland requests without libwayland 1.20 (anomalyco#1846)

Files in the merge result versus local main (38):
api/0.8.0.txt
bun.lock
packages/core/package.json
packages/core/src/edit-buffer.test.ts
packages/core/src/edit-buffer.ts
packages/core/src/index.ts
packages/core/src/lib/extmarks-multiwidth.test.ts
packages/core/src/lib/extmarks.ts
packages/core/src/lib/terminal-capability-detection.test.ts
packages/core/src/native-abi.generated.ts
packages/core/src/renderables/Markdown.ts
packages/core/src/renderables/__tests__/Markdown.test.ts
packages/core/src/renderer.ts
packages/core/src/zig.ts
packages/examples/package.json
packages/keymap/package.json
packages/native/include/opentui.h
packages/native/package.json
packages/native/src/clipboard/host.zig
packages/native/src/clipboard/linux.zig
packages/native/src/clipboard/wayland.zig
packages/native/src/clipboard/x11.zig
packages/native/src/context-abi.zig
packages/native/src/context-editor-abi.zig
packages/native/src/edit-buffer.zig
packages/native/src/renderer.zig
packages/native/src/session.zig
packages/native/src/terminal.zig
packages/native/src/tests/renderer_test.zig
packages/qrcode/package.json
packages/react/package.json
packages/solid/package.json
packages/ssh/package.json
packages/three/package.json
packages/web/package.json
packages/web/src/content/docs/core-concepts/clipboard.mdx
packages/web/src/content/docs/core-concepts/renderer.mdx
packages/web/src/content/docs/releases/0.8.0.md

The OpenAI File Downloader user-agent line stays at AGENTS.md:64. Local commits stay on main. Local master is fast-forwarded only by git push . HEAD:master, which refuses a non-fast-forward.
Pushed only to the Dicklesworthstone/opentui fork (main, and master only when fork/master is a strict ancestor). anomalyco/opentui is not pushed. Experiment refs on origin stay unfolded.

Co-Authored-By: Claude <[email protected]>
simonklee added a commit that referenced this pull request Oct 30, 2026
Follow-up to #1845 and #1849 with three small Wayland clipboard fixes
from their review.

A queued `wl_keyboard.enter` for a destroyed core helper reaches OpenTUI
with a nil surface. The leave handler ignored nil, but the enter handler
matched it against the missing helper and marked focus as entered. The
flag outlived the helper, so the next WSLg read could report ready
before its new helper had focus and read an unfocused offer. Enter now
ignores nil like leave, and an acquisition asserts that it starts
without focus.

Two test gaps are closed. The Wayland setup table now covers a relative
`XDG_RUNTIME_DIR` and an absolute `WAYLAND_DISPLAY`, which pins the
checks that skip libwayland's default connect. The focus-loss restart
test now checks every poll, so a restart that settles the read before
the new helper has focus fails it.

The Wayland setup test also no longer unsets `XDG_RUNTIME_DIR`.
Unsetting it corrupted the environment that Zig keeps from startup, so
in a Wayland session a failing native test hung `zig build test` with no
output; such a failure now reports.

Each new row fails under the break it guards; native and core suites
pass. Source +0 lines, tests +1.
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