diff --git a/Cargo.toml b/Cargo.toml index 12d5aa3..f12773a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,6 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" syntect = "5.3.0" uuid = "1.22.0" + +[profile.release] +debug = true diff --git a/profile.json.gz b/profile.json.gz new file mode 100644 index 0000000..e7234fd Binary files /dev/null and b/profile.json.gz differ diff --git a/rklipd/Cargo.toml b/rklipd/Cargo.toml index 7de2839..098a80c 100644 --- a/rklipd/Cargo.toml +++ b/rklipd/Cargo.toml @@ -20,3 +20,6 @@ x11rb = "0.13.2" [features] x11 = [] wayland = [] + +[profile.release] +debug = true diff --git a/rklipd/profile.json.gz b/rklipd/profile.json.gz new file mode 100644 index 0000000..b13e124 Binary files /dev/null and b/rklipd/profile.json.gz differ diff --git a/rklipd/src/config.rs b/rklipd/src/config.rs index 2bc5d02..ce3b556 100644 --- a/rklipd/src/config.rs +++ b/rklipd/src/config.rs @@ -24,27 +24,31 @@ impl Config { while i < args.len() { match args[i].as_str() { "--max-entries" => { - if let Some(v) = args.get(i + 1).and_then(|s| s.parse().ok()) { - cfg.max_entries = v; - i += 1; - } else { - eprintln!("--max-entries requiert une valeur entière positive"); + i += 1; + match args.get(i).and_then(|s| s.parse::().ok()) { + Some(0) => eprintln!("--max-entries doit être > 0"), + Some(v) => cfg.max_entries = v, + None => eprintln!("--max-entries requiert une valeur entière positive"), } } "--max-entry-size-kb" => { - if let Some(v) = args.get(i + 1).and_then(|s| s.parse().ok()) { - cfg.max_entry_size_kb = v; - i += 1; - } else { - eprintln!("--max-entry-size-kb requiert une valeur entière positive"); + i += 1; + match args.get(i).and_then(|s| s.parse::().ok()) { + Some(0) => eprintln!("--max-entry-size-kb doit être > 0"), + Some(v) => cfg.max_entry_size_kb = v, + None => { + eprintln!("--max-entry-size-kb requiert une valeur entière positive") + } } } "--expiry-days" => { - if let Some(v) = args.get(i + 1).and_then(|s| s.parse::().ok()) { - cfg.expiry_days = Some(v); - i += 1; - } else { - eprintln!("--expiry-days requiert une valeur entière positive"); + i += 1; + match args.get(i).and_then(|s| s.parse::().ok()) { + Some(0) => eprintln!( + "--expiry-days doit être > 0 (0 supprimerait tout immédiatement)" + ), + Some(v) => cfg.expiry_days = Some(v), + None => eprintln!("--expiry-days requiert une valeur entière positive"), } } "--help" | "-h" => { diff --git a/rklipd/src/database.rs b/rklipd/src/database.rs index e3f7069..cbfd2ac 100644 --- a/rklipd/src/database.rs +++ b/rklipd/src/database.rs @@ -101,9 +101,7 @@ impl Database { ExtendedColorType::Rgb8, )?; } - None => { - return Ok(()); - } + None => return Ok(()), } ("image", format!("{}.jpg", img.id)) } @@ -115,7 +113,6 @@ impl Database { )?; self.trim_to_max()?; - Ok(()) } @@ -124,8 +121,10 @@ impl Database { return Ok(()); } + let tx = self.conn.unchecked_transaction()?; + let image_files: Vec = { - let mut stmt = self.conn.prepare( + let mut stmt = tx.prepare( "SELECT content FROM history WHERE type = 'image' AND id NOT IN ( @@ -137,13 +136,15 @@ impl Database { .collect() }; - self.conn.execute( + tx.execute( "DELETE FROM history WHERE id NOT IN ( SELECT id FROM history ORDER BY timestamp DESC LIMIT ?1 )", [self.max_entries as i64], )?; + tx.commit()?; + for filename in image_files { let path = Path::new(&self.dir_path).join("images").join(&filename); if path.exists() { @@ -213,18 +214,19 @@ impl Database { let cutoff_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64 - (days as i64 * 86_400_000); + let tx = self.conn.unchecked_transaction()?; + let image_files: Vec = { - let mut stmt = self - .conn - .prepare("SELECT content FROM history WHERE type = 'image' AND timestamp < ?1")?; + let mut stmt = + tx.prepare("SELECT content FROM history WHERE type = 'image' AND timestamp < ?1")?; stmt.query_map([cutoff_ms], |row| row.get(0))? .filter_map(|r| r.ok()) .collect() }; - let count = self - .conn - .execute("DELETE FROM history WHERE timestamp < ?1", [cutoff_ms])?; + let count = tx.execute("DELETE FROM history WHERE timestamp < ?1", [cutoff_ms])?; + + tx.commit()?; for filename in image_files { let path = Path::new(&self.dir_path).join("images").join(&filename); diff --git a/rklipd/src/main.rs b/rklipd/src/main.rs index 24bb575..a41d82e 100644 --- a/rklipd/src/main.rs +++ b/rklipd/src/main.rs @@ -48,13 +48,17 @@ fn main() -> Result<(), Box> { let db_for_expiry = Arc::clone(&db); std::thread::spawn(move || { loop { - std::thread::sleep(Duration::from_secs(3600)); - let lock = db_for_expiry.lock().unwrap(); - match lock.delete_entries_older_than(days) { - Ok(0) => {} - Ok(n) => println!("Expiration : {n} entrée(s) supprimée(s) (> {days} jours)"), - Err(e) => eprintln!("Erreur expiration : {e}"), + { + let lock = db_for_expiry.lock().unwrap(); + match lock.delete_entries_older_than(days) { + Ok(0) => {} + Ok(n) => { + println!("Expiration : {n} entrée(s) supprimée(s) (> {days} jours)") + } + Err(e) => eprintln!("Erreur expiration : {e}"), + } } + std::thread::sleep(Duration::from_secs(3600)); } }); } diff --git a/rklipd/src/monitor.rs b/rklipd/src/monitor.rs index 5964fa4..05f1d07 100644 --- a/rklipd/src/monitor.rs +++ b/rklipd/src/monitor.rs @@ -1,28 +1,42 @@ use crate::database::Database; +use crate::models::ClipboardEntry; use arboard::Clipboard; use std::error::Error; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, mpsc}; pub fn start(db: Arc>, clipboard: Clipboard) -> Result<(), Box> { + let (tx, rx) = mpsc::channel::(); + + std::thread::spawn(move || { + for entry in rx { + let lock = db.lock().unwrap(); + if let Err(e) = lock.append(entry) { + eprintln!("SQLite write error: {e}"); + } else { + println!("SQLite updated!"); + } + } + }); + #[cfg(all(feature = "wayland", not(feature = "x11")))] { - crate::ws::wayland::start(db, clipboard) + crate::ws::wayland::start(tx, clipboard) } #[cfg(all(feature = "x11", not(feature = "wayland")))] { - crate::ws::x11::start(db, clipboard) + crate::ws::x11::start(tx, clipboard) } #[cfg(all(feature = "x11", feature = "wayland"))] { - let _ = (db, clipboard); + let _ = (tx, clipboard); Err("Les features 'x11' et 'wayland' sont mutuellement exclusives".into()) } #[cfg(not(any(feature = "x11", feature = "wayland")))] { - let _ = (db, clipboard); + let _ = (tx, clipboard); Err("Aucune feature de système de fenêtrage activée (--features x11 ou wayland)".into()) } } diff --git a/rklipd/src/ws/wayland.rs b/rklipd/src/ws/wayland.rs index d56045e..1b1b804 100644 --- a/rklipd/src/ws/wayland.rs +++ b/rklipd/src/ws/wayland.rs @@ -1,15 +1,22 @@ -use crate::database::Database; use crate::models::{ClipboardData, ClipboardEntry, Image}; +use std::collections::hash_map::DefaultHasher; use std::error::Error; -use std::sync::{Arc, Mutex}; +use std::hash::{Hash, Hasher}; +use std::sync::mpsc; use std::time::SystemTime; use uuid::Uuid; use wayland_clipboard_listener::{WlClipboardPasteStream, WlListenType}; const MAX_IMAGE_PIXELS: usize = 3840 * 2160; +fn hash_bytes(data: &[u8]) -> u64 { + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + hasher.finish() +} + pub fn start( - db: Arc>, + tx: mpsc::Sender, _clipboard: arboard::Clipboard, ) -> Result<(), Box> { let mut stream = WlClipboardPasteStream::init(WlListenType::ListenOnCopy) @@ -17,9 +24,11 @@ pub fn start( println!("Écoute du presse-papier Wayland..."); + let mut last_text: Option = None; + let mut last_image_hash: Option = None; + for msg in stream.paste_stream().flatten() { - let context = &msg.context; - let data: &[u8] = context.context.as_slice(); + let data: &[u8] = msg.context.context.as_slice(); if data.is_empty() { continue; @@ -27,14 +36,22 @@ pub fn start( let entry = if let Ok(text) = String::from_utf8(data.to_vec()) { let text = text.trim_end_matches('\n').to_string(); - if text.is_empty() { + if text.is_empty() || Some(&text) == last_text.as_ref() { continue; } + last_text = Some(text.clone()); + last_image_hash = None; + println!("Clipboard update (texte)"); ClipboardEntry { content: ClipboardData::Text(text), timestamp: SystemTime::now(), } } else { + let hash = hash_bytes(data); + if Some(hash) == last_image_hash { + continue; + } + match image::load_from_memory(data) { Ok(img) => { let (width, height) = (img.width(), img.height()); @@ -48,9 +65,15 @@ pub fn start( 3840, 2160 ); + last_image_hash = Some(hash); + last_text = None; continue; } + last_image_hash = Some(hash); + last_text = None; + println!("Clipboard update (image)"); + let rgba = img.into_rgba8(); ClipboardEntry { content: ClipboardData::Image(Image { @@ -69,15 +92,10 @@ pub fn start( } }; - println!("Clipboard update détecté"); - - let db_clone = Arc::clone(&db); - std::thread::spawn(move || { - let db_lock = db_clone.lock().unwrap(); - if let Err(e) = db_lock.append(entry) { - eprintln!("SQLite error : {e}"); - } - }); + if tx.send(entry).is_err() { + eprintln!("Wayland : writer thread disparu, arrêt"); + break; + } } Ok(()) diff --git a/rklipd/src/ws/x11.rs b/rklipd/src/ws/x11.rs index 11fe6c8..357f44b 100644 --- a/rklipd/src/ws/x11.rs +++ b/rklipd/src/ws/x11.rs @@ -1,10 +1,9 @@ -use crate::database::Database; use crate::models::{ClipboardData, ClipboardEntry, Image}; use arboard::Clipboard; use std::collections::hash_map::DefaultHasher; use std::error::Error; use std::hash::{Hash, Hasher}; -use std::sync::{Arc, Mutex}; +use std::sync::mpsc; use std::thread; use std::time::{Duration, SystemTime}; use uuid::Uuid; @@ -22,7 +21,10 @@ fn hash_bytes(data: &[u8]) -> u64 { hasher.finish() } -pub fn start(db: Arc>, mut clipboard: Clipboard) -> Result<(), Box> { +pub fn start( + tx: mpsc::Sender, + mut clipboard: Clipboard, +) -> Result<(), Box> { let (conn, screen_num) = RustConnection::connect(None).map_err(|e| format!("Connexion X11 impossible : {e}"))?; @@ -49,15 +51,14 @@ pub fn start(db: Arc>, mut clipboard: Clipboard) -> Result<(), B .reply()?; let clipboard_atom = conn.intern_atom(false, b"CLIPBOARD")?.reply()?.atom; - conn.xfixes_select_selection_input( win, clipboard_atom, SelectionEventMask::SET_SELECTION_OWNER, )? .check()?; - conn.flush()?; + println!("Clipboard monitor démarré (X11 XFIXES — zéro polling)"); let mut last_text: Option = None; @@ -68,14 +69,14 @@ pub fn start(db: Arc>, mut clipboard: Clipboard) -> Result<(), B if let Event::XfixesSelectionNotify(_) = event { thread::sleep(Duration::from_millis(50)); - handle_clipboard_event(&mut clipboard, &db, &mut last_text, &mut last_image_hash); + handle_clipboard_event(&mut clipboard, &tx, &mut last_text, &mut last_image_hash); } } } fn handle_clipboard_event( clipboard: &mut Clipboard, - db: &Arc>, + tx: &mpsc::Sender, last_text: &mut Option, last_image_hash: &mut Option, ) { @@ -89,13 +90,15 @@ fn handle_clipboard_event( *last_image_hash = None; println!("Clipboard update (texte)"); - spawn_db_write( - Arc::clone(db), - ClipboardEntry { + if tx + .send(ClipboardEntry { content: ClipboardData::Text(text), timestamp: SystemTime::now(), - }, - ); + }) + .is_err() + { + eprintln!("X11 : writer thread disparu"); + } } Err(_) => { @@ -124,29 +127,20 @@ fn handle_clipboard_event( *last_text = None; println!("Clipboard update (image)"); - spawn_db_write( - Arc::clone(db), - ClipboardEntry { - content: ClipboardData::Image(crate::models::Image { + if tx + .send(ClipboardEntry { + content: ClipboardData::Image(Image { raw_pixels: Some(img_data.bytes.into_owned()), width: img_data.width as u32, height: img_data.height as u32, id: Uuid::new_v4(), }), timestamp: SystemTime::now(), - }, - ); + }) + .is_err() + { + eprintln!("X11 : writer thread disparu"); + } } } } - -fn spawn_db_write(db: Arc>, entry: ClipboardEntry) { - thread::spawn(move || { - let lock = db.lock().unwrap(); - if let Err(e) = lock.append(entry) { - eprintln!("SQLite write error: {e}"); - } else { - println!("SQLite updated!"); - } - }); -} diff --git a/src/app.rs b/src/app.rs index e9f05d5..eb82c79 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,19 +3,22 @@ use crate::ipc::{self, HistoryItem}; use chrono::{Local, NaiveDate, TimeZone}; use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use image::DynamicImage; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span}; use ratatui::widgets::ListState; use ratatui_image::{picker::Picker, protocol}; use regex::Regex; use std::collections::{HashMap, VecDeque}; +use std::sync::Arc; use std::time::{Duration, Instant}; -use syntect::highlighting::ThemeSet; +use syntect::easy::HighlightLines; +use syntect::highlighting::{FontStyle as SynFontStyle, ThemeSet}; use syntect::parsing::SyntaxSet; +use syntect::util::LinesWithEndings; const PREVIEW_MAX_WIDTH: u32 = 1280; const PREVIEW_MAX_HEIGHT: u32 = 720; - const IMAGE_CACHE_MAX: usize = 8; - const PAGE_SIZE: usize = 50; #[derive(PartialEq, Clone)] @@ -56,7 +59,9 @@ pub struct App { pub type_filter: TypeFilter, pub loaded_count: usize, pub has_more: bool, - image_cache: HashMap, + pub preview_highlighted: Option>>, + pub preview_lang: Option, + image_cache: HashMap>, image_cache_order: VecDeque, } @@ -85,6 +90,53 @@ impl TypeFilter { } } +fn syn_color(c: syntect::highlighting::Color) -> Color { + Color::Rgb(c.r, c.g, c.b) +} + +pub fn highlight_code( + content: &str, + syntax_set: &SyntaxSet, + theme_set: &ThemeSet, +) -> Vec> { + let theme = &theme_set.themes["base16-ocean.dark"]; + let syntax = syntax_set + .find_syntax_by_first_line(content) + .unwrap_or_else(|| syntax_set.find_syntax_plain_text()); + + let mut h = HighlightLines::new(syntax, theme); + let mut lines = Vec::new(); + + for (no, line) in LinesWithEndings::from(content).enumerate() { + let ranges = h.highlight_line(line, syntax_set).unwrap_or_default(); + let mut spans = vec![Span::styled( + format!("{:>4} │ ", no + 1), + Style::default().fg(Color::Rgb(80, 80, 100)), + )]; + for (style, text) in &ranges { + let mut s = Style::default().fg(syn_color(style.foreground)); + if style.font_style.contains(SynFontStyle::BOLD) { + s = s.add_modifier(Modifier::BOLD); + } + if style.font_style.contains(SynFontStyle::ITALIC) { + s = s.add_modifier(Modifier::ITALIC); + } + spans.push(Span::styled(text.trim_end_matches('\n').to_string(), s)); + } + lines.push(Line::from(spans)); + } + lines +} + +pub fn detect_lang(content: &str, syntax_set: &SyntaxSet) -> Option { + let s = syntax_set.find_syntax_by_first_line(content)?; + if s.name == "Plain Text" { + None + } else { + Some(s.name.clone()) + } +} + impl App { pub fn new() -> Self { let items = ipc::fetch_history(PAGE_SIZE).unwrap_or_default(); @@ -94,9 +146,19 @@ impl App { let picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks()); - let salt = directories::ProjectDirs::from("com", "zefad", "rklipd") - .and_then(|d| Crypto::load_or_create_salt(d.data_dir()).ok()) - .unwrap_or_else(|| vec![0u8; 32]); + let salt = match directories::ProjectDirs::from("com", "zefad", "rklipd") { + Some(dirs) => match Crypto::load_or_create_salt(dirs.data_dir()) { + Ok(s) => s, + Err(e) => { + eprintln!("Erreur sel cryptographique : {e}"); + vec![0u8; 32] + } + }, + None => { + eprintln!("Impossible de déterminer le répertoire de données"); + vec![0u8; 32] + } + }; let mut app = Self { mode: Mode::Normal, @@ -120,6 +182,8 @@ impl App { type_filter: TypeFilter::All, loaded_count: PAGE_SIZE, has_more, + preview_highlighted: None, + preview_lang: None, image_cache: HashMap::new(), image_cache_order: VecDeque::new(), }; @@ -169,29 +233,34 @@ impl App { &mut self, filename: &str, base_dir: &std::path::Path, - ) -> Option { - 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); - } - } + ) -> Option> { + if self.image_cache.contains_key(filename) { + self.image_cache_order.retain(|k| k != filename); self.image_cache_order.push_back(filename.to_string()); - self.image_cache.insert(filename.to_string(), img); + return self.image_cache.get(filename).cloned(); } - self.image_cache.get(filename).cloned() + 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); + } + } + let arc = Arc::new(img); + self.image_cache_order.push_back(filename.to_string()); + self.image_cache + .insert(filename.to_string(), Arc::clone(&arc)); + Some(arc) } pub fn format_timestamp(ts_ms: i64) -> String { @@ -238,7 +307,15 @@ impl App { return false; } } - true + match self.type_filter { + TypeFilter::All => true, + TypeFilter::Text => { + !item.content.ends_with(".jpg") && !item.content.ends_with(".png") + } + TypeFilter::Image => { + item.content.ends_with(".jpg") || item.content.ends_with(".png") + } + } }) .cloned() .collect(); @@ -289,12 +366,6 @@ impl App { matched.into_iter().map(|(_, i)| i).collect() }; - self.filtered_items.retain(|item| match self.type_filter { - TypeFilter::All => true, - TypeFilter::Text => !item.content.ends_with(".jpg") && !item.content.ends_with(".png"), - TypeFilter::Image => item.content.ends_with(".jpg") || item.content.ends_with(".png"), - }); - self.list_state.select(if self.filtered_items.is_empty() { None } else { @@ -313,16 +384,12 @@ impl App { 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(); } @@ -367,13 +434,13 @@ impl App { self.list_state.select(new_sel); } } + self.last_selected_index = None; self.update_preview(); } pub fn undo_delete(&mut self) { if let Some(item) = self.undo_stack.pop() { 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(self.loaded_count) { self.has_more = new_items.len() == self.loaded_count; self.all_items = new_items; @@ -545,6 +612,8 @@ impl App { self.current_image = None; self.image_cache.clear(); self.image_cache_order.clear(); + self.preview_highlighted = None; + self.preview_lang = None; self.loaded_count = PAGE_SIZE; self.has_more = false; self.set_status("Historique effacé".into()); @@ -561,6 +630,8 @@ impl App { self.last_selected_index = idx; self.current_image = None; self.preview_scroll = 0; + self.preview_highlighted = None; + self.preview_lang = None; let content = match self.get_selected_item().map(|i| i.content.clone()) { Some(c) => c, @@ -569,12 +640,15 @@ impl App { if content.ends_with(".jpg") || content.ends_with(".png") { if let Some(dirs) = directories::ProjectDirs::from("com", "zefad", "rklipd") { - if let Some(img) = self.get_cached_image(&content, dirs.data_dir()) { - // new_resize_protocol attend un DynamicImage — on clone depuis le cache - // (l'image est déjà redimensionnée ≤ 1280×720, clone = ~3,5 Mo max) + if let Some(arc_img) = self.get_cached_image(&content, dirs.data_dir()) { + let img = (*arc_img).clone(); self.current_image = Some(self.picker.new_resize_protocol(img)); } } + } else if !Crypto::is_any_encrypted(&content) { + self.preview_lang = detect_lang(&content, &self.syntax_set); + self.preview_highlighted = + Some(highlight_code(&content, &self.syntax_set, &self.theme_set)); } } @@ -596,7 +670,6 @@ impl App { return; }; - // 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() diff --git a/src/crypto.rs b/src/crypto.rs index 1cf243f..8425fc7 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -34,6 +34,14 @@ impl Crypto { if bytes.len() == SALT_LEN { return Ok(bytes); } + return Err(format!( + "Fichier sel corrompu ({} octets au lieu de {}). \ + Supprimez {:?} manuellement si vous souhaitez réinitialiser le chiffrement.", + bytes.len(), + SALT_LEN, + path + ) + .into()); } let mut salt = vec![0u8; SALT_LEN]; OsRng.fill_bytes(&mut salt); diff --git a/src/ipc.rs b/src/ipc.rs index 3b4a482..53549f5 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -63,8 +63,11 @@ pub fn set_clipboard(content: String) -> bool { ) } -pub fn delete_entry(content: String) { - let _ = send_request(&IpcRequest::DeleteEntry { content }); +pub fn delete_entry(content: String) -> bool { + matches!( + send_request(&IpcRequest::DeleteEntry { content }), + Some(IpcResponse::Ok) + ) } pub fn update_entry(old_content: String, new_content: String) -> bool { diff --git a/src/main.rs b/src/main.rs index 177d5ee..74296c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -177,8 +177,14 @@ fn run_app(terminal: &mut Terminal>, app: &mut App) Mode::ConfirmDelete => match key.code { KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => { if let Some(item) = app.get_selected_item() { - ipc::delete_entry(item.content.clone()); - app.delete_selected(); + let content = item.content.clone(); + if ipc::delete_entry(content) { + app.delete_selected(); + } else { + app.set_error( + "Erreur : daemon injoignable, entrée non supprimée".into(), + ); + } } app.mode = Mode::Normal; } diff --git a/src/ui.rs b/src/ui.rs index 94eb52b..ba9100a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,4 @@ -use crate::app::{App, Mode}; +use crate::app::{App, Mode, detect_lang, highlight_code}; use crate::crypto::Crypto; use ratatui::{ Frame, @@ -8,55 +8,6 @@ use ratatui::{ widgets::{Block, BorderType, Borders, List, ListItem, Padding, Paragraph}, }; use ratatui_image::StatefulImage; -use syntect::easy::HighlightLines; -use syntect::highlighting::FontStyle as SynFontStyle; -use syntect::util::LinesWithEndings; - -fn syn_color(c: syntect::highlighting::Color) -> Color { - Color::Rgb(c.r, c.g, c.b) -} - -fn highlight_code(content: &str, app: &App) -> Vec> { - let ps = &app.syntax_set; - let ts = &app.theme_set; - let theme = &ts.themes["base16-ocean.dark"]; - - let syntax = ps - .find_syntax_by_first_line(content) - .unwrap_or_else(|| ps.find_syntax_plain_text()); - - let mut h = HighlightLines::new(syntax, theme); - let mut lines = Vec::new(); - - for (no, line) in LinesWithEndings::from(content).enumerate() { - let ranges = h.highlight_line(line, ps).unwrap_or_default(); - let mut spans = vec![Span::styled( - format!("{:>4} │ ", no + 1), - Style::default().fg(Color::Rgb(80, 80, 100)), - )]; - for (style, text) in &ranges { - let mut s = Style::default().fg(syn_color(style.foreground)); - if style.font_style.contains(SynFontStyle::BOLD) { - s = s.add_modifier(Modifier::BOLD); - } - if style.font_style.contains(SynFontStyle::ITALIC) { - s = s.add_modifier(Modifier::ITALIC); - } - spans.push(Span::styled(text.trim_end_matches('\n').to_string(), s)); - } - lines.push(Line::from(spans)); - } - lines -} - -fn detect_lang(content: &str, app: &App) -> Option { - let s = app.syntax_set.find_syntax_by_first_line(content)?; - if s.name == "Plain Text" { - None - } else { - Some(s.name.clone()) - } -} pub fn render(f: &mut Frame, app: &mut App) { let outer = Layout::default() @@ -103,7 +54,7 @@ pub fn render(f: &mut Frame, app: &mut App) { let preview: String = item .content .lines() - .next() + .find(|l| !l.trim().is_empty()) .unwrap_or("") .chars() .take(28) @@ -139,12 +90,7 @@ pub fn render(f: &mut Frame, app: &mut App) { let selected_content = app.get_selected_item().map(|i| i.content.clone()); - let lang = selected_content - .as_deref() - .filter(|c| !Crypto::is_any_encrypted(c) && !c.ends_with(".jpg") && !c.ends_with(".png")) - .and_then(|c| detect_lang(c, app)); - - let preview_title = match &lang { + let preview_title = match &app.preview_lang { Some(l) => format!(" Prévisualisation — {} ", l), None => " Prévisualisation ".to_string(), }; @@ -165,20 +111,19 @@ pub fn render(f: &mut Frame, app: &mut App) { let inner = preview_block.inner(panels[1]); f.render_widget(preview_block, panels[1]); - if app.current_image.is_some() { - let state = app.current_image.as_mut().unwrap(); + let scroll = (app.preview_scroll, 0); + + if let Some(state) = app.current_image.as_mut() { f.render_stateful_widget(StatefulImage::default(), inner, state); } else if let Some(content) = &selected_content { - let scroll = (app.preview_scroll, 0); if Crypto::is_any_encrypted(content) { f.render_widget( Paragraph::new("🔒 Contenu chiffré\n\nAppuyez sur [e] pour déchiffrer.") .scroll(scroll), inner, ); - } else { - let lines = highlight_code(content, app); - f.render_widget(Paragraph::new(lines).scroll(scroll), inner); + } else if let Some(lines) = &app.preview_highlighted { + f.render_widget(Paragraph::new(lines.clone()).scroll(scroll), inner); } } @@ -190,20 +135,6 @@ pub fn render(f: &mut Frame, app: &mut App) { Mode::PasswordInput => (" MOT DE PASSE ", Color::Magenta), }; - let extra = match &app.mode { - Mode::Search => format!(" /{}", app.input_buffer), - Mode::Command => format!(" :{}", app.input_buffer), - Mode::PasswordInput => format!(" {}", "●".repeat(app.input_buffer.len())), - _ => String::new(), - }; - - // let msg_span = if let Some((msg, _)) = &app.error_message { - // Span::styled(format!(" ⚠ {msg}"), Style::default().fg(Color::Red)) - // } else if let Some((msg, _)) = &app.status_message { - // Span::styled(format!(" ✓ {msg}"), Style::default().fg(Color::Green)) - // } else { - // Span::raw(extra) - // }; let filter_hint = match app.type_filter { crate::app::TypeFilter::All => String::new(), f => format!(" [{}]", f.label()), @@ -216,7 +147,6 @@ pub fn render(f: &mut Frame, app: &mut App) { } else { let extra = match &app.mode { Mode::Search => { - // Indicateur visuel du mode de recherche actif (fuzzy vs regexp) let mode_hint = if app.input_buffer.trim_start().starts_with('/') { "re" } else {