typetui 0.1.0

A terminal-based typing test.
Documentation
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::Modifier;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;

use crate::stats::TestResult;
use crate::ui::theme::Theme;

#[derive(Debug, Clone)]
pub struct ResultsState;

impl Default for ResultsState {
    fn default() -> Self {
        Self
    }
}

pub fn render(
    f: &mut Frame,
    result: &TestResult,
    _state: &ResultsState,
    theme: &Theme,
    area: Rect,
) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(2)
        .constraints([
            Constraint::Length(3),
            Constraint::Min(5),
            Constraint::Length(3),
        ])
        .split(area);

    render_title(f, result, theme, chunks[0]);
    render_main_results(f, result, theme, chunks[1]);
    render_help(f, theme, chunks[2]);
}

fn render_title(f: &mut Frame, result: &TestResult, theme: &Theme, area: Rect) {
    let title = Paragraph::new(vec![
        Line::from(Span::styled(
            " test complete! ",
            theme.style_accent().add_modifier(Modifier::BOLD),
        )),
        Line::from(Span::styled(
            format!("{} mode • {}{}s", result.mode.to_lowercase(), result.language, result.duration),
            theme.style_muted(),
        )),
    ])
    .alignment(Alignment::Center);

    f.render_widget(title, area);
}

fn render_main_results(f: &mut Frame, result: &TestResult, theme: &Theme, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3),
            Constraint::Length(3),
            Constraint::Length(3),
            Constraint::Min(0),
        ])
        .split(area);

    let wpm_row = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(chunks[0]);

    render_big_stat(
        f,
        "wpm",
        &format!("{:.0}", result.wpm),
        theme.style(theme.success),
        theme,
        wpm_row[0],
    );
    render_big_stat(
        f,
        "raw wpm",
        &format!("{:.0}", result.raw_wpm),
        theme.style(theme.accent),
        theme,
        wpm_row[1],
    );

    let acc_row = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(chunks[1]);

    let accuracy_style = if result.accuracy >= 95.0 {
        theme.style(theme.success)
    } else if result.accuracy >= 85.0 {
        theme.style(theme.warning)
    } else {
        theme.style(theme.error)
    };

    render_big_stat(
        f,
        "accuracy",
        &format!("{:.1}%", result.accuracy),
        accuracy_style,
        theme,
        acc_row[0],
    );
    render_big_stat(
        f,
        "errors",
        &format!("{}", result.errors),
        theme.style(theme.error),
        theme,
        acc_row[1],
    );

    let char_row = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(chunks[2]);

    render_big_stat(
        f,
        "characters",
        &format!("{}", result.total_chars),
        theme.style(theme.muted),
        theme,
        char_row[0],
    );
    render_big_stat(
        f,
        "duration",
        &format!("{}s", result.duration),
        theme.style(theme.muted),
        theme,
        char_row[1],
    );
}

fn render_big_stat(
    f: &mut Frame,
    label: &str,
    value: &str,
    value_style: ratatui::style::Style,
    theme: &Theme,
    area: Rect,
) {
    let text = vec![
        Line::from(vec![Span::styled(
            value,
            value_style.add_modifier(Modifier::BOLD),
        )]),
        Line::from(vec![Span::styled(label, theme.style_muted())]),
    ];

    let para = Paragraph::new(text).alignment(Alignment::Center);
    f.render_widget(para, area);
}

fn render_help(f: &mut Frame, theme: &Theme, area: Rect) {
    let help_text = vec![
        Span::styled("tab/r", theme.style_accent()),
        Span::styled(" restart • ", theme.style_muted()),
        Span::styled("ctrl+p", theme.style_accent()),
        Span::styled(" profile • ", theme.style_muted()),
        Span::styled("esc", theme.style_accent()),
        Span::styled(" menu", theme.style_muted()),
    ];

    let help = Paragraph::new(Line::from(help_text)).alignment(Alignment::Center);
    f.render_widget(help, area);
}