agentmux_cef\commands/
clipboard.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Clipboard commands — read/write the OS clipboard via Win32/macOS/Linux APIs.
5// CEF's Chromium blocks navigator.clipboard.readText() without a permission
6// policy header, so we route clipboard through the host process via IPC.
7
8/// Read text from the OS clipboard.
9pub fn read_clipboard() -> Result<serde_json::Value, String> {
10    let text = read_clipboard_text()?;
11    Ok(serde_json::json!(text))
12}
13
14/// Write text to the OS clipboard.
15pub fn write_clipboard(args: &serde_json::Value) -> Result<serde_json::Value, String> {
16    let text = args
17        .get("text")
18        .and_then(|v| v.as_str())
19        .ok_or_else(|| "missing 'text' argument".to_string())?;
20    write_clipboard_text(text)?;
21    Ok(serde_json::Value::Null)
22}
23
24#[cfg(target_os = "windows")]
25fn read_clipboard_text() -> Result<String, String> {
26    use windows_sys::Win32::System::DataExchange::*;
27    use windows_sys::Win32::System::Memory::*;
28    use windows_sys::Win32::System::Ole::CF_UNICODETEXT;
29
30    unsafe {
31        if OpenClipboard(std::ptr::null_mut()) == 0 {
32            return Err("Failed to open clipboard".into());
33        }
34        let handle = GetClipboardData(CF_UNICODETEXT as u32);
35        if handle.is_null() {
36            CloseClipboard();
37            return Ok(String::new());
38        }
39        let ptr = GlobalLock(handle) as *const u16;
40        if ptr.is_null() {
41            CloseClipboard();
42            return Err("Failed to lock clipboard data".into());
43        }
44        let mut len = 0;
45        while *ptr.add(len) != 0 {
46            len += 1;
47        }
48        let slice = std::slice::from_raw_parts(ptr, len);
49        let text = String::from_utf16_lossy(slice);
50        GlobalUnlock(handle);
51        CloseClipboard();
52        Ok(text)
53    }
54}
55
56#[cfg(target_os = "windows")]
57fn write_clipboard_text(text: &str) -> Result<(), String> {
58    use windows_sys::Win32::System::DataExchange::*;
59    use windows_sys::Win32::System::Memory::*;
60    use windows_sys::Win32::Foundation::GlobalFree;
61    use windows_sys::Win32::System::Ole::CF_UNICODETEXT;
62
63    let wide: Vec<u16> = text.encode_utf16().chain(std::iter::once(0)).collect();
64    let size = wide.len() * 2;
65
66    unsafe {
67        let hmem = GlobalAlloc(GMEM_MOVEABLE, size);
68        if hmem.is_null() {
69            return Err("Failed to allocate clipboard memory".into());
70        }
71        let ptr = GlobalLock(hmem) as *mut u16;
72        if ptr.is_null() {
73            GlobalFree(hmem);
74            return Err("Failed to lock clipboard memory".into());
75        }
76        std::ptr::copy_nonoverlapping(wide.as_ptr(), ptr, wide.len());
77        GlobalUnlock(hmem);
78
79        if OpenClipboard(std::ptr::null_mut()) == 0 {
80            GlobalFree(hmem);
81            return Err("Failed to open clipboard".into());
82        }
83        EmptyClipboard();
84        SetClipboardData(CF_UNICODETEXT as u32, hmem);
85        CloseClipboard();
86        Ok(())
87    }
88}
89
90#[cfg(target_os = "macos")]
91fn read_clipboard_text() -> Result<String, String> {
92    use std::process::Command;
93    let output = Command::new("pbpaste")
94        .output()
95        .map_err(|e| format!("pbpaste failed: {}", e))?;
96    Ok(String::from_utf8_lossy(&output.stdout).to_string())
97}
98
99#[cfg(target_os = "macos")]
100fn write_clipboard_text(text: &str) -> Result<(), String> {
101    use std::io::Write;
102    use std::process::{Command, Stdio};
103    let mut child = Command::new("pbcopy")
104        .stdin(Stdio::piped())
105        .spawn()
106        .map_err(|e| format!("pbcopy failed: {}", e))?;
107    child
108        .stdin
109        .as_mut()
110        .unwrap()
111        .write_all(text.as_bytes())
112        .map_err(|e| format!("pbcopy write failed: {}", e))?;
113    child.wait().map_err(|e| format!("pbcopy wait failed: {}", e))?;
114    Ok(())
115}
116
117#[cfg(target_os = "linux")]
118fn is_wayland() -> bool {
119    std::env::var("WAYLAND_DISPLAY").map_or(false, |v| !v.is_empty())
120}
121
122#[cfg(target_os = "linux")]
123fn read_clipboard_text() -> Result<String, String> {
124    use std::process::Command;
125    if is_wayland() {
126        if let Ok(output) = Command::new("wl-paste").args(["--no-newline"]).output() {
127            if output.status.success() {
128                return Ok(String::from_utf8_lossy(&output.stdout).to_string());
129            }
130        }
131    }
132    // X11 fallback: xclip, then xsel
133    let output = Command::new("xclip")
134        .args(["-selection", "clipboard", "-o"])
135        .output()
136        .or_else(|_| Command::new("xsel").args(["--clipboard", "--output"]).output())
137        .map_err(|e| format!("clipboard read failed (install wl-paste, xclip, or xsel): {}", e))?;
138    Ok(String::from_utf8_lossy(&output.stdout).to_string())
139}
140
141#[cfg(target_os = "linux")]
142fn write_clipboard_text(text: &str) -> Result<(), String> {
143    use std::io::Write;
144    use std::process::{Command, Stdio};
145    if is_wayland() {
146        if let Ok(mut child) = Command::new("wl-copy").stdin(Stdio::piped()).spawn() {
147            if let Some(stdin) = child.stdin.as_mut() {
148                let _ = stdin.write_all(text.as_bytes());
149            }
150            if let Ok(status) = child.wait() {
151                if status.success() {
152                    return Ok(());
153                }
154            }
155        }
156    }
157    // X11 fallback: xclip, then xsel
158    let mut child = Command::new("xclip")
159        .args(["-selection", "clipboard"])
160        .stdin(Stdio::piped())
161        .spawn()
162        .or_else(|_| {
163            Command::new("xsel")
164                .args(["--clipboard", "--input"])
165                .stdin(Stdio::piped())
166                .spawn()
167        })
168        .map_err(|e| format!("clipboard write failed (install wl-copy, xclip, or xsel): {}", e))?;
169    child
170        .stdin
171        .as_mut()
172        .unwrap()
173        .write_all(text.as_bytes())
174        .map_err(|e| format!("clipboard write failed: {}", e))?;
175    child.wait().map_err(|e| format!("clipboard write failed: {}", e))?;
176    Ok(())
177}