Skip to content

Make copy actually copy on Wayland (main-process clipboard + OSC 52) - #55

Merged
abasiri merged 3 commits into
doctly:mainfrom
ymajoros:fix/clipboard-copy-wayland
Aug 1, 2026
Merged

Make copy actually copy on Wayland (main-process clipboard + OSC 52)#55
abasiri merged 3 commits into
doctly:mainfrom
ymajoros:fix/clipboard-copy-wayland

Conversation

@ymajoros

Copy link
Copy Markdown
Contributor

Fixes #54.

Copy was a black hole on Linux/Wayland — paste fine, copy gone. Two reasons, both about the renderer's rocky relationship with the clipboard:

1. Ctrl+C went through the renderer. navigator.clipboard.writeText(...).catch(() => {}) rejects on Ozone/Wayland (Chromium wants window focus + a user gesture) and the empty .catch swallowed it whole. Textbook silent failure.

2. OSC 52 wasn't handled at all. That's the sequence Claude Code uses to copy. xterm doesn't register a handler for it on its own, so those copies went nowhere.

What this does

Adds a small clipboard-write-text IPC that calls the main-process clipboard.writeText. The main process has none of the renderer's focus/gesture hangups, so it just writes on Wayland. Then:

  • Ctrl+C (with a selection) uses it instead of navigator.clipboard
  • registered an OSC 52 handler that base64-decodes the payload and routes it through the same IPC (and ignores the ? read-back query, which we don't answer)

Paste was never broken, so it's untouched.

The diff (3 files, ~30 lines)

  • main.js — import clipboard, add the IPC handler
  • preload.js — expose writeClipboard
  • public/terminal-manager.js — use it for Ctrl+C, add the OSC 52 handler

terminal.parser.registerOscHandler is stable xterm API and allowProposedApi: true is already set, so nothing new is required capability-wise.

Testing

Ubuntu / GNOME 49 / Wayland, native Ozone.

  • Before: Claude Code copy, Ctrl+C, and right-click Copy all dead, no error.
  • After: Claude Code's copy and Ctrl+C-with-selection both land in the system clipboard and paste cleanly into other apps.

I only have the Linux box, so I'd love a second pair of eyes confirming macOS/Windows still behave — though main-process clipboard is the boring-and-reliable option on every platform, so I'd be surprised if it regressed anything.

Copying out of the terminal did nothing on Linux/Wayland. Two separate holes:

- Ctrl+C copy used navigator.clipboard.writeText in the renderer. Chromium
  gates that on window focus + a user gesture, and it's effectively dead under
  Ozone/Wayland. The trailing .catch(() => {}) ate the rejection, so it failed
  in total silence.
- OSC 52 (how Claude Code itself copies) wasn't wired up at all — xterm doesn't
  do it for you, so those copies just evaporated.

Both now go through the main-process clipboard over IPC, which doesn't care about
focus or gestures. Paste was never affected, so it's left alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymajoros

Copy link
Copy Markdown
Contributor Author

Confirmed working. Built this branch and ran it on Ubuntu / GNOME Shell 49 / native Wayland (Ozone), Electron 41.0.3 — the same box where stock 0.0.30 copies nothing.

Both paths now land in the system clipboard and paste cleanly into other apps:

  • OSC 52 (Claude Code's copy) — tested with printf '\e]52;c;%s\a' "$(printf hi | base64)"
  • Ctrl+C with a selection

Paste was unaffected throughout. No regressions spotted in normal terminal use.

JeanBaptisteRenard added a commit to devsuitup/switchboard that referenced this pull request May 30, 2026
…18)

navigator.clipboard.writeText is gated on focus/user-activation and
silently fails on Linux/Wayland (Ozone). Replace with a main-process
clipboard.writeText via a new clipboard-write-text IPC.

Also register an xterm OSC 52 handler so programs inside the terminal
(notably Claude Code's copy-to-clipboard) can set the system clipboard;
xterm doesn't wire OSC 52 itself.

Port of upstream PR doctly#55 (ymajoros). Closes upstream
issue #54 from our fork's perspective; the upstream PR will close it
fully when merged.
abasiri and others added 2 commits July 31, 2026 22:31
…-back refusal

Two review follow-ups on the OSC 52 handler.

writeClipboard is an ipcRenderer.invoke, so it returns a promise. The code it
replaced ended in .catch(() => {}); without it an IPC failure surfaces as an
unhandled rejection. Restored.

The handler's security property — that a read-back query ("<selection>;?") is
consumed but never answered — lived in a bare `b64 === '?'` check that reads
like an unimplemented case. Answering it would write the user's clipboard back
into the terminal, letting any program in the session exfiltrate whatever was
last copied. Extracted the parse into decodeOsc52Payload() so the refusal is
documented and covered by tests that say why, rather than being one condition
away from someone "completing" it.

Pure refactor otherwise: verified the extracted helper produces identical
(handled, written) results to the previous inline logic across write, query,
empty, separator-less, multi-byte and malformed-base64 payloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@abasiri

abasiri commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Reviewed this and I'm happy with the approach — pushed two small follow-ups to the branch (6368f2e) rather than sending you back and forth. Also merged current main in, since the branch was 7 behind and #75 has since added the module.exports block this now extends.

1. writeClipboard rejection. It's an ipcRenderer.invoke, so it returns a promise, and the navigator.clipboard.writeText(...) call it replaced ended in .catch(() => {}). Without that an IPC failure becomes an unhandled rejection. Restored.

2. Pinned the read-back refusal. This was the part I wanted to protect. if (!b64 || b64 === '?') return true; is doing real security work — answering a read-back query would write the user's clipboard back into the terminal, letting anything running in the session exfiltrate whatever they last copied. But it reads like an unimplemented case, and it's one well-meaning commit away from being "finished". I extracted the parse into decodeOsc52Payload() so the refusal is documented where it lives, and added test/clipboard-osc52.test.js covering write / query / empty / separator-less / multi-byte / malformed-base64, with the query test stating the reason.

Otherwise a pure refactor — I checked the extracted helper returns identical (handled, written) results to your inline logic across all twelve of those payload shapes before pushing. 12/12 tests green.

On the wider question of whether OSC 52 writes should sit behind a setting: I considered it and decided no. The capability is the feature here — it's how Claude Code copies — so a setting defaulting off just reintroduces the bug, and defaulting on is a knob nobody turns. The exfiltration direction is already refused, xterm.js bounds the payload at 10MB, and these terminals run agents with full shell access, so gating clipboard writes while handing out Bash wouldn't be buying anything real.

Thanks for the fix — the Wayland diagnosis and the main-process routing were both right. Merging once you've had a chance to look over my two commits.

@abasiri
abasiri merged commit 54d0edb into doctly:main Aug 1, 2026
4 of 5 checks passed
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.

Copy to clipboard silently does nothing on Linux/Wayland (paste works fine)

2 participants