From 46dfa0fd493c537baa1dd9b3c58139d192a6f02a Mon Sep 17 00:00:00 2001 From: zeefaad Date: Thu, 21 May 2026 10:47:49 +0200 Subject: [PATCH] add fav + opti --- rklipd/src/database.rs | 12 +++- src/app.rs | 41 +++++++++----- src/ui.rs | 121 ++++++++++++++++++++++------------------- 3 files changed, 103 insertions(+), 71 deletions(-) diff --git a/rklipd/src/database.rs b/rklipd/src/database.rs index 6aa51ce..fc262aa 100644 --- a/rklipd/src/database.rs +++ b/rklipd/src/database.rs @@ -30,6 +30,14 @@ impl Database { PRAGMA foreign_keys=ON;", )?; + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL DEFAULT 1);", + )?; + conn.execute( + "INSERT 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, @@ -296,12 +304,14 @@ impl Database { } pub fn clear_history(&self) -> Result<(), Box> { + self.conn.execute("DELETE FROM history", [])?; + 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(()) } } diff --git a/src/app.rs b/src/app.rs index 617dbd5..5f03833 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,6 +9,7 @@ use ratatui::widgets::ListState; use ratatui_image::{picker::Picker, protocol}; use regex::Regex; use std::collections::{HashMap, VecDeque}; +use std::path::PathBuf; use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; use syntect::easy::HighlightLines; @@ -22,6 +23,7 @@ const IMAGE_CACHE_MAX: usize = 8; const PAGE_SIZE: usize = 50; const MAX_HIGHLIGHT_LINES: usize = 500; const SYNC_INTERVAL_MS: u64 = 1000; +const UNDO_STACK_MAX: usize = 50; #[inline] pub fn is_image(s: &str) -> bool { @@ -41,10 +43,9 @@ pub fn extract_url(content: &str) -> Option { let re = RE .get_or_init(|| Regex::new(r#"https?://[^\s<>"'()\[\]{}]+"#).expect("URL regex invalide")); re.find(content).map(|m| { - let url = m - .as_str() - .trim_end_matches(|c: char| matches!(c, '.' | ',' | ';' | ':' | '!' | '?')); - url.to_string() + m.as_str() + .trim_end_matches(|c: char| matches!(c, '.' | ',' | ';' | ':' | '!' | '?')) + .to_string() }) } @@ -113,9 +114,11 @@ pub struct App { pub has_more: bool, pub preview_highlighted: Option>>, pub preview_lang: Option, + pub data_dir: Option, last_sync: Instant, image_cache: HashMap>, image_cache_order: VecDeque, + matcher: SkimMatcherV2, } fn syn_color(c: syntect::highlighting::Color) -> Color { @@ -165,7 +168,6 @@ pub fn highlight_code( let syntax = detect_syntax(content, syntax_set); let mut h = HighlightLines::new(syntax, theme); let mut lines = Vec::new(); - let total_lines = content.lines().count(); for (no, line) in LinesWithEndings::from(content) @@ -205,6 +207,9 @@ pub fn highlight_code( impl App { pub fn new() -> Self { + let data_dir = directories::ProjectDirs::from("com", "zefad", "rklipd") + .map(|d| d.data_dir().to_path_buf()); + let items = ipc::fetch_history(PAGE_SIZE).unwrap_or_default(); let has_more = items.len() == PAGE_SIZE; let mut list_state = ListState::default(); @@ -212,8 +217,8 @@ impl App { let picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks()); - let salt = match directories::ProjectDirs::from("com", "zefad", "rklipd") { - Some(dirs) => match Crypto::load_or_create_salt(dirs.data_dir()) { + let salt = match &data_dir { + Some(dir) => match Crypto::load_or_create_salt(dir) { Ok(s) => s, Err(e) => { eprintln!("Erreur sel cryptographique : {e}"); @@ -251,8 +256,10 @@ impl App { last_sync: Instant::now() - Duration::from_secs(10), preview_highlighted: None, preview_lang: None, + data_dir, image_cache: HashMap::new(), image_cache_order: VecDeque::new(), + matcher: SkimMatcherV2::default(), }; app.update_preview(); app @@ -409,11 +416,10 @@ impl App { } } } else { - let matcher = SkimMatcherV2::default(); let mut matched: Vec<(i64, HistoryItem)> = base .into_iter() .filter_map(|item| { - let score = matcher.fuzzy_match(&search_str(&item), &text_query)?; + let score = self.matcher.fuzzy_match(&search_str(&item), &text_query)?; let adjusted = score + if item.pinned { 1000 } else { 0 }; Some((adjusted, item)) }) @@ -472,7 +478,13 @@ impl App { if let Some(i) = self.list_state.selected() { if i < self.filtered_items.len() { let item = self.filtered_items.remove(i); + + // Borner la pile d'annulation pour éviter une fuite mémoire + if self.undo_stack.len() >= UNDO_STACK_MAX { + self.undo_stack.remove(0); + } self.undo_stack.push(item.clone()); + self.all_items.retain(|x| x.content != item.content); if is_image(&item.content) { self.image_cache.remove(&item.content); @@ -564,10 +576,11 @@ impl App { match extract_url(&content) { Some(url) => match std::process::Command::new("xdg-open").arg(&url).spawn() { Ok(_) => { - let preview = if url.len() > 48 { - format!("{}…", &url[..48]) + let preview: String = url.chars().take(48).collect(); + let preview = if url.chars().count() > 48 { + format!("{preview}…") } else { - url.clone() + preview }; self.set_status(format!("Ouverture : {preview}")); } @@ -756,8 +769,8 @@ impl App { }; if is_image(&content) { - if let Some(dirs) = directories::ProjectDirs::from("com", "zefad", "rklipd") { - if let Some(arc_img) = self.get_cached_image(&content, dirs.data_dir()) { + if let Some(dir) = self.data_dir.clone() { + if let Some(arc_img) = self.get_cached_image(&content, &dir) { let img = (*arc_img).clone(); self.current_image = Some(self.picker.new_resize_protocol(img)); } diff --git a/src/ui.rs b/src/ui.rs index 1790f60..2bbe3a1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -286,10 +286,6 @@ fn render_statusbar(f: &mut Frame, app: &mut App, area: Rect) { ); } -// --------------------------------------------------------------------------- -// Overlay d'aide -// --------------------------------------------------------------------------- - fn centered_rect(width: u16, height: u16, area: Rect) -> Rect { let x = area.x + (area.width.saturating_sub(width)) / 2; let y = area.y + (area.height.saturating_sub(height)) / 2; @@ -297,135 +293,148 @@ fn centered_rect(width: u16, height: u16, area: Rect) -> Rect { } fn help_lines() -> Vec> { - let k = |s: &'static str| { + let key = |s: &'static str| { Span::styled( s, Style::default() - .fg(Color::Yellow) + .fg(Color::Rgb(255, 215, 100)) .add_modifier(Modifier::BOLD), ) }; - let d = |s: &'static str| Span::styled(s, Style::default().fg(Color::White)); + let desc = |s: &'static str| Span::styled(s, Style::default().fg(Color::Rgb(200, 205, 220))); let sep = || Span::raw(" "); - let h = |s: &'static str| { + let header = |s: &'static str| { Span::styled( s, Style::default() - .fg(Color::Cyan) + .fg(Color::Rgb(130, 190, 255)) .add_modifier(Modifier::BOLD | Modifier::UNDERLINED), ) }; - let dim = |s: &'static str| Span::styled(s, Style::default().fg(Color::DarkGray)); + let dim = |s: &'static str| Span::styled(s, Style::default().fg(Color::Rgb(100, 105, 130))); + let divider = || { + Line::from(Span::styled( + " ─────────────────────────────────────────────────────────────", + Style::default().fg(Color::Rgb(55, 60, 90)), + )) + }; vec![ - Line::from(vec![h("Navigation")]), + Line::from(vec![header(" Navigation")]), + divider(), Line::from(vec![ - k(" j / ↓"), + key(" j / ↓"), sep(), - d("Bas"), + desc("Bas"), sep(), sep(), - k("k / ↑"), + key("k / ↑"), sep(), - d("Haut"), + desc("Haut"), ]), Line::from(vec![ - k(" g g"), + key(" g g"), sep(), - d("Premier"), + desc("Premier"), sep(), sep(), - k("G"), + key("G"), sep(), - d("Dernier"), + desc("Dernier"), ]), Line::from(vec![ - k(" Ctrl+j"), + key(" Ctrl+j"), sep(), - d("Scroll prévisualisation ↓"), + desc("Scroll prévisualisation ↓"), sep(), - k("Ctrl+k"), + key("Ctrl+k"), sep(), - d("↑"), + desc("↑"), ]), Line::from(""), - Line::from(vec![h("Actions")]), - Line::from(vec![k(" Entrée"), sep(), d("Coller & quitter")]), + Line::from(vec![header(" Actions")]), + divider(), + Line::from(vec![key(" Entrée"), sep(), desc("Coller et quitter")]), + Line::from(vec![key(" d d"), sep(), desc("Supprimer (confirmation)")]), + Line::from(vec![key(" u"), sep(), desc("Annuler la suppression")]), + Line::from(vec![key(" p"), sep(), desc("★ Épingler / désépingler")]), + Line::from(vec![key(" e"), sep(), desc("🔒 Chiffrer / déchiffrer")]), Line::from(vec![ - k(" d d"), + key(" o"), sep(), - d("Supprimer (demande confirmation)"), + desc("Ouvrir l'URL dans le navigateur"), ]), - Line::from(vec![k(" u"), sep(), d("Annuler la suppression")]), - Line::from(vec![k(" p"), sep(), d("★ Épingler / désépingler")]), - Line::from(vec![k(" e"), sep(), d("🔒 Chiffrer / déchiffrer")]), - Line::from(vec![k(" o"), sep(), d("Ouvrir l'URL dans le navigateur")]), Line::from(vec![ - k(" t"), + key(" t"), sep(), - d("Filtrer par type (Tous → Texte → Image)"), + desc("Filtrer : Tous → Texte → Image"), ]), Line::from(""), - Line::from(vec![h("Recherche (mode /)")]), + Line::from(vec![header(" Recherche ( / )")]), + divider(), Line::from(vec![ - k(" /texte"), + key(" /texte"), sep(), - d("Fuzzy search"), + desc("Fuzzy"), sep(), - k("//regex"), + key("//regex"), sep(), - d("Regex (préfixe /)"), + desc("Regex"), ]), Line::from(vec![ - k(" after:YYYY-MM-DD"), + key(" after:YYYY-MM-DD"), sep(), - d("Après date"), + desc("Après date"), sep(), - k("before:..."), + key("before:…"), sep(), - d("Avant date"), + desc("Avant date"), ]), Line::from(""), - Line::from(vec![h("Commandes (:)")]), + Line::from(vec![header(" Commandes ( : )")]), + divider(), Line::from(vec![ - k(" :clear"), + key(" :clear"), sep(), - d("Effacer tout l'historique"), + desc("Tout effacer"), sep(), - k(":password"), + key(":password"), sep(), - d("Mot de passe session"), + desc("Mot de passe session"), sep(), - k(":q"), + key(":q"), sep(), - d("Quitter"), + desc("Quitter"), ]), Line::from(""), - Line::from(vec![dim(" ? / Esc pour fermer cette aide")]), + Line::from(vec![dim(" Appuyez sur ? ou Esc pour fermer")]), ] } fn render_help_overlay(f: &mut Frame, area: Rect) { - let popup = centered_rect(68, 25, area); + let popup = centered_rect(70, 27, area); f.render_widget(Clear, popup); + let bg = Color::Rgb(22, 26, 50); + let border_color = Color::Rgb(80, 130, 220); + let block = Block::default() .title(Span::styled( - " ? Aide — Raccourcis clavier ", + " ? Raccourcis clavier ", Style::default() - .fg(Color::White) + .fg(Color::Rgb(200, 220, 255)) .add_modifier(Modifier::BOLD), )) .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(Color::Cyan)) - .style(Style::default().bg(Color::Rgb(18, 18, 28))); + .border_style(Style::default().fg(border_color)) + .style(Style::default().bg(bg)); let inner = block.inner(popup); f.render_widget(block, popup); f.render_widget( - Paragraph::new(help_lines()).style(Style::default().bg(Color::Rgb(18, 18, 28))), + Paragraph::new(help_lines()).style(Style::default().bg(bg)), inner, ); }