collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use crate::api::ImageData;
use anyhow::Result;

/// 5 MB — refuse clipboard images larger than this (API limit & OOM prevention).
const MAX_IMAGE_BYTES: usize = 5 * 1024 * 1024;

/// Clipboard manager supporting text and images
pub struct Clipboard {
    #[cfg(not(target_os = "linux"))]
    inner: arboard::Clipboard,
}

impl Clipboard {
    pub fn new() -> Result<Self> {
        Ok(Self {
            #[cfg(not(target_os = "linux"))]
            inner: arboard::Clipboard::new()?,
        })
    }

    /// Get text from clipboard
    pub fn get_text(&mut self) -> Result<Option<String>> {
        #[cfg(not(target_os = "linux"))]
        {
            match self.inner.get_text() {
                Ok(text) => Ok(Some(text)),
                Err(arboard::Error::ContentNotAvailable) => Ok(None),
                Err(e) => Err(anyhow::anyhow!("Clipboard error: {}", e)),
            }
        }

        #[cfg(target_os = "linux")]
        {
            Ok(get_text_linux())
        }
    }

    /// Get image from clipboard
    pub fn get_image(&mut self) -> Result<Option<ImageData>> {
        #[cfg(not(target_os = "linux"))]
        {
            match self.inner.get_image() {
                Ok(img) => {
                    let bytes = img.bytes.into_owned();
                    if bytes.len() > MAX_IMAGE_BYTES {
                        return Err(anyhow::anyhow!(
                            "Clipboard image too large: {} bytes (max {}MB)",
                            bytes.len(),
                            MAX_IMAGE_BYTES / 1024 / 1024
                        ));
                    }
                    Ok(Some(ImageData::new(bytes)))
                }
                Err(arboard::Error::ContentNotAvailable) => Ok(None),
                Err(e) => Err(anyhow::anyhow!("Clipboard error: {}", e)),
            }
        }

        #[cfg(target_os = "linux")]
        {
            Ok(get_image_linux())
        }
    }

    /// Check if clipboard has image
    pub fn has_image(&mut self) -> bool {
        self.get_image().ok().flatten().is_some()
    }
}

#[cfg(target_os = "linux")]
fn get_text_linux() -> Option<String> {
    // Try wl-paste first (Wayland)
    if let Ok(output) = std::process::Command::new("wl-paste").output() {
        if output.status.success() {
            return Some(String::from_utf8_lossy(&output.stdout).to_string());
        }
    }

    // Fallback to xclip (X11)
    if let Ok(output) = std::process::Command::new("xclip")
        .args(["-selection", "clipboard", "-o"])
        .output()
    {
        if output.status.success() {
            return Some(String::from_utf8_lossy(&output.stdout).to_string());
        }
    }

    None
}

#[cfg(target_os = "linux")]
fn get_image_linux() -> Option<ImageData> {
    // Try wl-paste first (Wayland)
    if let Ok(output) = std::process::Command::new("wl-paste")
        .args(["--type", "image/png"])
        .output()
    {
        if output.status.success() && !output.stdout.is_empty() {
            if output.stdout.len() <= MAX_IMAGE_BYTES {
                return Some(ImageData::new(output.stdout));
            }
        }
    }

    // Fallback to xclip (X11)
    if let Ok(output) = std::process::Command::new("xclip")
        .args(["-selection", "clipboard", "-t", "image/png", "-o"])
        .output()
    {
        if output.status.success() && !output.stdout.is_empty() {
            if output.stdout.len() <= MAX_IMAGE_BYTES {
                return Some(ImageData::new(output.stdout));
            }
        }
    }

    None
}