This commit is contained in:
2026-05-21 09:39:12 +02:00
parent fc085a8a83
commit 041e90a8f2
15 changed files with 269 additions and 207 deletions

View File

@ -20,3 +20,6 @@ x11rb = "0.13.2"
[features]
x11 = []
wayland = []
[profile.release]
debug = true

BIN
rklipd/profile.json.gz Normal file

Binary file not shown.

View File

@ -24,27 +24,31 @@ impl Config {
while i < args.len() {
match args[i].as_str() {
"--max-entries" => {
if let Some(v) = args.get(i + 1).and_then(|s| s.parse().ok()) {
cfg.max_entries = v;
i += 1;
} else {
eprintln!("--max-entries requiert une valeur entière positive");
i += 1;
match args.get(i).and_then(|s| s.parse::<usize>().ok()) {
Some(0) => eprintln!("--max-entries doit être > 0"),
Some(v) => cfg.max_entries = v,
None => eprintln!("--max-entries requiert une valeur entière positive"),
}
}
"--max-entry-size-kb" => {
if let Some(v) = args.get(i + 1).and_then(|s| s.parse().ok()) {
cfg.max_entry_size_kb = v;
i += 1;
} else {
eprintln!("--max-entry-size-kb requiert une valeur entière positive");
i += 1;
match args.get(i).and_then(|s| s.parse::<usize>().ok()) {
Some(0) => eprintln!("--max-entry-size-kb doit être > 0"),
Some(v) => cfg.max_entry_size_kb = v,
None => {
eprintln!("--max-entry-size-kb requiert une valeur entière positive")
}
}
}
"--expiry-days" => {
if let Some(v) = args.get(i + 1).and_then(|s| s.parse::<u64>().ok()) {
cfg.expiry_days = Some(v);
i += 1;
} else {
eprintln!("--expiry-days requiert une valeur entière positive");
i += 1;
match args.get(i).and_then(|s| s.parse::<u64>().ok()) {
Some(0) => eprintln!(
"--expiry-days doit être > 0 (0 supprimerait tout immédiatement)"
),
Some(v) => cfg.expiry_days = Some(v),
None => eprintln!("--expiry-days requiert une valeur entière positive"),
}
}
"--help" | "-h" => {

View File

@ -101,9 +101,7 @@ impl Database {
ExtendedColorType::Rgb8,
)?;
}
None => {
return Ok(());
}
None => return Ok(()),
}
("image", format!("{}.jpg", img.id))
}
@ -115,7 +113,6 @@ impl Database {
)?;
self.trim_to_max()?;
Ok(())
}
@ -124,8 +121,10 @@ impl Database {
return Ok(());
}
let tx = self.conn.unchecked_transaction()?;
let image_files: Vec<String> = {
let mut stmt = self.conn.prepare(
let mut stmt = tx.prepare(
"SELECT content FROM history
WHERE type = 'image'
AND id NOT IN (
@ -137,13 +136,15 @@ impl Database {
.collect()
};
self.conn.execute(
tx.execute(
"DELETE FROM history WHERE id NOT IN (
SELECT id FROM history ORDER BY timestamp DESC LIMIT ?1
)",
[self.max_entries as i64],
)?;
tx.commit()?;
for filename in image_files {
let path = Path::new(&self.dir_path).join("images").join(&filename);
if path.exists() {
@ -213,18 +214,19 @@ impl Database {
let cutoff_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64
- (days as i64 * 86_400_000);
let tx = self.conn.unchecked_transaction()?;
let image_files: Vec<String> = {
let mut stmt = self
.conn
.prepare("SELECT content FROM history WHERE type = 'image' AND timestamp < ?1")?;
let mut stmt =
tx.prepare("SELECT content FROM history WHERE type = 'image' AND timestamp < ?1")?;
stmt.query_map([cutoff_ms], |row| row.get(0))?
.filter_map(|r| r.ok())
.collect()
};
let count = self
.conn
.execute("DELETE FROM history WHERE timestamp < ?1", [cutoff_ms])?;
let count = tx.execute("DELETE FROM history WHERE timestamp < ?1", [cutoff_ms])?;
tx.commit()?;
for filename in image_files {
let path = Path::new(&self.dir_path).join("images").join(&filename);

View File

@ -48,13 +48,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_for_expiry = Arc::clone(&db);
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_secs(3600));
let lock = db_for_expiry.lock().unwrap();
match lock.delete_entries_older_than(days) {
Ok(0) => {}
Ok(n) => println!("Expiration : {n} entrée(s) supprimée(s) (> {days} jours)"),
Err(e) => eprintln!("Erreur expiration : {e}"),
{
let lock = db_for_expiry.lock().unwrap();
match lock.delete_entries_older_than(days) {
Ok(0) => {}
Ok(n) => {
println!("Expiration : {n} entrée(s) supprimée(s) (> {days} jours)")
}
Err(e) => eprintln!("Erreur expiration : {e}"),
}
}
std::thread::sleep(Duration::from_secs(3600));
}
});
}

View File

@ -1,28 +1,42 @@
use crate::database::Database;
use crate::models::ClipboardEntry;
use arboard::Clipboard;
use std::error::Error;
use std::sync::{Arc, Mutex};
use std::sync::{Arc, Mutex, mpsc};
pub fn start(db: Arc<Mutex<Database>>, clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
let (tx, rx) = mpsc::channel::<ClipboardEntry>();
std::thread::spawn(move || {
for entry in rx {
let lock = db.lock().unwrap();
if let Err(e) = lock.append(entry) {
eprintln!("SQLite write error: {e}");
} else {
println!("SQLite updated!");
}
}
});
#[cfg(all(feature = "wayland", not(feature = "x11")))]
{
crate::ws::wayland::start(db, clipboard)
crate::ws::wayland::start(tx, clipboard)
}
#[cfg(all(feature = "x11", not(feature = "wayland")))]
{
crate::ws::x11::start(db, clipboard)
crate::ws::x11::start(tx, clipboard)
}
#[cfg(all(feature = "x11", feature = "wayland"))]
{
let _ = (db, clipboard);
let _ = (tx, clipboard);
Err("Les features 'x11' et 'wayland' sont mutuellement exclusives".into())
}
#[cfg(not(any(feature = "x11", feature = "wayland")))]
{
let _ = (db, clipboard);
let _ = (tx, clipboard);
Err("Aucune feature de système de fenêtrage activée (--features x11 ou wayland)".into())
}
}

View File

@ -1,15 +1,22 @@
use crate::database::Database;
use crate::models::{ClipboardData, ClipboardEntry, Image};
use std::collections::hash_map::DefaultHasher;
use std::error::Error;
use std::sync::{Arc, Mutex};
use std::hash::{Hash, Hasher};
use std::sync::mpsc;
use std::time::SystemTime;
use uuid::Uuid;
use wayland_clipboard_listener::{WlClipboardPasteStream, WlListenType};
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(
db: Arc<Mutex<Database>>,
tx: mpsc::Sender<ClipboardEntry>,
_clipboard: arboard::Clipboard,
) -> Result<(), Box<dyn Error>> {
let mut stream = WlClipboardPasteStream::init(WlListenType::ListenOnCopy)
@ -17,9 +24,11 @@ pub fn start(
println!("Écoute du presse-papier Wayland...");
let mut last_text: Option<String> = None;
let mut last_image_hash: Option<u64> = None;
for msg in stream.paste_stream().flatten() {
let context = &msg.context;
let data: &[u8] = context.context.as_slice();
let data: &[u8] = msg.context.context.as_slice();
if data.is_empty() {
continue;
@ -27,14 +36,22 @@ pub fn start(
let entry = if let Ok(text) = String::from_utf8(data.to_vec()) {
let text = text.trim_end_matches('\n').to_string();
if text.is_empty() {
if text.is_empty() || Some(&text) == last_text.as_ref() {
continue;
}
last_text = Some(text.clone());
last_image_hash = None;
println!("Clipboard update (texte)");
ClipboardEntry {
content: ClipboardData::Text(text),
timestamp: SystemTime::now(),
}
} else {
let hash = hash_bytes(data);
if Some(hash) == last_image_hash {
continue;
}
match image::load_from_memory(data) {
Ok(img) => {
let (width, height) = (img.width(), img.height());
@ -48,9 +65,15 @@ pub fn start(
3840,
2160
);
last_image_hash = Some(hash);
last_text = None;
continue;
}
last_image_hash = Some(hash);
last_text = None;
println!("Clipboard update (image)");
let rgba = img.into_rgba8();
ClipboardEntry {
content: ClipboardData::Image(Image {
@ -69,15 +92,10 @@ pub fn start(
}
};
println!("Clipboard update détecté");
let db_clone = Arc::clone(&db);
std::thread::spawn(move || {
let db_lock = db_clone.lock().unwrap();
if let Err(e) = db_lock.append(entry) {
eprintln!("SQLite error : {e}");
}
});
if tx.send(entry).is_err() {
eprintln!("Wayland : writer thread disparu, arrêt");
break;
}
}
Ok(())

View File

@ -1,10 +1,9 @@
use crate::database::Database;
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::{Arc, Mutex};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, SystemTime};
use uuid::Uuid;
@ -22,7 +21,10 @@ fn hash_bytes(data: &[u8]) -> u64 {
hasher.finish()
}
pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
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}"))?;
@ -49,15 +51,14 @@ pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), B
.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;
@ -68,14 +69,14 @@ pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), B
if let Event::XfixesSelectionNotify(_) = event {
thread::sleep(Duration::from_millis(50));
handle_clipboard_event(&mut clipboard, &db, &mut last_text, &mut last_image_hash);
handle_clipboard_event(&mut clipboard, &tx, &mut last_text, &mut last_image_hash);
}
}
}
fn handle_clipboard_event(
clipboard: &mut Clipboard,
db: &Arc<Mutex<Database>>,
tx: &mpsc::Sender<ClipboardEntry>,
last_text: &mut Option<String>,
last_image_hash: &mut Option<u64>,
) {
@ -89,13 +90,15 @@ fn handle_clipboard_event(
*last_image_hash = None;
println!("Clipboard update (texte)");
spawn_db_write(
Arc::clone(db),
ClipboardEntry {
if tx
.send(ClipboardEntry {
content: ClipboardData::Text(text),
timestamp: SystemTime::now(),
},
);
})
.is_err()
{
eprintln!("X11 : writer thread disparu");
}
}
Err(_) => {
@ -124,29 +127,20 @@ fn handle_clipboard_event(
*last_text = None;
println!("Clipboard update (image)");
spawn_db_write(
Arc::clone(db),
ClipboardEntry {
content: ClipboardData::Image(crate::models::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");
}
}
}
}
fn spawn_db_write(db: Arc<Mutex<Database>>, entry: ClipboardEntry) {
thread::spawn(move || {
let lock = db.lock().unwrap();
if let Err(e) = lock.append(entry) {
eprintln!("SQLite write error: {e}");
} else {
println!("SQLite updated!");
}
});
}