Skip to content
Open
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
42 changes: 41 additions & 1 deletion packages/tui/src/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export function createClipboardAdapter(clipboard: CoreClipboardService): OwnedCl
if (result.status === "limit-exceeded") {
throw new RangeError("Clipboard content exceeded configured read or image conversion limits")
}
return undefined
// WSL keeps the clipboard on the Windows side: Windows Terminal owns the selection and
// the native host clipboard has no bridge to it, so this read fails unless WSLg forwards
// Wayland. Read the Windows clipboard through interop instead.
if (result.status === "cancelled" || !isWSL()) return undefined
return readWindowsClipboard()
}

if (result.representation.mimeType === "image/png") {
Expand Down Expand Up @@ -65,3 +69,39 @@ export function createClipboardAdapter(clipboard: CoreClipboardService): OwnedCl
},
}
}

const WINDOWS_CLIPBOARD_IMAGE = [
"Add-Type -AssemblyName System.Windows.Forms",
"$image = [System.Windows.Forms.Clipboard]::GetImage()",
"if (-not $image) { exit 0 }",
"$stream = New-Object System.IO.MemoryStream",
"$image.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)",
"[System.Convert]::ToBase64String($stream.ToArray())",
].join("; ")

// Containers on Docker Desktop or devcontainers on Windows also run the WSL kernel but have no
// interop, so detect WSL by the variables it sets instead of the kernel release.
function isWSL() {
return process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
}

async function readWindowsClipboard(): Promise<ClipboardContent | undefined> {
if (!Bun.which("powershell.exe")) {
throw new Error("Clipboard image paste under WSL needs powershell.exe interop, which is not available")
}
const child = Bun.spawn(["powershell.exe", "-NonInteractive", "-NoProfile", "-Command", WINDOWS_CLIPBOARD_IMAGE], {
stdin: "ignore",
stdout: "pipe",
stderr: "ignore",
})
const timeout = setTimeout(() => child.kill(), 10_000)
try {
const output = await new Response(child.stdout).text()
if ((await child.exited) !== 0) throw new Error("Reading the Windows clipboard image failed")
const data = output.trim()
if (!data) return undefined
return { data, mime: "image/png" }
} finally {
clearTimeout(timeout)
}
}
Loading