diff --git a/src/tui/keys.rs b/src/tui/keys.rs index 0b28b91..217cd64 100644 --- a/src/tui/keys.rs +++ b/src/tui/keys.rs @@ -7,17 +7,28 @@ use super::state::{Mode, State}; /// Event handling pub async fn handle_key(state: &mut State, key_event: KeyEvent) { match state.mode { - Mode::List => match key_event.code { - KeyCode::Char('e') => state.mode = Mode::Edit, - KeyCode::Home => state.selected_place.select_first(), - KeyCode::End => state.selected_place.select_last(), - KeyCode::PageUp => state.prev_page(), - KeyCode::PageDown => state.next_page(), - KeyCode::Up | KeyCode::Char('k') => state.selected_place.select_previous(), - KeyCode::Down | KeyCode::Char('j') => state.selected_place.select_next(), - KeyCode::Esc | KeyCode::Char('q') => state.quit = true, - _ => {} - }, + Mode::List => { + if state.confirmation.is_some() { + match key_event.code { + KeyCode::Char('y') => state.proceed_confirmation().await, + KeyCode::Char('n') | KeyCode::Esc => state.cancel_confirmation(), + _ => {} + } + } else { + match key_event.code { + KeyCode::Char('d') => state.confirm_deletion(), + KeyCode::Char('e') => state.mode = Mode::Edit, + KeyCode::Home => state.selected_place.select_first(), + KeyCode::End => state.selected_place.select_last(), + KeyCode::PageUp => state.prev_page(), + KeyCode::PageDown => state.next_page(), + KeyCode::Up | KeyCode::Char('k') => state.selected_place.select_previous(), + KeyCode::Down | KeyCode::Char('j') => state.selected_place.select_next(), + KeyCode::Esc | KeyCode::Char('q') => state.quit = true, + _ => {} + } + } + } Mode::Edit => match (key_event.modifiers, key_event.code) { (KeyModifiers::NONE, KeyCode::Esc) => state.mode = Mode::List, (KeyModifiers::NONE, KeyCode::Tab) => {} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index d88994b..3ba35a8 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -27,9 +27,6 @@ pub async fn tui(places_repository: DbPlacesRepository) -> Result<()> { Event::Tick => { state.fetch_places().await; } - Event::SlowTick => { - // state.cleanup_finished_tasks(); - } Event::Resize(_, h) => state.height = h, Event::Quit => state.quit = true, _ => {} diff --git a/src/tui/state.rs b/src/tui/state.rs index a791b0c..96732be 100644 --- a/src/tui/state.rs +++ b/src/tui/state.rs @@ -13,6 +13,7 @@ pub struct State { places_repository: DbPlacesRepository, pub places: Vec, places_status: DataStatus, + pub confirmation: Option, pub selected_place: TableState, pub quit: bool, } @@ -27,6 +28,12 @@ enum DataStatus { Old, } +pub enum ConfirmationStatus { + Deletion(i64), + #[expect(dead_code)] + Save(i64), +} + impl State { pub fn new(places_repository: DbPlacesRepository, height: u16) -> Self { Self { @@ -37,6 +44,7 @@ impl State { places_status: DataStatus::Old, places: vec![], selected_place: TableState::default(), + confirmation: None, quit: false, } } @@ -74,4 +82,36 @@ impl State { } self.places_status = DataStatus::Fresh; } + + pub fn confirm_deletion(&mut self) { + if let Some(Some(id)) = self + .selected_place + .selected() + .map(|index| self.places.get(index).map(|p| p.id)) + { + self.confirmation = Some(ConfirmationStatus::Deletion(id)) + } + } + + pub fn cancel_confirmation(&mut self) { + self.confirmation = None; + } + + pub async fn proceed_confirmation(&mut self) { + let Some(confirmation) = &self.confirmation else { + return; + }; + + match confirmation { + ConfirmationStatus::Deletion(id) => { + if let Err(err) = self.places_repository.delete_place(*id).await { + tracing::error!("{err}"); + } + } + ConfirmationStatus::Save(_) => todo!(), + } + + self.confirmation = None; + self.places_status = DataStatus::Old; + } } diff --git a/src/tui/terminal.rs b/src/tui/terminal.rs index 2f8f28c..6d9765d 100644 --- a/src/tui/terminal.rs +++ b/src/tui/terminal.rs @@ -20,8 +20,6 @@ pub enum Event { Closed, /// Triggers background actions Tick, - /// Triggers less frequent background actions - SlowTick, /// UI Render Render, FocusGained, @@ -49,8 +47,7 @@ pub struct Tui { impl Tui { pub fn new() -> Result { let tick_rate = 4.0; - let slow_tick_rate = 0.25; - let frame_rate = 30.0; + let frame_rate = 20.0; let terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?; let cancellation_token = CancellationToken::new(); @@ -60,17 +57,14 @@ impl Tui { let cancellation_token = cancellation_token.clone(); let sender = sender.clone(); let tick_delay = std::time::Duration::from_secs_f64(1.0 / tick_rate); - let slow_tick_delay = std::time::Duration::from_secs_f64(1.0 / slow_tick_rate); let render_delay = std::time::Duration::from_secs_f64(1.0 / frame_rate); tokio::spawn(async move { let mut reader = crossterm::event::EventStream::new(); let mut tick_interval = tokio::time::interval(tick_delay); - let mut slow_tick_interval = tokio::time::interval(slow_tick_delay); let mut render_interval = tokio::time::interval(render_delay); sender.send(Event::Init)?; loop { let tick_delay = tick_interval.tick(); - let slow_tick_delay = slow_tick_interval.tick(); let render_delay = render_interval.tick(); let crossterm_event = reader.next().fuse(); tokio::select! { @@ -104,9 +98,6 @@ impl Tui { _ = tick_delay => { sender.send(Event::Tick)?; }, - _ = slow_tick_delay => { - sender.send(Event::SlowTick)?; - }, _ = render_delay => { sender.send(Event::Render)?; } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index ead1cf7..cf6e619 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -2,12 +2,12 @@ use itertools::Itertools; use ratatui::Frame; -use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect}; use ratatui::style::{Color, Style, Stylize}; -use ratatui::text::{Line, Span, ToSpan}; -use ratatui::widgets::{Paragraph, Row, Table}; +use ratatui::text::{Line, Span, Text, ToSpan}; +use ratatui::widgets::{Block, Clear, Padding, Paragraph, Row, Table}; -use super::state::{Mode, State}; +use super::state::{ConfirmationStatus, Mode, State}; /// UI drawing pub fn ui_draw(state: &mut State, f: &mut Frame<'_>) { @@ -54,7 +54,36 @@ fn main_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) { Mode::List => list_draw(state, f, area), Mode::Edit => edit_draw(state, f, area), } + confirmation_dialog_draw(state, f, area); } + +fn confirmation_dialog_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) { + let Some(confirmation) = &state.confirmation else { + return; + }; + + let dialog_area = center(area, Constraint::Percentage(80), Constraint::Percentage(60)); + let (action, id) = match confirmation { + ConfirmationStatus::Deletion(id) => ("delete", id), + ConfirmationStatus::Save(id) => ("save", id), + }; + let confirmation_dialog = Paragraph::new(Text::from_iter([ + Line::from_iter([ + "Do you want to ".to_span(), + action.to_span(), + " place with id: ".to_span(), + id.to_span(), + ]), + Line::from(""), + Line::from("Y/N".to_span().bold()), + ])) + .centered() + .block(Block::bordered().padding(Padding::uniform(1))); + + f.render_widget(Clear, dialog_area); + f.render_widget(confirmation_dialog, dialog_area); +} + fn list_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) { let places = state.places.iter().map(|p| { Row::new([ @@ -99,7 +128,18 @@ fn list_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) { f.render_stateful_widget(places_table, area, &mut state.selected_place); } -fn edit_draw(_state: &mut State, _f: &mut Frame<'_>, _area: Rect) {} +fn edit_draw(_state: &mut State, f: &mut Frame<'_>, area: Rect) { + let dialog_area = center(area, Constraint::Percentage(80), Constraint::Percentage(60)); + let confirmation_dialog = + Paragraph::new(Text::from_iter([ + Line::from("Not implemented yet :(").italic() + ])) + .centered() + .block(Block::bordered().padding(Padding::uniform(1))); + + f.render_widget(Clear, dialog_area); + f.render_widget(confirmation_dialog, dialog_area); +} #[expect(unstable_name_collisions)] fn footer_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) { @@ -114,6 +154,7 @@ fn footer_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) { ("PgUp", "Prev Page"), ("PgDown", "Next Page"), ("e", "Edit"), + ("d", "Delete"), ] .map(|(key, action)| keybinding(key, action).to_vec()) .into_iter() @@ -145,3 +186,11 @@ fn keybinding(key: &'static str, action: &'static str) -> [Span<'static>; 5] { Span::styled(" ", black), ] } + +fn center(area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect { + let [area] = Layout::horizontal([horizontal]) + .flex(Flex::Center) + .areas(area); + let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area); + area +}