use std::fmt;
#[derive(Debug, Clone)]
pub enum ClipboardError {
Init(String),
Op(String),
}
impl fmt::Display for ClipboardError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Init(m) => write!(f, "Clipboard unavailable: {m}"),
Self::Op(m) => write!(f, "Clipboard error: {m}"),
}
}
}
impl std::error::Error for ClipboardError {}
pub trait ClipboardBackend: Send {
fn set_text(&mut self, text: &str) -> Result<(), ClipboardError>;
fn set_secret_text(&mut self, text: &str) -> Result<ClipboardOutcome, ClipboardError>;
fn get_text(&mut self) -> Result<String, ClipboardError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClipboardOutcome {
pub excluded_from_history: Option<bool>,
}
#[derive(Default)]
pub struct SystemClipboard {
inner: Option<arboard::Clipboard>,
}
impl SystemClipboard {
pub fn new() -> Self {
Self::default()
}
fn ensure(&mut self) -> Result<&mut arboard::Clipboard, ClipboardError> {
match &mut self.inner {
Some(c) => Ok(c),
slot @ None => {
let c =
arboard::Clipboard::new().map_err(|e| ClipboardError::Init(e.to_string()))?;
Ok(slot.insert(c))
}
}
}
}
impl ClipboardBackend for SystemClipboard {
fn set_text(&mut self, text: &str) -> Result<(), ClipboardError> {
let res = self
.ensure()?
.set_text(text)
.map_err(|e| ClipboardError::Op(e.to_string()));
if res.is_err() {
self.inner = None;
}
res
}
#[cfg(target_os = "macos")]
fn set_secret_text(&mut self, text: &str) -> Result<ClipboardOutcome, ClipboardError> {
use arboard::SetExtApple;
let cb = self.ensure()?;
let res = cb
.set()
.exclude_from_history()
.text(text)
.map_err(|e| ClipboardError::Op(e.to_string()));
match res {
Ok(()) => Ok(ClipboardOutcome {
excluded_from_history: Some(true),
}),
Err(e) => {
self.inner = None;
Err(e)
}
}
}
#[cfg(not(target_os = "macos"))]
fn set_secret_text(&mut self, text: &str) -> Result<ClipboardOutcome, ClipboardError> {
self.set_text(text).map(|()| ClipboardOutcome {
excluded_from_history: None,
})
}
fn get_text(&mut self) -> Result<String, ClipboardError> {
let res = self
.ensure()?
.get_text()
.map_err(|e| ClipboardError::Op(e.to_string()));
if res.is_err() {
self.inner = None;
}
res
}
}
#[derive(Default, Clone)]
pub struct FakeClipboard {
pub state: std::sync::Arc<std::sync::Mutex<Option<String>>>,
pub last_was_secret: std::sync::Arc<std::sync::Mutex<bool>>,
}
impl ClipboardBackend for FakeClipboard {
fn set_text(&mut self, text: &str) -> Result<(), ClipboardError> {
*self.state.lock().unwrap() = Some(text.to_string());
*self.last_was_secret.lock().unwrap() = false;
Ok(())
}
fn set_secret_text(&mut self, text: &str) -> Result<ClipboardOutcome, ClipboardError> {
*self.state.lock().unwrap() = Some(text.to_string());
*self.last_was_secret.lock().unwrap() = true;
Ok(ClipboardOutcome {
excluded_from_history: Some(true),
})
}
fn get_text(&mut self) -> Result<String, ClipboardError> {
self.state
.lock()
.unwrap()
.clone()
.ok_or(ClipboardError::Op("empty".into()))
}
}
#[derive(Default, Clone)]
pub struct UnavailableClipboard;
impl ClipboardBackend for UnavailableClipboard {
fn set_text(&mut self, _text: &str) -> Result<(), ClipboardError> {
Err(ClipboardError::Init("no display server (test)".into()))
}
fn set_secret_text(&mut self, _text: &str) -> Result<ClipboardOutcome, ClipboardError> {
Err(ClipboardError::Init("no display server (test)".into()))
}
fn get_text(&mut self) -> Result<String, ClipboardError> {
Err(ClipboardError::Init("no display server (test)".into()))
}
}