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);
pub fn copy_to_system_clipboard(text: &str, use_osc52: bool, use_system_clipboard: bool) {
if use_osc52 {
if let Err(e) = execute!(stdout(), CopyToClipboard::to_clipboard_from(text)) {
tracing::debug!("OSC 52 clipboard copy failed: {}", e);
}
#[allow(clippy::let_underscore_must_use)]
let _ = stdout().flush();
}
if use_system_clipboard {
set_system_clipboard_text(text);
}
}
fn set_system_clipboard_text(text: &str) {
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);
return;
}
}
}
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() {
#[allow(clippy::let_underscore_must_use)]
let _ = cb.set_text(text);
}
}
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct PendingClipboard {
pub text: String,
pub use_osc52: bool,
pub use_system_clipboard: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Clipboard {
internal: String,
internal_only: bool,
use_osc52: bool,
use_system_clipboard: bool,
session_mode: bool,
pending_clipboard: Option<PendingClipboard>,
}
impl Clipboard {
pub fn new() -> Self {
Self {
internal: String::new(),
internal_only: false,
use_osc52: true,
use_system_clipboard: true,
session_mode: false,
pending_clipboard: None,
}
}
pub fn apply_config(&mut self, config: &crate::config::ClipboardConfig) {
self.use_osc52 = config.use_osc52;
self.use_system_clipboard = config.use_system_clipboard;
}
pub fn set_internal_only(&mut self, enabled: bool) {
self.internal_only = enabled;
}
pub fn set_session_mode(&mut self, enabled: bool) {
self.session_mode = enabled;
}
pub fn take_pending_clipboard(&mut self) -> Option<PendingClipboard> {
self.pending_clipboard.take()
}
pub fn copy_html(&mut self, html: &str, plain_text: &str) -> bool {
self.internal = plain_text.to_string();
if !self.use_system_clipboard {
return false;
}
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();
if self.session_mode {
self.pending_clipboard = Some(PendingClipboard {
text,
use_osc52: self.use_osc52,
use_system_clipboard: self.use_system_clipboard,
});
return;
}
copy_to_system_clipboard(&text, self.use_osc52, self.use_system_clipboard);
}
pub fn paste(&mut self) -> Option<String> {
if self.internal_only {
return self.paste_internal();
}
if self.use_system_clipboard {
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 self.use_system_clipboard {
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");
}
#[test]
fn test_clipboard_config_disables_osc52() {
let mut clipboard = Clipboard::new();
let config = crate::config::ClipboardConfig {
use_osc52: false,
use_system_clipboard: true,
};
clipboard.apply_config(&config);
assert!(!clipboard.use_osc52);
assert!(clipboard.use_system_clipboard);
}
#[test]
fn test_clipboard_config_disables_system() {
let mut clipboard = Clipboard::new();
let config = crate::config::ClipboardConfig {
use_osc52: true,
use_system_clipboard: false,
};
clipboard.apply_config(&config);
assert!(clipboard.use_osc52);
assert!(!clipboard.use_system_clipboard);
}
#[test]
fn test_clipboard_internal_only_mode() {
let mut clipboard = Clipboard::new();
let config = crate::config::ClipboardConfig {
use_osc52: false,
use_system_clipboard: false,
};
clipboard.apply_config(&config);
clipboard.copy("internal only".to_string());
assert_eq!(clipboard.get_internal(), "internal only");
}
}