use crossterm::clipboard::CopyToClipboard;
use crossterm::execute;
use std::io::{stdout, Write};
use std::sync::Mutex;
static SYSTEM_CLIPBOARD: Mutex<Option<arboard::Clipboard>> = Mutex::new(None);
#[derive(Debug, Clone, Default)]
pub struct Clipboard {
internal: String,
internal_only: bool,
}
impl Clipboard {
pub fn new() -> Self {
Self {
internal: String::new(),
internal_only: false,
}
}
pub fn set_internal_only(&mut self, enabled: bool) {
self.internal_only = enabled;
}
pub fn copy_html(&mut self, html: &str, plain_text: &str) -> bool {
self.internal = plain_text.to_string();
if let Ok(mut guard) = SYSTEM_CLIPBOARD.lock() {
if guard.is_none() {
match arboard::Clipboard::new() {
Ok(cb) => *guard = Some(cb),
Err(e) => {
tracing::debug!("arboard clipboard init failed for HTML: {}", e);
return false;
}
}
}
if let Some(clipboard) = guard.as_mut() {
match clipboard.set_html(html, Some(plain_text)) {
Ok(()) => {
tracing::debug!("HTML copied to clipboard ({} bytes)", html.len());
return true;
}
Err(e) => {
tracing::debug!("arboard HTML copy failed: {}", e);
}
}
}
}
false
}
pub fn copy(&mut self, text: String) {
self.internal = text.clone();
let osc52_result = execute!(stdout(), CopyToClipboard::to_clipboard_from(&text));
if let Err(e) = &osc52_result {
tracing::debug!("Crossterm OSC 52 clipboard copy failed: {}", e);
}
let _ = stdout().flush();
if let Ok(mut guard) = SYSTEM_CLIPBOARD.lock() {
if guard.is_none() {
match arboard::Clipboard::new() {
Ok(cb) => *guard = Some(cb),
Err(e) => {
tracing::debug!("arboard clipboard init failed: {}", e);
}
}
}
if let Some(clipboard) = guard.as_mut() {
if let Err(e) = clipboard.set_text(&text) {
tracing::debug!("arboard copy failed: {}, recreating clipboard", e);
drop(guard);
if let Ok(mut guard) = SYSTEM_CLIPBOARD.lock() {
if let Ok(new_clipboard) = arboard::Clipboard::new() {
*guard = Some(new_clipboard);
if let Some(cb) = guard.as_mut() {
let _ = cb.set_text(&text);
}
}
}
}
}
}
}
pub fn paste(&mut self) -> Option<String> {
if self.internal_only {
return self.paste_internal();
}
if let Ok(mut guard) = SYSTEM_CLIPBOARD.lock() {
if guard.is_none() {
if let Ok(cb) = arboard::Clipboard::new() {
*guard = Some(cb);
}
}
if let Some(clipboard) = guard.as_mut() {
if let Ok(text) = clipboard.get_text() {
if !text.is_empty() {
self.internal = text.clone();
return Some(text);
}
}
}
}
if self.internal.is_empty() {
None
} else {
Some(self.internal.clone())
}
}
pub fn get_internal(&self) -> &str {
&self.internal
}
pub fn set_internal(&mut self, text: String) {
self.internal = text;
}
pub fn paste_internal(&self) -> Option<String> {
if self.internal.is_empty() {
None
} else {
Some(self.internal.clone())
}
}
pub fn is_empty(&self) -> bool {
if !self.internal.is_empty() {
return false;
}
if let Ok(mut guard) = SYSTEM_CLIPBOARD.lock() {
if guard.is_none() {
if let Ok(cb) = arboard::Clipboard::new() {
*guard = Some(cb);
}
}
if let Some(clipboard) = guard.as_mut() {
if let Ok(text) = clipboard.get_text() {
return text.is_empty();
}
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clipboard_internal() {
let mut clipboard = Clipboard::new();
assert!(clipboard.get_internal().is_empty());
clipboard.set_internal("test".to_string());
assert_eq!(clipboard.get_internal(), "test");
}
#[test]
fn test_clipboard_copy_updates_internal() {
let mut clipboard = Clipboard::new();
clipboard.copy("hello".to_string());
assert_eq!(clipboard.get_internal(), "hello");
}
}