diff --git a/src/app.rs b/src/app.rs index 5f9504b..a2105a4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -246,7 +246,13 @@ impl App { } self.last_result = Some(result); - self.screen = AppScreen::LessonResult; + + // Adaptive mode auto-continues to next lesson (like keybr.com) + if self.lesson_mode == LessonMode::Adaptive { + self.start_lesson(); + } else { + self.screen = AppScreen::LessonResult; + } self.save_data(); } diff --git a/src/main.rs b/src/main.rs index 7715468..4c1f5e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -207,7 +207,8 @@ fn handle_lesson_key(app: &mut App, key: KeyEvent) { match key.code { KeyCode::Esc => { let has_progress = app.lesson.as_ref().is_some_and(|l| l.cursor > 0); - if has_progress { + if has_progress && app.lesson_mode != LessonMode::Adaptive { + // Non-adaptive: show result screen for partial lesson if let Some(ref lesson) = app.lesson { let result = LessonResult::from_lesson(lesson, &app.lesson_events, app.lesson_mode.as_str()); app.last_result = Some(result); @@ -488,7 +489,7 @@ fn render_lesson(frame: &mut ratatui::Frame, app: &App) { } if let Some(sidebar_area) = app_layout.sidebar { - let sidebar = StatsSidebar::new(lesson, app.theme); + let sidebar = StatsSidebar::new(lesson, app.last_result.as_ref(), app.theme); frame.render_widget(sidebar, sidebar_area); } diff --git a/src/ui/components/stats_sidebar.rs b/src/ui/components/stats_sidebar.rs index 2b7f119..beda39c 100644 --- a/src/ui/components/stats_sidebar.rs +++ b/src/ui/components/stats_sidebar.rs @@ -1,20 +1,22 @@ use ratatui::buffer::Buffer; -use ratatui::layout::Rect; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::Style; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph, Widget}; use crate::session::lesson::LessonState; +use crate::session::result::LessonResult; use crate::ui::theme::Theme; pub struct StatsSidebar<'a> { lesson: &'a LessonState, + last_result: Option<&'a LessonResult>, theme: &'a Theme, } impl<'a> StatsSidebar<'a> { - pub fn new(lesson: &'a LessonState, theme: &'a Theme) -> Self { - Self { lesson, theme } + pub fn new(lesson: &'a LessonState, last_result: Option<&'a LessonResult>, theme: &'a Theme) -> Self { + Self { lesson, last_result, theme } } } @@ -22,66 +24,136 @@ impl Widget for StatsSidebar<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let colors = &self.theme.colors; - let wpm = self.lesson.wpm(); - let accuracy = self.lesson.accuracy(); - let progress = self.lesson.progress() * 100.0; - let correct = self.lesson.correct_count(); - let incorrect = self.lesson.typo_count(); - let elapsed = self.lesson.elapsed_secs(); + let has_last = self.last_result.is_some(); - let wpm_str = format!("{wpm:.0}"); - let acc_str = format!("{accuracy:.1}%"); - let prog_str = format!("{progress:.0}%"); - let correct_str = format!("{correct}"); - let incorrect_str = format!("{incorrect}"); - let elapsed_str = format!("{elapsed:.1}s"); + // Split sidebar into current stats and last lesson sections + let sections = if has_last { + Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(10), Constraint::Min(10)]) + .split(area) + } else { + Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(10), Constraint::Length(0)]) + .split(area) + }; - let lines = vec![ - Line::from(vec![ - Span::styled("WPM: ", Style::default().fg(colors.fg())), - Span::styled(&*wpm_str, Style::default().fg(colors.accent())), - ]), - Line::from(""), - Line::from(vec![ - Span::styled("Accuracy: ", Style::default().fg(colors.fg())), - Span::styled( - &*acc_str, - Style::default().fg(if accuracy >= 95.0 { - colors.success() - } else if accuracy >= 85.0 { - colors.warning() - } else { - colors.error() - }), - ), - ]), - Line::from(""), - Line::from(vec![ - Span::styled("Progress: ", Style::default().fg(colors.fg())), - Span::styled(&*prog_str, Style::default().fg(colors.accent())), - ]), - Line::from(""), - Line::from(vec![ - Span::styled("Correct: ", Style::default().fg(colors.fg())), - Span::styled(&*correct_str, Style::default().fg(colors.success())), - ]), - Line::from(vec![ - Span::styled("Errors: ", Style::default().fg(colors.fg())), - Span::styled(&*incorrect_str, Style::default().fg(colors.error())), - ]), - Line::from(""), - Line::from(vec![ - Span::styled("Time: ", Style::default().fg(colors.fg())), - Span::styled(&*elapsed_str, Style::default().fg(colors.fg())), - ]), - ]; + // Current lesson stats + { + let wpm = self.lesson.wpm(); + let accuracy = self.lesson.accuracy(); + let progress = self.lesson.progress() * 100.0; + let correct = self.lesson.correct_count(); + let incorrect = self.lesson.typo_count(); + let elapsed = self.lesson.elapsed_secs(); - let block = Block::bordered() - .title(" Stats ") - .border_style(Style::default().fg(colors.border())) - .style(Style::default().bg(colors.bg())); + let wpm_str = format!("{wpm:.0}"); + let acc_str = format!("{accuracy:.1}%"); + let prog_str = format!("{progress:.0}%"); + let correct_str = format!("{correct}"); + let incorrect_str = format!("{incorrect}"); + let elapsed_str = format!("{elapsed:.1}s"); - let paragraph = Paragraph::new(lines).block(block); - paragraph.render(area, buf); + let lines = vec![ + Line::from(vec![ + Span::styled("WPM: ", Style::default().fg(colors.fg())), + Span::styled(wpm_str, Style::default().fg(colors.accent())), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Accuracy: ", Style::default().fg(colors.fg())), + Span::styled( + acc_str, + Style::default().fg(if accuracy >= 95.0 { + colors.success() + } else if accuracy >= 85.0 { + colors.warning() + } else { + colors.error() + }), + ), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Progress: ", Style::default().fg(colors.fg())), + Span::styled(prog_str, Style::default().fg(colors.accent())), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Correct: ", Style::default().fg(colors.fg())), + Span::styled(correct_str, Style::default().fg(colors.success())), + ]), + Line::from(vec![ + Span::styled("Errors: ", Style::default().fg(colors.fg())), + Span::styled(incorrect_str, Style::default().fg(colors.error())), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Time: ", Style::default().fg(colors.fg())), + Span::styled(elapsed_str, Style::default().fg(colors.fg())), + ]), + ]; + + let block = Block::bordered() + .title(" Stats ") + .border_style(Style::default().fg(colors.border())) + .style(Style::default().bg(colors.bg())); + + let paragraph = Paragraph::new(lines).block(block); + paragraph.render(sections[0], buf); + } + + // Last lesson stats + if let Some(last) = self.last_result { + let wpm_str = format!("{:.0}", last.wpm); + let acc_str = format!("{:.1}%", last.accuracy); + let chars_str = format!("{}", last.total_chars); + let time_str = format!("{:.1}s", last.elapsed_secs); + let errors_str = format!("{}", last.incorrect); + + let lines = vec![ + Line::from(vec![ + Span::styled("WPM: ", Style::default().fg(colors.fg())), + Span::styled(wpm_str, Style::default().fg(colors.accent())), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Accuracy: ", Style::default().fg(colors.fg())), + Span::styled( + acc_str, + Style::default().fg(if last.accuracy >= 95.0 { + colors.success() + } else if last.accuracy >= 85.0 { + colors.warning() + } else { + colors.error() + }), + ), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Chars: ", Style::default().fg(colors.fg())), + Span::styled(chars_str, Style::default().fg(colors.fg())), + ]), + Line::from(vec![ + Span::styled("Errors: ", Style::default().fg(colors.fg())), + Span::styled(errors_str, Style::default().fg(colors.error())), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Time: ", Style::default().fg(colors.fg())), + Span::styled(time_str, Style::default().fg(colors.fg())), + ]), + ]; + + let block = Block::bordered() + .title(" Last Lesson ") + .border_style(Style::default().fg(colors.border())) + .style(Style::default().bg(colors.bg())); + + let paragraph = Paragraph::new(lines).block(block); + paragraph.render(sections[1], buf); + } } }