Files
Thing/src-tauri/src/clipboard/monitor.rs
T

177 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 剪贴板监听线程:基于 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<Storage>,
app: AppHandle,
settings: Arc<Mutex<super::manager::ClipboardSettings>>,
suppress: Arc<Mutex<Option<u32>>>,
stop: Arc<AtomicBool>,
) -> 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<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,
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::<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())
}