use std::path::{Path, PathBuf};
use std::rc::Rc;
use super::osc52::write_osc52;
use super::service::{ClipboardConfig, ClipboardService};
#[derive(Clone)]
pub struct ClipboardHandle {
service: Rc<ClipboardService>,
config: ClipboardConfig,
}
impl ClipboardHandle {
pub(crate) fn new(service: Rc<ClipboardService>, config: ClipboardConfig) -> Self {
Self { service, config }
}
pub fn copy(&self, text: &str) -> Result<(), super::error::ClipboardError> {
self.service.write_clipboard_text(text)?;
if self.config.enable_osc52 {
write_osc52(text);
}
if self.config.enable_primary_selection && self.service.supports_primary_selection() {
let _ = self.service.write_primary_selection_text(text);
}
Ok(())
}
pub fn read(&self) -> Result<String, super::error::ClipboardError> {
self.service.read_clipboard_text()
}
pub fn copy_files<P: AsRef<Path>>(
&self,
paths: &[P],
) -> Result<(), super::error::ClipboardError> {
use super::error::{ClipboardError, ClipboardOperation};
if paths.is_empty() {
return Err(ClipboardError::invalid_input(
ClipboardOperation::WriteFileClipboard,
"no paths to copy",
));
}
let resolved = paths
.iter()
.map(|path| {
let path = path.as_ref();
path.canonicalize().map_err(|err| {
ClipboardError::invalid_input(
ClipboardOperation::WriteFileClipboard,
format!("{}: {}", path.display(), err),
)
})
})
.collect::<Result<Vec<_>, _>>()?;
self.service.write_clipboard_files(&resolved)
}
pub fn read_files(&self) -> Result<Vec<PathBuf>, super::error::ClipboardError> {
self.service.read_clipboard_files()
}
pub fn supports_files(&self) -> bool {
self.service.supports_file_clipboard()
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
use crate::clipboard::error::{ClipboardError, ClipboardOperation};
use crate::clipboard::provider::ClipboardProvider;
use crate::clipboard::service::default_clipboard_reporter;
#[derive(Default)]
struct Recorded {
files_written: Vec<Vec<PathBuf>>,
texts_written: Vec<String>,
files_on_clipboard: Vec<PathBuf>,
supports_files: bool,
}
struct RecordingProvider(Rc<RefCell<Recorded>>);
impl ClipboardProvider for RecordingProvider {
fn read_clipboard_text(&mut self) -> Result<String, ClipboardError> {
Ok(String::new())
}
fn write_clipboard_text(&mut self, text: &str) -> Result<(), ClipboardError> {
self.0.borrow_mut().texts_written.push(text.to_string());
Ok(())
}
fn read_clipboard_files(&mut self) -> Result<Vec<PathBuf>, ClipboardError> {
Ok(self.0.borrow().files_on_clipboard.clone())
}
fn write_clipboard_files(&mut self, paths: &[PathBuf]) -> Result<(), ClipboardError> {
self.0.borrow_mut().files_written.push(paths.to_vec());
Ok(())
}
fn supports_file_clipboard(&self) -> bool {
self.0.borrow().supports_files
}
}
fn handle_with(recorded: Rc<RefCell<Recorded>>) -> ClipboardHandle {
let service = ClipboardService::new(
Box::new(RecordingProvider(Rc::clone(&recorded))),
default_clipboard_reporter(),
);
ClipboardHandle::new(Rc::new(service), ClipboardConfig::default())
}
struct MinimalProvider;
impl ClipboardProvider for MinimalProvider {
fn read_clipboard_text(&mut self) -> Result<String, ClipboardError> {
Ok(String::new())
}
fn write_clipboard_text(&mut self, _text: &str) -> Result<(), ClipboardError> {
Ok(())
}
}
#[test]
fn copy_files_rejects_empty_input_without_touching_provider() {
let recorded = Rc::new(RefCell::new(Recorded::default()));
let handle = handle_with(Rc::clone(&recorded));
let err = handle.copy_files::<&str>(&[]).unwrap_err();
assert!(matches!(
err,
ClipboardError::InvalidInput {
operation: ClipboardOperation::WriteFileClipboard,
..
}
));
assert!(recorded.borrow().files_written.is_empty());
}
#[test]
fn copy_files_rejects_missing_path_without_partial_write() {
let recorded = Rc::new(RefCell::new(Recorded::default()));
let handle = handle_with(Rc::clone(&recorded));
let err = handle
.copy_files(&["Cargo.toml", "definitely/not/here.txt"])
.unwrap_err();
match err {
ClipboardError::InvalidInput { message, .. } => {
assert!(
message.contains("definitely/not/here.txt"),
"error should name the offending path, got: {message}"
);
}
other => panic!("expected InvalidInput, got {other:?}"),
}
assert!(
recorded.borrow().files_written.is_empty(),
"nothing should reach the clipboard when one path is bad"
);
}
#[test]
fn copy_files_resolves_relative_paths_to_absolute() {
let recorded = Rc::new(RefCell::new(Recorded::default()));
let handle = handle_with(Rc::clone(&recorded));
handle.copy_files(&["Cargo.toml"]).unwrap();
let written = &recorded.borrow().files_written;
assert_eq!(written.len(), 1);
assert_eq!(written[0].len(), 1);
assert!(written[0][0].is_absolute());
assert!(written[0][0].ends_with("Cargo.toml"));
}
#[test]
fn copy_files_does_not_also_write_text() {
let recorded = Rc::new(RefCell::new(Recorded::default()));
let handle = handle_with(Rc::clone(&recorded));
handle.copy_files(&["Cargo.toml"]).unwrap();
assert!(recorded.borrow().texts_written.is_empty());
}
#[test]
fn read_files_passes_provider_result_through() {
let recorded = Rc::new(RefCell::new(Recorded::default()));
recorded.borrow_mut().files_on_clipboard = vec![PathBuf::from("/tmp/a"), "/tmp/b".into()];
let handle = handle_with(Rc::clone(&recorded));
assert_eq!(
handle.read_files().unwrap(),
vec![PathBuf::from("/tmp/a"), PathBuf::from("/tmp/b")]
);
}
#[test]
fn supports_files_reflects_provider_capability() {
let recorded = Rc::new(RefCell::new(Recorded::default()));
let handle = handle_with(Rc::clone(&recorded));
assert!(!handle.supports_files());
recorded.borrow_mut().supports_files = true;
assert!(handle.supports_files());
}
#[test]
fn provider_defaults_report_file_clipboard_unsupported() {
let mut provider = MinimalProvider;
assert!(!provider.supports_file_clipboard());
assert!(matches!(
provider.read_clipboard_files(),
Err(ClipboardError::Unsupported {
operation: ClipboardOperation::ReadFileClipboard
})
));
assert!(matches!(
provider.write_clipboard_files(&[]),
Err(ClipboardError::Unsupported {
operation: ClipboardOperation::WriteFileClipboard
})
));
}
}