diff --git a/src/ui/fancy_ui/display.rs b/src/ui/fancy_ui/display.rs index acbb31d..34be11c 100644 --- a/src/ui/fancy_ui/display.rs +++ b/src/ui/fancy_ui/display.rs @@ -113,10 +113,11 @@ struct ComputedLayout { progress: Rect, graph: Rect, args_display: Rect, + quit_modal: Rect, } impl From for ComputedLayout { - fn from(value: Rect) -> Self { + fn from(area: Rect) -> Self { let root = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -124,18 +125,42 @@ impl From for ComputedLayout { Constraint::Min(10), Constraint::Length(10), ]) - .split(value); + .split(area); let info_pane = root[2]; + let quit_modal = centered_rect(area, 40, 4); + Self { graph: root[1], progress: root[0], args_display: info_pane, + quit_modal, } } } +/// Given an outer rect and desired inner rect dimensions, returns the inner rect. +fn centered_rect(r: Rect, w: u16, h: u16) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(h), + Constraint::Fill(1), + ]) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Length(w), + Constraint::Fill(1), + ]) + .split(popup_layout[1])[1] +} + pub fn draw( state: &mut State, terminal: &mut Terminal, @@ -184,6 +209,10 @@ pub fn draw( } else { f.render_widget(info_table, layout.args_display); } + + if let Some(qm) = state.quit_modal { + f.render_widget(qm, layout.quit_modal) + } })?; Ok(()) } diff --git a/src/ui/fancy_ui/state.rs b/src/ui/fancy_ui/state.rs index dcdda2f..eb1c805 100644 --- a/src/ui/fancy_ui/state.rs +++ b/src/ui/fancy_ui/state.rs @@ -8,7 +8,7 @@ use crate::{ writer_process::ipc::StatusMessage, }; -use super::widgets::SpeedChartState; +use super::widgets::{QuitModal, QuitModalResult, SpeedChartState}; #[derive(Debug, PartialEq, Clone)] pub enum UIEvent { @@ -23,6 +23,7 @@ pub struct State { pub target_filename: String, pub child: WriterState, pub graph_state: SpeedChartState, + pub quit_modal: Option, } impl State { @@ -32,6 +33,7 @@ impl State { target_filename: params.target.devnode.to_string_lossy().to_string(), child: WriterState::initial(now, !params.compression.is_identity(), input_file_bytes), graph_state: SpeedChartState::default(), + quit_modal: None, } } @@ -51,13 +53,34 @@ impl State { fn on_term_event(self, ev: Event) -> anyhow::Result { match ev { Event::Key(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, + code, + modifiers, .. - }) => { - info!("Got CTRL-C, quitting"); - Err(Quit)? + }) => self.handle_key_down((code, modifiers)), + _ => Ok(self), + } + } + + fn handle_key_down(mut self, (kc, km): (KeyCode, KeyModifiers)) -> anyhow::Result { + if let Some(qm) = &self.quit_modal { + return match qm.handle_key_down(kc) { + Some(QuitModalResult::Quit) => Err(Quit.into()), + Some(QuitModalResult::Stay) => Ok(Self { + quit_modal: None, + ..self + }), + None => Ok(self), + }; + } + + match (kc, km) { + (KeyCode::Char('c'), KeyModifiers::CONTROL) + | (KeyCode::Esc, _) + | (KeyCode::Char('q'), _) => { + info!("Got request to quit, spawning prompt"); + self.quit_modal = Some(QuitModal::new()); + Ok(self) } _ => Ok(self), } diff --git a/src/ui/fancy_ui/widgets.rs b/src/ui/fancy_ui/widgets.rs index f1e8ce9..057e120 100644 --- a/src/ui/fancy_ui/widgets.rs +++ b/src/ui/fancy_ui/widgets.rs @@ -1,14 +1,15 @@ use std::time::Instant; use bytesize::ByteSize; +use crossterm::event::KeyCode; use ratatui::{ layout::{Alignment, Constraint, Rect}, - style::{Color, Style}, + style::{Color, Style, Stylize}, symbols, text::Span, widgets::{ - Axis, Block, Borders, Cell, Chart, Dataset, Gauge, GraphType, Row, StatefulWidget, Table, - Widget, + Axis, Block, BorderType, Borders, Cell, Chart, Clear, Dataset, Gauge, GraphType, Paragraph, + Row, StatefulWidget, Table, Widget, }, }; @@ -160,7 +161,11 @@ impl WriterProgressBar { } => WriterProgressBar::from_simple( write_hist.bytes_encountered(), *total_write_bytes, - if error.is_some() { "Error!" } else { "Done!" }, + if error.is_some() { + "Error!" + } else { + "Done! Press q to quit." + }, if error.is_some() { Style::default().fg(Color::White).bg(Color::Red) } else { @@ -267,3 +272,58 @@ impl Widget for WritingInfoTable<'_> { Widget::render(self.make_info_table(), area, buf) } } + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct QuitModal { + _private: (), +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum QuitModalResult { + /// Quit the program. + Quit, + + /// Stay in the program. + Stay, +} + +impl QuitModal { + pub fn new() -> Self { + Self { _private: () } + } + + /// Handle a key down event. If this would conclude the modal, returns the result. + /// Otherwise, if an indecisive keystroke was detected and we are to stay inside the + /// modal, returns None. + pub fn handle_key_down(self, kc: KeyCode) -> Option { + use KeyCode::*; + use QuitModalResult::*; + match kc { + Esc => Some(Stay), + Char('q') | Char('Q') => Some(Quit), + _ => None, + } + } +} + +impl Widget for QuitModal { + fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let prompt = + Paragraph::new("Are you sure you want to quit?\nPress q again to quit, ESC to stay") + .alignment(Alignment::Center) + .style(Style::new().yellow()) + .block( + Block::new() + .bg(Color::Red) + .border_style(Style::new().white()) + .border_type(BorderType::Plain) + .borders(Borders::ALL), + ); + + Clear.render(area, buf); + prompt.render(area, buf); + } +}