refonte
This commit is contained in:
170
rklipd/Cargo.lock
generated
170
rklipd/Cargo.lock
generated
@ -8,6 +8,41 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aligned"
|
||||
version = "0.4.3"
|
||||
@ -133,6 +168,12 @@ dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
@ -205,6 +246,16 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-master"
|
||||
version = "4.0.0"
|
||||
@ -242,6 +293,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@ -282,6 +342,26 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
@ -451,6 +531,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
@ -497,6 +587,16 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.1"
|
||||
@ -609,6 +709,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpolate_name"
|
||||
version = "0.2.4"
|
||||
@ -995,6 +1104,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@ -1071,6 +1186,18 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@ -1176,7 +1303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1186,7 +1313,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1298,7 +1434,9 @@ checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||
name = "rklipd"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"arboard",
|
||||
"base64",
|
||||
"clipboard-master",
|
||||
"directories",
|
||||
"image",
|
||||
@ -1453,6 +1591,12 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@ -1498,6 +1642,12 @@ dependencies = [
|
||||
"zune-jpeg 0.4.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@ -1510,6 +1660,16 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
@ -1539,6 +1699,12 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
|
||||
@ -13,6 +13,8 @@ wayland-clipboard-listener = "0.6.0"
|
||||
directories = "6.0.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
base64 = "0.22.1"
|
||||
aes-gcm = "0.10.3"
|
||||
|
||||
[features]
|
||||
x11 = []
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
use arboard::Clipboard;
|
||||
use std::error::Error;
|
||||
use std::time::SystemTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||||
|
||||
// pub trait ImageDataExt {
|
||||
// fn to_png(&self) -> Result<Vec<u8>, Box<dyn Error>>;
|
||||
// }
|
||||
//
|
||||
// impl ImageDataExt for ImageData<'_> {
|
||||
// fn to_png(&self) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
// let mut buffer = Vec::new();
|
||||
// let encoder = PngEncoder::new(&mut buffer);
|
||||
// encoder.write_image(
|
||||
// &self.bytes,
|
||||
// self.width as u32,
|
||||
// self.height as u32,
|
||||
// ExtendedColorType::Rgba8,
|
||||
// )?;
|
||||
// Ok(buffer)
|
||||
// }
|
||||
// }
|
||||
|
||||
impl ClipboardEntry {
|
||||
pub fn new(clipboard: &mut Clipboard) -> Result<ClipboardEntry, Box<dyn Error>> {
|
||||
let clipboard_data_opt: Option<ClipboardData> = match clipboard.get_text() {
|
||||
Ok(text) => Some(ClipboardData::Text(text)),
|
||||
Err(_) => match clipboard.get_image() {
|
||||
Ok(image) => {
|
||||
let id = Uuid::new_v4();
|
||||
Some(ClipboardData::Image(Image {
|
||||
raw_pixels: Some(image.bytes.into_owned()),
|
||||
width: image.width as u32,
|
||||
height: image.height as u32,
|
||||
id,
|
||||
}))
|
||||
}
|
||||
Err(_) => None,
|
||||
},
|
||||
};
|
||||
|
||||
let Some(clipboard_data) = clipboard_data_opt else {
|
||||
return Err("Clipboard empty".into());
|
||||
};
|
||||
|
||||
Ok(ClipboardEntry {
|
||||
content: clipboard_data,
|
||||
timestamp: SystemTime::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
72
rklipd/src/crypto.rs
Normal file
72
rklipd/src/crypto.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, Key, Nonce,
|
||||
aead::{Aead, AeadCore, KeyInit, OsRng},
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct Crypto {
|
||||
key: [u8; 32],
|
||||
}
|
||||
|
||||
impl Crypto {
|
||||
pub fn load_or_create(data_dir: &Path) -> Result<Self, Box<dyn Error>> {
|
||||
let key_path = data_dir.join("master.key");
|
||||
|
||||
if key_path.exists() {
|
||||
let key_bytes = fs::read(&key_path)?;
|
||||
if key_bytes.len() != 32 {
|
||||
return Err("Fichier de clé invalide (attendu 32 octets)".into());
|
||||
}
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&key_bytes);
|
||||
Ok(Self { key })
|
||||
} else {
|
||||
let key = Aes256Gcm::generate_key(OsRng);
|
||||
let key_bytes: [u8; 32] = key.into();
|
||||
fs::write(&key_path, key_bytes)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
Ok(Self { key: key_bytes })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, plaintext: &str) -> Result<String, Box<dyn Error>> {
|
||||
let key = Key::<Aes256Gcm>::from_slice(&self.key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
let ciphertext = cipher
|
||||
.encrypt(&nonce, plaintext.as_bytes())
|
||||
.map_err(|e| format!("Erreur de chiffrement : {e}"))?;
|
||||
let mut combined = nonce.to_vec();
|
||||
combined.extend_from_slice(&ciphertext);
|
||||
Ok(format!("enc:{}", BASE64.encode(combined)))
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, encrypted: &str) -> Result<String, Box<dyn Error>> {
|
||||
let encoded = encrypted
|
||||
.strip_prefix("enc:")
|
||||
.ok_or("Pas une entrée chiffrée")?;
|
||||
let combined = BASE64.decode(encoded)?;
|
||||
if combined.len() < 12 {
|
||||
return Err("Données chiffrées trop courtes".into());
|
||||
}
|
||||
let (nonce_bytes, ciphertext) = combined.split_at(12);
|
||||
let key = Key::<Aes256Gcm>::from_slice(&self.key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, ciphertext)
|
||||
.map_err(|e| format!("Erreur de déchiffrement : {e}"))?;
|
||||
Ok(String::from_utf8(plaintext)?)
|
||||
}
|
||||
|
||||
pub fn is_encrypted(content: &str) -> bool {
|
||||
content.starts_with("enc:")
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::codecs::png::PngEncoder;
|
||||
use image::{ExtendedColorType, ImageEncoder};
|
||||
use rusqlite::Connection;
|
||||
use std::error::Error;
|
||||
@ -17,23 +16,32 @@ pub struct Database {
|
||||
impl Database {
|
||||
pub fn init(dir_path: &str) -> Result<Self, Box<dyn Error>> {
|
||||
let base_path = Path::new(dir_path);
|
||||
let images_path = base_path.join("images");
|
||||
std::fs::create_dir_all(&images_path)?;
|
||||
fs::create_dir_all(base_path.join("images"))?;
|
||||
|
||||
let db_path = base_path.join("clipboard.db");
|
||||
let conn = Connection::open(base_path.join("clipboard.db"))?;
|
||||
|
||||
let conn = Connection::open(&db_path)?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);
|
||||
INSERT OR IGNORE INTO schema_version (version) SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM schema_version);",
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
)",
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute_batch(
|
||||
"DELETE FROM history WHERE id NOT IN (
|
||||
SELECT MAX(id) FROM history GROUP BY content
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_history_content ON history(content);",
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
conn,
|
||||
dir_path: dir_path.to_string(),
|
||||
@ -41,61 +49,62 @@ impl Database {
|
||||
}
|
||||
|
||||
pub fn append(&self, entry: ClipboardEntry) -> Result<(), Box<dyn Error>> {
|
||||
let timestamp_millis = entry.timestamp.duration_since(UNIX_EPOCH)?.as_millis() as i64;
|
||||
let ts = entry.timestamp.duration_since(UNIX_EPOCH)?.as_millis() as i64;
|
||||
|
||||
let (entry_type, content) = match &entry.content {
|
||||
ClipboardData::Text(text) => ("text", text.clone()),
|
||||
let (kind, content) = match &entry.content {
|
||||
ClipboardData::Text(t) => {
|
||||
if t.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
("text", t.clone())
|
||||
}
|
||||
ClipboardData::Image(img) => {
|
||||
if let Some(raw_pixels) = &img.raw_pixels {
|
||||
let img_path = img.file_path(&self.dir_path);
|
||||
|
||||
let file = fs::File::create(&img_path)?;
|
||||
let rgb_pixels: Vec<u8> = raw_pixels
|
||||
if let Some(px) = &img.raw_pixels {
|
||||
let path = img.file_path(&self.dir_path);
|
||||
let file = fs::File::create(&path)?;
|
||||
let rgb: Vec<u8> = px
|
||||
.chunks_exact(4)
|
||||
.flat_map(|rgba| [rgba[0], rgba[1], rgba[2]])
|
||||
.collect();
|
||||
let encoder = JpegEncoder::new_with_quality(file, 70);
|
||||
encoder.write_image(
|
||||
&rgb_pixels,
|
||||
JpegEncoder::new_with_quality(file, 70).write_image(
|
||||
&rgb,
|
||||
img.width,
|
||||
img.height,
|
||||
ExtendedColorType::Rgb8,
|
||||
)?;
|
||||
}
|
||||
("image", img.id.to_string())
|
||||
("image", format!("{}.jpg", img.id))
|
||||
}
|
||||
};
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT INTO history (type, content, timestamp) VALUES (?1, ?2, ?3)",
|
||||
(entry_type, content, timestamp_millis),
|
||||
"INSERT OR REPLACE INTO history (type, content, timestamp) VALUES (?1, ?2, ?3)",
|
||||
(kind, &content, ts),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_history(&self) -> Result<Vec<ClipboardEntry>, Box<dyn Error>> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT type, content, timestamp FROM history ORDER BY timestamp ASC")?;
|
||||
pub fn read_history(&self, limit: usize) -> Result<Vec<ClipboardEntry>, Box<dyn Error>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT type, content, timestamp FROM history ORDER BY timestamp DESC LIMIT ?1",
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let ty: String = row.get(0)?;
|
||||
let content: String = row.get(1)?;
|
||||
let timestamp: i64 = row.get(2)?;
|
||||
Ok((ty, content, timestamp))
|
||||
let rows = stmt.query_map([limit as i64], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
let (ty, content, timestamp) = row?;
|
||||
|
||||
let timestamp = UNIX_EPOCH + Duration::from_millis(timestamp as u64);
|
||||
let (ty, content, ts_ms) = row?;
|
||||
let timestamp = UNIX_EPOCH + Duration::from_millis(ts_ms as u64);
|
||||
let data = if ty == "text" {
|
||||
ClipboardData::Text(content)
|
||||
} else {
|
||||
let id = Uuid::parse_str(&content)?;
|
||||
let id = Uuid::parse_str(content.trim_end_matches(".jpg"))?;
|
||||
ClipboardData::Image(Image {
|
||||
id,
|
||||
raw_pixels: None,
|
||||
@ -103,13 +112,11 @@ impl Database {
|
||||
height: 0,
|
||||
})
|
||||
};
|
||||
|
||||
entries.push(ClipboardEntry {
|
||||
content: data,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
@ -118,4 +125,27 @@ impl Database {
|
||||
.execute("DELETE FROM history WHERE content = ?1", [content])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_entry_content(&self, old: &str, new: &str) -> Result<(), Box<dyn Error>> {
|
||||
let rows_affected = self.conn.execute(
|
||||
"UPDATE history SET content = ?1 WHERE content = ?2",
|
||||
[new, old],
|
||||
)?;
|
||||
if rows_affected == 0 {
|
||||
return Err(format!("Entrée introuvable : {old}").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_history(&self) -> Result<(), Box<dyn Error>> {
|
||||
let images_dir = Path::new(&self.dir_path).join("images");
|
||||
|
||||
if images_dir.exists() {
|
||||
fs::remove_dir_all(&images_dir)?;
|
||||
}
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
|
||||
self.conn.execute("DELETE FROM history", [])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,55 @@
|
||||
use crate::crypto::Crypto;
|
||||
use crate::database::Database;
|
||||
use crate::models::ClipboardData;
|
||||
use crate::models::{ClipboardData, ClipboardEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct HistoryItem {
|
||||
pub content: String,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum IpcRequest {
|
||||
GetHistory { limit: usize },
|
||||
SetClipboard { content: String },
|
||||
DeleteEntry { content: String },
|
||||
GetHistory {
|
||||
limit: usize,
|
||||
},
|
||||
SetClipboard {
|
||||
content: String,
|
||||
},
|
||||
DeleteEntry {
|
||||
content: String,
|
||||
},
|
||||
UpdateEntry {
|
||||
old_content: String,
|
||||
new_content: String,
|
||||
},
|
||||
AddEntry {
|
||||
content: String,
|
||||
},
|
||||
ClearHistory,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum IpcResponse {
|
||||
History(Vec<String>),
|
||||
History(Vec<HistoryItem>),
|
||||
Ok,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
pub fn start_server(db: Arc<Mutex<Database>>, socket_path: &Path) {
|
||||
fn reply(stream: &mut std::os::unix::net::UnixStream, resp: IpcResponse) {
|
||||
if let Ok(json) = serde_json::to_string(&resp) {
|
||||
let _ = stream.write_all(json.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_server(db: Arc<Mutex<Database>>, crypto: Arc<Crypto>, socket_path: &Path) {
|
||||
if socket_path.exists() {
|
||||
let _ = fs::remove_file(socket_path);
|
||||
}
|
||||
@ -27,99 +57,157 @@ pub fn start_server(db: Arc<Mutex<Database>>, socket_path: &Path) {
|
||||
let listener = match UnixListener::bind(socket_path) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("Error while creating socket {}", e);
|
||||
eprintln!("Erreur socket IPC : {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
println!("ipc server listening {:?}", socket_path);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Err(e) = fs::set_permissions(socket_path, fs::Permissions::from_mode(0o600)) {
|
||||
eprintln!("Impossible de restreindre les permissions du socket : {e}");
|
||||
}
|
||||
}
|
||||
|
||||
println!("IPC server en écoute sur {:?}", socket_path);
|
||||
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(mut stream) => {
|
||||
let db_clone = Arc::clone(&db);
|
||||
let crypto_clone = Arc::clone(&crypto);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut buffer = String::new();
|
||||
let mut buf = String::new();
|
||||
if stream.read_to_string(&mut buf).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
if stream.read_to_string(&mut buffer).is_ok() {
|
||||
if let Ok(request) = serde_json::from_str::<IpcRequest>(&buffer) {
|
||||
match request {
|
||||
IpcRequest::GetHistory { limit } => {
|
||||
let db_lock = db_clone.lock().unwrap();
|
||||
let req = match serde_json::from_str::<IpcRequest>(&buf) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("IPC parse error : {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO Implem read_history(limit)
|
||||
let history = db_lock.read_history().unwrap_or_default();
|
||||
|
||||
let items: Vec<String> = history
|
||||
.into_iter()
|
||||
.rev()
|
||||
.take(limit)
|
||||
.map(|entry| match entry.content {
|
||||
ClipboardData::Text(t) => t,
|
||||
ClipboardData::Image(img) => format!("{}.jpg", img.id),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = IpcResponse::History(items);
|
||||
let response_json = serde_json::to_string(&response).unwrap();
|
||||
let _ = stream.write_all(response_json.as_bytes());
|
||||
}
|
||||
IpcRequest::SetClipboard { content } => {
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
if content.ends_with(".jpg") || content.ends_with(".png") {
|
||||
if let Some(proj_dirs) = directories::ProjectDirs::from(
|
||||
"com", "zefad", "rklipd",
|
||||
) {
|
||||
let img_path = proj_dirs
|
||||
.data_dir()
|
||||
.join("images")
|
||||
.join(&content);
|
||||
if let Ok(img) = image::open(&img_path) {
|
||||
let rgba = img.into_rgba8();
|
||||
let img_data = arboard::ImageData {
|
||||
width: rgba.width() as usize,
|
||||
height: rgba.height() as usize,
|
||||
bytes: std::borrow::Cow::Borrowed(
|
||||
rgba.as_raw(),
|
||||
),
|
||||
};
|
||||
let _ = clipboard.set_image(img_data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = clipboard.set_text(content);
|
||||
}
|
||||
match req {
|
||||
IpcRequest::GetHistory { limit } => {
|
||||
let lock = db_clone.lock().unwrap();
|
||||
let history = lock.read_history(limit).unwrap_or_default();
|
||||
let items: Vec<HistoryItem> = history
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
let content = match e.content {
|
||||
ClipboardData::Text(t) => t,
|
||||
ClipboardData::Image(img) => format!("{}.jpg", img.id),
|
||||
};
|
||||
let ts = e
|
||||
.timestamp
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
as i64;
|
||||
HistoryItem {
|
||||
content,
|
||||
timestamp: ts,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
reply(&mut stream, IpcResponse::History(items));
|
||||
}
|
||||
|
||||
IpcRequest::DeleteEntry { content } => {
|
||||
{
|
||||
let db_lock = db_clone.lock().unwrap();
|
||||
let _ = db_lock.delete_entry_by_content(&content);
|
||||
}
|
||||
IpcRequest::SetClipboard { content } => {
|
||||
let actual =
|
||||
if content.starts_with("enc:") || content.starts_with("enc2:") {
|
||||
crypto_clone.decrypt(&content).unwrap_or(content)
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
if content.ends_with(".jpg") || content.ends_with(".png") {
|
||||
if let Some(proj_dirs) =
|
||||
match arboard::Clipboard::new() {
|
||||
Ok(mut cb) => {
|
||||
if actual.ends_with(".jpg") || actual.ends_with(".png") {
|
||||
if let Some(dirs) =
|
||||
directories::ProjectDirs::from("com", "zefad", "rklipd")
|
||||
{
|
||||
let img_path =
|
||||
proj_dirs.data_dir().join("images").join(&content);
|
||||
if img_path.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&img_path) {
|
||||
eprintln!("Error while deleting image: {}", e);
|
||||
} else {
|
||||
println!("Image deleted : {}", content);
|
||||
}
|
||||
let path = dirs.data_dir().join("images").join(&actual);
|
||||
if let Ok(img) = image::open(&path) {
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) =
|
||||
(rgba.width() as usize, rgba.height() as usize);
|
||||
let _ = cb.set_image(arboard::ImageData {
|
||||
width: w,
|
||||
height: h,
|
||||
bytes: std::borrow::Cow::Owned(rgba.into_raw()),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = cb.set_text(actual);
|
||||
}
|
||||
reply(&mut stream, IpcResponse::Ok);
|
||||
}
|
||||
Err(e) => reply(&mut stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::DeleteEntry { content } => {
|
||||
{
|
||||
let lock = db_clone.lock().unwrap();
|
||||
let _ = lock.delete_entry_by_content(&content);
|
||||
}
|
||||
if !content.starts_with("enc:")
|
||||
&& !content.starts_with("enc2:")
|
||||
&& (content.ends_with(".jpg") || content.ends_with(".png"))
|
||||
{
|
||||
if let Some(dirs) =
|
||||
directories::ProjectDirs::from("com", "zefad", "rklipd")
|
||||
{
|
||||
let p = dirs.data_dir().join("images").join(&content);
|
||||
if p.exists() {
|
||||
let _ = fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
reply(&mut stream, IpcResponse::Ok);
|
||||
}
|
||||
|
||||
IpcRequest::UpdateEntry {
|
||||
old_content,
|
||||
new_content,
|
||||
} => {
|
||||
let lock = db_clone.lock().unwrap();
|
||||
match lock.update_entry_content(&old_content, &new_content) {
|
||||
Ok(_) => reply(&mut stream, IpcResponse::Ok),
|
||||
Err(e) => reply(&mut stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::AddEntry { content } => {
|
||||
let entry = ClipboardEntry {
|
||||
content: ClipboardData::Text(content),
|
||||
timestamp: SystemTime::now(),
|
||||
};
|
||||
let lock = db_clone.lock().unwrap();
|
||||
match lock.append(entry) {
|
||||
Ok(_) => reply(&mut stream, IpcResponse::Ok),
|
||||
Err(e) => reply(&mut stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
IpcRequest::ClearHistory => {
|
||||
let lock = db_clone.lock().unwrap();
|
||||
match lock.clear_history() {
|
||||
Ok(_) => reply(&mut stream, IpcResponse::Ok),
|
||||
Err(e) => reply(&mut stream, IpcResponse::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => eprintln!("Erreur de connexion IPC: {}", e),
|
||||
Err(e) => eprintln!("Erreur connexion IPC : {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,35 @@
|
||||
use crate::database::Database;
|
||||
use arboard::Clipboard;
|
||||
use directories::ProjectDirs;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
mod clipboard;
|
||||
mod crypto;
|
||||
mod database;
|
||||
mod ipc;
|
||||
mod models;
|
||||
mod monitor;
|
||||
mod ws;
|
||||
|
||||
use crate::crypto::Crypto;
|
||||
use crate::database::Database;
|
||||
use arboard::Clipboard;
|
||||
use directories::ProjectDirs;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let clipboard = Clipboard::new()?;
|
||||
|
||||
let proj_dirs = ProjectDirs::from("com", "zefad", "rklipd").expect("Unable to open dir");
|
||||
let proj_dirs =
|
||||
ProjectDirs::from("com", "zefad", "rklipd").expect("Impossible d'ouvrir le répertoire");
|
||||
let dir_path = proj_dirs.data_dir();
|
||||
let dir_path_str = dir_path.to_str().expect("Invalid path").to_string();
|
||||
let dir_path_str = dir_path.to_str().expect("Chemin invalide").to_string();
|
||||
|
||||
let db = Arc::new(Mutex::new(Database::init(&dir_path_str)?));
|
||||
let crypto = Arc::new(Crypto::load_or_create(dir_path)?);
|
||||
|
||||
let socket_path = dir_path.join("rklip.sock");
|
||||
let db_for_ipc = Arc::clone(&db);
|
||||
let crypto_for_ipc = Arc::clone(&crypto);
|
||||
std::thread::spawn(move || {
|
||||
crate::ipc::start_server(db_for_ipc, &socket_path);
|
||||
crate::ipc::start_server(db_for_ipc, crypto_for_ipc, &socket_path);
|
||||
});
|
||||
|
||||
println!("rklipd starting...");
|
||||
println!("rklipd démarrage...");
|
||||
monitor::start(db, clipboard)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -3,23 +3,26 @@ use arboard::Clipboard;
|
||||
use std::error::Error;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
use crate::ws;
|
||||
#[cfg(feature = "x11")]
|
||||
pub fn start(db: Arc<Mutex<Database>>, clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
||||
ws::x11::start(db, clipboard)?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(feature = "wayland", not(feature = "x11")))]
|
||||
{
|
||||
crate::ws::wayland::start(db, clipboard)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
use crate::ws;
|
||||
#[cfg(feature = "wayland")]
|
||||
pub fn start(db: Arc<Mutex<Database>>, clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
||||
ws::wayland::start(db, clipboard)?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(feature = "x11", not(feature = "wayland")))]
|
||||
{
|
||||
crate::ws::x11::start(db, clipboard)
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "x11", feature = "wayland")))]
|
||||
pub fn start(_db: Arc<Mutex<Database>>, _clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
||||
Err("No window system feature enabled".into())
|
||||
#[cfg(all(feature = "x11", feature = "wayland"))]
|
||||
{
|
||||
let _ = (db, clipboard);
|
||||
Err("Les features 'x11' et 'wayland' sont mutuellement exclusives".into())
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "x11", feature = "wayland")))]
|
||||
{
|
||||
let _ = (db, clipboard);
|
||||
Err("Aucune feature de système de fenêtrage activée (--features x11 ou wayland)".into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,53 +1,72 @@
|
||||
use crate::{database::Database, models::ClipboardEntry};
|
||||
use arboard::Clipboard;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
error::Error,
|
||||
sync::mpsc::channel,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use crate::database::Database;
|
||||
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||||
use std::error::Error;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
use uuid::Uuid;
|
||||
use wayland_clipboard_listener::{WlClipboardPasteStream, WlListenType};
|
||||
|
||||
pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
||||
let (tx, rx) = channel();
|
||||
pub fn start(
|
||||
db: Arc<Mutex<Database>>,
|
||||
_clipboard: arboard::Clipboard,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut stream = WlClipboardPasteStream::init(WlListenType::ListenOnCopy)
|
||||
.map_err(|e| format!("Impossible d'initialiser Wayland : {e}"))?;
|
||||
|
||||
std::thread::spawn(
|
||||
move || match WlClipboardPasteStream::init(WlListenType::ListenOnCopy) {
|
||||
Ok(mut stream) => {
|
||||
for _ in stream.paste_stream().flatten() {
|
||||
std::thread::sleep(Duration::new(1, 0));
|
||||
if let Err(e) = tx.send(()) {
|
||||
eprintln!("{}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
},
|
||||
);
|
||||
println!("Écoute du presse-papier Wayland...");
|
||||
|
||||
for _ in rx {
|
||||
println!("Clipboard update!");
|
||||
if let Ok(entry) = ClipboardEntry::new(&mut clipboard) {
|
||||
let db_clone = Arc::clone(&db);
|
||||
for msg in stream.paste_stream().flatten() {
|
||||
let context = &msg.context;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let db_lock = db_clone.lock().unwrap();
|
||||
let data: &[u8] = context.context.as_slice();
|
||||
|
||||
if let Err(e) = db_lock.append(entry) {
|
||||
eprintln!("SQLite writing error: {}", e);
|
||||
} else {
|
||||
println!("SQLite edited!");
|
||||
}
|
||||
});
|
||||
if data.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// match ClipboardEntry::new(&mut clipboard) {
|
||||
// Ok(entry) => db.append(entry)?,
|
||||
// Err(e) => eprintln!("{}", e),
|
||||
// }
|
||||
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() {
|
||||
continue;
|
||||
}
|
||||
|
||||
ClipboardEntry {
|
||||
content: ClipboardData::Text(text),
|
||||
timestamp: SystemTime::now(),
|
||||
}
|
||||
} else {
|
||||
match image::load_from_memory(data) {
|
||||
Ok(img) => {
|
||||
let (width, height) = (img.width(), img.height());
|
||||
let rgba = img.into_rgba8();
|
||||
|
||||
ClipboardEntry {
|
||||
content: ClipboardData::Image(Image {
|
||||
raw_pixels: Some(rgba.into_raw()),
|
||||
width,
|
||||
height,
|
||||
id: Uuid::new_v4(),
|
||||
}),
|
||||
timestamp: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Clipboard ignoré : {e}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -1,49 +1,83 @@
|
||||
use crate::{database::Database, models::ClipboardEntry};
|
||||
use crate::database::Database;
|
||||
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||||
use arboard::Clipboard;
|
||||
use clipboard_master::{CallbackResult, ClipboardHandler, Master};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::error::Error;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
error::Error,
|
||||
sync::mpsc::{Sender, channel},
|
||||
};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Handler {
|
||||
pub clipboard_tx: Sender<()>,
|
||||
}
|
||||
|
||||
impl ClipboardHandler for Handler {
|
||||
fn on_clipboard_change(&mut self) -> CallbackResult {
|
||||
if let Err(e) = self.clipboard_tx.send(()) {
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
CallbackResult::Next
|
||||
}
|
||||
fn hash_bytes(data: &[u8]) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
data.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
||||
let (tx, rx) = channel();
|
||||
println!("Clipboard monitor started (X11 polling mode)...");
|
||||
|
||||
let mut master = Master::new(Handler { clipboard_tx: tx })?;
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = master.run() {
|
||||
eprintln!("Clipboard monitor error : {}", e);
|
||||
}
|
||||
});
|
||||
let mut last_text: Option<String> = None;
|
||||
let mut last_image_hash: Option<u64> = None;
|
||||
|
||||
for _ in rx {
|
||||
println!("Clipboard update!");
|
||||
if let Ok(entry) = ClipboardEntry::new(&mut clipboard) {
|
||||
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 writing error: {}", e);
|
||||
} else {
|
||||
println!("SQLite edited!");
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
|
||||
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() {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
last_text = Some(text.clone());
|
||||
last_image_hash = None;
|
||||
println!("Clipboard update (text)!");
|
||||
|
||||
let entry = ClipboardEntry {
|
||||
content: ClipboardData::Text(text),
|
||||
timestamp: SystemTime::now(),
|
||||
};
|
||||
spawn_db_write(Arc::clone(&db), entry);
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
let Ok(img_data) = clipboard.get_image() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let hash = hash_bytes(&img_data.bytes);
|
||||
if Some(hash) == last_image_hash {
|
||||
continue;
|
||||
}
|
||||
|
||||
last_image_hash = Some(hash);
|
||||
last_text = None;
|
||||
println!("Clipboard update (image)!");
|
||||
|
||||
let entry = 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(),
|
||||
};
|
||||
spawn_db_write(Arc::clone(&db), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_db_write(db: Arc<Mutex<Database>>, entry: ClipboardEntry) {
|
||||
thread::spawn(move || {
|
||||
let db_lock = db.lock().unwrap();
|
||||
if let Err(e) = db_lock.append(entry) {
|
||||
eprintln!("SQLite writing error: {}", e);
|
||||
} else {
|
||||
println!("SQLite updated!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user