huge opti

This commit is contained in:
2026-05-20 23:49:53 +02:00
parent 8ea259531e
commit fc085a8a83
7 changed files with 355 additions and 220 deletions

1
Cargo.lock generated
View File

@ -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
View File

@ -1445,6 +1445,7 @@ dependencies = [
"serde_json", "serde_json",
"uuid", "uuid",
"wayland-clipboard-listener", "wayland-clipboard-listener",
"x11rb",
] ]
[[package]] [[package]]

View File

@ -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 = []

View File

@ -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!");
} }

View File

@ -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();
}
} }
} }
} }

View File

@ -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();
} }

View File

@ -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()