Skip to content

Commit 5db9d49

Browse files
committed
feat(clipboard): add middle-click paste from X11 primary selection
Adds readPrimary() function supporting wl-paste, xclip, and xsel for reading the X11 primary selection buffer. Adds middle-click handler to the prompt textarea. Upstream: anomalyco/opencode#16379 https://claude.ai/code/session_01R2NB6YZNAFe57T3uhSCGo9
1 parent e7f1ec8 commit 5db9d49

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

packages/core/src/components/prompt/index.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import { readFile, stat } from "node:fs/promises";
14+
import { platform } from "node:os";
1415
import { basename } from "node:path";
1516

1617
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -565,6 +566,25 @@ export function Prompt({
565566
commandContext,
566567
]);
567568

569+
const handleMouseDown = useCallback(
570+
async (e: { button?: number; preventDefault?: () => void }) => {
571+
if (e.button !== 1) {
572+
return; // 1 = middle button
573+
}
574+
if (disabled) {
575+
return;
576+
}
577+
e.preventDefault?.();
578+
const text = await Clipboard.readPrimary();
579+
const textarea = textareaRef.current;
580+
if (!(text && textarea)) {
581+
return;
582+
}
583+
textarea.insertText(text);
584+
},
585+
[disabled]
586+
);
587+
568588
const handleKeyDown = useCallback(
569589
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex keyboard event handling
570590
async (e: KeyEvent) => {
@@ -967,6 +987,7 @@ export function Prompt({
967987
minHeight={1}
968988
onContentChange={handleContentChange}
969989
onKeyDown={handleKeyDown}
990+
onMouseDown={handleMouseDown}
970991
onPaste={handlePaste}
971992
onSubmit={handleSubmit}
972993
placeholder={displayPlaceholder}

packages/core/src/lib/clipboard.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,39 @@ const getCopyMethod = lazy(() => {
208208
};
209209
});
210210

211+
async function readPrimary(): Promise<string | undefined> {
212+
const os = platform();
213+
if (os === "linux") {
214+
if (
215+
process.env.WAYLAND_DISPLAY &&
216+
which.sync("wl-paste", { nothrow: true })
217+
) {
218+
const result = await execa("wl-paste", ["--primary", "--no-newline"], {
219+
reject: false,
220+
});
221+
if (result.stdout) {
222+
return result.stdout;
223+
}
224+
}
225+
if (which.sync("xclip", { nothrow: true })) {
226+
const result = await execa("xclip", ["-selection", "primary", "-o"], {
227+
reject: false,
228+
});
229+
if (result.stdout) {
230+
return result.stdout;
231+
}
232+
}
233+
if (which.sync("xsel", { nothrow: true })) {
234+
const result = await execa("xsel", ["--primary", "--output"], {
235+
reject: false,
236+
});
237+
if (result.stdout) {
238+
return result.stdout;
239+
}
240+
}
241+
}
242+
}
243+
211244
async function copy(text: string): Promise<void> {
212245
writeOsc52(text);
213246
await getCopyMethod()(text);
@@ -216,4 +249,5 @@ async function copy(text: string): Promise<void> {
216249
export const Clipboard = {
217250
read,
218251
copy,
252+
readPrimary,
219253
};

0 commit comments

Comments
 (0)