148 lines
4.4 KiB
Rust
148 lines
4.4 KiB
Rust
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||
use arboard::Clipboard;
|
||
use std::collections::hash_map::DefaultHasher;
|
||
use std::error::Error;
|
||
use std::hash::{Hash, Hasher};
|
||
use std::sync::mpsc;
|
||
use std::thread;
|
||
use std::time::{Duration, SystemTime};
|
||
use uuid::Uuid;
|
||
use x11rb::connection::Connection;
|
||
use x11rb::protocol::Event;
|
||
use x11rb::protocol::xfixes::{ConnectionExt as XfixesExt, SelectionEventMask};
|
||
use x11rb::protocol::xproto::{ConnectionExt as XprotoExt, CreateWindowAux, WindowClass};
|
||
use x11rb::rust_connection::RustConnection;
|
||
|
||
const MAX_IMAGE_PIXELS: usize = 3840 * 2160;
|
||
|
||
fn hash_bytes(data: &[u8]) -> u64 {
|
||
let mut hasher = DefaultHasher::new();
|
||
data.hash(&mut hasher);
|
||
hasher.finish()
|
||
}
|
||
|
||
pub fn start(
|
||
tx: mpsc::Sender<ClipboardEntry>,
|
||
mut clipboard: Clipboard,
|
||
) -> Result<(), Box<dyn Error>> {
|
||
let (conn, screen_num) =
|
||
RustConnection::connect(None).map_err(|e| format!("Connexion X11 impossible : {e}"))?;
|
||
|
||
let root = conn.setup().roots[screen_num].root;
|
||
|
||
let win = conn.generate_id()?;
|
||
conn.create_window(
|
||
0,
|
||
win,
|
||
root,
|
||
0,
|
||
0,
|
||
1,
|
||
1,
|
||
0,
|
||
WindowClass::INPUT_ONLY,
|
||
0,
|
||
&CreateWindowAux::new(),
|
||
)?
|
||
.check()?;
|
||
|
||
conn.xfixes_query_version(5, 0)
|
||
.map_err(|e| format!("Extension XFIXES indisponible : {e}"))?
|
||
.reply()?;
|
||
|
||
let clipboard_atom = conn.intern_atom(false, b"CLIPBOARD")?.reply()?.atom;
|
||
conn.xfixes_select_selection_input(
|
||
win,
|
||
clipboard_atom,
|
||
SelectionEventMask::SET_SELECTION_OWNER,
|
||
)?
|
||
.check()?;
|
||
conn.flush()?;
|
||
|
||
println!("Clipboard monitor démarré (X11 XFIXES — zéro polling)");
|
||
|
||
let mut last_text: Option<String> = None;
|
||
let mut last_image_hash: Option<u64> = None;
|
||
|
||
loop {
|
||
let event = conn.wait_for_event()?;
|
||
|
||
if let Event::XfixesSelectionNotify(_) = event {
|
||
thread::sleep(Duration::from_millis(50));
|
||
handle_clipboard_event(&mut clipboard, &tx, &mut last_text, &mut last_image_hash);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn handle_clipboard_event(
|
||
clipboard: &mut Clipboard,
|
||
tx: &mpsc::Sender<ClipboardEntry>,
|
||
last_text: &mut Option<String>,
|
||
last_image_hash: &mut Option<u64>,
|
||
) {
|
||
match clipboard.get_text() {
|
||
Ok(raw) => {
|
||
let text = raw.trim_end_matches('\n').to_string();
|
||
if text.is_empty() || Some(&text) == last_text.as_ref() {
|
||
return;
|
||
}
|
||
*last_text = Some(text.clone());
|
||
*last_image_hash = None;
|
||
println!("Clipboard update (texte)");
|
||
|
||
if tx
|
||
.send(ClipboardEntry {
|
||
content: ClipboardData::Text(text),
|
||
timestamp: SystemTime::now(),
|
||
})
|
||
.is_err()
|
||
{
|
||
eprintln!("X11 : writer thread disparu");
|
||
}
|
||
}
|
||
|
||
Err(_) => {
|
||
let Ok(img_data) = clipboard.get_image() else {
|
||
return;
|
||
};
|
||
|
||
let pixel_count = img_data.width * img_data.height;
|
||
if pixel_count > MAX_IMAGE_PIXELS {
|
||
eprintln!(
|
||
"Image ignorée : {}×{} ({} Mpx > limite 4K)",
|
||
img_data.width,
|
||
img_data.height,
|
||
pixel_count / 1_000_000
|
||
);
|
||
let sentinel_hash = hash_bytes(&img_data.bytes[..img_data.bytes.len().min(256)]);
|
||
*last_image_hash = Some(sentinel_hash);
|
||
*last_text = None;
|
||
return;
|
||
}
|
||
|
||
let hash = hash_bytes(&img_data.bytes);
|
||
if Some(hash) == *last_image_hash {
|
||
return;
|
||
}
|
||
*last_image_hash = Some(hash);
|
||
*last_text = None;
|
||
println!("Clipboard update (image)");
|
||
|
||
if tx
|
||
.send(ClipboardEntry {
|
||
content: ClipboardData::Image(Image {
|
||
raw_pixels: Some(img_data.bytes.into_owned()),
|
||
width: img_data.width as u32,
|
||
height: img_data.height as u32,
|
||
id: Uuid::new_v4(),
|
||
}),
|
||
timestamp: SystemTime::now(),
|
||
})
|
||
.is_err()
|
||
{
|
||
eprintln!("X11 : writer thread disparu");
|
||
}
|
||
}
|
||
}
|
||
}
|