use anyhow::{Context, Result};
use std::process::Command;
#[derive(Debug, Clone, Copy)]
enum ClipboardBackend {
Wayland,
X11,
MacOS,
Windows,
}
fn detect_backend() -> Option<ClipboardBackend> {
if cfg!(target_os = "macos")
&& Command::new("which")
.arg("pbpaste")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return Some(ClipboardBackend::MacOS);
}
if cfg!(target_os = "windows") {
return Some(ClipboardBackend::Windows);
}
if std::env::var("WAYLAND_DISPLAY").is_ok()
&& Command::new("which")
.arg("wl-paste")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return Some(ClipboardBackend::Wayland);
}
if std::env::var("DISPLAY").is_ok()
&& Command::new("which")
.arg("xclip")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return Some(ClipboardBackend::X11);
}
None
}
pub fn has_image() -> bool {
match detect_backend() {
Some(ClipboardBackend::Wayland) => Command::new("wl-paste")
.arg("--list-types")
.output()
.map(|o| {
let types = String::from_utf8_lossy(&o.stdout);
types.contains("image/png") || types.contains("image/jpeg")
})
.unwrap_or(false),
Some(ClipboardBackend::X11) => Command::new("xclip")
.args(["-selection", "clipboard", "-t", "TARGETS", "-o"])
.output()
.map(|o| {
let types = String::from_utf8_lossy(&o.stdout);
types.contains("image/png") || types.contains("image/jpeg")
})
.unwrap_or(false),
Some(ClipboardBackend::MacOS) => {
Command::new("osascript")
.args(["-e", "clipboard info"])
.output()
.map(|o| {
let info = String::from_utf8_lossy(&o.stdout);
info.contains("PNGf") || info.contains("JPEG") || info.contains("TIFF")
})
.unwrap_or(false)
},
Some(ClipboardBackend::Windows) => {
Command::new("powershell")
.args([
"-NoProfile",
"-Command",
"Add-Type -AssemblyName System.Windows.Forms; \
[System.Windows.Forms.Clipboard]::ContainsImage()",
])
.output()
.map(|o| {
let out = String::from_utf8_lossy(&o.stdout);
out.trim() == "True"
})
.unwrap_or(false)
},
None => false,
}
}
pub fn read_image_bytes() -> Result<(Vec<u8>, String)> {
let backend = detect_backend()
.context("No clipboard backend detected (need xclip, wl-paste, pbpaste, or PowerShell)")?;
match backend {
ClipboardBackend::Wayland | ClipboardBackend::X11 => {
for (mime, format) in [("image/png", "png"), ("image/jpeg", "jpeg")] {
let output = match backend {
ClipboardBackend::Wayland => {
Command::new("wl-paste").args(["--type", mime]).output()
},
ClipboardBackend::X11 => Command::new("xclip")
.args(["-selection", "clipboard", "-t", mime, "-o"])
.output(),
_ => unreachable!(),
};
if let Ok(output) = output
&& output.status.success()
&& !output.stdout.is_empty()
{
return Ok((output.stdout, format.to_string()));
}
}
anyhow::bail!("No image data found in clipboard")
},
ClipboardBackend::MacOS => {
let temp_path = std::env::temp_dir().join("mermaid-clipboard-paste.png");
let temp_str = temp_path.to_string_lossy();
let script = format!(
"set theFile to POSIX file \"{}\"\n\
tell application \"System Events\" to set theData to the clipboard as «class PNGf»\n\
set fp to open for access theFile with write permission\n\
write theData to fp\n\
close access fp",
temp_str
);
let pngpaste_output = Command::new("pngpaste").arg(&temp_path).output();
let success = if let Ok(output) = pngpaste_output
&& output.status.success()
{
true
} else {
Command::new("osascript")
.args(["-e", &script])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if success {
let bytes = std::fs::read(&temp_path)
.context("Failed to read clipboard image from temp file")?;
let _ = std::fs::remove_file(&temp_path);
if !bytes.is_empty() {
return Ok((bytes, "png".to_string()));
}
}
anyhow::bail!("No image data found in clipboard (macOS)")
},
ClipboardBackend::Windows => {
let temp_path = std::env::temp_dir().join("mermaid-clipboard-paste.png");
let temp_str = temp_path.to_string_lossy();
let script = format!(
"Add-Type -AssemblyName System.Windows.Forms; \
$img = [System.Windows.Forms.Clipboard]::GetImage(); \
if ($img) {{ $img.Save('{}', [System.Drawing.Imaging.ImageFormat]::Png) }}",
temp_str
);
let output = Command::new("powershell")
.args(["-NoProfile", "-Command", &script])
.output();
if let Ok(output) = output
&& output.status.success()
&& temp_path.exists()
{
let bytes = std::fs::read(&temp_path)
.context("Failed to read clipboard image from temp file")?;
let _ = std::fs::remove_file(&temp_path);
if !bytes.is_empty() {
return Ok((bytes, "png".to_string()));
}
}
anyhow::bail!("No image data found in clipboard (Windows)")
},
}
}
pub fn read_text() -> Result<String> {
let backend = detect_backend()
.context("No clipboard backend detected (need xclip, wl-paste, pbpaste, or PowerShell)")?;
let output = match backend {
ClipboardBackend::Wayland => Command::new("wl-paste")
.args(["--type", "text/plain"])
.output(),
ClipboardBackend::X11 => Command::new("xclip")
.args(["-selection", "clipboard", "-o"])
.output(),
ClipboardBackend::MacOS => Command::new("pbpaste").output(),
ClipboardBackend::Windows => Command::new("powershell")
.args(["-NoProfile", "-Command", "Get-Clipboard"])
.output(),
};
let output = output.context("Failed to execute clipboard command")?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
anyhow::bail!("Clipboard does not contain text")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_backend() {
let _ = detect_backend();
}
#[test]
fn test_has_image_no_crash() {
let _ = has_image();
}
}