Skip to content

feat: Extended Clipboard: clipboardEmpty, supports.unicodeClipboard, no stale client text - #40

Merged
sidorares merged 1 commit into
masterfrom
fix/extended-clipboard-state
Oct 15, 2026
Merged

sidorares merged 1 commit into
masterfrom
fix/extended-clipboard-state

Conversation

@sidorares

@sidorares sidorares commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Three changes to the client's Extended Clipboard state. All of them are backward compatible: the clipboard event fires exactly as before, and the new API is one event and one supports field.

#36: clipboardEmpty

A notify without the text format, or a provide without text, now emits clipboardEmpty (no arguments): the server's clipboard changed and holds no text. It was emptied, or it holds only other formats, such as an image.

I chose a new event over clipboard with ''. It's the more backward-compatible option: existing clipboard listeners see exactly the events they saw before. With '' they would get a new event in a common TigerVNC case. Say an X client has text on the clipboard and VNC client A calls updateClipboard. Every other client gets the server's "no text" notify first (see below), so with '' a viewer that mirrors clipboard would clear its local clipboard. Any code that waits for the next clipboard after a peer's change would also receive '' instead of the text. That includes this repo's own "UTF-8 text from one client to another" integration test.

clipboard with '' still means what it meant: the server sent empty text. To wait for whatever a copy on the server produced, listen for both events. The README's events section documents this.

The echo: what TigerVNC does

Traced in TigerVNC v1.16.2. The same code has been there since the Extended Clipboard landed in 1.10, and TurboVNC 3.3.1's cutpaste.c is a port of it.

  1. The client's notify. SConnection::handleClipboardNotify receives the client's notify (text). It calls VNCServerST::handleClipboardAnnounce, which makes that client the clipboardClient and calls Xvnc's vncHandleClipboardAnnounce.
  2. Taking the selections. vncHandleClipboardAnnounce takes PRIMARY (with SetPrimary, the default), then CLIPBOARD. It does this through vncOwnSelection, which runs the X server's SelectionCallback for each.
  3. The callback. vncSelectionCallback checks whether the selection just taken is activeSelection, the X selection whose text the server last announced. If it is, it calls vncMaybeRequestCache(), sets activeSelection = None, and calls vncAnnounceClipboard(FALSE).
  4. The notify to every client. VNCServerST::announceClipboard loops over all clients, the sender included. SConnection::announceClipboard(false) writes notify with no formats to each Extended Clipboard client.
  5. Order on the sender's connection. Since 1.12, vncMaybeRequestCache first sends a request (text) to the clipboardClient to cache its data. The sender therefore sees request (text), then notify (no formats), both while the server handles its notify.

So the echo comes only when activeSelection was set. That variable is set in exactly one place: when the server finds text on a selection that an X client took, followed by vncAnnounceClipboard(TRUE). That call sends every client a notify (text) or a provide. The variable is cleared only together with the notify without text.

Some consequences of this code:

  • No echo when there's nothing to replace. When a VNC client owns the clipboard, activeSelection is None. Another client's change then sends nothing at all, and neither does an X client that copies something without text.
  • The echo always follows a text announcement. A notify without text always comes after a text announcement, and at most once per announcement.
  • New clients aren't told. TigerVNC never announces its current clipboard to a client that connects. The only places that write notify are announceClipboard and the reply to a peek.
  • TigerVNC's viewer keeps its text. CConnection::handleClipboardNotify drops its own clipboard on a notify with text, but not on one without text.

The rule (deterministic, no timer)

