use crate::error::ClipboardError;
use std::sync::mpsc;
use std::time::Duration;
const CLIPBOARD_SIZE_LIMIT: usize = 1024 * 1024 * 1024;
const CLIPBOARD_TIMEOUT: Duration = Duration::from_secs(10);
const fn check_size_limit(content_len: usize, limit: usize) -> Result<(), ClipboardError> {
if content_len > limit {
return Err(ClipboardError::SizeExceeded { content_len, limit });
}
Ok(())
}
pub fn copy_to_clipboard(content: &str) -> Result<(), ClipboardError> {
check_size_limit(content.len(), CLIPBOARD_SIZE_LIMIT)?;
let (tx, rx) = mpsc::channel();
let content = content.to_string();
let join_handle = std::thread::Builder::new()
.name("clipboard-writer".into())
.spawn(move || {
let result = (|| -> Result<(), ClipboardError> {
let mut clipboard =
arboard::Clipboard::new().map_err(|e| ClipboardError::Access(e.to_string()))?;
clipboard
.set_text(content)
.map_err(|e| ClipboardError::Write(e.to_string()))?;
Ok(())
})();
let _ = tx.send(result);
})
.map_err(ClipboardError::ThreadSpawn)?;
match rx.recv_timeout(CLIPBOARD_TIMEOUT) {
Ok(result) => {
if let Err(panic_payload) = join_handle.join() {
std::panic::resume_unwind(panic_payload);
}
result
}
Err(mpsc::RecvTimeoutError::Timeout) => {
log::debug!(
"Clipboard thread still running after {CLIPBOARD_TIMEOUT:?}; \
it will be cleaned up on process exit"
);
Err(ClipboardError::Timeout)
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
match join_handle.join() {
Err(panic_payload) => std::panic::resume_unwind(panic_payload),
Ok(()) => Err(ClipboardError::ThreadLost),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clipboard_size_limit_matches_documented_hard_limit() {
assert_eq!(
CLIPBOARD_SIZE_LIMIT,
1024 * 1024 * 1024,
"CLIPBOARD_SIZE_LIMIT must be 1GB to match documented hard limit"
);
}
#[test]
fn test_size_limit_rejects_oversized_content() {
let result = check_size_limit(1001, 1000);
assert!(
matches!(
result,
Err(ClipboardError::SizeExceeded {
content_len: 1001,
limit: 1000
})
),
"Expected SizeExceeded {{ 1001, 1000 }}, got {result:?}"
);
}
#[test]
fn test_size_limit_accepts_content_at_boundary() {
assert!(check_size_limit(1000, 1000).is_ok());
}
#[test]
fn test_size_limit_accepts_content_under_limit() {
assert!(check_size_limit(999, 1000).is_ok());
}
#[test]
fn test_size_limit_accepts_empty() {
assert!(check_size_limit(0, 1000).is_ok());
}
#[test]
fn test_clipboard_with_small_content() {
let content = "test content";
let _ = copy_to_clipboard(content);
}
#[test]
fn test_empty_content() {
let _ = copy_to_clipboard("");
}
#[test]
fn test_unicode_content() {
let content = "Hello 世界 🌍 مرحبا";
let _ = copy_to_clipboard(content);
}
#[test]
fn test_multiline_content() {
let content = "line 1\nline 2\nline 3";
let _ = copy_to_clipboard(content);
}
}