diff --git a/profile.json.gz b/profile.json.gz deleted file mode 100644 index e7234fd..0000000 Binary files a/profile.json.gz and /dev/null differ diff --git a/rklipd/profile.json.gz b/rklipd/profile.json.gz deleted file mode 100644 index b13e124..0000000 Binary files a/rklipd/profile.json.gz and /dev/null differ diff --git a/rklipd/src/database.rs b/rklipd/src/database.rs index cbfd2ac..09ec52d 100644 --- a/rklipd/src/database.rs +++ b/rklipd/src/database.rs @@ -80,26 +80,30 @@ impl Database { ClipboardData::Image(img) => { match &img.raw_pixels { Some(px) => { - if px.len() > self.max_entry_size_bytes * 4 { - eprintln!( - "Image rejetée dans DB : {} Mo > limite {} Mo", - px.len() / 1_048_576, - (self.max_entry_size_bytes * 4) / 1_048_576 - ); - return Ok(()); - } - let path = img.file_path(&self.dir_path); - let file = fs::File::create(&path)?; let rgb: Vec = px .chunks_exact(4) .flat_map(|rgba| [rgba[0], rgba[1], rgba[2]]) .collect(); - JpegEncoder::new_with_quality(file, 70).write_image( + + let mut jpeg_buf = Vec::new(); + JpegEncoder::new_with_quality(&mut jpeg_buf, 70).write_image( &rgb, img.width, img.height, ExtendedColorType::Rgb8, )?; + + if jpeg_buf.len() > self.max_entry_size_bytes { + eprintln!( + "Image rejetée dans DB : JPEG {} Ko > limite {} Ko", + jpeg_buf.len() / 1024, + self.max_entry_size_bytes / 1024 + ); + return Ok(()); + } + + let path = img.file_path(&self.dir_path); + fs::write(&path, &jpeg_buf)?; } None => return Ok(()), } diff --git a/rklipd/src/monitor.rs b/rklipd/src/monitor.rs index 05f1d07..95c1ad2 100644 --- a/rklipd/src/monitor.rs +++ b/rklipd/src/monitor.rs @@ -9,7 +9,13 @@ pub fn start(db: Arc>, clipboard: Clipboard) -> Result<(), Box l, + Err(poisoned) => { + eprintln!("Mutex DB empoisonné, récupération forcée"); + poisoned.into_inner() + } + }; if let Err(e) = lock.append(entry) { eprintln!("SQLite write error: {e}"); } else { diff --git a/rklipd/src/ws/x11.rs b/rklipd/src/ws/x11.rs index 357f44b..76fd6b2 100644 --- a/rklipd/src/ws/x11.rs +++ b/rklipd/src/ws/x11.rs @@ -114,7 +114,8 @@ fn handle_clipboard_event( img_data.height, pixel_count / 1_000_000 ); - *last_image_hash = Some(pixel_count as u64); + let sentinel_hash = hash_bytes(&img_data.bytes[..img_data.bytes.len().min(256)]); + *last_image_hash = Some(sentinel_hash); *last_text = None; return; } diff --git a/src/app.rs b/src/app.rs index eb82c79..1cab53f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,6 +20,13 @@ const PREVIEW_MAX_WIDTH: u32 = 1280; const PREVIEW_MAX_HEIGHT: u32 = 720; const IMAGE_CACHE_MAX: usize = 8; const PAGE_SIZE: usize = 50; +const MAX_HIGHLIGHT_LINES: usize = 500; +const SYNC_INTERVAL_MS: u64 = 1000; + +#[inline] +pub fn is_image(s: &str) -> bool { + s.ends_with(".jpg") || s.ends_with(".png") +} #[derive(PartialEq, Clone)] pub enum Mode { @@ -37,34 +44,6 @@ pub enum PendingAction { PasteEncrypted, } -pub struct App { - pub mode: Mode, - pub all_items: Vec, - pub filtered_items: Vec, - pub list_state: ListState, - pub input_buffer: String, - pub should_quit: bool, - pub undo_stack: Vec, - pub current_image: Option, - pub last_selected_index: Option, - pub picker: Picker, - pub preview_scroll: u16, - pub crypto: Option, - pub salt: Vec, - pub pending_action: Option, - pub error_message: Option<(String, Instant)>, - pub status_message: Option<(String, Instant)>, - pub syntax_set: SyntaxSet, - pub theme_set: ThemeSet, - pub type_filter: TypeFilter, - pub loaded_count: usize, - pub has_more: bool, - pub preview_highlighted: Option>>, - pub preview_lang: Option, - image_cache: HashMap>, - image_cache_order: VecDeque, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TypeFilter { All, @@ -90,6 +69,35 @@ impl TypeFilter { } } +pub struct App { + pub mode: Mode, + pub all_items: Vec, + pub filtered_items: Vec, + pub list_state: ListState, + pub input_buffer: String, + pub should_quit: bool, + pub undo_stack: Vec, + pub current_image: Option, + pub last_selected_index: Option, + pub picker: Picker, + pub preview_scroll: u16, + pub crypto: Option, + pub salt: Vec, + pub pending_action: Option, + pub error_message: Option<(String, Instant)>, + pub status_message: Option<(String, Instant)>, + pub syntax_set: SyntaxSet, + pub theme_set: ThemeSet, + pub type_filter: TypeFilter, + pub loaded_count: usize, + pub has_more: bool, + pub preview_highlighted: Option>>, + pub preview_lang: Option, + last_sync: Instant, + image_cache: HashMap>, + image_cache_order: VecDeque, +} + fn syn_color(c: syntect::highlighting::Color) -> Color { Color::Rgb(c.r, c.g, c.b) } @@ -100,14 +108,14 @@ pub fn highlight_code( 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 syntax = detect_syntax(content, syntax_set); let mut h = HighlightLines::new(syntax, theme); let mut lines = Vec::new(); - for (no, line) in LinesWithEndings::from(content).enumerate() { + for (no, line) in LinesWithEndings::from(content) + .enumerate() + .take(MAX_HIGHLIGHT_LINES) + { let ranges = h.highlight_line(line, syntax_set).unwrap_or_default(); let mut spans = vec![Span::styled( format!("{:>4} │ ", no + 1), @@ -125,11 +133,51 @@ pub fn highlight_code( } lines.push(Line::from(spans)); } + + let total_lines = content.lines().count(); + if total_lines > MAX_HIGHLIGHT_LINES { + lines.push(Line::from(Span::styled( + format!( + " … {} lignes supplémentaires non affichées", + total_lines - MAX_HIGHLIGHT_LINES + ), + Style::default().fg(Color::Rgb(100, 100, 120)), + ))); + } + lines } +fn detect_syntax<'a>( + content: &str, + syntax_set: &'a SyntaxSet, +) -> &'a syntect::parsing::SyntaxReference { + if let Some(s) = syntax_set.find_syntax_by_first_line(content) { + if s.name != "Plain Text" { + return s; + } + } + + for line in content.lines().take(3) { + let trimmed = line.trim_start_matches(|c: char| !c.is_alphanumeric() && c != '.'); + if let Some(word) = trimmed.split_whitespace().last() { + if let Some(ext) = word.rsplit('.').next() { + if ext.len() <= 6 && ext.chars().all(|c| c.is_ascii_alphanumeric()) { + if let Some(s) = syntax_set.find_syntax_by_extension(ext) { + if s.name != "Plain Text" { + return s; + } + } + } + } + } + } + + syntax_set.find_syntax_plain_text() +} + pub fn detect_lang(content: &str, syntax_set: &SyntaxSet) -> Option { - let s = syntax_set.find_syntax_by_first_line(content)?; + let s = detect_syntax(content, syntax_set); if s.name == "Plain Text" { None } else { @@ -182,6 +230,7 @@ impl App { type_filter: TypeFilter::All, loaded_count: PAGE_SIZE, has_more, + last_sync: Instant::now() - Duration::from_secs(10), preview_highlighted: None, preview_lang: None, image_cache: HashMap::new(), @@ -268,10 +317,13 @@ impl App { let nsecs = ((ts_ms % 1000) * 1_000_000) as u32; match Local.timestamp_opt(secs, nsecs) { chrono::LocalResult::Single(dt) => { - let diff = Local::now().signed_duration_since(dt); - if diff.num_days() == 0 { + let today = Local::now().date_naive(); + let entry_date = dt.date_naive(); + let diff_days = (today - entry_date).num_days(); + + if diff_days == 0 { dt.format("%H:%M:%S").to_string() - } else if diff.num_days() < 365 { + } else if diff_days < 365 { dt.format("%d %b %H:%M").to_string() } else { dt.format("%d/%m/%Y").to_string() @@ -309,12 +361,8 @@ impl App { } 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") - } + TypeFilter::Text => !is_image(&item.content), + TypeFilter::Image => is_image(&item.content), } }) .cloned() @@ -323,7 +371,7 @@ impl App { let search_str = |item: &HistoryItem| -> String { if Crypto::is_any_encrypted(&item.content) { "[chiffré]".to_string() - } else if item.content.ends_with(".jpg") || item.content.ends_with(".png") { + } else if is_image(&item.content) { format!("image {}", item.content) } else { item.content.clone() @@ -420,7 +468,7 @@ impl App { let item = self.filtered_items.remove(i); self.undo_stack.push(item.clone()); self.all_items.retain(|x| x.content != item.content); - if item.content.ends_with(".jpg") || item.content.ends_with(".png") { + if is_image(&item.content) { self.image_cache.remove(&item.content); self.image_cache_order.retain(|k| k != &item.content); } @@ -638,7 +686,7 @@ impl App { None => return, }; - if content.ends_with(".jpg") || content.ends_with(".png") { + 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()) { let img = (*arc_img).clone(); @@ -655,6 +703,7 @@ impl App { pub fn scroll_preview_down(&mut self) { self.preview_scroll = self.preview_scroll.saturating_add(3); } + pub fn scroll_preview_up(&mut self) { self.preview_scroll = self.preview_scroll.saturating_sub(3); } @@ -666,6 +715,11 @@ impl App { } pub fn sync_with_daemon(&mut self) { + if self.last_sync.elapsed() < Duration::from_millis(SYNC_INTERVAL_MS) { + return; + } + self.last_sync = Instant::now(); + let Some(new) = ipc::fetch_history(self.loaded_count) else { return; }; @@ -701,6 +755,7 @@ impl App { pub fn set_error(&mut self, msg: String) { self.error_message = Some((msg, Instant::now())); } + pub fn set_status(&mut self, msg: String) { self.status_message = Some((msg, Instant::now())); }