use std::sync::Arc;
pub trait Clipboard: Send + Sync + std::fmt::Debug {
fn set_text(&self, text: &str) -> Result<(), String>;
}
#[derive(Debug, Default)]
pub struct SystemClipboard;
impl Clipboard for SystemClipboard {
fn set_text(&self, text: &str) -> Result<(), String> {
let mut clipboard = arboard::Clipboard::new().map_err(|err| err.to_string())?;
clipboard
.set_text(text.to_string())
.map_err(|err| err.to_string())
}
}
pub fn default_clipboard() -> Arc<dyn Clipboard> {
Arc::new(SystemClipboard)
}
#[cfg(test)]
pub(crate) mod test_doubles {
use super::Clipboard;
use std::sync::Mutex;
#[derive(Debug, Default)]
pub(crate) struct RecordingClipboard {
copies: Mutex<Vec<String>>,
}
impl RecordingClipboard {
pub(crate) fn copies(&self) -> Vec<String> {
self.copies.lock().expect("clipboard mutex").clone()
}
}
impl Clipboard for RecordingClipboard {
fn set_text(&self, text: &str) -> Result<(), String> {
self.copies
.lock()
.expect("clipboard mutex")
.push(text.to_string());
Ok(())
}
}
#[derive(Debug)]
pub(crate) struct FailingClipboard {
pub(crate) reason: String,
}
impl FailingClipboard {
pub(crate) fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
impl Clipboard for FailingClipboard {
fn set_text(&self, _text: &str) -> Result<(), String> {
Err(self.reason.clone())
}
}
}
#[cfg(test)]
mod tests {
use super::test_doubles::{FailingClipboard, RecordingClipboard};
use super::Clipboard;
#[test]
fn test_doubles_record_or_refuse_without_touching_the_os_clipboard() {
let recording = RecordingClipboard::default();
assert!(recording.set_text("payload").is_ok());
assert!(recording.set_text("second").is_ok());
assert_eq!(
recording.copies(),
vec!["payload".to_string(), "second".to_string()]
);
let failing = FailingClipboard::new("no clipboard provider");
assert_eq!(
failing.set_text("payload"),
Err("no clipboard provider".to_string())
);
}
}