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::content::Mode;
use crate::ui::theme::Theme;

#[derive(Debug, Clone)]
pub struct MenuState {
    pub selected_mode: Mode,
    pub selected_language: String,
    pub selected_duration: u64,
    pub language_options: Vec<String>,
}

impl MenuState {
    pub fn new(mode: Mode, language: String, duration: u64) -> Self {
        let language_options = mode.languages().iter().map(|&s| s.to_string()).collect();
        Self {
            selected_mode: mode,
            selected_language: language,
            selected_duration: duration,
            language_options,
        }
    }
}

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

    // Title
    let title = Paragraph::new(vec![
        Line::from(Span::styled(" typetui ", theme.style_accent())),
        Line::from(Span::styled(" a beautiful terminal typing test ", theme.style_muted())),
    ])
    .alignment(Alignment::Center);
    f.render_widget(title, chunks[0]);

    // Menu options
    let menu_chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(6),
            Constraint::Length(5),
            Constraint::Min(0),
        ])
        .split(chunks[1]);

    render_settings_summary(f, state, theme, menu_chunks[0]);
    render_start_button(f, theme, menu_chunks[1]);

    // Help text
    let help = Paragraph::new(Line::from(vec![
        Span::styled("enter ", theme.style_accent()),
        Span::styled("start  ", theme.style_muted()),
        Span::styled("ctrl+m ", theme.style_accent()),
        Span::styled("mode  ", theme.style_muted()),
        Span::styled("ctrl+l ", theme.style_accent()),
        Span::styled("lang  ", theme.style_muted()),
        Span::styled("ctrl+d ", theme.style_accent()),
        Span::styled("duration  ", theme.style_muted()),
        Span::styled("ctrl+t ", theme.style_accent()),
        Span::styled("theme  ", theme.style_muted()),
        Span::styled("ctrl+p ", theme.style_accent()),
        Span::styled("profile  ", theme.style_muted()),
        Span::styled("esc ", theme.style_accent()),
        Span::styled("quit", theme.style_muted()),
    ]))
    .alignment(Alignment::Center);
    f.render_widget(help, chunks[2]);
}

fn render_settings_summary(f: &mut Frame, state: &MenuState, theme: &Theme, area: Rect) {
    let text = vec![
        Line::from(vec![
            Span::styled("mode: ", theme.style_muted()),
            Span::styled(
                state.selected_mode.as_str(),
                theme.style(theme.accent).add_modifier(Modifier::BOLD),
            ),
        ]),
        Line::from(vec![
            Span::styled("language: ", theme.style_muted()),
            Span::styled(
                &state.selected_language,
                theme.style(theme.accent).add_modifier(Modifier::BOLD),
            ),
        ]),
        Line::from(vec![
            Span::styled("duration: ", theme.style_muted()),
            Span::styled(
                format!("{}s", state.selected_duration),
                theme.style(theme.accent).add_modifier(Modifier::BOLD),
            ),
        ]),
    ];

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

fn render_start_button(f: &mut Frame, theme: &Theme, area: Rect) {
    let style = theme
        .style(theme.success)
        .add_modifier(Modifier::BOLD);

    let text = vec![
        Line::from(""),
        Line::from(vec![Span::styled(
            "  press enter to start  ",
            style,
        )]),
    ];

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