use crate::config::{Config, OutputDestination};
use anyhow::{anyhow, Result};
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::sync::{Arc, Mutex};
pub struct OutputWriterSetup {
pub writer: Box<dyn Write + Send>,
pub clipboard_buffer: Option<Arc<Mutex<Vec<u8>>>>,
}
pub fn setup_output_writer(config: &Config) -> Result<OutputWriterSetup> {
let mut clipboard_buffer = None;
let writer: Box<dyn Write + Send> = match &config.output_destination {
OutputDestination::Stdout => Box::new(io::stdout()),
OutputDestination::File(path) => {
let file =
File::create(path).map_err(|e| crate::errors::io_error_with_path(e, path))?;
Box::new(BufWriter::new(file)) }
OutputDestination::Clipboard => {
let buffer = Arc::new(Mutex::new(Vec::<u8>::new()));
clipboard_buffer = Some(buffer.clone()); Box::new(ArcMutexVecWriter(buffer)) }
};
Ok(OutputWriterSetup {
writer,
clipboard_buffer,
})
}
pub fn finalize_output(
mut writer: Box<dyn Write + Send>, clipboard_buffer: Option<Arc<Mutex<Vec<u8>>>>,
config: &Config,
) -> Result<()> {
writer.flush()?;
if config.output_destination == OutputDestination::Clipboard {
if let Some(buffer_arc) = clipboard_buffer {
let buffer = buffer_arc
.lock()
.map_err(|e| anyhow!("Failed to lock clipboard buffer mutex: {}", e))?;
let content = String::from_utf8(buffer.clone())?;
copy_to_clipboard(&content)?;
} else {
return Err(anyhow!(
"Clipboard destination specified, but no buffer found during finalization."
));
}
}
Ok(())
}
#[cfg(feature = "clipboard")]
fn copy_to_clipboard(content: &str) -> Result<()> {
use crate::errors::AppError; use arboard::Clipboard;
let mut clipboard = Clipboard::new().map_err(|e| AppError::ClipboardError(e.to_string()))?;
clipboard
.set_text(content)
.map_err(|e| AppError::ClipboardError(e.to_string()))?;
Ok(())
}
#[cfg(not(feature = "clipboard"))]
fn copy_to_clipboard(_content: &str) -> Result<()> {
Err(anyhow!(
"Clipboard feature is not enabled, cannot use --paste."
))
}
#[derive(Debug, Clone)]
struct ArcMutexVecWriter(Arc<Mutex<Vec<u8>>>);
impl Write for ArcMutexVecWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut buffer = self
.0 .lock()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Mutex poisoned: {}", e)))?;
buffer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
let mut buffer = self
.0 .lock()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Mutex poisoned: {}", e)))?;
buffer.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::tests::create_mock_config; use tempfile::NamedTempFile;
#[test]
fn test_write_impl_for_arc_mutex_vec() -> io::Result<()> {
let buffer_arc: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
let mut writer = ArcMutexVecWriter(buffer_arc.clone());
write!(writer, "Hello")?;
writer.write_all(b", ")?;
write!(writer, "World!")?;
writer.flush()?;
let buffer = buffer_arc.lock().unwrap();
assert_eq!(*buffer, b"Hello, World!");
Ok(())
}
#[test]
fn test_setup_output_writer_stdout() {
let config = create_mock_config(false, false, false, false); let setup_result = setup_output_writer(&config);
assert!(setup_result.is_ok());
let setup = setup_result.unwrap();
assert!(setup.clipboard_buffer.is_none());
}
#[test]
fn test_setup_output_writer_file() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let path = temp_file.path().to_path_buf();
let mut config = create_mock_config(false, false, false, false);
config.output_destination = OutputDestination::File(path.clone());
let setup_result = setup_output_writer(&config);
assert!(setup_result.is_ok());
let mut setup = setup_result.unwrap();
assert!(setup.clipboard_buffer.is_none());
write!(setup.writer, "Test content")?;
setup.writer.flush()?;
drop(setup.writer);
let content = std::fs::read_to_string(&path)?;
assert_eq!(content, "Test content");
Ok(())
}
#[test]
fn test_setup_output_writer_clipboard() {
let mut config = create_mock_config(false, false, false, false);
config.output_destination = OutputDestination::Clipboard;
let setup_result = setup_output_writer(&config);
assert!(setup_result.is_ok());
let setup = setup_result.unwrap();
assert!(setup.clipboard_buffer.is_some());
let mut writer = setup.writer;
write!(writer, "Clipboard test").unwrap();
writer.flush().unwrap();
let buffer_arc = setup.clipboard_buffer.unwrap();
let buffer = buffer_arc.lock().unwrap();
assert_eq!(*buffer, b"Clipboard test");
}
#[test]
fn test_finalize_output_stdout() -> Result<()> {
let config = create_mock_config(false, false, false, false); let writer = Box::new(io::sink()); let buffer = None;
finalize_output(writer, buffer, &config)?; Ok(())
}
#[test]
fn test_finalize_output_file() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let path = temp_file.path().to_path_buf();
let file = File::create(&path)?;
let writer = Box::new(BufWriter::new(file));
let mut config = create_mock_config(false, false, false, false);
config.output_destination = OutputDestination::File(path);
let buffer = None;
finalize_output(writer, buffer, &config)?; Ok(())
}
#[test]
fn test_finalize_output_clipboard_buffer_access() {
let mut config = create_mock_config(false, false, false, false);
config.output_destination = OutputDestination::Clipboard;
let buffer_arc: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(b"clipboard data".to_vec()));
let writer = Box::new(ArcMutexVecWriter(buffer_arc.clone())); let clipboard_buffer = Some(buffer_arc.clone());
let result = finalize_output(writer, clipboard_buffer, &config);
#[cfg(not(feature = "clipboard"))]
{
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Clipboard feature is not enabled"));
}
#[cfg(feature = "clipboard")]
{
use crate::errors::AppError; if let Err(e) = result {
assert!(e
.downcast_ref::<AppError>()
.map_or(false, |ae| matches!(ae, AppError::ClipboardError(_))));
}
}
}
#[test]
fn test_finalize_output_clipboard_missing_buffer() {
let mut config = create_mock_config(false, false, false, false);
config.output_destination = OutputDestination::Clipboard;
let writer = Box::new(io::sink()); let clipboard_buffer = None;
let result = finalize_output(writer, clipboard_buffer, &config);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("no buffer found during finalization"));
}
}