177 lines
5.3 KiB
Rust
177 lines
5.3 KiB
Rust
//! 剪贴板监听线程:基于 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())
|
||
}
|