huge opti
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2487,6 +2487,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wayland-clipboard-listener",
|
"wayland-clipboard-listener",
|
||||||
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
1
rklipd/Cargo.lock
generated
1
rklipd/Cargo.lock
generated
@ -1445,6 +1445,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wayland-clipboard-listener",
|
"wayland-clipboard-listener",
|
||||||
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -15,6 +15,7 @@ serde = { version = "1.0.228", features = ["derive"] }
|
|||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
aes-gcm = "0.10.3"
|
aes-gcm = "0.10.3"
|
||||||
|
x11rb = "0.13.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
x11 = []
|
x11 = []
|
||||||
|
|||||||
@ -8,6 +8,11 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use uuid::Uuid;
|
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;
|
const MAX_IMAGE_PIXELS: usize = 3840 * 2160;
|
||||||
|
|
||||||
@ -18,81 +23,128 @@ fn hash_bytes(data: &[u8]) -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
||||||
println!("Clipboard monitor started (X11 polling mode)...");
|
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_text: Option<String> = None;
|
||||||
let mut last_image_hash: Option<u64> = None;
|
let mut last_image_hash: Option<u64> = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
thread::sleep(Duration::from_millis(500));
|
let event = conn.wait_for_event()?;
|
||||||
|
|
||||||
match clipboard.get_text() {
|
if let Event::XfixesSelectionNotify(_) = event {
|
||||||
Ok(raw) => {
|
thread::sleep(Duration::from_millis(50));
|
||||||
let text = raw.trim_end_matches('\n').to_string();
|
handle_clipboard_event(&mut clipboard, &db, &mut last_text, &mut last_image_hash);
|
||||||
if text.is_empty() || Some(&text) == last_text.as_ref() {
|
}
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
last_text = Some(text.clone());
|
fn handle_clipboard_event(
|
||||||
last_image_hash = None;
|
clipboard: &mut Clipboard,
|
||||||
println!("Clipboard update (text)!");
|
db: &Arc<Mutex<Database>>,
|
||||||
|
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)");
|
||||||
|
|
||||||
let entry = ClipboardEntry {
|
spawn_db_write(
|
||||||
|
Arc::clone(db),
|
||||||
|
ClipboardEntry {
|
||||||
content: ClipboardData::Text(text),
|
content: ClipboardData::Text(text),
|
||||||
timestamp: SystemTime::now(),
|
timestamp: SystemTime::now(),
|
||||||
};
|
},
|
||||||
spawn_db_write(Arc::clone(&db), entry);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
*last_image_hash = Some(pixel_count as u64);
|
||||||
|
*last_text = None;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(_) => {
|
let hash = hash_bytes(&img_data.bytes);
|
||||||
let Ok(img_data) = clipboard.get_image() else {
|
if Some(hash) == *last_image_hash {
|
||||||
continue;
|
return;
|
||||||
};
|
}
|
||||||
|
*last_image_hash = Some(hash);
|
||||||
|
*last_text = None;
|
||||||
|
println!("Clipboard update (image)");
|
||||||
|
|
||||||
let pixel_count = img_data.width * img_data.height;
|
spawn_db_write(
|
||||||
if pixel_count > MAX_IMAGE_PIXELS {
|
Arc::clone(db),
|
||||||
eprintln!(
|
ClipboardEntry {
|
||||||
"Image ignorée : {}×{} ({} Mpx > limite {}×{})",
|
content: ClipboardData::Image(crate::models::Image {
|
||||||
img_data.width,
|
|
||||||
img_data.height,
|
|
||||||
pixel_count / 1_000_000,
|
|
||||||
3840,
|
|
||||||
2160
|
|
||||||
);
|
|
||||||
last_image_hash = Some(pixel_count as u64);
|
|
||||||
last_text = None;
|
|
||||||
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()),
|
raw_pixels: Some(img_data.bytes.into_owned()),
|
||||||
width: img_data.width as u32,
|
width: img_data.width as u32,
|
||||||
height: img_data.height as u32,
|
height: img_data.height as u32,
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
}),
|
}),
|
||||||
timestamp: SystemTime::now(),
|
timestamp: SystemTime::now(),
|
||||||
};
|
},
|
||||||
spawn_db_write(Arc::clone(&db), entry);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_db_write(db: Arc<Mutex<Database>>, entry: ClipboardEntry) {
|
fn spawn_db_write(db: Arc<Mutex<Database>>, entry: ClipboardEntry) {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let db_lock = db.lock().unwrap();
|
let lock = db.lock().unwrap();
|
||||||
if let Err(e) = db_lock.append(entry) {
|
if let Err(e) = lock.append(entry) {
|
||||||
eprintln!("SQLite writing error: {}", e);
|
eprintln!("SQLite write error: {e}");
|
||||||
} else {
|
} else {
|
||||||
println!("SQLite updated!");
|
println!("SQLite updated!");
|
||||||
}
|
}
|
||||||
|
|||||||
283
src/app.rs
283
src/app.rs
@ -2,9 +2,11 @@ use crate::crypto::Crypto;
|
|||||||
use crate::ipc::{self, HistoryItem};
|
use crate::ipc::{self, HistoryItem};
|
||||||
use chrono::{Local, NaiveDate, TimeZone};
|
use chrono::{Local, NaiveDate, TimeZone};
|
||||||
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
|
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
|
||||||
|
use image::DynamicImage;
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use ratatui_image::{picker::Picker, protocol};
|
use ratatui_image::{picker::Picker, protocol};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use syntect::highlighting::ThemeSet;
|
use syntect::highlighting::ThemeSet;
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
@ -12,6 +14,10 @@ use syntect::parsing::SyntaxSet;
|
|||||||
const PREVIEW_MAX_WIDTH: u32 = 1280;
|
const PREVIEW_MAX_WIDTH: u32 = 1280;
|
||||||
const PREVIEW_MAX_HEIGHT: u32 = 720;
|
const PREVIEW_MAX_HEIGHT: u32 = 720;
|
||||||
|
|
||||||
|
const IMAGE_CACHE_MAX: usize = 8;
|
||||||
|
|
||||||
|
const PAGE_SIZE: usize = 50;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Normal,
|
Normal,
|
||||||
@ -48,6 +54,10 @@ pub struct App {
|
|||||||
pub syntax_set: SyntaxSet,
|
pub syntax_set: SyntaxSet,
|
||||||
pub theme_set: ThemeSet,
|
pub theme_set: ThemeSet,
|
||||||
pub type_filter: TypeFilter,
|
pub type_filter: TypeFilter,
|
||||||
|
pub loaded_count: usize,
|
||||||
|
pub has_more: bool,
|
||||||
|
image_cache: HashMap<String, DynamicImage>,
|
||||||
|
image_cache_order: VecDeque<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -77,7 +87,8 @@ impl TypeFilter {
|
|||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let items = ipc::fetch_history(200).unwrap_or_default();
|
let items = ipc::fetch_history(PAGE_SIZE).unwrap_or_default();
|
||||||
|
let has_more = items.len() == PAGE_SIZE;
|
||||||
let mut list_state = ListState::default();
|
let mut list_state = ListState::default();
|
||||||
list_state.select(if items.is_empty() { None } else { Some(0) });
|
list_state.select(if items.is_empty() { None } else { Some(0) });
|
||||||
|
|
||||||
@ -107,11 +118,82 @@ impl App {
|
|||||||
syntax_set: SyntaxSet::load_defaults_newlines(),
|
syntax_set: SyntaxSet::load_defaults_newlines(),
|
||||||
theme_set: ThemeSet::load_defaults(),
|
theme_set: ThemeSet::load_defaults(),
|
||||||
type_filter: TypeFilter::All,
|
type_filter: TypeFilter::All,
|
||||||
|
loaded_count: PAGE_SIZE,
|
||||||
|
has_more,
|
||||||
|
image_cache: HashMap::new(),
|
||||||
|
image_cache_order: VecDeque::new(),
|
||||||
};
|
};
|
||||||
app.update_preview();
|
app.update_preview();
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn try_load_more(&mut self) -> bool {
|
||||||
|
if !self.has_more {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_limit = self.loaded_count + PAGE_SIZE;
|
||||||
|
let Some(items) = ipc::fetch_history(new_limit) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if items.len() <= self.all_items.len() {
|
||||||
|
self.has_more = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.has_more = items.len() == new_limit;
|
||||||
|
self.loaded_count = new_limit;
|
||||||
|
|
||||||
|
let selected_content = self.get_selected_item().map(|i| i.content.clone());
|
||||||
|
self.all_items = items;
|
||||||
|
self.update_search();
|
||||||
|
|
||||||
|
if let Some(content) = selected_content {
|
||||||
|
if let Some(pos) = self
|
||||||
|
.filtered_items
|
||||||
|
.iter()
|
||||||
|
.position(|x| x.content == content)
|
||||||
|
{
|
||||||
|
self.list_state.select(Some(pos));
|
||||||
|
self.last_selected_index = None;
|
||||||
|
self.update_preview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_status(format!("{} entrées chargées", self.all_items.len()));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cached_image(
|
||||||
|
&mut self,
|
||||||
|
filename: &str,
|
||||||
|
base_dir: &std::path::Path,
|
||||||
|
) -> Option<DynamicImage> {
|
||||||
|
if !self.image_cache.contains_key(filename) {
|
||||||
|
let path = base_dir.join("images").join(filename);
|
||||||
|
if !path.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let img = image::open(&path).ok()?;
|
||||||
|
let img = if img.width() > PREVIEW_MAX_WIDTH || img.height() > PREVIEW_MAX_HEIGHT {
|
||||||
|
img.thumbnail(PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT)
|
||||||
|
} else {
|
||||||
|
img
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.image_cache.len() >= IMAGE_CACHE_MAX {
|
||||||
|
if let Some(oldest) = self.image_cache_order.pop_front() {
|
||||||
|
self.image_cache.remove(&oldest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.image_cache_order.push_back(filename.to_string());
|
||||||
|
self.image_cache.insert(filename.to_string(), img);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.image_cache.get(filename).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn format_timestamp(ts_ms: i64) -> String {
|
pub fn format_timestamp(ts_ms: i64) -> String {
|
||||||
let secs = ts_ms / 1000;
|
let secs = ts_ms / 1000;
|
||||||
let nsecs = ((ts_ms % 1000) * 1_000_000) as u32;
|
let nsecs = ((ts_ms % 1000) * 1_000_000) as u32;
|
||||||
@ -222,14 +304,59 @@ impl App {
|
|||||||
self.update_preview();
|
self.update_preview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
if self.filtered_items.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = self.list_state.selected().unwrap_or(0);
|
||||||
|
let last = self.filtered_items.len() - 1;
|
||||||
|
|
||||||
|
if current >= last {
|
||||||
|
if self.try_load_more() {
|
||||||
|
// try_load_more restaure la sélection sur le même item ;
|
||||||
|
// on peut maintenant avancer d'un cran
|
||||||
|
let current = self.list_state.selected().unwrap_or(0);
|
||||||
|
if current + 1 < self.filtered_items.len() {
|
||||||
|
self.list_state.select(Some(current + 1));
|
||||||
|
self.update_preview();
|
||||||
|
}
|
||||||
|
// Sinon (le filtre actif masque les nouveaux items) : on reste
|
||||||
|
} else {
|
||||||
|
// Fin réelle — wrap vers le haut
|
||||||
|
self.list_state.select(Some(0));
|
||||||
|
self.update_preview();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.list_state.select(Some(current + 1));
|
||||||
|
self.update_preview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&mut self) {
|
||||||
|
if self.filtered_items.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = self.list_state.selected().map_or(0, |i| {
|
||||||
|
if i == 0 {
|
||||||
|
self.filtered_items.len() - 1
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.list_state.select(Some(i));
|
||||||
|
self.update_preview();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn delete_selected(&mut self) {
|
pub fn delete_selected(&mut self) {
|
||||||
if let Some(i) = self.list_state.selected() {
|
if let Some(i) = self.list_state.selected() {
|
||||||
if i < self.filtered_items.len() {
|
if i < self.filtered_items.len() {
|
||||||
let item = self.filtered_items.remove(i);
|
let item = self.filtered_items.remove(i);
|
||||||
// On stocke juste l'item (plus l'index filtré qui était faux)
|
|
||||||
self.undo_stack.push(item.clone());
|
self.undo_stack.push(item.clone());
|
||||||
self.all_items.retain(|x| x.content != item.content);
|
self.all_items.retain(|x| x.content != item.content);
|
||||||
|
if item.content.ends_with(".jpg") || item.content.ends_with(".png") {
|
||||||
|
self.image_cache.remove(&item.content);
|
||||||
|
self.image_cache_order.retain(|k| k != &item.content);
|
||||||
|
}
|
||||||
let new_sel = if self.filtered_items.is_empty() {
|
let new_sel = if self.filtered_items.is_empty() {
|
||||||
None
|
None
|
||||||
} else if i >= self.filtered_items.len() {
|
} else if i >= self.filtered_items.len() {
|
||||||
@ -246,15 +373,14 @@ impl App {
|
|||||||
pub fn undo_delete(&mut self) {
|
pub fn undo_delete(&mut self) {
|
||||||
if let Some(item) = self.undo_stack.pop() {
|
if let Some(item) = self.undo_stack.pop() {
|
||||||
ipc::add_entry(item.content.clone());
|
ipc::add_entry(item.content.clone());
|
||||||
|
// Re-sync depuis le daemon pour avoir l'ordre chronologique correct
|
||||||
if let Some(new_items) = ipc::fetch_history(200) {
|
if let Some(new_items) = ipc::fetch_history(self.loaded_count) {
|
||||||
|
self.has_more = new_items.len() == self.loaded_count;
|
||||||
self.all_items = new_items;
|
self.all_items = new_items;
|
||||||
} else {
|
} else {
|
||||||
self.all_items.insert(0, item.clone());
|
self.all_items.insert(0, item.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_search();
|
self.update_search();
|
||||||
|
|
||||||
if let Some(pos) = self
|
if let Some(pos) = self
|
||||||
.filtered_items
|
.filtered_items
|
||||||
.iter()
|
.iter()
|
||||||
@ -272,16 +398,13 @@ impl App {
|
|||||||
Some(i) => i.content.clone(),
|
Some(i) => i.content.clone(),
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
if Crypto::is_legacy_encrypted(&content) {
|
if Crypto::is_legacy_encrypted(&content) {
|
||||||
self.set_error(
|
self.set_error(
|
||||||
"Entrée chiffrée avec l'ancienne clé machine — non modifiable ici".into(),
|
"Entrée chiffrée avec l'ancienne clé machine — non modifiable ici".into(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.crypto = None;
|
self.crypto = None;
|
||||||
|
|
||||||
if Crypto::is_password_encrypted(&content) {
|
if Crypto::is_password_encrypted(&content) {
|
||||||
self.pending_action = Some(PendingAction::DecryptSelected);
|
self.pending_action = Some(PendingAction::DecryptSelected);
|
||||||
} else {
|
} else {
|
||||||
@ -319,14 +442,12 @@ impl App {
|
|||||||
Some(i) => i.content.clone(),
|
Some(i) => i.content.clone(),
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
let result = match &self.crypto {
|
||||||
let encrypt_result = match &self.crypto {
|
Some(k) => k.encrypt(&content),
|
||||||
Some(key) => key.encrypt(&content),
|
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
self.crypto = None;
|
self.crypto = None;
|
||||||
|
match result {
|
||||||
match encrypt_result {
|
|
||||||
Ok(enc) => {
|
Ok(enc) => {
|
||||||
if ipc::update_entry(content.clone(), enc.clone()) {
|
if ipc::update_entry(content.clone(), enc.clone()) {
|
||||||
self.replace_content(&content, enc);
|
self.replace_content(&content, enc);
|
||||||
@ -344,14 +465,12 @@ impl App {
|
|||||||
Some(i) => i.content.clone(),
|
Some(i) => i.content.clone(),
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
let result = match &self.crypto {
|
||||||
let decrypt_result = match &self.crypto {
|
Some(k) => k.decrypt(&content),
|
||||||
Some(key) => key.decrypt(&content),
|
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
self.crypto = None;
|
self.crypto = None;
|
||||||
|
match result {
|
||||||
match decrypt_result {
|
|
||||||
Ok(plain) => {
|
Ok(plain) => {
|
||||||
if ipc::update_entry(content.clone(), plain.clone()) {
|
if ipc::update_entry(content.clone(), plain.clone()) {
|
||||||
self.replace_content(&content, plain);
|
self.replace_content(&content, plain);
|
||||||
@ -360,11 +479,9 @@ impl App {
|
|||||||
self.set_error("Erreur mise à jour BDD".into());
|
self.set_error("Erreur mise à jour BDD".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => self.set_error(
|
||||||
self.set_error(
|
"Déchiffrement échoué — mauvais mot de passe. Réessayez avec [e].".into(),
|
||||||
"Déchiffrement échoué — mauvais mot de passe. Réessayez avec [e].".into(),
|
),
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,14 +490,12 @@ impl App {
|
|||||||
Some(i) => i.content.clone(),
|
Some(i) => i.content.clone(),
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
let result = match &self.crypto {
|
||||||
let decrypt_result = match &self.crypto {
|
Some(k) => k.decrypt(&content),
|
||||||
Some(key) => key.decrypt(&content),
|
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
self.crypto = None;
|
self.crypto = None;
|
||||||
|
match result {
|
||||||
match decrypt_result {
|
|
||||||
Ok(plain) => {
|
Ok(plain) => {
|
||||||
if ipc::set_clipboard(plain) {
|
if ipc::set_clipboard(plain) {
|
||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
@ -388,11 +503,9 @@ impl App {
|
|||||||
self.set_error("Erreur : impossible de définir le presse-papier".into());
|
self.set_error("Erreur : impossible de définir le presse-papier".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => self.set_error(
|
||||||
self.set_error(
|
"Déchiffrement échoué — mauvais mot de passe. Réessayez avec Entrée.".into(),
|
||||||
"Déchiffrement échoué — mauvais mot de passe. Réessayez avec Entrée.".into(),
|
),
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,6 +543,10 @@ impl App {
|
|||||||
self.undo_stack.clear();
|
self.undo_stack.clear();
|
||||||
self.list_state.select(None);
|
self.list_state.select(None);
|
||||||
self.current_image = None;
|
self.current_image = None;
|
||||||
|
self.image_cache.clear();
|
||||||
|
self.image_cache_order.clear();
|
||||||
|
self.loaded_count = PAGE_SIZE;
|
||||||
|
self.has_more = false;
|
||||||
self.set_status("Historique effacé".into());
|
self.set_status("Historique effacé".into());
|
||||||
} else {
|
} else {
|
||||||
self.set_error("Erreur lors de l'effacement".into());
|
self.set_error("Erreur lors de l'effacement".into());
|
||||||
@ -452,18 +569,10 @@ impl App {
|
|||||||
|
|
||||||
if content.ends_with(".jpg") || content.ends_with(".png") {
|
if content.ends_with(".jpg") || content.ends_with(".png") {
|
||||||
if let Some(dirs) = directories::ProjectDirs::from("com", "zefad", "rklipd") {
|
if let Some(dirs) = directories::ProjectDirs::from("com", "zefad", "rklipd") {
|
||||||
let path = dirs.data_dir().join("images").join(&content);
|
if let Some(img) = self.get_cached_image(&content, dirs.data_dir()) {
|
||||||
if path.exists() {
|
// new_resize_protocol attend un DynamicImage — on clone depuis le cache
|
||||||
if let Ok(img) = image::open(&path) {
|
// (l'image est déjà redimensionnée ≤ 1280×720, clone = ~3,5 Mo max)
|
||||||
let img = if img.width() > PREVIEW_MAX_WIDTH
|
self.current_image = Some(self.picker.new_resize_protocol(img));
|
||||||
|| img.height() > PREVIEW_MAX_HEIGHT
|
|
||||||
{
|
|
||||||
img.thumbnail(PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT)
|
|
||||||
} else {
|
|
||||||
img
|
|
||||||
};
|
|
||||||
self.current_image = Some(self.picker.new_resize_protocol(img));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -476,36 +585,6 @@ impl App {
|
|||||||
self.preview_scroll = self.preview_scroll.saturating_sub(3);
|
self.preview_scroll = self.preview_scroll.saturating_sub(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&mut self) {
|
|
||||||
if self.filtered_items.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = self.list_state.selected().map_or(0, |i| {
|
|
||||||
if i >= self.filtered_items.len() - 1 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
i + 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.list_state.select(Some(i));
|
|
||||||
self.update_preview();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn previous(&mut self) {
|
|
||||||
if self.filtered_items.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let i = self.list_state.selected().map_or(0, |i| {
|
|
||||||
if i == 0 {
|
|
||||||
self.filtered_items.len() - 1
|
|
||||||
} else {
|
|
||||||
i - 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.list_state.select(Some(i));
|
|
||||||
self.update_preview();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_selected_item(&self) -> Option<&HistoryItem> {
|
pub fn get_selected_item(&self) -> Option<&HistoryItem> {
|
||||||
self.list_state
|
self.list_state
|
||||||
.selected()
|
.selected()
|
||||||
@ -513,30 +592,34 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_with_daemon(&mut self) {
|
pub fn sync_with_daemon(&mut self) {
|
||||||
if let Some(new) = ipc::fetch_history(200) {
|
let Some(new) = ipc::fetch_history(self.loaded_count) else {
|
||||||
let changed = self.all_items.len() != new.len()
|
return;
|
||||||
|| self
|
};
|
||||||
.all_items
|
|
||||||
|
// Mise à jour du flag has_more lors de chaque sync
|
||||||
|
self.has_more = new.len() == self.loaded_count;
|
||||||
|
|
||||||
|
let changed = self.all_items.len() != new.len()
|
||||||
|
|| self
|
||||||
|
.all_items
|
||||||
|
.iter()
|
||||||
|
.zip(&new)
|
||||||
|
.any(|(a, b)| a.content != b.content);
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
let selected_content = self.get_selected_item().map(|i| i.content.clone());
|
||||||
|
self.all_items = new;
|
||||||
|
self.update_search();
|
||||||
|
|
||||||
|
if let Some(content) = selected_content {
|
||||||
|
if let Some(pos) = self
|
||||||
|
.filtered_items
|
||||||
.iter()
|
.iter()
|
||||||
.zip(&new)
|
.position(|x| x.content == content)
|
||||||
.any(|(a, b)| a.content != b.content);
|
{
|
||||||
|
self.list_state.select(Some(pos));
|
||||||
if changed {
|
self.last_selected_index = None;
|
||||||
let selected_content = self.get_selected_item().map(|i| i.content.clone());
|
self.update_preview();
|
||||||
|
|
||||||
self.all_items = new;
|
|
||||||
self.update_search();
|
|
||||||
|
|
||||||
if let Some(content) = selected_content {
|
|
||||||
if let Some(pos) = self
|
|
||||||
.filtered_items
|
|
||||||
.iter()
|
|
||||||
.position(|x| x.content == content)
|
|
||||||
{
|
|
||||||
self.list_state.select(Some(pos));
|
|
||||||
self.last_selected_index = None;
|
|
||||||
self.update_preview();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
src/main.rs
131
src/main.rs
@ -58,79 +58,74 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App)
|
|||||||
}
|
}
|
||||||
|
|
||||||
match app.mode {
|
match app.mode {
|
||||||
// Dans la boucle, Mode::Normal :
|
Mode::Normal => match key.code {
|
||||||
Mode::Normal => {
|
KeyCode::Enter => app.paste_selected(),
|
||||||
// FIX: les deux blocs match étaient redondants et mal nommés.
|
KeyCode::Char('j') | KeyCode::Down => app.next(),
|
||||||
// On fusionne la logique en un seul bloc clair.
|
KeyCode::Char('k') | KeyCode::Up => app.previous(),
|
||||||
match key.code {
|
KeyCode::Char('G') => {
|
||||||
KeyCode::Enter => app.paste_selected(),
|
if !app.filtered_items.is_empty() {
|
||||||
KeyCode::Char('j') | KeyCode::Down => app.next(),
|
let l = app.filtered_items.len() - 1;
|
||||||
KeyCode::Char('k') | KeyCode::Up => app.previous(),
|
app.list_state.select(Some(l));
|
||||||
KeyCode::Char('G') => {
|
app.update_preview();
|
||||||
|
}
|
||||||
|
last_d = false;
|
||||||
|
last_g = false;
|
||||||
|
}
|
||||||
|
KeyCode::Char('g') => {
|
||||||
|
last_d = false;
|
||||||
|
if last_g {
|
||||||
if !app.filtered_items.is_empty() {
|
if !app.filtered_items.is_empty() {
|
||||||
let l = app.filtered_items.len() - 1;
|
app.list_state.select(Some(0));
|
||||||
app.list_state.select(Some(l));
|
|
||||||
app.update_preview();
|
app.update_preview();
|
||||||
}
|
}
|
||||||
last_d = false;
|
|
||||||
last_g = false;
|
|
||||||
}
|
|
||||||
KeyCode::Char('g') => {
|
|
||||||
last_d = false;
|
|
||||||
if last_g {
|
|
||||||
if !app.filtered_items.is_empty() {
|
|
||||||
app.list_state.select(Some(0));
|
|
||||||
app.update_preview();
|
|
||||||
}
|
|
||||||
last_g = false;
|
|
||||||
} else {
|
|
||||||
last_g = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char('d') => {
|
|
||||||
last_g = false;
|
|
||||||
if last_d {
|
|
||||||
app.mode = Mode::ConfirmDelete;
|
|
||||||
last_d = false;
|
|
||||||
} else {
|
|
||||||
last_d = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char('u') => {
|
|
||||||
app.undo_delete();
|
|
||||||
last_d = false;
|
|
||||||
last_g = false;
|
|
||||||
}
|
|
||||||
KeyCode::Char('e') => {
|
|
||||||
app.toggle_encrypt();
|
|
||||||
last_d = false;
|
|
||||||
last_g = false;
|
|
||||||
}
|
|
||||||
KeyCode::Char('t') => {
|
|
||||||
app.cycle_type_filter();
|
|
||||||
last_d = false;
|
|
||||||
last_g = false;
|
|
||||||
}
|
|
||||||
KeyCode::Char('/') => {
|
|
||||||
app.mode = Mode::Search;
|
|
||||||
app.input_buffer.clear();
|
|
||||||
app.update_search();
|
|
||||||
last_d = false;
|
|
||||||
last_g = false;
|
|
||||||
}
|
|
||||||
KeyCode::Char(':') => {
|
|
||||||
app.mode = Mode::Command;
|
|
||||||
app.input_buffer.clear();
|
|
||||||
last_d = false;
|
|
||||||
last_g = false;
|
|
||||||
}
|
|
||||||
KeyCode::Char('q') => app.should_quit = true,
|
|
||||||
_ => {
|
|
||||||
last_d = false;
|
|
||||||
last_g = false;
|
last_g = false;
|
||||||
|
} else {
|
||||||
|
last_g = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
KeyCode::Char('d') => {
|
||||||
|
last_g = false;
|
||||||
|
if last_d {
|
||||||
|
app.mode = Mode::ConfirmDelete;
|
||||||
|
last_d = false;
|
||||||
|
} else {
|
||||||
|
last_d = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('u') => {
|
||||||
|
app.undo_delete();
|
||||||
|
last_d = false;
|
||||||
|
last_g = false;
|
||||||
|
}
|
||||||
|
KeyCode::Char('e') => {
|
||||||
|
app.toggle_encrypt();
|
||||||
|
last_d = false;
|
||||||
|
last_g = false;
|
||||||
|
}
|
||||||
|
KeyCode::Char('t') => {
|
||||||
|
app.cycle_type_filter();
|
||||||
|
last_d = false;
|
||||||
|
last_g = false;
|
||||||
|
}
|
||||||
|
KeyCode::Char('/') => {
|
||||||
|
app.mode = Mode::Search;
|
||||||
|
app.input_buffer.clear();
|
||||||
|
app.update_search();
|
||||||
|
last_d = false;
|
||||||
|
last_g = false;
|
||||||
|
}
|
||||||
|
KeyCode::Char(':') => {
|
||||||
|
app.mode = Mode::Command;
|
||||||
|
app.input_buffer.clear();
|
||||||
|
last_d = false;
|
||||||
|
last_g = false;
|
||||||
|
}
|
||||||
|
KeyCode::Char('q') => app.should_quit = true,
|
||||||
|
_ => {
|
||||||
|
last_d = false;
|
||||||
|
last_g = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
Mode::Search => match key.code {
|
Mode::Search => match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
@ -169,7 +164,6 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App)
|
|||||||
match cmd.as_str() {
|
match cmd.as_str() {
|
||||||
"q" | "quit" => app.should_quit = true,
|
"q" | "quit" => app.should_quit = true,
|
||||||
"clear" => app.clear_history(),
|
"clear" => app.clear_history(),
|
||||||
// :p pour définir/changer le mot de passe
|
|
||||||
"p" | "password" => {
|
"p" | "password" => {
|
||||||
app.pending_action = None;
|
app.pending_action = None;
|
||||||
app.mode = Mode::PasswordInput;
|
app.mode = Mode::PasswordInput;
|
||||||
@ -215,7 +209,6 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Idle : synchronisation avec le daemon
|
|
||||||
app.sync_with_daemon();
|
app.sync_with_daemon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -237,7 +237,11 @@ pub fn render(f: &mut Frame, app: &mut App) {
|
|||||||
} else {
|
} else {
|
||||||
app.list_state.selected().unwrap_or(0) + 1
|
app.list_state.selected().unwrap_or(0) + 1
|
||||||
};
|
};
|
||||||
let counter = format!(" {}/{} ", current, total);
|
let counter = if app.has_more {
|
||||||
|
format!(" {}/{}+ ", current, total)
|
||||||
|
} else {
|
||||||
|
format!(" {}/{} ", current, total)
|
||||||
|
};
|
||||||
let clen = counter.len() as u16;
|
let clen = counter.len() as u16;
|
||||||
|
|
||||||
let status_cols = Layout::default()
|
let status_cols = Layout::default()
|
||||||
|
|||||||
Reference in New Issue
Block a user