Skip to content

feat(tui): add linux_clipboard_selection config for primary buffer support - #32370

Open
bornmw wants to merge 15 commits into
anomalyco:devfrom
bornmw:linux-clipboard-selection
Open

feat(tui): add linux_clipboard_selection config for primary buffer support#32370
bornmw wants to merge 15 commits into
anomalyco:devfrom
bornmw:linux-clipboard-selection

Conversation

@bornmw

@bornmw bornmw commented Jun 15, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #43176

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds Linux clipboard selection support with a new linux_clipboard_selection config option (tui.json):

  • clipboard: Standard clipboard only (Ctrl+V)
  • primary: Targets the middle-click buffer; TUI paste reads from it too
  • both: Updates both buffers simultaneously (default)

With the default both, copies on Linux now also write the primary selection — this extends existing copy behavior rather than replacing it.

Also fixes a Linux clipboard bug found while testing this: text copied via wl-copy would not paste into other applications because wl-copy offers piped content as application/octet-stream (its MIME auto-detection runs on an extensionless temp file). The fix passes an explicit --type text/plain;charset=utf-8.

The option is Linux-only; on other platforms the regular clipboard is always used.

How did you verify your code works?

I select text and the selection goes to the configured destination - Clipboard / Primary / Both (default). Verified against wl-paste and wl-paste -p on a Wayland session, bun typecheck, and the TUI test suite.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

_If you do not follow this template your PR will be automatically rejected.

@github-actions

Copy link
Copy Markdown
Contributor

Hey! Your PR title Linux clipboard selection doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Duplicate PRs Found

  1. PR fix: enable primary clipboard copy for Wayland/X11 to fix Linux middle-click paste #6370: "fix: enable primary clipboard copy for Wayland/X11 to fix Linux middle-click paste"

  2. PR fix(tui): prevent false clipboard copy success on Linux #31252: "fix(tui): prevent false clipboard copy success on Linux"

  3. PR fix(opencode): report clipboard copy failures #27861: "fix(opencode): report clipboard copy failures"

Note: PR #6370 appears to be the most relevant, as it specifically addresses enabling PRIMARY clipboard copy for Linux middle-click paste functionality—the exact feature being implemented in PR #32370.

@bornmw

bornmw commented Jun 15, 2026

Copy link
Copy Markdown
Author

The following comment was made by an LLM, it may be inaccurate:

Potential Duplicate PRs Found

I reviewed the potential duplicates flagged by the bot, and here is how this PR compares:

PR #31252 and PR #27861 are not duplicates. They handle clipboard failure reporting and error validation rather than the primary selection buffer.

PR #6370 attempts to solve the same underlying issue (adding PRIMARY selection for Linux middle-click paste), but there are a few key differences. First, that PR has been stalled since December. Second, my implementation introduces a more flexible configuration approach—letting the user explicitly choose between Clipboard, Primary, or Both (defaulting to Both)—whereas #6370 uses a simple boolean toggle. Finally, this PR optimizes the copy operation under the hood by utilizing Promise.allSettled to update the buffers in parallel.

Given these architectural differences and the inactivity on #6370, I believe this PR provides a more robust and complete solution to closing #29963. Let me know if any changes are needed!

@bornmw
bornmw force-pushed the linux-clipboard-selection branch from b1cb38a to c6e0c83 Compare June 15, 2026 04:19
@bornmw bornmw changed the title Linux clipboard selection fix(cli) Linux clipboard selection Jun 15, 2026
@bornmw bornmw changed the title fix(cli) Linux clipboard selection fix(cli): Linux clipboard selection Jun 24, 2026
@bornmw bornmw changed the title fix(cli): Linux clipboard selection feat(opencode): add linux_clipboard_selection config for primary buffer support Aug 18, 2026
@bornmw
bornmw force-pushed the linux-clipboard-selection branch from 693c292 to f5bfa85 Compare August 18, 2026 04:10

