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, mut clipboard: Clipboard, ) -> Result<(), Box> { 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 = None; let mut last_image_hash: Option = 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, last_text: &mut Option, last_image_hash: &mut Option, ) { 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"); } } } }