diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 3a9996902ddf..7e44f5e8871f 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -5,6 +5,8 @@ import path from "path" import fs from "fs/promises" import * as Filesystem from "../../../../util/filesystem" import * as Process from "../../../../util/process" +import { Config } from "../../../../config/index.js" +import { AppRuntime } from "../../../../effect/app-runtime.js" // Lazy load which and clipboardy to avoid expensive execa/which/isexe chain at startup const getWhich = lazy(async () => { @@ -126,16 +128,35 @@ const getCopyMethod = lazy(async () => { if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) { console.log("clipboard: using wl-copy") return async (text: string) => { + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())).catch( + () => ({}) as Config.Info, + ) + const enablePrimary = config.clipboard?.linux?.enablePrimaryCopy ?? false const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) if (!proc.stdin) return proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) + if (enablePrimary) { + const procPrimary = Process.spawn(["wl-copy", "--primary"], { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }) + if (!procPrimary.stdin) return + procPrimary.stdin.write(text) + procPrimary.stdin.end() + await procPrimary.exited.catch(() => {}) + } } } if (which("xclip")) { console.log("clipboard: using xclip") return async (text: string) => { + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())).catch( + () => ({}) as Config.Info, + ) + const enablePrimary = config.clipboard?.linux?.enablePrimaryCopy ?? false const proc = Process.spawn(["xclip", "-selection", "clipboard"], { stdin: "pipe", stdout: "ignore", @@ -145,11 +166,26 @@ const getCopyMethod = lazy(async () => { proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) + if (enablePrimary) { + const procPrimary = Process.spawn(["xclip", "-selection", "primary"], { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }) + if (!procPrimary.stdin) return + procPrimary.stdin.write(text) + procPrimary.stdin.end() + await procPrimary.exited.catch(() => {}) + } } } if (which("xsel")) { console.log("clipboard: using xsel") return async (text: string) => { + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())).catch( + () => ({}) as Config.Info, + ) + const enablePrimary = config.clipboard?.linux?.enablePrimaryCopy ?? false const proc = Process.spawn(["xsel", "--clipboard", "--input"], { stdin: "pipe", stdout: "ignore", @@ -159,6 +195,17 @@ const getCopyMethod = lazy(async () => { proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) + if (enablePrimary) { + const procPrimary = Process.spawn(["xsel", "--primary", "--input"], { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }) + if (!procPrimary.stdin) return + procPrimary.stdin.write(text) + procPrimary.stdin.end() + await procPrimary.exited.catch(() => {}) + } } } } diff --git a/packages/opencode/src/config/clipboard.ts b/packages/opencode/src/config/clipboard.ts new file mode 100644 index 000000000000..5921b20c6f1c --- /dev/null +++ b/packages/opencode/src/config/clipboard.ts @@ -0,0 +1,19 @@ +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" + +const Linux = Schema.Struct({ + enablePrimaryCopy: Schema.optional(Schema.Boolean).annotate({ + description: "Copy to primary clipboard in addition to regular clipboard on Linux (Wayland/X11)", + }), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) + +export const Info = Schema.Struct({ + linux: Schema.optional(Linux).annotate({ + description: "Linux-specific clipboard settings", + }), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) + +export type Info = Schema.Schema.Type + +export * as ConfigClipboard from "./clipboard" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 817f8c3e3870..20f278184509 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -26,6 +26,7 @@ import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { InstanceRef } from "@/effect/instance-ref" import { zod } from "@/util/effect-zod" import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema" +import { ConfigClipboard } from "./clipboard" import { ConfigAgent } from "./agent" import { ConfigCommand } from "./command" import { ConfigFormatter } from "./formatter" @@ -109,6 +110,7 @@ export const Info = Schema.Struct({ description: "Command configuration, see https://opencode.ai/docs/commands", }), skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }), + clipboard: Schema.optional(ConfigClipboard.Info).annotate({ description: "Clipboard configuration" }), watcher: Schema.optional( Schema.Struct({ ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),