@bornmw bornmw left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

PR #32370feat(opencode): add linux_clipboard_selection config for primary buffer support

Verdict: Request changes (small)

The feature is sound and the bundled fix is the real value. The issues are formatting, one behavioral gap, and some type hygiene.

What is good

  • The wl-copy --type text/plain;charset=utf-8 fix is the most important line in this PR. Without it, piped wl-copy offers only application/octet-stream (wl-copy dumps stdin to a temp file named stdin, and xdg-mime content-sniffing on an extensionless file returns octet-stream), which text editors reject. This is arguably a hidden bug fix inside a feature PR — worth calling out explicitly in the description.
  • Clean separation: copyCommand stays pure, getCopyMethod routes by selection, Promise.allSettled for "both" is correct.
  • Schema annotation carries the description into the generated tui.json schema.
  • Tests cover all copyCommand branches including the MIME fix.

Issues

  1. OSC52 bypasses the selection setting (most substantive). write() calls writeOsc52(text) unconditionally before routing to the selection-specific method (clipboard.ts). With linux_clipboard_selection: "primary", the regular clipboard is still written via OSC52 whenever the terminal supports it (tmux, kitty, wezterm...). OSC52 also cannot target the primary buffer. "primary" mode is therefore not primary-only. Suggest skipping OSC52 for "primary" (or threading selection into it).

  2. Formatting: the PR introduces Prettier violations in all 4 modified source files. The repo uses prettier (printWidth 120, semi: false) and base dev is clean, but this PR fails bunx prettier --check on clipboard.ts (xsel ternary, copyMethod type), clipboard.tsx (138-char ClipboardProvider signature), config/index.tsx (Resolved Omit), and clipboard.test.ts (long toEqual arrays). CI only runs tsgo typecheck so this will not block, but it deviates from repo convention — run bunx prettier --write on the changed files.

  3. Union types duplicated in three files; exported ClipboardSelection is unused. "clipboard" | "primary" | "both" appears in clipboard.ts, config/index.tsx (Resolved), and clipboard.tsx (ClipboardConfig); copyCommand uses an inline "clipboard" | "primary" instead of the exported ClipboardSelection. Define one shared type and reuse it.

  4. Non-Linux platforms silently ignore the selection. The osascript (macOS) and PowerShell (Windows) branches do not read selection, so primary/both degrade to clipboard-only writes there. Defensible given the linux_ scope, but worth documenting (or gating the option in resolve()).

  5. Minor: props.linuxClipboardSelection! non-null assertion in ClipboardProvider is avoidable; PR history has 2 merge commits + "trigger title check" (squash on merge); PR body says "Closes #43176" while the duplicate-PR comment references #29963 — keep the linked issue consistent.

Note (not a blocker)

  • Runtime write() routing ("both"/allSettled) has no test coverage — acceptable given spawn mocking cost, but a natural follow-up.

@bornmw bornmw changed the title feat(opencode): add linux_clipboard_selection config for primary buffer support feat(tui): add linux_clipboard_selection config for primary buffer support Aug 19, 2026
@bornmw

bornmw commented Aug 19, 2026

Copy link
Copy Markdown
Author

Addressed all review comments:

  1. OSC52 now skips for primary selection
  2. Files formatted with prettier
  3. ClipboardSelection type unified across files
  4. Non-null assertion removed in ClipboardProvider
  5. PR title updated to feat(tui): add linux_clipboard_selection config

@bornmw

bornmw commented Aug 19, 2026

Copy link
Copy Markdown
Author

