Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/opencode/src/cli/cmd/tui/util/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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(() => {})
}
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions packages/opencode/src/config/clipboard.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Info>

export * as ConfigClipboard from "./clipboard"
2 changes: 2 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))),
Expand Down
Loading