Skip to content

fix(clipboard): keep PRIMARY process-local on macOS/Windows and report its ownership - #349

Merged
eval-exec merged 4 commits into
eval-exec:mainfrom
tag-und-nacht:fix/primary-selection-private-pasteboard
Sep 5, 2026
Merged

eval-exec merged 4 commits into
eval-exec:mainfrom
tag-und-nacht:fix/primary-selection-private-pasteboard

Conversation

@tag-und-nacht

Copy link
Copy Markdown
Contributor

Problem

On macOS every region deactivation logged

WARN neomacs_display_runtime::clipboard: clipboard set failed: PRIMARY selection is not supported on this platform selection=Primary

and neomacs-primary-selection-set turned that error into a Lisp error signal. select-active-regions defaults to t (GNU keyboard.c:14338), so deactivate-mark calls gui-set-selection 'PRIMARY after every mouse drag or shift-selection, and the arboard backend's non-Linux branch rejected the request unconditionally.

GNU baseline (emacs-31.0.90)

GNU never rejects PRIMARY on a platform without X selections:

  • NS maps PRIMARY to a private pasteboard named "Selection" (src/nsselect.m:56, :547), written by ns-own-selection-internal (:397-446), read by ns-get-selection (:514-541), cleared by ns-disown-selection-internal (:449-466), and reports ownership by comparing the pasteboard change count it recorded (ns-selection-owner-p, :494-511).
  • w32 keeps PRIMARY as a Lisp property (lisp/term/w32-win.el:364-367, :417-447) and answers ownership from it (:449-451).
  • deactivate-mark (lisp/simple.el:7056-7066) republishes the region to PRIMARY only when gui-backend-selection-owner-p holds or nobody owns PRIMARY (Bug#11772).

Change

Commit 1 — the arboard backend owns a closed PrivatePasteboard { Disowned, Owned(String) } for PRIMARY on non-Linux targets, so store, load and disown succeed and stay process-local while CLIPBOARD still goes to the system clipboard. Linux is untouched: it keeps the Wayland data-device backend or arboard's X11 PRIMARY kind (branch byte-identical).

Commit 2 — found by adversarial review of commit 1: once the store held text, gui-backend-selection-exists-p answered t while the neo window-system had no gui-backend-selection-owner-p (the generic default answers nil), which is exactly the foreign-owner state deactivate-mark refuses to overwrite. A region deactivated inside one command left the earlier PRIMARY value in place. neo-win.el now records ownership when it sets or disowns PRIMARY through the backend and answers the owner predicate from that record, following w32; nil means PRIMARY as in GNU.

Declared divergences

  • The named NSPasteboard is reachable by other processes that know its name, which is why NS declares ns_send_types (:131-134, :418) and compares change counts (:459-461, :528-529). Neomacs never exposes such a pasteboard, so a foreign owner cannot arise and the in-process value is authoritative.
  • ns-sent-selection-hooks (:438-443) is not run; the "Selection value may not be nil" error (:413-414) has no counterpart because neo-win.el routes nil to a disown.
  • Ownership is recorded on the Lisp side (w32 shape) rather than queried from the backend: neither arboard nor smithay-clipboard exposes selection ownership, so a backend query could not be answered on Linux.
  • On Linux PRIMARY is a real X11/Wayland selection another client may take after us; that hand-over is not observed (GNU X asks the server via x-selection-owner-p, x-win.el:1359-1361). The ported keyboard.c post-command path already republishes PRIMARY unconditionally on every command with an active region, so this adds no new clobbering path.
  • CLIPBOARD ownership reports nil, like w32; gui--selection-value-internal only trusts the predicate for CLIPBOARD on x and haiku (lisp/select.el:230-236).
  • Pre-existing, not touched: the no-display-host fallback in stubs.rs keeps its own thread-local PRIMARY text, so there are now two process-local PRIMARY stores in the tree. Worth folding together in a follow-up.

Tests (red first)

  • arboard_backend_keeps_primary_in_a_private_pasteboard failed on main with the exact warning text; private_pasteboard_round_trips_store_load_and_clear pins the store.
  • real_gui_primary_selection_follows_deactivate_mark_after_prior_ownership (neomacs-gui-tests, fixture primary-selection.el) drives the Lisp contract through the built binary: after (gui-set-selection 'PRIMARY "old"), selecting and deactivating "new" inside one command must read back "new" with owner-p t, and disowning must read nil with owner-p nil. It failed on commit 1 with owned_before=false, after_deactivate="old" and passes after commit 2 with NEOMACS_GUI_TEST_BACKEND=macos.
  • cargo fmt --check and cargo clippy --tests on neomacs-display-runtime and neomacs-gui-tests are clean.

Not run

Verified on macOS arm64 only. The Linux branch is unchanged and was reasoned about, not built; Windows compiles the same non-Linux branch as before and was not built.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FzChXMKZaxRsqnoXaMzh8N

@eval-exec
eval-exec requested review from eval-exec and a balanced review from Copilot September 5, 2026 00:49

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.

🔵 Needs a closer look

It changes cross-platform PRIMARY selection ownership semantics (including Linux/X11, where gui-backend-selection-owner-p now returns tracked ownership instead of nil) and the author verified only macOS arm64 while CI runs the new test on X11, so human validation is warranted.

Pull request overview

This PR fixes a macOS/Windows regression where every region deactivation logged PRIMARY selection is not supported on this platform and turned into a Lisp error. GNU Emacs never rejects PRIMARY on non-X platforms (NS uses a private pasteboard; w32 a Lisp property), so the arboard backend now keeps PRIMARY in a process-local store on non-Linux targets, and neo-win.el tracks ownership so deactivate-mark correctly republishes the region instead of leaving a stale value.

Changes:

  • Add a PrivatePasteboard { Disowned, Owned(String) } process-local store for PRIMARY in the arboard backend on non-Linux targets, making store/load/disown succeed while CLIPBOARD still uses the system clipboard (Linux path unchanged).
  • Record PRIMARY ownership in neo-win.el (neo--owns-primary-selection) and implement gui-backend-selection-owner-p for the neo window-system, following the w32 model.
  • Add Rust unit tests and a real-GUI integration test (with a new primary-selection.el fixture) covering the store and the deactivate-mark ownership contract; update the ClipboardSelection doc comment.
File summaries
File Description
crates/neomacs-display-runtime/src/clipboard.rs Adds PrivatePasteboard and routes non-Linux PRIMARY store/load through it instead of erroring; adds unit tests.
lisp/term/neo-win.el Tracks PRIMARY ownership and answers gui-backend-selection-owner-p for the neo display.
crates/neomacs-display-runtime/src/thread_comm.rs Clarifies the ClipboardSelection doc comment for the new process-local semantics.
crates/neomacs-gui-tests/fixtures/primary-selection.el New fixture exercising the deactivate-mark/ownership Lisp contract.
crates/neomacs-gui-tests/tests/real_gui_smoke.rs New integration test asserting PRIMARY ownership follows deactivate-mark.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0
  • Review effort level: Balanced

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

@eval-exec

Copy link
Copy Markdown
Owner

I studied the GNU Emacs NS, w32, X/PGTK, command-loop, and deactivate-mark selection paths before reviewing this.

The macOS reproduction, private PRIMARY store, enum-based local state, and real-GUI regression are a strong contribution. I approved the direction and will implement these follow-ups directly on this branch before merge.

Standards

The current ownership record is split between Rust PrivatePasteboard and the global Lisp Boolean neo--owns-primary-selection. That duplicates state and makes Lisp claim knowledge the platform backend may not possess. It also reduces a four-state domain—this process, another process, no owner, or unknown—to a Boolean.

PrivatePasteboard is a misleading name for an in-process store, and its comment contradicts itself about GNU NS cross-process visibility.

Spec

The new Lisp predicate applies on Linux even though the PR says Linux is untouched. If another X11 or Wayland client takes PRIMARY, the bit remains true and deactivate-mark can bypass the GNU Bug#11772 foreign-owner guard. The ordinary active-region update path does not remove that deactivation-specific regression.

Windows currently has compile coverage but no runnable real-GUI contract, and empty PRIMARY text is reported as owned but nonexistent.

Follow-up design

  • Make Rust the single owner of contents and ownership.
  • Use a closed SelectionOwner::{ThisProcess, OtherProcess, None, Unknown} model.
  • Rename the local value state to PrivateSelection::{Vacant, Owned(String)}.
  • Query ownership through the display-host and clipboard-service boundary instead of mirroring it in Lisp.
  • Keep macOS and Windows ownership authoritative; represent the current Linux observation limitation explicitly as Unknown, conservatively mapped through the GNU Boolean API.
  • Add red-first backend, Lisp-boundary, and real-GUI contract coverage, including empty text and platform-specific ownership behavior.

I will preserve the original fix while making these follow-ups here.

tag-und-nacht and others added 4 commits September 5, 2026 02:59
…tforms

On macOS every region deactivation logged

  WARN neomacs_display_runtime::clipboard: clipboard set failed: PRIMARY
  selection is not supported on this platform selection=Primary

and `neomacs-primary-selection-set` turned that error into a Lisp `error`
signal.  `select-active-regions` defaults to t (GNU keyboard.c:14338), so
`deactivate-mark` calls `gui-set-selection 'PRIMARY` after every mouse
drag or shift-selection, and the arboard backend's non-Linux branch
rejected the request unconditionally.

GNU Emacs never rejects PRIMARY on a platform without X selections.  The
NS port maps it to a private pasteboard named "Selection"
(emacs-31.0.90 src/nsselect.m:56, :547) that `ns-own-selection-internal`
(:397-446) writes and `ns-get-selection` (:514-541) reads back; the w32
port keeps it as a Lisp property (lisp/term/w32-win.el:364-367,
:417-447).  Follow that: the arboard backend now owns a typed
`PrivatePasteboard` for PRIMARY on non-Linux targets, so store, load and
disown succeed and stay process-local, while CLIPBOARD still goes to the
system clipboard.  Linux is untouched: it keeps the Wayland data-device
backend or arboard's X11 PRIMARY kind.

Declared divergence (in the type's doc): GNU NS compares pasteboard
change counts to detect a foreign owner of the named pasteboard; neomacs
never exposes one, so the in-process value is authoritative.

Tests (red first): the backend round trip failed with the exact warning
text before the change.  Run on macOS arm64 only; the Linux branch is
unchanged and was reasoned about, not built.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzChXMKZaxRsqnoXaMzh8N
…sh it

Adversarial review of the previous commit: once the private PRIMARY store
held any text, `gui-backend-selection-exists-p` answered t while the neo
window-system had no `gui-backend-selection-owner-p` at all, so the generic
default (lisp/select.el:349-354) answered nil.  That is the exact state GNU
`deactivate-mark` (emacs-31.0.90 lisp/simple.el:7056-7066) reserves for a
foreign owner (Bug#11772): both of its `gui-set-selection` calls became
dead, and a region deactivated inside one command left the earlier PRIMARY
value in place.  A real GUI run on macOS reproduced it: after
`(gui-set-selection 'PRIMARY "old")`, selecting and deactivating "new" in
one command still read back "old".

Every GNU port answers ownership from the record it wrote: NS compares the
pasteboard change count it stored (nsselect.m:494-511), w32 reads the
`x-selections` property it set (lisp/term/w32-win.el:364-367, :449-451),
X asks the server (x-win.el:1359-1361).  Follow w32: neo-win.el now records
ownership of PRIMARY when it sets or disowns it through the display backend
and answers `gui-backend-selection-owner-p` from that record, with nil
meaning PRIMARY as in GNU.  CLIPBOARD reports nil, like w32, because the
system clipboard changes hands without notice and
`gui--selection-value-internal` only trusts the predicate for CLIPBOARD on
x and haiku (lisp/select.el:230-236).

Declared divergence: on Linux PRIMARY is a real X11/Wayland selection that
another client may take after us; the display backend has no owner query,
so that hand-over is not observed.  The ported keyboard.c post-command
path already republishes PRIMARY unconditionally on every command with an
active region, so this does not introduce a new clobbering path there.

Also from the review: `PrivatePasteboard` is a closed Disowned/Owned enum
instead of an Option; its doc ledgers the unported NS pieces
(`ns_send_types`, change-count foreign-owner detection,
`ns-sent-selection-hooks`, the nil-value error); `ClipboardSelection`'s doc
states the per-platform meaning of Primary.

Tests (red first): the new real-GUI regression in neomacs-gui-tests drives
the Lisp contract through the built binary and failed on the previous
commit with owned_before=false and after_deactivate="old"; it passes now
on macOS arm64 (NEOMACS_GUI_TEST_BACKEND=macos).  Linux and Windows were
not run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzChXMKZaxRsqnoXaMzh8N
Represent selection ownership as an exhaustive Rust enum and carry it through the clipboard worker, display host, and Lisp primitive boundary.\n\nKeep process-local PRIMARY contents and ownership in one typed state, report unobservable Linux ownership conservatively, and treat an owned empty string as an existing selection. Cover worker, transport, headless, TTY, Lisp, and subr-surface behavior.
Assert the process-local macOS/Windows contract and the conservative Linux contract separately through the real GUI harness. Cover raw ownership states, deactivate-mark behavior, empty selection existence, and disowning where the backend supports an observable local owner.
@eval-exec
eval-exec force-pushed the fix/primary-selection-private-pasteboard branch from 11b875a to 0ce67d1 Compare September 5, 2026 07:00
@eval-exec
eval-exec merged commit 5a1369d into eval-exec:main Sep 5, 2026
24 of 25 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.

3 participants