//! 剪贴板监听线程:基于 GetClipboardSequenceNumber 轮询 //! //! 选用轮询而非 AddClipboardFormatListener 消息窗口:实现更简单、无需消息循环, //! 250ms 间隔兼顾响应速度与开销,且 GetClipboardSequenceNumber 不需要 OpenClipboard,开销极小。 use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use tauri::{AppHandle, Emitter}; use super::reader::{read_clipboard, ClipData}; use super::storage::{NewItem, Storage}; use windows_sys::Win32::System::DataExchange::GetClipboardSequenceNumber; /// 启动监听线程,返回 JoinHandle。 /// `suppress` 记录本应用 copy_back 写入后的剪贴板序列号,用于跳过自身写入产生的记录。 pub fn start_monitor( storage: Arc, app: AppHandle, settings: Arc>, suppress: Arc>>, stop: Arc, ) -> thread::JoinHandle<()> { thread::spawn(move || loop { if stop.load(Ordering::SeqCst) { break; } thread::sleep(Duration::from_millis(250)); if stop.load(Ordering::SeqCst) { break; } let seq = unsafe { GetClipboardSequenceNumber() }; // 首次记录基准;后续对比变化 let last = LAST_SEQ.with(|c| { let prev = c.get().unwrap_or(seq); c.set(Some(seq)); prev }); if seq == last { continue; } // 序列号变化:仅当变化来自本应用 copy_back(序列号精确匹配)时跳过, // 避免旧布尔标志在用户后续复制时被误吞。 if let Ok(mut s) = suppress.lock() { if *s == Some(seq) { *s = None; continue; } } let (rec_text, rec_image, rec_files, max_items, max_image_kb, dedup) = { let s = settings.lock().unwrap_or_else(|e| e.into_inner()); ( s.record_text, s.record_image, s.record_files, s.max_items, s.max_image_kb, s.dedup, ) }; let data = match read_clipboard() { Some(d) => d, None => continue, }; let item = match &data { ClipData::Text(t) => { if !rec_text { continue; } build_text_item(t) } ClipData::Image { dib, width, height } => { if !rec_image { continue; } if max_image_kb > 0 && (dib.len() as u64 / 1024) > max_image_kb { continue; } build_image_item(dib, *width, *height) } ClipData::Files(fs) => { if !rec_files { continue; } build_files_item(fs) } }; if let Some(_id) = storage.insert_or_touch(item, dedup) { storage.prune_to_max(max_items); let _ = app.emit(crate::constants::events::CLIPBOARD_CHANGED, ()); } }) } thread_local! { static LAST_SEQ: std::cell::Cell> = std::cell::Cell::new(None); } fn build_text_item(t: &str) -> NewItem { NewItem { kind: "text".into(), content: Some(t.to_string()), blob: None, thumb: None, preview: make_preview(t, 200), size: t.len() as i64, hash: hash_str(t), } } fn build_image_item(dib: &[u8], w: u32, h: u32) -> NewItem { NewItem { kind: "image".into(), content: None, blob: Some(dib.to_vec()), thumb: super::reader::dib_to_thumbnail(dib, 256), preview: format!("图片 {}×{}", w, h), size: dib.len() as i64, hash: hash_bytes(dib), } } fn build_files_item(files: &[String]) -> NewItem { let content = serde_json::to_string(files).unwrap_or_default(); let preview = if files.len() == 1 { make_preview(&files[0], 200) } else { format!( "{} 个文件 · {}", files.len(), make_preview(files.first().map(|s| s.as_str()).unwrap_or(""), 60) ) }; let hash = hash_str(&content); NewItem { kind: "files".into(), content: Some(content), blob: None, thumb: None, preview, size: files.iter().map(|f| f.len()).sum::() as i64, hash, } } fn make_preview(s: &str, max_chars: usize) -> String { let single: String = s .chars() .map(|c| if c.is_control() { ' ' } else { c }) .collect(); let trimmed = single.trim().to_string(); if trimmed.chars().count() <= max_chars { trimmed } else { let truncated: String = trimmed.chars().take(max_chars).collect(); format!("{}…", truncated) } } fn hash_str(s: &str) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; let mut h = DefaultHasher::new(); h.write(s.as_bytes()); format!("{:016x}", h.finish()) } fn hash_bytes(b: &[u8]) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; let mut h = DefaultHasher::new(); h.write(b); format!("{:016x}", h.finish()) }