Verified the changes against the latest head (115aa803e). Typecheck passes and all 196 TUI tests pass (195 pass / 1 skip). Four of the five items are solid:

  1. OSC52 skips for primary - write() now guards with if (selection !== "primary") before writeOsc52. Correct for all three selections: primary skips OSC52, both/clipboard keep it.
  2. ClipboardSelection unified - ClipboardConfig removed; clipboard.tsx imports type ClipboardSelection from clipboard.ts, which now includes "both". copyCommand and copyMethod reuse it. Typechecks cleanly.
  3. Non-null assertion removed - write(text, props.linuxClipboardSelection) narrows correctly in the closure; no error.
  4. PR title now feat(tui): add linux_clipboard_selection config for primary buffer support - conventional format.

One item is not actually done:

  • Prettier - bunx prettier --check still fails on the same 4 files. The violations are unchanged: clipboard.ts (xsel ternary), clipboard.tsx (138-char ClipboardProvider signature), config/index.tsx (description string and Resolved Omit), and clipboard.test.ts (long toEqual arrays). These don't block CI (it only runs tsgo), but they still deviate from repo convention. Run bunx prettier --write on the 4 files and the review item is fully closed.

@bornmw

bornmw commented Aug 19, 2026

Copy link
Copy Markdown
Author

Fixes applied. Prettier formatting now complete. Commit cb7bf05 updates all 4 files.

@bornmw

bornmw commented Aug 19, 2026

Copy link
Copy Markdown
Author

Confirmed — all review items are now closed. cb7bf057c is purely formatting (no logic changes): it reformats exactly the 4 flagged spots (clipboard.ts xsel ternary, clipboard.tsx ClipboardProvider signature, config/index.tsx description + Resolved Omit, clipboard.test.ts toEqual arrays). bunx prettier --check passes on all 4 files, bun typecheck is clean, and all 196 TUI tests pass (195 pass / 1 skip).

LGTM from the review side. Good to merge.

@bornmw bornmw left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

PR #32370feat(tui): add linux_clipboard_selection config for primary buffer support

Verdict: Request changes (medium)

The previous round's asks are all closed and verified on the current head. The remaining gaps: the selection contract is write-only, a silent no-op failure mode claims success, and the option is missing from tests and docs.

What is good (verified on head 2fdfe1c9b2)

  • All five items from the last review round are done. OSC52 is skipped for "primary" (the write() guard), prettier is clean on all 5 changed files (re-ran bunx prettier --check), ClipboardSelection is unified, the non-null assertion is gone, and the title is conventional.
  • The verification claims hold. bun typecheck is clean and all 196 TUI tests pass (195 pass / 1 skip).
  • Config pipeline integration is sound. The key flows through TuiConfig.Inforesolve() at both call sites, a typo fails loudly like any other key, no committed schema artifact needs regeneration (the tui.json schema is generated from Info at web build time), and tui-migrate correctly needs no change.
  • All write paths route through the context service (useClipboard), so selection routing applies everywhere — nothing bypasses it with a direct write import.

