Continue into another drill after completing one

This commit is contained in:
2026-02-15 00:41:59 +00:00
parent a0e8f3cafb
commit a51adafeb0
3 changed files with 142 additions and 63 deletions

View File

@@ -246,7 +246,13 @@ impl App {
} }
self.last_result = Some(result); 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(); self.save_data();
} }

View File

@@ -207,7 +207,8 @@ fn handle_lesson_key(app: &mut App, key: KeyEvent) {
match key.code { match key.code {
KeyCode::Esc => { KeyCode::Esc => {
let has_progress = app.lesson.as_ref().is_some_and(|l| l.cursor > 0); 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 { if let Some(ref lesson) = app.lesson {
let result = LessonResult::from_lesson(lesson, &app.lesson_events, app.lesson_mode.as_str()); let result = LessonResult::from_lesson(lesson, &app.lesson_events, app.lesson_mode.as_str());
app.last_result = Some(result); 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 { 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); frame.render_widget(sidebar, sidebar_area);
} }

View File

@@ -1,20 +1,22 @@
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph, Widget}; use ratatui::widgets::{Block, Paragraph, Widget};
use crate::session::lesson::LessonState; use crate::session::lesson::LessonState;
use crate::session::result::LessonResult;
use crate::ui::theme::Theme; use crate::ui::theme::Theme;
pub struct StatsSidebar<'a> { pub struct StatsSidebar<'a> {
lesson: &'a LessonState, lesson: &'a LessonState,
last_result: Option<&'a LessonResult>,
theme: &'a Theme, theme: &'a Theme,
} }
impl<'a> StatsSidebar<'a> { impl<'a> StatsSidebar<'a> {
pub fn new(lesson: &'a LessonState, theme: &'a Theme) -> Self { pub fn new(lesson: &'a LessonState, last_result: Option<&'a LessonResult>, theme: &'a Theme) -> Self {
Self { lesson, theme } Self { lesson, last_result, theme }
} }
} }
@@ -22,66 +24,136 @@ impl Widget for StatsSidebar<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let colors = &self.theme.colors; let colors = &self.theme.colors;
let wpm = self.lesson.wpm(); let has_last = self.last_result.is_some();
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 wpm_str = format!("{wpm:.0}"); // Split sidebar into current stats and last lesson sections
let acc_str = format!("{accuracy:.1}%"); let sections = if has_last {
let prog_str = format!("{progress:.0}%"); Layout::default()
let correct_str = format!("{correct}"); .direction(Direction::Vertical)
let incorrect_str = format!("{incorrect}"); .constraints([Constraint::Min(10), Constraint::Min(10)])
let elapsed_str = format!("{elapsed:.1}s"); .split(area)
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(10), Constraint::Length(0)])
.split(area)
};
let lines = vec![ // Current lesson stats
Line::from(vec![ {
Span::styled("WPM: ", Style::default().fg(colors.fg())), let wpm = self.lesson.wpm();
Span::styled(&*wpm_str, Style::default().fg(colors.accent())), let accuracy = self.lesson.accuracy();
]), let progress = self.lesson.progress() * 100.0;
Line::from(""), let correct = self.lesson.correct_count();
Line::from(vec![ let incorrect = self.lesson.typo_count();
Span::styled("Accuracy: ", Style::default().fg(colors.fg())), let elapsed = self.lesson.elapsed_secs();
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() let wpm_str = format!("{wpm:.0}");
.title(" Stats ") let acc_str = format!("{accuracy:.1}%");
.border_style(Style::default().fg(colors.border())) let prog_str = format!("{progress:.0}%");
.style(Style::default().bg(colors.bg())); let correct_str = format!("{correct}");
let incorrect_str = format!("{incorrect}");
let elapsed_str = format!("{elapsed:.1}s");
let paragraph = Paragraph::new(lines).block(block); let lines = vec![
paragraph.render(area, buf); 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);
}
} }
} }