mod app; mod crypto; mod ipc; mod models; mod ui; use app::{App, Mode}; use crossterm::{ event::{self, Event, KeyCode, KeyModifiers}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use ratatui::{Terminal, backend::CrosstermBackend}; use std::io; use std::time::Duration; fn main() -> Result<(), Box> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let mut app = App::new(); let res = run_app(&mut terminal, &mut app); disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; if let Err(err) = res { eprintln!("{:?}", err); } Ok(()) } fn run_app(terminal: &mut Terminal>, app: &mut App) -> io::Result<()> { let mut last_d = false; let mut last_g = false; loop { terminal.draw(|f| ui::render(f, app))?; app.tick_messages(); if event::poll(Duration::from_millis(250))? { if let Event::Key(key) = event::read()? { // Ctrl+j / Ctrl+k : scroll prévisualisation (tous modes sauf aide) if key.modifiers.contains(KeyModifiers::CONTROL) && app.mode != Mode::Help { match key.code { KeyCode::Char('j') => { app.scroll_preview_down(); continue; } KeyCode::Char('k') => { app.scroll_preview_up(); continue; } _ => {} } } match app.mode { // ---------------------------------------------------------- Mode::Help => { // N'importe quelle touche ferme l'aide app.mode = Mode::Normal; } // ---------------------------------------------------------- Mode::Normal => { last_d = handle_normal(app, key.code, last_d, &mut last_g); } // ---------------------------------------------------------- Mode::Search => match key.code { KeyCode::Esc => { app.mode = Mode::Normal; app.input_buffer.clear(); app.update_search(); } KeyCode::Enter => app.paste_selected(), KeyCode::Down => app.next(), KeyCode::Up => app.previous(), KeyCode::Char('o') if app.input_buffer.is_empty() => { // `o` sans texte saisi → ouvre URL app.open_url_selected(); } KeyCode::Char(c) => { app.input_buffer.push(c); app.update_search(); } KeyCode::Backspace => { app.input_buffer.pop(); app.update_search(); } _ => {} }, // ---------------------------------------------------------- Mode::Command => match key.code { KeyCode::Esc => { app.mode = Mode::Normal; app.input_buffer.clear(); } KeyCode::Char(c) => app.input_buffer.push(c), KeyCode::Backspace => { app.input_buffer.pop(); } KeyCode::Enter => { let cmd = app.input_buffer.trim().to_string(); app.input_buffer.clear(); app.mode = Mode::Normal; match cmd.as_str() { "q" | "quit" => app.should_quit = true, "clear" => app.clear_history(), "p" | "password" => { app.pending_action = None; app.mode = Mode::PasswordInput; } _ => app.set_error(format!("Commande inconnue : {cmd}")), } } _ => {} }, // ---------------------------------------------------------- Mode::ConfirmDelete => match key.code { KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => { if let Some(item) = app.get_selected_item() { 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; } KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { app.mode = Mode::Normal; } _ => {} }, // ---------------------------------------------------------- Mode::PasswordInput => match key.code { KeyCode::Esc => { app.mode = Mode::Normal; app.input_buffer.clear(); app.pending_action = None; } KeyCode::Char(c) => app.input_buffer.push(c), KeyCode::Backspace => { app.input_buffer.pop(); } KeyCode::Enter => { let pw = app.input_buffer.clone(); app.input_buffer.clear(); app.mode = Mode::Normal; app.apply_password(pw); } _ => {} }, } } } else { app.sync_with_daemon(); } if app.should_quit { return Ok(()); } } } /// Gère les touches en mode Normal. Retourne le nouvel état de `last_d`. fn handle_normal(app: &mut App, code: KeyCode, last_d: bool, last_g: &mut bool) -> bool { match code { KeyCode::Char('?') => { app.mode = Mode::Help; *last_g = false; false } KeyCode::Enter => { app.paste_selected(); *last_g = false; false } KeyCode::Char('j') | KeyCode::Down => { app.next(); *last_g = false; false } KeyCode::Char('k') | KeyCode::Up => { app.previous(); *last_g = false; false } KeyCode::Char('G') => { if !app.filtered_items.is_empty() { let l = app.filtered_items.len() - 1; app.list_state.select(Some(l)); app.update_preview(); } *last_g = false; false } KeyCode::Char('g') => { if *last_g { if !app.filtered_items.is_empty() { app.list_state.select(Some(0)); app.update_preview(); } *last_g = false; } else { *last_g = true; } false } KeyCode::Char('d') => { *last_g = false; if last_d { app.mode = Mode::ConfirmDelete; false } else { true // dernier appui était 'd' } } KeyCode::Char('u') => { app.undo_delete(); *last_g = false; false } KeyCode::Char('p') => { app.toggle_pin(); *last_g = false; false } KeyCode::Char('o') => { app.open_url_selected(); *last_g = false; false } KeyCode::Char('e') => { app.toggle_encrypt(); *last_g = false; false } KeyCode::Char('t') => { app.cycle_type_filter(); *last_g = false; false } KeyCode::Char('/') => { app.mode = Mode::Search; app.input_buffer.clear(); app.update_search(); *last_g = false; false } KeyCode::Char(':') => { app.mode = Mode::Command; app.input_buffer.clear(); *last_g = false; false } KeyCode::Char('q') => { app.should_quit = true; false } _ => { *last_g = false; false } } }