pub mod processor;
pub mod watcher;
use arboard::{Clipboard as Arboard, ImageData};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use crate::{ClaudeUtilsError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ClipboardContent {
#[serde(rename = "text/plain")]
Text {
data: String,
#[serde(skip_serializing_if = "Option::is_none")]
truncated: Option<bool>,
},
#[serde(rename = "image/png")]
ImagePng {
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
file: Option<String>, width: usize,
height: usize,
size: usize,
},
#[serde(rename = "image/jpeg")]
ImageJpeg {
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file: Option<String>,
width: usize,
height: usize,
size: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipboardMetadata {
pub timestamp: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipboardData {
#[serde(flatten)]
pub content: ClipboardContent,
pub metadata: ClipboardMetadata,
}
pub struct ClipboardManager {
clipboard: Arc<Mutex<Arboard>>,
}
impl ClipboardManager {
pub fn new() -> Result<Self> {
let clipboard = Arboard::new().map_err(|e| ClaudeUtilsError::Clipboard(e.to_string()))?;
Ok(Self {
clipboard: Arc::new(Mutex::new(clipboard)),
})
}
pub fn get_content(&self) -> Result<ClipboardData> {
let mut clipboard = self
.clipboard
.lock()
.map_err(|e| ClaudeUtilsError::Clipboard(format!("Lock error: {e}")))?;
if let Ok(image_data) = clipboard.get_image() {
return self.process_image(image_data);
}
if let Ok(text) = clipboard.get_text() {
return Ok(self.process_text(text));
}
Err(ClaudeUtilsError::Clipboard(
"No content in clipboard".to_string(),
))
}
pub fn set_content(&self, content: &ClipboardContent) -> Result<()> {
let mut clipboard = self
.clipboard
.lock()
.map_err(|e| ClaudeUtilsError::Clipboard(format!("Lock error: {e}")))?;
match content {
ClipboardContent::Text { data, .. } => {
clipboard
.set_text(data)
.map_err(|e| ClaudeUtilsError::Clipboard(e.to_string()))?;
}
ClipboardContent::ImagePng {
data: Some(base64_data),
width,
height,
..
}
| ClipboardContent::ImageJpeg {
data: Some(base64_data),
width,
height,
..
} => {
let bytes = BASE64.decode(base64_data).map_err(|e| {
ClaudeUtilsError::Clipboard(format!("Base64 decode error: {e}"))
})?;
let image_data = ImageData {
width: *width,
height: *height,
bytes: bytes.into(),
};
clipboard
.set_image(image_data)
.map_err(|e| ClaudeUtilsError::Clipboard(e.to_string()))?;
}
_ => {
return Err(ClaudeUtilsError::Clipboard(
"Cannot set clipboard from file reference".to_string(),
));
}
}
Ok(())
}
fn process_text(&self, text: String) -> ClipboardData {
let truncated = text.len() > crate::MAX_INLINE_SIZE;
let data = if truncated {
text.chars().take(crate::MAX_INLINE_SIZE).collect()
} else {
text.clone()
};
ClipboardData {
content: ClipboardContent::Text {
data,
truncated: if truncated { Some(true) } else { None },
},
metadata: ClipboardMetadata {
timestamp: chrono::Utc::now(),
source: None,
},
}
}
fn process_image(&self, image_data: ImageData<'_>) -> Result<ClipboardData> {
use image::{ImageFormat, RgbaImage};
let img = RgbaImage::from_raw(
image_data.width as u32,
image_data.height as u32,
image_data.bytes.to_vec(),
)
.ok_or_else(|| {
ClaudeUtilsError::ImageProcessing(image::ImageError::Limits(
image::error::LimitError::from_kind(image::error::LimitErrorKind::DimensionError),
))
})?;
let mut png_bytes = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png_bytes), ImageFormat::Png)?;
let size = png_bytes.len();
let (data, file) = if size <= crate::MAX_INLINE_SIZE {
(Some(BASE64.encode(&png_bytes)), None)
} else {
(None, None)
};
Ok(ClipboardData {
content: ClipboardContent::ImagePng {
data,
file,
width: image_data.width,
height: image_data.height,
size,
},
metadata: ClipboardMetadata {
timestamp: chrono::Utc::now(),
source: None,
},
})
}
pub fn get_raw_image(&self) -> Result<Vec<u8>> {
let mut clipboard = self
.clipboard
.lock()
.map_err(|e| ClaudeUtilsError::Clipboard(format!("Lock error: {e}")))?;
let image_data = clipboard
.get_image()
.map_err(|e| ClaudeUtilsError::Clipboard(e.to_string()))?;
let img = image::RgbaImage::from_raw(
image_data.width as u32,
image_data.height as u32,
image_data.bytes.to_vec(),
)
.ok_or_else(|| {
ClaudeUtilsError::ImageProcessing(image::ImageError::Limits(
image::error::LimitError::from_kind(image::error::LimitErrorKind::DimensionError),
))
})?;
let mut png_bytes = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut png_bytes),
image::ImageFormat::Png,
)?;
Ok(png_bytes)
}
}