use super::lock::lock_or_recover;
use crate::constants::MAX_CLIPBOARD_SIZE;
use std::io::{self, Write};
use std::process::{Command, Stdio};
use std::sync::OnceLock;
fn sanitize_clipboard_content(content: &str) -> String {
let stripped = crate::utils::ansi::strip_ansi(content);
stripped
.chars()
.filter(|&c| {
(c >= ' ' && c != '\x1b') || c == '\t' || c == '\n' || c == '\r'
})
.collect()
}
#[derive(Debug)]
pub enum ClipboardError {
NoClipboardTool,
Io(io::Error),
CommandFailed(String),
InvalidUtf8,
InvalidInput(String),
}
impl std::fmt::Display for ClipboardError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClipboardError::NoClipboardTool => write!(f, "No clipboard tool available"),
ClipboardError::Io(e) => write!(f, "I/O error: {}", e),
ClipboardError::CommandFailed(msg) => write!(f, "Command failed: {}", msg),
ClipboardError::InvalidUtf8 => write!(f, "Invalid UTF-8 in clipboard content"),
ClipboardError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
}
}
}
impl std::error::Error for ClipboardError {}
impl From<io::Error> for ClipboardError {
fn from(err: io::Error) -> Self {
ClipboardError::Io(err)
}
}
pub type ClipboardResult<T> = Result<T, ClipboardError>;
pub trait ClipboardBackend {
fn set(&self, content: &str) -> ClipboardResult<()>;
fn get(&self) -> ClipboardResult<String>;
fn has_text(&self) -> ClipboardResult<bool>;
fn clear(&self) -> ClipboardResult<()>;
}
static COPY_COMMAND_CACHE: OnceLock<Option<(&'static str, &'static [&'static str])>> =
OnceLock::new();
static PASTE_COMMAND_CACHE: OnceLock<Option<(&'static str, &'static [&'static str])>> =
OnceLock::new();
#[derive(Clone, Debug, Default)]
pub struct SystemClipboard;
impl SystemClipboard {
pub fn new() -> Self {
Self
}
fn copy_command() -> Option<(&'static str, &'static [&'static str])> {
*COPY_COMMAND_CACHE.get_or_init(|| {
#[cfg(target_os = "macos")]
{
Some(("pbcopy", &[]))
}
#[cfg(target_os = "linux")]
{
let check_cmd = |cmd: &str| -> bool {
Command::new(cmd)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
};
if check_cmd("xclip") {
Some(("xclip", &["-selection", "clipboard"]))
} else if check_cmd("xsel") {
Some(("xsel", &["--clipboard", "--input"]))
} else if check_cmd("wl-copy") {
Some(("wl-copy", &[]))
} else {
None
}
}
#[cfg(target_os = "windows")]
{
Some(("clip", &[]))
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
None
}
})
}
fn paste_command() -> Option<(&'static str, &'static [&'static str])> {
*PASTE_COMMAND_CACHE.get_or_init(|| {
#[cfg(target_os = "macos")]
{
Some(("pbpaste", &[]))
}
#[cfg(target_os = "linux")]
{
let check_cmd = |cmd: &str| -> bool {
Command::new(cmd)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
};
if check_cmd("xclip") {
Some(("xclip", &["-selection", "clipboard", "-o"]))
} else if check_cmd("xsel") {
Some(("xsel", &["--clipboard", "--output"]))
} else if check_cmd("wl-paste") {
Some(("wl-paste", &[]))
} else {
None
}
}
#[cfg(target_os = "windows")]
{
Some(("powershell", &["-Command", "Get-Clipboard"]))
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
None
}
})
}
}
impl ClipboardBackend for SystemClipboard {
fn set(&self, content: &str) -> ClipboardResult<()> {
if content.len() > MAX_CLIPBOARD_SIZE {
return Err(ClipboardError::InvalidInput(format!(
"Clipboard content too large ({} bytes, max {})",
content.len(),
MAX_CLIPBOARD_SIZE
)));
}
let sanitized = sanitize_clipboard_content(content);
let (cmd, args) = Self::copy_command().ok_or(ClipboardError::NoClipboardTool)?;
let mut child = Command::new(cmd)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(sanitized.as_bytes())?;
}
let status = child.wait()?;
if status.success() {
Ok(())
} else {
Err(ClipboardError::CommandFailed(format!(
"{} exited with status: {:?}",
cmd,
status.code()
)))
}
}
fn get(&self) -> ClipboardResult<String> {
let (cmd, args) = Self::paste_command().ok_or(ClipboardError::NoClipboardTool)?;
let output = Command::new(cmd)
.args(args)
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()?;
if output.status.success() {
if output.stdout.len() > MAX_CLIPBOARD_SIZE {
return Err(ClipboardError::InvalidInput(format!(
"Clipboard content too large: {} bytes (max: {})",
output.stdout.len(),
MAX_CLIPBOARD_SIZE
)));
}
String::from_utf8(output.stdout).map_err(|_| ClipboardError::InvalidUtf8)
} else {
Err(ClipboardError::CommandFailed(format!(
"{} exited with status: {:?}",
cmd,
output.status.code()
)))
}
}
fn has_text(&self) -> ClipboardResult<bool> {
match self.get() {
Ok(content) => Ok(!content.is_empty()),
Err(ClipboardError::NoClipboardTool) => Err(ClipboardError::NoClipboardTool),
_ => Ok(false),
}
}
fn clear(&self) -> ClipboardResult<()> {
self.set("")
}
}
#[derive(Clone, Debug, Default)]
pub struct MemoryClipboard {
content: std::sync::Arc<std::sync::Mutex<String>>,
}
impl MemoryClipboard {
pub fn new() -> Self {
Self {
content: std::sync::Arc::new(std::sync::Mutex::new(String::new())),
}
}
}
impl ClipboardBackend for MemoryClipboard {
fn set(&self, content: &str) -> ClipboardResult<()> {
let mut guard = lock_or_recover(&self.content);
*guard = content.to_string();
Ok(())
}
fn get(&self) -> ClipboardResult<String> {
let guard = lock_or_recover(&self.content);
Ok(guard.clone())
}
fn has_text(&self) -> ClipboardResult<bool> {
let guard = lock_or_recover(&self.content);
Ok(!guard.is_empty())
}
fn clear(&self) -> ClipboardResult<()> {
let mut guard = lock_or_recover(&self.content);
guard.clear();
Ok(())
}
}
pub struct Clipboard {
backend: Box<dyn ClipboardBackend + Send + Sync>,
}
impl Clipboard {
pub fn new() -> Self {
Self {
backend: Box::new(SystemClipboard::new()),
}
}
pub fn with_backend<B: ClipboardBackend + Send + Sync + 'static>(backend: B) -> Self {
Self {
backend: Box::new(backend),
}
}
pub fn memory() -> Self {
Self {
backend: Box::new(MemoryClipboard::new()),
}
}
pub fn set(&self, content: &str) -> ClipboardResult<()> {
self.backend.set(content)
}
pub fn get(&self) -> ClipboardResult<String> {
self.backend.get()
}
pub fn has_text(&self) -> ClipboardResult<bool> {
self.backend.has_text()
}
pub fn clear(&self) -> ClipboardResult<()> {
self.backend.clear()
}
}
impl Default for Clipboard {
fn default() -> Self {
Self::new()
}
}
pub fn copy(content: &str) -> ClipboardResult<()> {
SystemClipboard::new().set(content)
}
pub fn paste() -> ClipboardResult<String> {
SystemClipboard::new().get()
}
pub fn has_text() -> ClipboardResult<bool> {
SystemClipboard::new().has_text()
}
pub fn clear() -> ClipboardResult<()> {
SystemClipboard::new().clear()
}
#[derive(Clone, Debug)]
pub struct ClipboardHistory {
entries: Vec<String>,
max_size: usize,
}
impl ClipboardHistory {
pub fn new(max_size: usize) -> Self {
Self {
entries: Vec::with_capacity(max_size),
max_size,
}
}
pub fn push(&mut self, content: String) {
if self.entries.first() == Some(&content) {
return;
}
self.entries.retain(|e| e != &content);
self.entries.insert(0, content);
self.entries.truncate(self.max_size);
}
pub fn get(&self, index: usize) -> Option<&str> {
self.entries.get(index).map(|s| s.as_str())
}
pub fn latest(&self) -> Option<&str> {
self.get(0)
}
pub fn entries(&self) -> &[String] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
impl Default for ClipboardHistory {
fn default() -> Self {
Self::new(100)
}
}