opti
This commit is contained in:
@ -20,3 +20,6 @@ serde = { version = "1.0.228", features = ["derive"] }
|
|||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
syntect = "5.3.0"
|
syntect = "5.3.0"
|
||||||
uuid = "1.22.0"
|
uuid = "1.22.0"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|||||||
BIN
profile.json.gz
Normal file
BIN
profile.json.gz
Normal file
Binary file not shown.
@ -20,3 +20,6 @@ x11rb = "0.13.2"
|
|||||||
[features]
|
[features]
|
||||||
x11 = []
|
x11 = []
|
||||||
wayland = []
|
wayland = []
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|||||||
BIN
rklipd/profile.json.gz
Normal file
BIN
rklipd/profile.json.gz
Normal file
Binary file not shown.
@ -24,27 +24,31 @@ impl Config {
|
|||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
match args[i].as_str() {
|
match args[i].as_str() {
|
||||||
"--max-entries" => {
|
"--max-entries" => {
|
||||||
if let Some(v) = args.get(i + 1).and_then(|s| s.parse().ok()) {
|
i += 1;
|
||||||
cfg.max_entries = v;
|
match args.get(i).and_then(|s| s.parse::<usize>().ok()) {
|
||||||
i += 1;
|
Some(0) => eprintln!("--max-entries doit être > 0"),
|
||||||
} else {
|
Some(v) => cfg.max_entries = v,
|
||||||
eprintln!("--max-entries requiert une valeur entière positive");
|
None => eprintln!("--max-entries requiert une valeur entière positive"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"--max-entry-size-kb" => {
|
"--max-entry-size-kb" => {
|
||||||
if let Some(v) = args.get(i + 1).and_then(|s| s.parse().ok()) {
|
i += 1;
|
||||||
cfg.max_entry_size_kb = v;
|
match args.get(i).and_then(|s| s.parse::<usize>().ok()) {
|
||||||
i += 1;
|
Some(0) => eprintln!("--max-entry-size-kb doit être > 0"),
|
||||||
} else {
|
Some(v) => cfg.max_entry_size_kb = v,
|
||||||
eprintln!("--max-entry-size-kb requiert une valeur entière positive");
|
None => {
|
||||||
|
eprintln!("--max-entry-size-kb requiert une valeur entière positive")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"--expiry-days" => {
|
"--expiry-days" => {
|
||||||
if let Some(v) = args.get(i + 1).and_then(|s| s.parse::<u64>().ok()) {
|
i += 1;
|
||||||
cfg.expiry_days = Some(v);
|
match args.get(i).and_then(|s| s.parse::<u64>().ok()) {
|
||||||
i += 1;
|
Some(0) => eprintln!(
|
||||||
} else {
|
"--expiry-days doit être > 0 (0 supprimerait tout immédiatement)"
|
||||||
eprintln!("--expiry-days requiert une valeur entière positive");
|
),
|
||||||
|
Some(v) => cfg.expiry_days = Some(v),
|
||||||
|
None => eprintln!("--expiry-days requiert une valeur entière positive"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
|
|||||||
@ -101,9 +101,7 @@ impl Database {
|
|||||||
ExtendedColorType::Rgb8,
|
ExtendedColorType::Rgb8,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
None => {
|
None => return Ok(()),
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
("image", format!("{}.jpg", img.id))
|
("image", format!("{}.jpg", img.id))
|
||||||
}
|
}
|
||||||
@ -115,7 +113,6 @@ impl Database {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.trim_to_max()?;
|
self.trim_to_max()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,8 +121,10 @@ impl Database {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tx = self.conn.unchecked_transaction()?;
|
||||||
|
|
||||||
let image_files: Vec<String> = {
|
let image_files: Vec<String> = {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = tx.prepare(
|
||||||
"SELECT content FROM history
|
"SELECT content FROM history
|
||||||
WHERE type = 'image'
|
WHERE type = 'image'
|
||||||
AND id NOT IN (
|
AND id NOT IN (
|
||||||
@ -137,13 +136,15 @@ impl Database {
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
self.conn.execute(
|
tx.execute(
|
||||||
"DELETE FROM history WHERE id NOT IN (
|
"DELETE FROM history WHERE id NOT IN (
|
||||||
SELECT id FROM history ORDER BY timestamp DESC LIMIT ?1
|
SELECT id FROM history ORDER BY timestamp DESC LIMIT ?1
|
||||||
)",
|
)",
|
||||||
[self.max_entries as i64],
|
[self.max_entries as i64],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
for filename in image_files {
|
for filename in image_files {
|
||||||
let path = Path::new(&self.dir_path).join("images").join(&filename);
|
let path = Path::new(&self.dir_path).join("images").join(&filename);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
@ -213,18 +214,19 @@ impl Database {
|
|||||||
let cutoff_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64
|
let cutoff_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64
|
||||||
- (days as i64 * 86_400_000);
|
- (days as i64 * 86_400_000);
|
||||||
|
|
||||||
|
let tx = self.conn.unchecked_transaction()?;
|
||||||
|
|
||||||
let image_files: Vec<String> = {
|
let image_files: Vec<String> = {
|
||||||
let mut stmt = self
|
let mut stmt =
|
||||||
.conn
|
tx.prepare("SELECT content FROM history WHERE type = 'image' AND timestamp < ?1")?;
|
||||||
.prepare("SELECT content FROM history WHERE type = 'image' AND timestamp < ?1")?;
|
|
||||||
stmt.query_map([cutoff_ms], |row| row.get(0))?
|
stmt.query_map([cutoff_ms], |row| row.get(0))?
|
||||||
.filter_map(|r| r.ok())
|
.filter_map(|r| r.ok())
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
let count = self
|
let count = tx.execute("DELETE FROM history WHERE timestamp < ?1", [cutoff_ms])?;
|
||||||
.conn
|
|
||||||
.execute("DELETE FROM history WHERE timestamp < ?1", [cutoff_ms])?;
|
tx.commit()?;
|
||||||
|
|
||||||
for filename in image_files {
|
for filename in image_files {
|
||||||
let path = Path::new(&self.dir_path).join("images").join(&filename);
|
let path = Path::new(&self.dir_path).join("images").join(&filename);
|
||||||
|
|||||||
@ -48,13 +48,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let db_for_expiry = Arc::clone(&db);
|
let db_for_expiry = Arc::clone(&db);
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(Duration::from_secs(3600));
|
{
|
||||||
let lock = db_for_expiry.lock().unwrap();
|
let lock = db_for_expiry.lock().unwrap();
|
||||||
match lock.delete_entries_older_than(days) {
|
match lock.delete_entries_older_than(days) {
|
||||||
Ok(0) => {}
|
Ok(0) => {}
|
||||||
Ok(n) => println!("Expiration : {n} entrée(s) supprimée(s) (> {days} jours)"),
|
Ok(n) => {
|
||||||
Err(e) => eprintln!("Erreur expiration : {e}"),
|
println!("Expiration : {n} entrée(s) supprimée(s) (> {days} jours)")
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Erreur expiration : {e}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
std::thread::sleep(Duration::from_secs(3600));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,42 @@
|
|||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
use crate::models::ClipboardEntry;
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex, mpsc};
|
||||||
|
|
||||||
pub fn start(db: Arc<Mutex<Database>>, clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
pub fn start(db: Arc<Mutex<Database>>, clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
||||||
|
let (tx, rx) = mpsc::channel::<ClipboardEntry>();
|
||||||
|
|
||||||
|
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")))]
|
#[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")))]
|
#[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"))]
|
#[cfg(all(feature = "x11", feature = "wayland"))]
|
||||||
{
|
{
|
||||||
let _ = (db, clipboard);
|
let _ = (tx, clipboard);
|
||||||
Err("Les features 'x11' et 'wayland' sont mutuellement exclusives".into())
|
Err("Les features 'x11' et 'wayland' sont mutuellement exclusives".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(feature = "x11", feature = "wayland")))]
|
#[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())
|
Err("Aucune feature de système de fenêtrage activée (--features x11 ou wayland)".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
use crate::database::Database;
|
|
||||||
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::sync::mpsc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use wayland_clipboard_listener::{WlClipboardPasteStream, WlListenType};
|
use wayland_clipboard_listener::{WlClipboardPasteStream, WlListenType};
|
||||||
|
|
||||||
const MAX_IMAGE_PIXELS: usize = 3840 * 2160;
|
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(
|
pub fn start(
|
||||||
db: Arc<Mutex<Database>>,
|
tx: mpsc::Sender<ClipboardEntry>,
|
||||||
_clipboard: arboard::Clipboard,
|
_clipboard: arboard::Clipboard,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let mut stream = WlClipboardPasteStream::init(WlListenType::ListenOnCopy)
|
let mut stream = WlClipboardPasteStream::init(WlListenType::ListenOnCopy)
|
||||||
@ -17,9 +24,11 @@ pub fn start(
|
|||||||
|
|
||||||
println!("Écoute du presse-papier Wayland...");
|
println!("Écoute du presse-papier Wayland...");
|
||||||
|
|
||||||
|
let mut last_text: Option<String> = None;
|
||||||
|
let mut last_image_hash: Option<u64> = None;
|
||||||
|
|
||||||
for msg in stream.paste_stream().flatten() {
|
for msg in stream.paste_stream().flatten() {
|
||||||
let context = &msg.context;
|
let data: &[u8] = msg.context.context.as_slice();
|
||||||
let data: &[u8] = context.context.as_slice();
|
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
@ -27,14 +36,22 @@ pub fn start(
|
|||||||
|
|
||||||
let entry = if let Ok(text) = String::from_utf8(data.to_vec()) {
|
let entry = if let Ok(text) = String::from_utf8(data.to_vec()) {
|
||||||
let text = text.trim_end_matches('\n').to_string();
|
let text = text.trim_end_matches('\n').to_string();
|
||||||
if text.is_empty() {
|
if text.is_empty() || Some(&text) == last_text.as_ref() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
last_text = Some(text.clone());
|
||||||
|
last_image_hash = None;
|
||||||
|
println!("Clipboard update (texte)");
|
||||||
ClipboardEntry {
|
ClipboardEntry {
|
||||||
content: ClipboardData::Text(text),
|
content: ClipboardData::Text(text),
|
||||||
timestamp: SystemTime::now(),
|
timestamp: SystemTime::now(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let hash = hash_bytes(data);
|
||||||
|
if Some(hash) == last_image_hash {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
match image::load_from_memory(data) {
|
match image::load_from_memory(data) {
|
||||||
Ok(img) => {
|
Ok(img) => {
|
||||||
let (width, height) = (img.width(), img.height());
|
let (width, height) = (img.width(), img.height());
|
||||||
@ -48,9 +65,15 @@ pub fn start(
|
|||||||
3840,
|
3840,
|
||||||
2160
|
2160
|
||||||
);
|
);
|
||||||
|
last_image_hash = Some(hash);
|
||||||
|
last_text = None;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
last_image_hash = Some(hash);
|
||||||
|
last_text = None;
|
||||||
|
println!("Clipboard update (image)");
|
||||||
|
|
||||||
let rgba = img.into_rgba8();
|
let rgba = img.into_rgba8();
|
||||||
ClipboardEntry {
|
ClipboardEntry {
|
||||||
content: ClipboardData::Image(Image {
|
content: ClipboardData::Image(Image {
|
||||||
@ -69,15 +92,10 @@ pub fn start(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("Clipboard update détecté");
|
if tx.send(entry).is_err() {
|
||||||
|
eprintln!("Wayland : writer thread disparu, arrêt");
|
||||||
let db_clone = Arc::clone(&db);
|
break;
|
||||||
std::thread::spawn(move || {
|
}
|
||||||
let db_lock = db_clone.lock().unwrap();
|
|
||||||
if let Err(e) = db_lock.append(entry) {
|
|
||||||
eprintln!("SQLite error : {e}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
use crate::database::Database;
|
|
||||||
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
use crate::models::{ClipboardData, ClipboardEntry, Image};
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -22,7 +21,10 @@ fn hash_bytes(data: &[u8]) -> u64 {
|
|||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), Box<dyn Error>> {
|
pub fn start(
|
||||||
|
tx: mpsc::Sender<ClipboardEntry>,
|
||||||
|
mut clipboard: Clipboard,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let (conn, screen_num) =
|
let (conn, screen_num) =
|
||||||
RustConnection::connect(None).map_err(|e| format!("Connexion X11 impossible : {e}"))?;
|
RustConnection::connect(None).map_err(|e| format!("Connexion X11 impossible : {e}"))?;
|
||||||
|
|
||||||
@ -49,15 +51,14 @@ pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), B
|
|||||||
.reply()?;
|
.reply()?;
|
||||||
|
|
||||||
let clipboard_atom = conn.intern_atom(false, b"CLIPBOARD")?.reply()?.atom;
|
let clipboard_atom = conn.intern_atom(false, b"CLIPBOARD")?.reply()?.atom;
|
||||||
|
|
||||||
conn.xfixes_select_selection_input(
|
conn.xfixes_select_selection_input(
|
||||||
win,
|
win,
|
||||||
clipboard_atom,
|
clipboard_atom,
|
||||||
SelectionEventMask::SET_SELECTION_OWNER,
|
SelectionEventMask::SET_SELECTION_OWNER,
|
||||||
)?
|
)?
|
||||||
.check()?;
|
.check()?;
|
||||||
|
|
||||||
conn.flush()?;
|
conn.flush()?;
|
||||||
|
|
||||||
println!("Clipboard monitor démarré (X11 XFIXES — zéro polling)");
|
println!("Clipboard monitor démarré (X11 XFIXES — zéro polling)");
|
||||||
|
|
||||||
let mut last_text: Option<String> = None;
|
let mut last_text: Option<String> = None;
|
||||||
@ -68,14 +69,14 @@ pub fn start(db: Arc<Mutex<Database>>, mut clipboard: Clipboard) -> Result<(), B
|
|||||||
|
|
||||||
if let Event::XfixesSelectionNotify(_) = event {
|
if let Event::XfixesSelectionNotify(_) = event {
|
||||||
thread::sleep(Duration::from_millis(50));
|
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(
|
fn handle_clipboard_event(
|
||||||
clipboard: &mut Clipboard,
|
clipboard: &mut Clipboard,
|
||||||
db: &Arc<Mutex<Database>>,
|
tx: &mpsc::Sender<ClipboardEntry>,
|
||||||
last_text: &mut Option<String>,
|
last_text: &mut Option<String>,
|
||||||
last_image_hash: &mut Option<u64>,
|
last_image_hash: &mut Option<u64>,
|
||||||
) {
|
) {
|
||||||
@ -89,13 +90,15 @@ fn handle_clipboard_event(
|
|||||||
*last_image_hash = None;
|
*last_image_hash = None;
|
||||||
println!("Clipboard update (texte)");
|
println!("Clipboard update (texte)");
|
||||||
|
|
||||||
spawn_db_write(
|
if tx
|
||||||
Arc::clone(db),
|
.send(ClipboardEntry {
|
||||||
ClipboardEntry {
|
|
||||||
content: ClipboardData::Text(text),
|
content: ClipboardData::Text(text),
|
||||||
timestamp: SystemTime::now(),
|
timestamp: SystemTime::now(),
|
||||||
},
|
})
|
||||||
);
|
.is_err()
|
||||||
|
{
|
||||||
|
eprintln!("X11 : writer thread disparu");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@ -124,29 +127,20 @@ fn handle_clipboard_event(
|
|||||||
*last_text = None;
|
*last_text = None;
|
||||||
println!("Clipboard update (image)");
|
println!("Clipboard update (image)");
|
||||||
|
|
||||||
spawn_db_write(
|
if tx
|
||||||
Arc::clone(db),
|
.send(ClipboardEntry {
|
||||||
ClipboardEntry {
|
content: ClipboardData::Image(Image {
|
||||||
content: ClipboardData::Image(crate::models::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(),
|
||||||
},
|
})
|
||||||
);
|
.is_err()
|
||||||
|
{
|
||||||
|
eprintln!("X11 : writer thread disparu");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_db_write(db: Arc<Mutex<Database>>, 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!");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
159
src/app.rs
159
src/app.rs
@ -3,19 +3,22 @@ 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 image::DynamicImage;
|
||||||
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
use ratatui::text::{Line, Span};
|
||||||
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::collections::{HashMap, VecDeque};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
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::parsing::SyntaxSet;
|
||||||
|
use syntect::util::LinesWithEndings;
|
||||||
|
|
||||||
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 IMAGE_CACHE_MAX: usize = 8;
|
||||||
|
|
||||||
const PAGE_SIZE: usize = 50;
|
const PAGE_SIZE: usize = 50;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
@ -56,7 +59,9 @@ pub struct App {
|
|||||||
pub type_filter: TypeFilter,
|
pub type_filter: TypeFilter,
|
||||||
pub loaded_count: usize,
|
pub loaded_count: usize,
|
||||||
pub has_more: bool,
|
pub has_more: bool,
|
||||||
image_cache: HashMap<String, DynamicImage>,
|
pub preview_highlighted: Option<Vec<Line<'static>>>,
|
||||||
|
pub preview_lang: Option<String>,
|
||||||
|
image_cache: HashMap<String, Arc<DynamicImage>>,
|
||||||
image_cache_order: VecDeque<String>,
|
image_cache_order: VecDeque<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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<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 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<String> {
|
||||||
|
let s = syntax_set.find_syntax_by_first_line(content)?;
|
||||||
|
if s.name == "Plain Text" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(s.name.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let items = ipc::fetch_history(PAGE_SIZE).unwrap_or_default();
|
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 picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks());
|
||||||
|
|
||||||
let salt = directories::ProjectDirs::from("com", "zefad", "rklipd")
|
let salt = match directories::ProjectDirs::from("com", "zefad", "rklipd") {
|
||||||
.and_then(|d| Crypto::load_or_create_salt(d.data_dir()).ok())
|
Some(dirs) => match Crypto::load_or_create_salt(dirs.data_dir()) {
|
||||||
.unwrap_or_else(|| vec![0u8; 32]);
|
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 {
|
let mut app = Self {
|
||||||
mode: Mode::Normal,
|
mode: Mode::Normal,
|
||||||
@ -120,6 +182,8 @@ impl App {
|
|||||||
type_filter: TypeFilter::All,
|
type_filter: TypeFilter::All,
|
||||||
loaded_count: PAGE_SIZE,
|
loaded_count: PAGE_SIZE,
|
||||||
has_more,
|
has_more,
|
||||||
|
preview_highlighted: None,
|
||||||
|
preview_lang: None,
|
||||||
image_cache: HashMap::new(),
|
image_cache: HashMap::new(),
|
||||||
image_cache_order: VecDeque::new(),
|
image_cache_order: VecDeque::new(),
|
||||||
};
|
};
|
||||||
@ -169,29 +233,34 @@ impl App {
|
|||||||
&mut self,
|
&mut self,
|
||||||
filename: &str,
|
filename: &str,
|
||||||
base_dir: &std::path::Path,
|
base_dir: &std::path::Path,
|
||||||
) -> Option<DynamicImage> {
|
) -> Option<Arc<DynamicImage>> {
|
||||||
if !self.image_cache.contains_key(filename) {
|
if self.image_cache.contains_key(filename) {
|
||||||
let path = base_dir.join("images").join(filename);
|
self.image_cache_order.retain(|k| k != 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_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 {
|
pub fn format_timestamp(ts_ms: i64) -> String {
|
||||||
@ -238,7 +307,15 @@ impl App {
|
|||||||
return false;
|
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()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
@ -289,12 +366,6 @@ impl App {
|
|||||||
matched.into_iter().map(|(_, i)| i).collect()
|
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() {
|
self.list_state.select(if self.filtered_items.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@ -313,16 +384,12 @@ impl App {
|
|||||||
|
|
||||||
if current >= last {
|
if current >= last {
|
||||||
if self.try_load_more() {
|
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);
|
let current = self.list_state.selected().unwrap_or(0);
|
||||||
if current + 1 < self.filtered_items.len() {
|
if current + 1 < self.filtered_items.len() {
|
||||||
self.list_state.select(Some(current + 1));
|
self.list_state.select(Some(current + 1));
|
||||||
self.update_preview();
|
self.update_preview();
|
||||||
}
|
}
|
||||||
// Sinon (le filtre actif masque les nouveaux items) : on reste
|
|
||||||
} else {
|
} else {
|
||||||
// Fin réelle — wrap vers le haut
|
|
||||||
self.list_state.select(Some(0));
|
self.list_state.select(Some(0));
|
||||||
self.update_preview();
|
self.update_preview();
|
||||||
}
|
}
|
||||||
@ -367,13 +434,13 @@ impl App {
|
|||||||
self.list_state.select(new_sel);
|
self.list_state.select(new_sel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.last_selected_index = None;
|
||||||
self.update_preview();
|
self.update_preview();
|
||||||
}
|
}
|
||||||
|
|
||||||
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(self.loaded_count) {
|
if let Some(new_items) = ipc::fetch_history(self.loaded_count) {
|
||||||
self.has_more = new_items.len() == self.loaded_count;
|
self.has_more = new_items.len() == self.loaded_count;
|
||||||
self.all_items = new_items;
|
self.all_items = new_items;
|
||||||
@ -545,6 +612,8 @@ impl App {
|
|||||||
self.current_image = None;
|
self.current_image = None;
|
||||||
self.image_cache.clear();
|
self.image_cache.clear();
|
||||||
self.image_cache_order.clear();
|
self.image_cache_order.clear();
|
||||||
|
self.preview_highlighted = None;
|
||||||
|
self.preview_lang = None;
|
||||||
self.loaded_count = PAGE_SIZE;
|
self.loaded_count = PAGE_SIZE;
|
||||||
self.has_more = false;
|
self.has_more = false;
|
||||||
self.set_status("Historique effacé".into());
|
self.set_status("Historique effacé".into());
|
||||||
@ -561,6 +630,8 @@ impl App {
|
|||||||
self.last_selected_index = idx;
|
self.last_selected_index = idx;
|
||||||
self.current_image = None;
|
self.current_image = None;
|
||||||
self.preview_scroll = 0;
|
self.preview_scroll = 0;
|
||||||
|
self.preview_highlighted = None;
|
||||||
|
self.preview_lang = None;
|
||||||
|
|
||||||
let content = match self.get_selected_item().map(|i| i.content.clone()) {
|
let content = match self.get_selected_item().map(|i| i.content.clone()) {
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
@ -569,12 +640,15 @@ 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") {
|
||||||
if let Some(img) = self.get_cached_image(&content, dirs.data_dir()) {
|
if let Some(arc_img) = self.get_cached_image(&content, dirs.data_dir()) {
|
||||||
// new_resize_protocol attend un DynamicImage — on clone depuis le cache
|
let img = (*arc_img).clone();
|
||||||
// (l'image est déjà redimensionnée ≤ 1280×720, clone = ~3,5 Mo max)
|
|
||||||
self.current_image = Some(self.picker.new_resize_protocol(img));
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mise à jour du flag has_more lors de chaque sync
|
|
||||||
self.has_more = new.len() == self.loaded_count;
|
self.has_more = new.len() == self.loaded_count;
|
||||||
|
|
||||||
let changed = self.all_items.len() != new.len()
|
let changed = self.all_items.len() != new.len()
|
||||||
|
|||||||
@ -34,6 +34,14 @@ impl Crypto {
|
|||||||
if bytes.len() == SALT_LEN {
|
if bytes.len() == SALT_LEN {
|
||||||
return Ok(bytes);
|
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];
|
let mut salt = vec![0u8; SALT_LEN];
|
||||||
OsRng.fill_bytes(&mut salt);
|
OsRng.fill_bytes(&mut salt);
|
||||||
|
|||||||
@ -63,8 +63,11 @@ pub fn set_clipboard(content: String) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_entry(content: String) {
|
pub fn delete_entry(content: String) -> bool {
|
||||||
let _ = send_request(&IpcRequest::DeleteEntry { content });
|
matches!(
|
||||||
|
send_request(&IpcRequest::DeleteEntry { content }),
|
||||||
|
Some(IpcResponse::Ok)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_entry(old_content: String, new_content: String) -> bool {
|
pub fn update_entry(old_content: String, new_content: String) -> bool {
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@ -177,8 +177,14 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App)
|
|||||||
Mode::ConfirmDelete => match key.code {
|
Mode::ConfirmDelete => match key.code {
|
||||||
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
|
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
|
||||||
if let Some(item) = app.get_selected_item() {
|
if let Some(item) = app.get_selected_item() {
|
||||||
ipc::delete_entry(item.content.clone());
|
let content = item.content.clone();
|
||||||
app.delete_selected();
|
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;
|
app.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
|
|||||||
86
src/ui.rs
86
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 crate::crypto::Crypto;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
@ -8,55 +8,6 @@ use ratatui::{
|
|||||||
widgets::{Block, BorderType, Borders, List, ListItem, Padding, Paragraph},
|
widgets::{Block, BorderType, Borders, List, ListItem, Padding, Paragraph},
|
||||||
};
|
};
|
||||||
use ratatui_image::StatefulImage;
|
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<Line<'static>> {
|
|
||||||
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<String> {
|
|
||||||
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) {
|
pub fn render(f: &mut Frame, app: &mut App) {
|
||||||
let outer = Layout::default()
|
let outer = Layout::default()
|
||||||
@ -103,7 +54,7 @@ pub fn render(f: &mut Frame, app: &mut App) {
|
|||||||
let preview: String = item
|
let preview: String = item
|
||||||
.content
|
.content
|
||||||
.lines()
|
.lines()
|
||||||
.next()
|
.find(|l| !l.trim().is_empty())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.chars()
|
.chars()
|
||||||
.take(28)
|
.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 selected_content = app.get_selected_item().map(|i| i.content.clone());
|
||||||
|
|
||||||
let lang = selected_content
|
let preview_title = match &app.preview_lang {
|
||||||
.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 {
|
|
||||||
Some(l) => format!(" Prévisualisation — {} ", l),
|
Some(l) => format!(" Prévisualisation — {} ", l),
|
||||||
None => " Prévisualisation ".to_string(),
|
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]);
|
let inner = preview_block.inner(panels[1]);
|
||||||
f.render_widget(preview_block, panels[1]);
|
f.render_widget(preview_block, panels[1]);
|
||||||
|
|
||||||
if app.current_image.is_some() {
|
let scroll = (app.preview_scroll, 0);
|
||||||
let state = app.current_image.as_mut().unwrap();
|
|
||||||
|
if let Some(state) = app.current_image.as_mut() {
|
||||||
f.render_stateful_widget(StatefulImage::default(), inner, state);
|
f.render_stateful_widget(StatefulImage::default(), inner, state);
|
||||||
} else if let Some(content) = &selected_content {
|
} else if let Some(content) = &selected_content {
|
||||||
let scroll = (app.preview_scroll, 0);
|
|
||||||
if Crypto::is_any_encrypted(content) {
|
if Crypto::is_any_encrypted(content) {
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new("🔒 Contenu chiffré\n\nAppuyez sur [e] pour déchiffrer.")
|
Paragraph::new("🔒 Contenu chiffré\n\nAppuyez sur [e] pour déchiffrer.")
|
||||||
.scroll(scroll),
|
.scroll(scroll),
|
||||||
inner,
|
inner,
|
||||||
);
|
);
|
||||||
} else {
|
} else if let Some(lines) = &app.preview_highlighted {
|
||||||
let lines = highlight_code(content, app);
|
f.render_widget(Paragraph::new(lines.clone()).scroll(scroll), inner);
|
||||||
f.render_widget(Paragraph::new(lines).scroll(scroll), inner);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,20 +135,6 @@ pub fn render(f: &mut Frame, app: &mut App) {
|
|||||||
Mode::PasswordInput => (" MOT DE PASSE ", Color::Magenta),
|
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 {
|
let filter_hint = match app.type_filter {
|
||||||
crate::app::TypeFilter::All => String::new(),
|
crate::app::TypeFilter::All => String::new(),
|
||||||
f => format!(" [{}]", f.label()),
|
f => format!(" [{}]", f.label()),
|
||||||
@ -216,7 +147,6 @@ pub fn render(f: &mut Frame, app: &mut App) {
|
|||||||
} else {
|
} else {
|
||||||
let extra = match &app.mode {
|
let extra = match &app.mode {
|
||||||
Mode::Search => {
|
Mode::Search => {
|
||||||
// Indicateur visuel du mode de recherche actif (fuzzy vs regexp)
|
|
||||||
let mode_hint = if app.input_buffer.trim_start().starts_with('/') {
|
let mode_hint = if app.input_buffer.trim_start().starts_with('/') {
|
||||||
"re"
|
"re"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user