Issues

  1. The selection contract is write-only: read() ignores the selection (most substantive). ClipboardProvider only wraps write with the selection; read is passed through bare. The default ctrl+v (input_paste) routes to prompt.pasteclipboard.read?.(), and read() only ever reads the clipboard buffer (wl-paste default, xclip -selection clipboard, clipboardy). With "primary", every opencode copy lands in the primary buffer only, then opencode's own paste reads the stale clipboard. Middle-click itself is fine (the TUI has no middle-click handler; with mouse capture off the terminal pastes natively from primary), so the gap is specifically opencode's own ctrl+v path. Suggest making read selection-aware (wl-paste --primary / xclip -selection primary for "primary"), or explicitly documenting that the paste keybind is unaffected by the setting.

  2. Silent no-op and unconditional success toast. write() never rejects (.catch(() => undefined) everywhere, Promise.allSettled for "both"), and the clipboardy fallback does if (selection === "primary") return — a silent no-op. Both resolve "successfully" into the "Copied to clipboard" toast, so a failed or skipped copy claims success. At minimum the fallback should fall back to the clipboard instead of no-op'ing, or the failure should surface.

  3. Config tests missing in config.test.tsx. That file already has the pattern: "validates config constraints" decodes Info and asserts invalid values throw (e.g. cursor: { style: "beam" }), and "resolves host-neutral defaults" asserts defaults like mouse: true. Nothing covers linux_clipboard_selection — no valid-value decode, no invalid-value throw, no resolve({}).linux_clipboard_selection === "both". A few lines in an existing file.

  4. Docs: linux_clipboard_selection is missing from the TUI options reference. tui.mdx documents diff_style, cursor, mouse, attention and the rest; this option appears nowhere in the repo's docs. Every user-facing option is listed there.

  5. Non-Linux platforms silently ignore the selection (previous item #4, still open). The osascript branch ignores it; the win32 copyCommand branch returns the same PowerShell command for every selection, so "primary" writes the normal clipboard and "both" spawns the identical command twice in parallel. Defensible given the linux_ scope, but it's still undocumented and un-gated.

Note (not a blocker)

  • The "both" default is a silent behavior change for all Linux users (copies now also write the primary selection). Fine, but the PR body frames it as a new option — one sentence about the default changing existing copy behavior would help, alongside calling out the wl-copy --type text/plain fix.
  • copyCommand accepts "both" but implicitly behaves as "clipboard"Exclude<ClipboardSelection, "both"> would make the contract honest. Resolved also still inlines the union instead of importing ClipboardSelection (previous item #3 half-closed).
  • PR body: "Also includes a new cursor config" — Cursor already exists on dev; stale from an earlier iteration.
  • History still carries 3 merge commits + "trigger title check" — squash on merge.
  • No test exercises the OSC52 guard or the both/allSettled path — acceptable follow-up as previously noted.

@kylebakerio

Copy link
Copy Markdown

cannot wait for this to be merged in.

@bornmw

bornmw commented Aug 21, 2026

Copy link
Copy Markdown
Author

All addressed in 23da22c44 (fix(tui): selection-aware clipboard read, config tests and docs). Item by item:

  1. Selection-aware read — the provider now wraps read exactly like write: with "primary", TUI paste (ctrl+v) reads the primary buffer (wl-paste -p / xclip -selection primary), and the image path uses the same buffer. With "both" the paste reads the clipboard (middle-click stays terminal-native). The clipboardy text fallback only addresses the clipboard, but it is also the consistent last resort for "primary" because the write path degrades to clipboardy→clipboard on systems without native tools (see #2), so read and write agree there.

  2. Silent no-op removed — the clipboardy fallback now writes the clipboard instead of returning for a "primary" it cannot service. Genuinely failed native writes are still swallowed by the existing .catch(() => undefined) pattern; surfacing per-buffer toast errors is a larger change (the copy call sites would need to handle rejection) — happy to follow up separately rather than expand this PR.

  3. Config tests — added valid-value decode, invalid-value throw ("middle"), default resolve to "both", and override resolution in config.test.tsx.

  4. Docslinux_clipboard_selection now listed in the tui.mdx options reference and the example tui.json.

  5. Non-Linux behavior — documented instead of gated: the schema description and docs both state the option is Linux-only and ignored on other platforms (the runtime already resolves to clipboard-only commands there).

Notes addressed as well:

  • new shared ClipboardBuffer = Exclude<ClipboardSelection, "both"> used by both copyCommand and read
  • Resolved references the imported ClipboardSelection instead of inlining the union
  • "both" no longer spawns an identical command twice — on win32 the clipboard and primary commands resolve to the same PowerShell invocation; a JSON.stringify comparison now suppresses the duplicate
  • PR body updated: stale cursor line removed, default-both behavior change called out, wl-copy --type MIME fix described

Verified: bunx prettier --check clean on all changed files, bun typecheck clean, 196 TUI tests pass (195 pass / 1 skip). read("primary") also functionally verified against a live Wayland session.

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.

[FEATURE]: Add support for Linux PRIMARY selection (middle-click paste)

2 participants