use crate::error::TalkError;
#[cfg(feature = "ui")]
use crate::x11::clipboard::{x11_clipboard_get, x11_clipboard_set, ClipboardServeHandle};
use async_trait::async_trait;
#[async_trait]
pub trait Clipboard: Send + Sync {
async fn get_text(&self) -> Result<String, TalkError>;
async fn set_text(&self, text: &str) -> Result<(), TalkError>;
}
#[cfg(feature = "ui")]
pub struct X11Clipboard {
serve_handle: std::sync::Mutex<Option<ClipboardServeHandle>>,
}
#[cfg(feature = "ui")]
impl Default for X11Clipboard {
fn default() -> Self {
Self {
serve_handle: std::sync::Mutex::new(None),
}
}
}
#[cfg(feature = "ui")]
impl X11Clipboard {
pub fn new() -> Self {
Self::default()
}
pub fn last_served_count(&self) -> u32 {
self.serve_handle
.lock()
.ok()
.and_then(|guard| guard.as_ref().map(|h| h.served_count()))
.unwrap_or(0)
}
pub async fn wait_until_served(&self, baseline: u32, timeout: std::time::Duration) -> u32 {
let deadline = std::time::Instant::now() + timeout;
let poll = std::time::Duration::from_millis(SERVED_POLL_INTERVAL_MS);
loop {
let count = self.last_served_count();
if count > baseline {
return count;
}
if std::time::Instant::now() >= deadline {
return count;
}
tokio::time::sleep(poll).await;
}
}
pub fn target_fetch_count(&self, target_client_base: u32) -> u32 {
self.serve_handle
.lock()
.ok()
.and_then(|guard| {
guard
.as_ref()
.map(|h| h.fetches_by_client(target_client_base))
})
.unwrap_or(0)
}
pub fn resource_id_mask(&self) -> Option<u32> {
self.serve_handle
.lock()
.ok()
.and_then(|guard| guard.as_ref().map(|h| h.resource_id_mask()))
}
pub async fn wait_until_target_fetched(
&self,
target_client_base: u32,
expected: u32,
timeout: std::time::Duration,
) -> u32 {
let deadline = std::time::Instant::now() + timeout;
let poll = std::time::Duration::from_millis(SERVED_POLL_INTERVAL_MS);
loop {
let count = self.target_fetch_count(target_client_base);
if count >= expected {
return count;
}
if std::time::Instant::now() >= deadline {
return count;
}
tokio::time::sleep(poll).await;
}
}
}
#[cfg(feature = "ui")]
const SERVED_POLL_INTERVAL_MS: u64 = 5;
#[cfg(feature = "ui")]
#[async_trait]
impl Clipboard for X11Clipboard {
async fn get_text(&self) -> Result<String, TalkError> {
let result = tokio::task::spawn_blocking(x11_clipboard_get)
.await
.map_err(|e| TalkError::Clipboard(format!("clipboard task panicked: {e}")))?;
let text = result.unwrap_or_default();
log::trace!("clipboard get_text -> {}", crate::paste::log_preview(&text),);
Ok(text)
}
async fn set_text(&self, text: &str) -> Result<(), TalkError> {
log::trace!("clipboard set_text <- {}", crate::paste::log_preview(text),);
let owned = text.to_string();
let handle = tokio::task::spawn_blocking(move || x11_clipboard_set(&owned))
.await
.map_err(|e| TalkError::Clipboard(format!("clipboard task panicked: {e}")))?
.ok_or_else(|| {
TalkError::Clipboard("failed to claim clipboard ownership".to_string())
})?;
let mut guard = self
.serve_handle
.lock()
.map_err(|e| TalkError::Clipboard(format!("clipboard lock poisoned: {e}")))?;
*guard = Some(handle);
Ok(())
}
}
pub struct MockClipboard {
content: std::sync::Arc<tokio::sync::Mutex<String>>,
}
impl Default for MockClipboard {
fn default() -> Self {
Self {
content: std::sync::Arc::new(tokio::sync::Mutex::new(String::new())),
}
}
}
impl MockClipboard {
pub fn new() -> Self {
Self::default()
}
pub fn with_content(text: impl Into<String>) -> Self {
Self {
content: std::sync::Arc::new(tokio::sync::Mutex::new(text.into())),
}
}
}
#[async_trait]
impl Clipboard for MockClipboard {
async fn get_text(&self) -> Result<String, TalkError> {
Ok(self.content.lock().await.clone())
}
async fn set_text(&self, text: &str) -> Result<(), TalkError> {
*self.content.lock().await = text.to_string();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mock_clipboard_starts_empty() {
let clipboard = MockClipboard::new();
let text = clipboard.get_text().await.unwrap();
assert_eq!(text, "");
}
#[tokio::test]
async fn test_mock_clipboard_set_and_get() {
let clipboard = MockClipboard::new();
clipboard.set_text("hello world").await.unwrap();
let text = clipboard.get_text().await.unwrap();
assert_eq!(text, "hello world");
}
#[tokio::test]
async fn test_mock_clipboard_overwrites_previous() {
let clipboard = MockClipboard::new();
clipboard.set_text("first").await.unwrap();
clipboard.set_text("second").await.unwrap();
let text = clipboard.get_text().await.unwrap();
assert_eq!(text, "second");
}
#[tokio::test]
async fn test_mock_clipboard_with_initial_content() {
let clipboard = MockClipboard::with_content("initial");
let text = clipboard.get_text().await.unwrap();
assert_eq!(text, "initial");
}
}