Skip to content

Commit 5fcd853

Browse files
committed
tui: enable primary clipboard copy for Wayland/X11 to fix Linux middle-click paste
Adds optional config `clipboard.linux.enablePrimaryCopy` to copy to primary clipboard in addition to regular clipboard, enabling middle-click paste on Linux and improving compatibility with environments like VMware Workstation that sync only the primary clipboard.
1 parent 7afa48b commit 5fcd853

2 files changed

Lines changed: 54 additions & 4 deletions

File tree

packages/opencode/src/cli/cmd/tui/util/clipboard.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { tmpdir } from "os"
66
import path from "path"
77
import { Filesystem } from "../../../../util/filesystem"
88
import { Process } from "../../../../util/process"
9+
import { Config } from "../../../../config/config.js"
910

1011
/**
1112
* Writes text to clipboard via OSC 52 escape sequence.
@@ -88,17 +89,30 @@ export namespace Clipboard {
8889
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
8990
console.log("clipboard: using wl-copy")
9091
return async (text: string) => {
91-
const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
92-
if (!proc.stdin) return
92+
const config = await Config.get().catch(() => ({}))
93+
const enablePrimary = (config as Config.Info).clipboard?.linux?.enablePrimaryCopy ?? false
94+
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
9395
proc.stdin.write(text)
9496
proc.stdin.end()
9597
await proc.exited.catch(() => {})
98+
if (enablePrimary) {
99+
const procPrimary = Bun.spawn(["wl-copy", "--primary"], {
100+
stdin: "pipe",
101+
stdout: "ignore",
102+
stderr: "ignore",
103+
})
104+
procPrimary.stdin.write(text)
105+
procPrimary.stdin.end()
106+
await procPrimary.exited.catch(() => {})
107+
}
96108
}
97109
}
98110
if (Bun.which("xclip")) {
99111
console.log("clipboard: using xclip")
100112
return async (text: string) => {
101-
const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
113+
const config = await Config.get().catch(() => ({}))
114+
const enablePrimary = (config as Config.Info).clipboard?.linux?.enablePrimaryCopy ?? false
115+
const proc = Bun.spawn(["xclip", "-selection", "clipboard"], {
102116
stdin: "pipe",
103117
stdout: "ignore",
104118
stderr: "ignore",
@@ -107,12 +121,24 @@ export namespace Clipboard {
107121
proc.stdin.write(text)
108122
proc.stdin.end()
109123
await proc.exited.catch(() => {})
124+
if (enablePrimary) {
125+
const procPrimary = Bun.spawn(["xclip", "-selection", "primary"], {
126+
stdin: "pipe",
127+
stdout: "ignore",
128+
stderr: "ignore",
129+
})
130+
procPrimary.stdin.write(text)
131+
procPrimary.stdin.end()
132+
await procPrimary.exited.catch(() => {})
133+
}
110134
}
111135
}
112136
if (Bun.which("xsel")) {
113137
console.log("clipboard: using xsel")
114138
return async (text: string) => {
115-
const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
139+
const config = await Config.get().catch(() => ({}))
140+
const enablePrimary = (config as Config.Info).clipboard?.linux?.enablePrimaryCopy ?? false
141+
const proc = Bun.spawn(["xsel", "--clipboard", "--input"], {
116142
stdin: "pipe",
117143
stdout: "ignore",
118144
stderr: "ignore",
@@ -121,6 +147,16 @@ export namespace Clipboard {
121147
proc.stdin.write(text)
122148
proc.stdin.end()
123149
await proc.exited.catch(() => {})
150+
if (enablePrimary) {
151+
const procPrimary = Bun.spawn(["xsel", "--primary", "--input"], {
152+
stdin: "pipe",
153+
stdout: "ignore",
154+
stderr: "ignore",
155+
})
156+
procPrimary.stdin.write(text)
157+
procPrimary.stdin.end()
158+
await procPrimary.exited.catch(() => {})
159+
}
124160
}
125161
}
126162
}

packages/opencode/src/config/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,20 @@ export namespace Config {
10191019
.object({
10201020
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
10211021
theme: z.string().optional().describe("Theme name to use for the interface"),
1022+
clipboard: z
1023+
.object({
1024+
linux: z
1025+
.object({
1026+
enablePrimaryCopy: z
1027+
.boolean()
1028+
.optional()
1029+
.default(false)
1030+
.describe("Copy to primary clipboard in addition to regular clipboard on Linux (Wayland/X11)"),
1031+
})
1032+
.optional(),
1033+
})
1034+
.optional()
1035+
.describe("Clipboard configuration"),
10221036
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
10231037
logLevel: Log.Level.optional().describe("Log level"),
10241038
tui: TUI.optional().describe("TUI specific settings"),

0 commit comments

Comments
 (0)