278 lines
9.5 KiB
Rust
278 lines
9.5 KiB
Rust
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<dyn std::error::Error>> {
|
|
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<CrosstermBackend<io::Stdout>>, 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
|
|
}
|
|
}
|
|
}
|