The client tracks one flag: whether the server may still hold text of its own.

  • Starts true. Nothing is known after connecting, so the flag starts true.
  • Set by the server's announcements. A notify with text, a provide with text, or a ServerCutText sets it. A notify or provide without text clears it.
  • Arms the echo. When updateClipboard replaces such text, one notify without text is expected. That notify is consumed without an event, and the client keeps its text (as TigerVNC's viewer does).
  • Disarmed by newer text. If the server reports newer text first, the expectation is dropped.

With TigerVNC and TurboVNC, this exactly matches when activeSelection is set, except in one race described below.

#37 part 1: the client's text after the server's clipboard changes

These now make the client stop holding the clipboard:

  • a notify with text
  • a provide (with or without text)
  • a plain ServerCutText

After that, a request gets no provide, and a peek gets a notify without text.

A notify without text leaves the client's text in place, which is what TigerVNC's viewer does. With TigerVNC that notify is either the echo or comes after a text announcement, and the text announcement has already dropped the client's text. Keeping the text means a missed echo can't stop the client from answering the server's requests, which would break pasting on the server.

#37 part 2: supports.unicodeClipboard

supports.unicodeClipboard is true when updateClipboard sends any text as UTF-8: the server's caps list the text format and the notify action. When it's false, text may go as Latin-1 with ? for other characters, so a caller can refuse such text instead. supports.extendedClipboard is unchanged. The field is recomputed whenever the server sends caps again (TigerVNC does on every SetEncodings).

updateClipboard now also checks the text format before sending a notify (text). A server whose caps don't list text gets ClientCutText instead of an announcement it would never act on. Short text within the server's unsolicited limit still goes as a provide to a server that takes provide but not notify. That's why the field promises UTF-8 only for text of any length when notify is there.

Tests

test/client.test.ts, "Extended Clipboard state", runs against the scripted peer:

  • Extended Clipboard: no event when the server's clipboard holds no text #36 repro. A notify without text, a provide with no formats, and a provide with an image only each emit clipboardEmpty, and no request is sent.
  • The echo, with TigerVNC's exact sequence. The server announces text, then the client calls updateClipboard. The server sends request (text) and the client provides. The server sends notify (no formats): no event, and a later request still gets the client's text. A second updateClipboard replaces nothing, so the next notify without text emits clipboardEmpty.
  • The echo right after connecting. The server's state is unknown, so the echo is expected.
  • A notify without text that is a change. This covers two cases: the server last said it had no text, or it reported newer text before the expected echo.
  • Extended Clipboard: stale text served after the server's clipboard changed, and no way to tell whether the server takes the client's text #37 repro and variants. After each of a notify with text, a provide, a provide without text, and a plain ServerCutText, a request gets no provide and a peek gets a notify without text.
  • supports.unicodeClipboard. It's false before caps and false with provide only. In that case short text goes as UTF-8 and long text as Latin-1. It's true with text and notify, and false again when the caps lack the text format, in which case updateClipboard sends ClientCutText.

Eight of these nine tests fail on master. The ninth, the echo right after connecting, passes there only because master emits nothing for a notify without text.

test/integration/features.test.ts gets a new test for TigerVNC and TurboVNC. It uses two clients, A and B. A's text is taken over by rfb2-clipboard, so the server holds text of its own. A then calls updateClipboard again, and the test expects clipboardEmpty on B only, never on A, before both see the new text. It passes in this PR's CI against TigerVNC 1.15.0 and TurboVNC 3.3.1 (run). B gets exactly one clipboardEmpty, which shows the server did send its "no text" notify to every client, and A gets none.

The issue's repro script, run against npm run build output:

master:
1. clipboard events after notify/provide without text: [] (expected one telling "no text")
2. provides after the server's clipboard changed: 1 (expected 0: the client no longer holds the clipboard)

this branch:
1. clipboard events after notify/provide without text: [] (expected one telling "no text")
2. provides after the server's clipboard changed: 0 (expected 0: the client no longer holds the clipboard)

this branch, with a clipboardEmpty listener added to the script:
1. clipboard events after notify/provide without text: ["<clipboardEmpty>","<clipboardEmpty>"] (expected one telling "no text")
2. provides after the server's clipboard changed: 0 (expected 0: the client no longer holds the clipboard)
supports: {"extendedClipboard":true,"unicodeClipboard":true}

Step 1 still prints [] for clipboard, by design: the "no text" report is the new event.

Limits

  • Servers that never echo. QEMU, for example, skips the clipboard's owner when it notifies. On such a server, the first notify without text after updateClipboard is taken as the echo if the server had text before, or hadn't said anything yet. That clipboardEmpty is missed, just as nothing was reported before this change.
  • A one-round-trip race with TigerVNC. If an X client copies new text while the client's notify is in flight, the client sees the server's text first and the echo after it. The echo is then reported as clipboardEmpty, and the client has already dropped its text. Only a sync point such as a Fence after the notify could order these. I left that out to keep the wire traffic unchanged.

Closes #36
Closes #37

…no stale client text

- clipboardEmpty: the server's clipboard changed and holds no text (a notify
  or provide without the text format). Not emitted for the notify without
  text that TigerVNC and TurboVNC send every client, the sender included,
  when a client's clipboard replaces text of the server's own.
- Once the server reports a newer clipboard of its own (a notify with text,
  a provide, or a plain ServerCutText), the client answers a request with
  no provide and a peek with a notify without text.
- supports.unicodeClipboard: updateClipboard sends any text as UTF-8 (the
  server takes text with the notify action). Without the text format in
  the server's caps, updateClipboard sends ClientCutText.

Closes #36
Closes #37
@sidorares
sidorares merged commit 94fd8c7 into master Oct 15, 2026
11 checks passed
@sidorares
sidorares deleted the fix/extended-clipboard-state branch October 15, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant