This commit is contained in:
2026-05-21 09:54:09 +02:00
parent 041e90a8f2
commit 4f18a72785
6 changed files with 125 additions and 59 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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<u8> = 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(()),
}

View File

@ -9,7 +9,13 @@ pub fn start(db: Arc<Mutex<Database>>, clipboard: Clipboard) -> Result<(), Box<d
std::thread::spawn(move || {
for entry in rx {
let lock = db.lock().unwrap();
let lock = match db.lock() {
Ok(l) => 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 {

View File

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

View File

@ -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<HistoryItem>,
pub filtered_items: Vec<HistoryItem>,
pub list_state: ListState,
pub input_buffer: String,
pub should_quit: bool,
pub undo_stack: Vec<HistoryItem>,
pub current_image: Option<protocol::StatefulProtocol>,
pub last_selected_index: Option<usize>,
pub picker: Picker,
pub preview_scroll: u16,
pub crypto: Option<Crypto>,
pub salt: Vec<u8>,
pub pending_action: Option<PendingAction>,
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<Vec<Line<'static>>>,
pub preview_lang: Option<String>,
image_cache: HashMap<String, Arc<DynamicImage>>,
image_cache_order: VecDeque<String>,
}
#[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<HistoryItem>,
pub filtered_items: Vec<HistoryItem>,
pub list_state: ListState,
pub input_buffer: String,
pub should_quit: bool,
pub undo_stack: Vec<HistoryItem>,
pub current_image: Option<protocol::StatefulProtocol>,
pub last_selected_index: Option<usize>,
pub picker: Picker,
pub preview_scroll: u16,
pub crypto: Option<Crypto>,
pub salt: Vec<u8>,
pub pending_action: Option<PendingAction>,
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<Vec<Line<'static>>>,
pub preview_lang: Option<String>,
last_sync: Instant,
image_cache: HashMap<String, Arc<DynamicImage>>,
image_cache_order: VecDeque<String>,
}
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<Line<'static>> {
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<String> {
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()));
}