This commit is contained in:
zhongluofeng
2026-07-30 09:09:29 +08:00
parent 74902c4cec
commit f452322aad
32 changed files with 4586 additions and 115 deletions
+169
View File
@@ -0,0 +1,169 @@
//! 剪贴板监听线程:基于 GetClipboardSequenceNumber 轮询
//!
//! 选用轮询而非 AddClipboardFormatListener 消息窗口:实现更简单、无需消息循环,
//! 800ms 间隔对剪贴板场景延迟可接受,且 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。
pub fn start_monitor(
storage: Arc<Storage>,
app: AppHandle,
settings: Arc<Mutex<super::manager::ClipboardSettings>>,
suppress: Arc<AtomicBool>,
stop: Arc<AtomicBool>,
) -> thread::JoinHandle<()> {
thread::spawn(move || loop {
if stop.load(Ordering::SeqCst) {
break;
}
thread::sleep(Duration::from_millis(800));
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;
}
// 序列号变化,处理一次
if suppress.swap(false, Ordering::SeqCst) {
// 由本应用 copy_back 触发,跳过记录
continue;
}
let (rec_text, rec_image, rec_files, max_items, max_image_kb, dedup) = {
let s = settings.lock().unwrap();
(
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("clipboard-changed", ());
}
})
}
thread_local! {
static LAST_SEQ: std::cell::Cell<Option<u32>> = std::cell::Cell::new(None);
}
fn build_text_item(t: &str) -> NewItem {
NewItem {
kind: "text".into(),
content: Some(t.to_string()),
blob: 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()),
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,
preview,
size: files.iter().map(|f| f.len()).sum::<usize>() 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())
}