From 3c1986e445c56f500f61b0b2a3dc0f6feb6c0ea9 Mon Sep 17 00:00:00 2001 From: Astrid Yu Date: Mon, 13 Nov 2023 08:48:08 -0800 Subject: [PATCH] Refactor TUI components to implement Widget trait --- src/byteseries.rs | 1 + src/ui/burn/fancy/display.rs | 20 ++- src/ui/burn/fancy/state.rs | 6 +- src/ui/burn/fancy/widgets.rs | 319 ++++++++++++++++++----------------- 4 files changed, 184 insertions(+), 162 deletions(-) diff --git a/src/byteseries.rs b/src/byteseries.rs index 7636b4b..7094772 100644 --- a/src/byteseries.rs +++ b/src/byteseries.rs @@ -78,6 +78,7 @@ impl ByteSeries { (b1 - b0) / window } + /// Returns a series of points representing a timeseries, aggregated by the given window size. pub fn speeds(&self, window: f64) -> impl Iterator + '_ { let bins = (self.last_datapoint().0 / window).ceil() as usize; (0..bins).map(move |i| { diff --git a/src/ui/burn/fancy/display.rs b/src/ui/burn/fancy/display.rs index 38b9709..80379ad 100644 --- a/src/ui/burn/fancy/display.rs +++ b/src/ui/burn/fancy/display.rs @@ -18,7 +18,7 @@ use crate::{ use super::{ state::{Quit, State}, - widgets::{make_info_table, make_progress_bar}, + widgets::{SpeedChart, WriterProgressBar, WritingInfoTable}, }; pub struct FancyUI<'a, B> @@ -138,7 +138,7 @@ pub fn draw( state: &mut State, terminal: &mut Terminal, ) -> anyhow::Result<()> { - let progress_bar = make_progress_bar(&state.child); + let progress_bar = WriterProgressBar::from_writer(&state.child); let final_time = match state.child { WriterState::Finished { finish_time, .. } => finish_time, @@ -150,16 +150,22 @@ pub fn draw( _ => None, }; - let info_table = make_info_table(&state.input_filename, &state.target_filename, &state.child); + let info_table = WritingInfoTable { + input_filename: &state.input_filename, + target_filename: &state.target_filename, + state: &state.child, + }; + + let speed_chart = SpeedChart { + state: &state.child, + final_time, + }; terminal.draw(|f| { let layout = ComputedLayout::from(f.size()); f.render_widget(progress_bar.render(), layout.progress); - - state - .ui_state - .draw_speed_chart(&state.child, f, layout.graph, final_time); + f.render_stateful_widget(speed_chart, layout.graph, &mut state.graph_state); if let Some(error) = error { f.render_widget( diff --git a/src/ui/burn/fancy/state.rs b/src/ui/burn/fancy/state.rs index 6304795..b96dc2c 100644 --- a/src/ui/burn/fancy/state.rs +++ b/src/ui/burn/fancy/state.rs @@ -8,7 +8,7 @@ use crate::{ writer_process::{ipc::StatusMessage, state_tracking::WriterState}, }; -use super::widgets::UIState; +use super::widgets::SpeedChartState; #[derive(Debug, PartialEq, Clone)] pub enum UIEvent { @@ -22,7 +22,7 @@ pub struct State { pub input_filename: String, pub target_filename: String, pub child: WriterState, - pub ui_state: UIState, + pub graph_state: SpeedChartState, } impl State { @@ -30,8 +30,8 @@ impl State { State { input_filename: params.input_file.to_string_lossy().to_string(), target_filename: params.target.devnode.to_string_lossy().to_string(), - ui_state: UIState::default(), child: WriterState::initial(now, !params.compression.is_identity(), input_file_bytes), + graph_state: SpeedChartState::default(), } } diff --git a/src/ui/burn/fancy/widgets.rs b/src/ui/burn/fancy/widgets.rs index 380a898..5069c58 100644 --- a/src/ui/burn/fancy/widgets.rs +++ b/src/ui/burn/fancy/widgets.rs @@ -6,127 +6,80 @@ use ratatui::{ style::{Color, Style}, symbols, text::Span, - widgets::{Axis, Block, Borders, Cell, Chart, Dataset, Gauge, GraphType, Row, Table}, - Frame, + widgets::{ + Axis, Block, Borders, Cell, Chart, Dataset, Gauge, GraphType, Row, StatefulWidget, Table, + Widget, + }, }; use crate::writer_process::state_tracking::WriterState; -#[derive(Debug, PartialEq, Clone)] -pub struct UIState { - graph_max_speed: f64, +pub struct SpeedChart<'a> { + pub state: &'a WriterState, + pub final_time: Instant, } -pub fn make_progress_bar(state: &WriterState) -> StateProgressBar { - match state { - WriterState::Writing(st) => StateProgressBar { - bytes_written: st.write_hist.bytes_encountered(), - label_state: "Burning...", - style: Style::default().fg(Color::Yellow), - ratio: st.approximate_ratio(), - display_total_bytes: st.total_raw_bytes, - }, - WriterState::Verifying { - verify_hist, - total_write_bytes, - .. - } => StateProgressBar::from_simple( - verify_hist.bytes_encountered(), - *total_write_bytes, - "Verifying...", - Style::default().fg(Color::Blue).bg(Color::Yellow), - ), - WriterState::Finished { - write_hist, - error, - total_write_bytes, - .. - } => StateProgressBar::from_simple( - write_hist.bytes_encountered(), - *total_write_bytes, - if error.is_some() { "Error!" } else { "Done!" }, - if error.is_some() { - Style::default().fg(Color::White).bg(Color::Red) - } else { - Style::default().fg(Color::Green).bg(Color::Black) - }, - ), - } +#[derive(Debug, Default, Clone, PartialEq)] +pub struct SpeedChartState { + /// Due to sample aliasing, the maximum displayed Y value might increase or + /// decrease. This keeps track of the maximum Y value ever observed, to prevent + /// the chart limits from rapidly changing from the aliasing. + max_y_limit: f64, } -impl UIState { - pub fn draw_speed_chart( - &mut self, - state: &WriterState, - frame: &mut Frame<'_>, - area: Rect, - final_time: Instant, - ) { - let wdata = state.write_hist(); - let max_time = f64::max(final_time.duration_since(wdata.start()).as_secs_f64(), 3.0); - let window = max_time / frame.size().width as f64; - - let wspeeds: Vec<(f64, f64)> = wdata.speeds(window).collect(); - let vspeeds: Option> = state.verify_hist().map(|vdata| { - vdata +impl StatefulWidget for SpeedChart<'_> { + type State = SpeedChartState; + + fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) { + let write_data = self.state.write_hist(); + let max_time = f64::max( + self.final_time + .duration_since(write_data.start()) + .as_secs_f64(), + 3.0, + ); + let window = max_time / area.width as f64; + + let write_speeds: Vec<(f64, f64)> = write_data.speeds(window).collect(); + let verify_speeds: Option> = self.state.verify_hist().map(|verify_data| { + verify_data .speeds(window) .into_iter() - .map(|(x, y)| (x + wdata.last_datapoint().0, y)) + .map(|(x, y)| (x + write_data.last_datapoint().0, y)) .collect() }); // update max y-axis - self.graph_max_speed = if let Some(vs) = &vspeeds { - wspeeds - .iter() - .chain(vs.iter()) - .map(|x| x.1) - .fold(self.graph_max_speed, f64::max) - } else { - wspeeds - .iter() - .map(|x| x.1) - .fold(self.graph_max_speed, f64::max) - }; - - let n_x_ticks = (frame.size().width / 16).min(9); - let n_y_ticks = (frame.size().height / 4).min(5); - - let x_ticks: Vec<_> = (0..=n_x_ticks) - .map(|i| { - let x = i as f64 * max_time / n_x_ticks as f64; - Span::from(format!("{x:.1}s")) - }) - .collect(); - - let y_ticks: Vec<_> = (0..=n_y_ticks) - .map(|i| { - let y = i as f64 * self.graph_max_speed / n_y_ticks as f64; - let bytes = ByteSize::b(y as u64); - Span::from(format!("{bytes}/s")) - }) - .collect(); - - let mut datasets = vec![Dataset::default() - .name("Write") - .graph_type(GraphType::Scatter) + state.max_y_limit = write_speeds + .iter() + .chain(verify_speeds.iter().flatten()) + .map(|&(_x, y)| y) + .fold(state.max_y_limit, f64::max); + + // Calculate ticks + let (x_ticks, y_ticks) = calculate_ticks(area, max_time, state.max_y_limit); + + // Generate datasets + let dataset_style = Dataset::default() .marker(symbols::Marker::Braille) + .graph_type(GraphType::Line); + + let mut datasets = vec![dataset_style + .clone() + .name("Write") .style(Style::default().fg(Color::Yellow)) - .graph_type(GraphType::Line) - .data(&wspeeds)]; + .data(&write_speeds)]; - if let Some(vdata) = &vspeeds { + if let Some(vdata) = &verify_speeds { datasets.push( - Dataset::default() + dataset_style .name("Verify") - .graph_type(GraphType::Scatter) - .marker(symbols::Marker::Braille) .style(Style::default().fg(Color::Blue)) - .graph_type(GraphType::Line) .data(&vdata), ); } + // Finally, build the chart! let chart = Chart::new(datasets) .block(Block::default().title("Speed").borders(Borders::ALL)) .x_axis( @@ -137,16 +90,39 @@ impl UIState { ) .y_axis( Axis::default() - .bounds([0.0, self.graph_max_speed]) + .bounds([0.0, state.max_y_limit]) .labels(y_ticks) .labels_alignment(Alignment::Right), ); - frame.render_widget(chart, area); + chart.render(area, buf) } } -pub struct StateProgressBar { +fn calculate_ticks( + area: Rect, + max_time: f64, + highest_value: f64, +) -> (Vec>, Vec>) { + let n_x_ticks = (area.width / 16).min(9); + let n_y_ticks = (area.height / 4).min(5); + let x_ticks: Vec<_> = (0..=n_x_ticks) + .map(|i| { + let x = i as f64 * max_time / n_x_ticks as f64; + Span::from(format!("{x:.1}s")) + }) + .collect(); + let y_ticks: Vec<_> = (0..=n_y_ticks) + .map(|i| { + let y = i as f64 * highest_value / n_y_ticks as f64; + let bytes = ByteSize::b(y as u64); + Span::from(format!("{bytes}/s")) + }) + .collect(); + (x_ticks, y_ticks) +} + +pub struct WriterProgressBar { bytes_written: u64, display_total_bytes: Option, ratio: f64, @@ -154,7 +130,44 @@ pub struct StateProgressBar { style: Style, } -impl StateProgressBar { +impl WriterProgressBar { + pub fn from_writer(state: &WriterState) -> WriterProgressBar { + match state { + WriterState::Writing(st) => WriterProgressBar { + bytes_written: st.write_hist.bytes_encountered(), + label_state: "Burning...", + style: Style::default().fg(Color::Yellow), + ratio: st.approximate_ratio(), + display_total_bytes: st.total_raw_bytes, + }, + WriterState::Verifying { + verify_hist, + total_write_bytes, + .. + } => WriterProgressBar::from_simple( + verify_hist.bytes_encountered(), + *total_write_bytes, + "Verifying...", + Style::default().fg(Color::Blue).bg(Color::Yellow), + ), + WriterState::Finished { + write_hist, + error, + total_write_bytes, + .. + } => WriterProgressBar::from_simple( + write_hist.bytes_encountered(), + *total_write_bytes, + if error.is_some() { "Error!" } else { "Done!" }, + if error.is_some() { + Style::default().fg(Color::White).bg(Color::Red) + } else { + Style::default().fg(Color::Green).bg(Color::Black) + }, + ), + } + } + fn from_simple(bytes_written: u64, max: u64, label_state: &'static str, style: Style) -> Self { Self { bytes_written, @@ -189,65 +202,67 @@ impl StateProgressBar { } } -impl Default for UIState { - fn default() -> Self { - Self { - graph_max_speed: 0.0, - } - } +pub struct WritingInfoTable<'a> { + pub input_filename: &'a str, + pub target_filename: &'a str, + pub state: &'a WriterState, } -pub fn make_info_table<'a>( - input_filename: &'a str, - target_filename: &'a str, - state: &'a WriterState, -) -> Table<'a> { - let wdata = state.write_hist(); - - let mut rows = vec![ - Row::new([Cell::from("Input"), Cell::from(input_filename)]), - Row::new([Cell::from("Output"), Cell::from(target_filename)]), - Row::new([ - Cell::from("Avg. Write"), - Cell::from(format!("{}", wdata.total_avg_speed())), - ]), - ]; - - match &state { - WriterState::Writing(st) => { - rows.push(Row::new([ - Cell::from("ETA Write"), - Cell::from(format!("{}", st.eta_write())), - ])); - } - WriterState::Verifying { - verify_hist: vdata, - total_write_bytes, - .. - } => { - rows.push(Row::new([ - Cell::from("Avg. Verify"), - Cell::from(format!("{}", vdata.total_avg_speed())), - ])); - rows.push(Row::new([ - Cell::from("ETA verify"), - Cell::from(format!("{}", vdata.estimated_time_left(*total_write_bytes))), - ])); - } - WriterState::Finished { - verify_hist: vdata, .. - } => { - if let Some(vdata) = vdata { +impl WritingInfoTable<'_> { + fn make_info_table(&self) -> Table<'_> { + let wdata = self.state.write_hist(); + + let mut rows = vec![ + Row::new([Cell::from("Input"), Cell::from(self.input_filename)]), + Row::new([Cell::from("Output"), Cell::from(self.target_filename)]), + Row::new([ + Cell::from("Avg. Write"), + Cell::from(format!("{}", wdata.total_avg_speed())), + ]), + ]; + + match &self.state { + WriterState::Writing(st) => { + rows.push(Row::new([ + Cell::from("ETA Write"), + Cell::from(format!("{}", st.eta_write())), + ])); + } + WriterState::Verifying { + verify_hist: vdata, + total_write_bytes, + .. + } => { rows.push(Row::new([ Cell::from("Avg. Verify"), Cell::from(format!("{}", vdata.total_avg_speed())), ])); + rows.push(Row::new([ + Cell::from("ETA verify"), + Cell::from(format!("{}", vdata.estimated_time_left(*total_write_bytes))), + ])); + } + WriterState::Finished { + verify_hist: vdata, .. + } => { + if let Some(vdata) = vdata { + rows.push(Row::new([ + Cell::from("Avg. Verify"), + Cell::from(format!("{}", vdata.total_avg_speed())), + ])); + } } } + + Table::new(rows) + .style(Style::default()) + .widths(&[Constraint::Length(16), Constraint::Percentage(100)]) + .block(Block::default().title("Stats").borders(Borders::ALL)) } +} - Table::new(rows) - .style(Style::default()) - .widths(&[Constraint::Length(16), Constraint::Percentage(100)]) - .block(Block::default().title("Stats").borders(Borders::ALL)) +impl Widget for WritingInfoTable<'_> { + fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) { + Widget::render(self.make_info_table(), area, buf) + } }