diff --git a/cspell.json b/cspell.json index 3dfa75f..ca42b52 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["crossterm","ratatui","donkeytype","tempfile","mockall","automock","foobarbaaz","foobarbazqux","withf","rngs","Szewnia","chrono","datetime"],"flagWords":[],"language":"en","version":"0.2"} +{"version":"0.2","language":"en","flagWords":[],"words":["crossterm","ratatui","donkeytype","tempfile","mockall","automock","foobarbaaz","foobarbazqux","withf","rngs","Szewnia","chrono","datetime","Datelike"]} diff --git a/src/main.rs b/src/main.rs index e9bf9f7..e086822 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,21 +76,13 @@ mod test_results; use anyhow::{Context, Result}; use clap::Parser; -use crossterm::event::{self, KeyCode}; +use crossterm::execute; use crossterm::terminal::supports_keyboard_enhancement; -use crossterm::{event::KeyEventKind, execute, ExecutableCommand}; use crossterm::{ event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use ratatui::{ - backend::CrosstermBackend, - style::{Color, Style}, - symbols, - text::Span, - widgets::{Axis, Block, Chart, Dataset, GraphType}, - Terminal, -}; +use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; use args::Args; @@ -124,13 +116,12 @@ fn main() -> anyhow::Result<()> { match res { Ok(test_results) => { if test_results.completed { - if let Err(err) = test_results.render_stats(&mut terminal) { + if let Err(err) = test_results.render_chart(&mut terminal) { eprintln!("{:?}", err); restore_terminal(terminal).context("Unable to restore terminal")?; return Err(err); } - if test_results.save { if let Err(err) = test_results.save_to_file() { eprintln!("{:?}", err); @@ -139,13 +130,6 @@ fn main() -> anyhow::Result<()> { return Err(err); } } - - if let Err(err) = show_results() { - eprintln!("{:?}", err); - - restore_terminal(terminal).context("Unable to restore terminal")?; - return Err(err); - } } else { println!("Test not finished."); } @@ -163,70 +147,6 @@ fn main() -> anyhow::Result<()> { } } -fn show_results() -> Result<()> { - io::stderr().execute(EnterAlternateScreen)?; - enable_raw_mode()?; - let mut terminal = Terminal::new(CrosstermBackend::new(io::stderr()))?; - terminal.clear()?; - - loop { - terminal.draw(|frame| { - let area = frame.size(); - - let datasets = vec![Dataset::default() - .name("data1") - .marker(symbols::Marker::Dot) - .graph_type(GraphType::Scatter) - .style(Style::default().fg(Color::Cyan)) - .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)])]; - - frame.render_widget( - Chart::new(datasets) - .block(Block::default().title("Chart")) - .x_axis( - Axis::default() - .title(Span::styled("X Axis", Style::default().fg(Color::Red))) - .style(Style::default().fg(Color::White)) - .bounds([0.0, 10.0]) - .labels( - ["0.0", "5.0", "10.0"] - .iter() - .cloned() - .map(Span::from) - .collect(), - ), - ) - .y_axis( - Axis::default() - .title(Span::styled("Y Axis", Style::default().fg(Color::Red))) - .style(Style::default().fg(Color::White)) - .bounds([0.0, 10.0]) - .labels( - ["0.0", "5.0", "10.0"] - .iter() - .cloned() - .map(Span::from) - .collect(), - ), - ), - area, - ); - })?; - - if event::poll(std::time::Duration::from_millis(100))? { - if let event::Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { - break; - } - } - } - } - - io::stderr().execute(LeaveAlternateScreen)?; - disable_raw_mode()?; - Ok(()) -} - /// prepares terminal window for rendering using tui fn configure_terminal() -> Result>, anyhow::Error> { enable_raw_mode().context("Unable to enable raw mode")?; diff --git a/src/test_results.rs b/src/test_results.rs index d8b4bcd..7b9eee9 100644 --- a/src/test_results.rs +++ b/src/test_results.rs @@ -4,11 +4,13 @@ //! and save those results and configuration of the test to a file. use anyhow::{Context, Result}; -use chrono::{DateTime, Local}; +use chrono::{DateTime, Datelike, Local, Timelike}; use crossterm::event::{self, Event, KeyCode}; use ratatui::{ prelude::{Backend, Constraint, Direction, Layout}, - widgets::Paragraph, + style::{Style, Stylize}, + widgets::{Bar, BarGroup, Block}, + widgets::{BarChart, Paragraph}, Terminal, }; use serde::{Deserialize, Serialize}; @@ -109,7 +111,7 @@ impl TestResults { } /// saves test statistics and configuration to a file in users home directory - pub fn save_to_file(self) -> Result<(), anyhow::Error> { + pub fn save_to_file(&self) -> Result<(), anyhow::Error> { let results_dir_path = dirs::home_dir() .context("Unable to get home directory")? .join(".local") @@ -151,10 +153,34 @@ impl TestResults { Ok(()) } - /// prints statistics in an easy to read format - pub fn render_stats(&self, terminal: &mut Terminal) -> Result<()> { - terminal - .draw(|frame| { + pub fn render_chart(&self, terminal: &mut Terminal) -> Result<()> { + let results_dir_path = dirs::home_dir() + .context("Unable to get home directory")? + .join(".local") + .join("share") + .join("donkeytype"); + + if !results_dir_path.exists() { + create_dir_all(results_dir_path.clone()) + .context("Unable to create results directory for results file")?; + } + + let results_file_path = results_dir_path.join("donkeytype-results.csv"); + + let mut reader = csv::Reader::from_path(results_file_path.clone()) + .context("Unable to create CSV Reader")?; + + let mut records: Vec = reader + .deserialize() + .collect::>() + .context("Unable to deserialize results")?; + + records.push(self.clone()); + + loop { + terminal.draw(|frame| { + let mut records_to_show = records.clone(); + let areas = Layout::default() .direction(Direction::Vertical) .constraints( @@ -169,6 +195,10 @@ impl TestResults { Constraint::Length(1), Constraint::Length(1), Constraint::Length(2), + Constraint::Length(12), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Min(1), Constraint::Length(1), ] .as_ref(), @@ -229,21 +259,91 @@ impl TestResults { areas[8], ); } + if let Some(typed_characters_count) = self.typed_characters_count { frame.render_widget( Paragraph::new(format!( "Characters typed after corrections: {}", - typed_characters_count + typed_characters_count, )), areas[9], ); } - frame.render_widget(Paragraph::new("Press to quit"), areas[10]); - }) - .context("Unable to render stats")?; - loop { - // Poll for key events + let bar_width = 5; + let frame_width = frame.size().width; + let bars_to_show = ((frame_width + 1) / (bar_width + 1)) as usize; + + if records.len() >= bars_to_show { + records_to_show = records[records.len() - bars_to_show..].to_vec(); + } + + frame.render_widget( + BarChart::default() + .block(Block::default().title("Previous chart")) + .bar_width(bar_width) + .bar_gap(1) + .bar_style(Style::new().white().on_black()) + .value_style(Style::new().black().on_white()) + .label_style(Style::new().yellow()) + .data( + BarGroup::default().bars( + &records_to_show + .iter() + .map(|r| { + Bar::default().value(if let Some(wpm) = r.wpm { + wpm as u64 + } else { + 0 + }) + }) + .collect::>(), + ), + ), + areas[10], + ); + frame.render_widget( + Paragraph::new( + records_to_show + .iter() + .map(|r| { + format!( + "{}:{} ", + fmt_num(r.local_datetime.hour()), + fmt_num(r.local_datetime.minute()) + ) + }) + .collect::(), + ), + areas[11], + ); + frame.render_widget( + Paragraph::new( + records_to_show + .iter() + .map(|r| { + format!( + "{}/{} ", + fmt_num(r.local_datetime.month()), + fmt_num(r.local_datetime.day()) + ) + }) + .collect::(), + ), + areas[12], + ); + frame.render_widget( + Paragraph::new( + records_to_show + .iter() + .map(|r| format!("{} ", r.local_datetime.year())) + .collect::(), + ), + areas[13], + ); + frame.render_widget(Paragraph::new("Press to quit"), areas[14]); + })?; + if event::poll(Duration::from_millis(100)).context("Unable to poll for event")? { if let Event::Key(key) = event::read().context("Unable to read event")? { match key.code { @@ -254,10 +354,17 @@ impl TestResults { } } } - sleep(Duration::from_millis(100)); } Ok(()) } } + +fn fmt_num(number: u32) -> String { + if number < 10 { + format!("0{}", number) + } else { + format!("{}", number) + } +}