use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io;
use std::time::{Duration, Instant};
use crate::config::Config;
use crate::stats::TestResult;
use crate::storage::Storage;
use crate::typing::TypingTest;
use crate::ui::menu::MenuState;
use crate::ui::results::ResultsState;
use crate::ui::screens::duration_select::DurationSelectState;
use crate::ui::screens::language_select::LanguageSelectState;
use crate::ui::screens::mode_select::ModeSelectState;
use crate::ui::screens::theme_select::ThemeSelectState;
use crate::ui::theme::Theme;
pub struct App {
pub screen: Screen,
pub config: Config,
pub theme: Theme,
pub menu_state: MenuState,
pub typing_test: Option<TypingTest>,
pub results_state: ResultsState,
pub last_result: Option<TestResult>,
pub should_exit: bool,
pub tick_rate: Duration,
pub last_tick: Instant,
pub language_select_state: Option<LanguageSelectState>,
pub mode_select_state: Option<ModeSelectState>,
pub duration_select_state: Option<DurationSelectState>,
pub theme_select_state: Option<ThemeSelectState>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Screen {
Menu,
Typing,
Results,
Profile,
LanguageSelect,
ModeSelect,
DurationSelect,
ThemeSelect,
}
impl App {
pub fn new() -> Self {
let config = Config::load();
let theme = config.theme();
let menu_state = MenuState::new(
config.mode(),
config.default_language.clone(),
config.duration(),
);
let typing_test = Some(TypingTest::new(
menu_state.selected_mode,
menu_state.selected_language.clone(),
menu_state.selected_duration,
));
Self {
screen: Screen::Typing,
config,
theme,
menu_state,
typing_test,
results_state: ResultsState::default(),
last_result: None,
should_exit: false,
tick_rate: Duration::from_millis(1000),
last_tick: Instant::now(),
language_select_state: None,
mode_select_state: None,
duration_select_state: None,
theme_select_state: None,
}
}
pub fn run(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
while !self.should_exit {
terminal.draw(|f| self.draw(f))?;
if self.last_tick.elapsed() >= self.tick_rate {
self.on_tick();
self.last_tick = Instant::now();
}
if event::poll(Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
self.handle_key(key.code, key.modifiers);
}
}
}
}
Ok(())
}
fn on_tick(&mut self) {
if let Some(ref mut test) = self.typing_test {
if test.is_active && !test.is_finished {
test.tick();
if test.is_finished {
self.finish_test();
}
}
}
}
fn handle_key(&mut self, code: KeyCode, modifiers: KeyModifiers) {
match self.screen {
Screen::Menu => self.handle_menu_key(code, modifiers),
Screen::Typing => self.handle_typing_key(code, modifiers),
Screen::Results => self.handle_results_key(code, modifiers),
Screen::Profile => self.handle_profile_key(code, modifiers),
Screen::LanguageSelect => self.handle_language_select_key(code),
Screen::ModeSelect => self.handle_mode_select_key(code),
Screen::DurationSelect => self.handle_duration_select_key(code),
Screen::ThemeSelect => self.handle_theme_select_key(code),
}
}
fn handle_menu_key(&mut self, code: KeyCode, modifiers: KeyModifiers) {
if modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::SUPER) {
match code {
KeyCode::Char('l') => {
self.language_select_state = Some(LanguageSelectState::new(
self.menu_state.selected_mode,
self.menu_state.selected_language.clone(),
));
self.screen = Screen::LanguageSelect;
return;
}
KeyCode::Char('m') => {
self.mode_select_state = Some(ModeSelectState::new(self.menu_state.selected_mode));
self.screen = Screen::ModeSelect;
return;
}
KeyCode::Char('d') => {
self.duration_select_state = Some(DurationSelectState::new(self.menu_state.selected_duration));
self.screen = Screen::DurationSelect;
return;
}
KeyCode::Char('t') => {
self.theme_select_state = Some(ThemeSelectState::new(self.theme.name));
self.screen = Screen::ThemeSelect;
return;
}
KeyCode::Char('p') => {
self.screen = Screen::Profile;
return;
}
_ => {}
}
}
match code {
KeyCode::Esc => self.should_exit = true,
KeyCode::Enter => self.start_test(),
_ => {}
}
}
fn handle_typing_key(&mut self, code: KeyCode, modifiers: KeyModifiers) {
if modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::SUPER) {
match code {
KeyCode::Char('p') => {
if let Some(ref test) = self.typing_test {
if test.is_finished {
self.screen = Screen::Profile;
}
}
}
_ => {}
}
return;
}
match code {
KeyCode::Esc => {
if let Some(ref test) = self.typing_test {
if test.is_finished {
self.screen = Screen::Menu;
} else {
self.screen = Screen::Menu;
self.typing_test = None;
}
}
}
KeyCode::Char(c) => {
if let Some(ref mut test) = self.typing_test {
if !test.is_finished {
test.handle_input(c);
}
}
}
KeyCode::Backspace => {
if let Some(ref mut test) = self.typing_test {
test.handle_backspace();
}
}
KeyCode::Enter => {
if let Some(ref mut test) = self.typing_test {
if !test.is_finished {
test.skip_to_end_of_line();
let next_indent = Self::get_next_line_indent(test);
test.handle_input('\n');
if let Some(indent) = next_indent {
for space in indent.chars() {
test.handle_input(space);
}
}
}
}
}
KeyCode::Tab => self.restart_test(),
_ => {}
}
}
fn handle_results_key(&mut self, code: KeyCode, modifiers: KeyModifiers) {
if modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::SUPER) {
match code {
KeyCode::Char('p') => self.screen = Screen::Profile,
_ => {}
}
return;
}
match code {
KeyCode::Esc => self.screen = Screen::Menu,
KeyCode::Tab => self.restart_test(),
KeyCode::Char('p') => self.screen = Screen::Profile,
KeyCode::Char('r') => self.restart_test(),
_ => {}
}
}
fn handle_profile_key(&mut self, code: KeyCode, modifiers: KeyModifiers) {
if modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::SUPER) {
match code {
KeyCode::Char('l') => {
self.language_select_state = Some(LanguageSelectState::new(
self.menu_state.selected_mode,
self.menu_state.selected_language.clone(),
));
self.screen = Screen::LanguageSelect;
}
KeyCode::Char('m') => {
self.mode_select_state = Some(ModeSelectState::new(self.menu_state.selected_mode));
self.screen = Screen::ModeSelect;
}
KeyCode::Char('d') => {
self.duration_select_state = Some(DurationSelectState::new(self.menu_state.selected_duration));
self.screen = Screen::DurationSelect;
}
KeyCode::Char('t') => {
self.theme_select_state = Some(ThemeSelectState::new(self.theme.name));
self.screen = Screen::ThemeSelect;
}
_ => {}
}
return;
}
match code {
KeyCode::Esc => self.screen = Screen::Menu,
KeyCode::Tab => self.restart_test(),
_ => {}
}
}
fn handle_language_select_key(&mut self, code: KeyCode) {
if let Some(ref mut state) = self.language_select_state {
match code {
KeyCode::Esc => {
self.language_select_state = None;
self.screen = Screen::Menu;
}
KeyCode::Enter => {
self.menu_state.selected_language = state.selected.clone();
self.menu_state.language_options = state.options.clone();
self.language_select_state = None;
self.screen = Screen::Menu;
}
KeyCode::Up | KeyCode::Left => state.prev(),
KeyCode::Down | KeyCode::Right => state.next(),
_ => {}
}
}
}
fn handle_mode_select_key(&mut self, code: KeyCode) {
if let Some(ref mut state) = self.mode_select_state {
match code {
KeyCode::Esc => {
self.mode_select_state = None;
self.screen = Screen::Menu;
}
KeyCode::Enter => {
self.menu_state.selected_mode = state.selected;
self.menu_state.language_options = state
.selected
.languages()
.iter()
.map(|&s| s.to_string())
.collect();
if !self.menu_state.language_options.contains(&self.menu_state.selected_language) {
self.menu_state.selected_language = state.selected.default_language().to_string();
}
self.mode_select_state = None;
self.screen = Screen::Menu;
}
KeyCode::Up | KeyCode::Left => state.prev(),
KeyCode::Down | KeyCode::Right => state.next(),
_ => {}
}
}
}
fn handle_duration_select_key(&mut self, code: KeyCode) {
if let Some(ref mut state) = self.duration_select_state {
match code {
KeyCode::Esc => {
self.duration_select_state = None;
self.screen = Screen::Menu;
}
KeyCode::Enter => {
self.menu_state.selected_duration = state.selected;
self.duration_select_state = None;
self.screen = Screen::Menu;
}
KeyCode::Up | KeyCode::Left => state.prev(),
KeyCode::Down | KeyCode::Right => state.next(),
_ => {}
}
}
}
fn handle_theme_select_key(&mut self, code: KeyCode) {
if let Some(ref mut state) = self.theme_select_state {
match code {
KeyCode::Esc => {
self.theme_select_state = None;
self.screen = Screen::Menu;
}
KeyCode::Enter => {
self.theme.name = state.selected;
self.theme = Theme::from_name(state.selected);
self.config.set_theme(state.selected);
let _ = self.config.save();
self.theme_select_state = None;
self.screen = Screen::Menu;
}
KeyCode::Up | KeyCode::Left => state.prev(),
KeyCode::Down | KeyCode::Right => state.next(),
_ => {}
}
}
}
fn start_test(&mut self) {
self.config.set_mode(self.menu_state.selected_mode);
self.config.default_language = self.menu_state.selected_language.clone();
self.config.set_duration(self.menu_state.selected_duration);
let _ = self.config.save();
self.typing_test = Some(TypingTest::new(
self.menu_state.selected_mode,
self.menu_state.selected_language.clone(),
self.menu_state.selected_duration,
));
self.screen = Screen::Typing;
}
fn restart_test(&mut self) {
if let Some(ref test) = self.typing_test {
self.typing_test = Some(TypingTest::new(
test.mode.clone(),
test.language.clone(),
test.duration_secs,
));
self.screen = Screen::Typing;
} else {
self.start_test();
}
}
fn get_next_line_indent(test: &TypingTest) -> Option<String> {
let target_chars: Vec<char> = test.target_text.chars().collect();
for i in test.cursor_position..target_chars.len() {
if target_chars[i] == '\n' {
let after_newline = &target_chars[i + 1..];
let indent: String = after_newline
.iter()
.take_while(|c| c.is_whitespace() && **c != '\n')
.collect();
return Some(indent);
}
}
None
}
fn finish_test(&mut self) {
if let Some(ref test) = self.typing_test {
let result = TestResult::new(
test.mode.as_str(),
&test.language,
test.duration_secs,
&test.stats,
test.elapsed_secs,
);
let _ = Storage::append_result(&result);
self.last_result = Some(result);
self.screen = Screen::Results;
}
}
fn draw(&self, f: &mut ratatui::Frame) {
let area = f.area();
match self.screen {
Screen::Menu => crate::ui::menu::render(f, &self.menu_state, &self.theme, area),
Screen::Typing => {
if let Some(ref test) = self.typing_test {
crate::ui::test::render(f, test, &self.theme, area);
}
}
Screen::Results => {
if let Some(ref result) = self.last_result {
crate::ui::results::render(f, result, &self.results_state, &self.theme, area);
}
}
Screen::Profile => crate::ui::profile::render(f, &self.theme, area),
Screen::LanguageSelect => {
crate::ui::menu::render(f, &self.menu_state, &self.theme, area);
if let Some(ref state) = self.language_select_state {
crate::ui::screens::language_select::render(f, state, &self.theme, area);
}
}
Screen::ModeSelect => {
crate::ui::menu::render(f, &self.menu_state, &self.theme, area);
if let Some(ref state) = self.mode_select_state {
crate::ui::screens::mode_select::render(f, state, &self.theme, area);
}
}
Screen::DurationSelect => {
crate::ui::menu::render(f, &self.menu_state, &self.theme, area);
if let Some(ref state) = self.duration_select_state {
crate::ui::screens::duration_select::render(f, state, &self.theme, area);
}
}
Screen::ThemeSelect => {
crate::ui::menu::render(f, &self.menu_state, &self.theme, area);
if let Some(ref state) = self.theme_select_state {
crate::ui::screens::theme_select::render(f, state, &self.theme, area);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::content::Mode;
#[test]
fn test_get_next_line_indent() {
let mut test = TypingTest::new(Mode::Code, "python".to_string(), 60);
test.target_text = "def hello():\n return x\n".to_string();
test.cursor_position = 12;
let indent = App::get_next_line_indent(&test);
assert_eq!(indent, Some(" ".to_string()));
}
#[test]
fn test_get_next_line_indent_no_newline() {
let mut test = TypingTest::new(Mode::Code, "python".to_string(), 60);
test.target_text = "single line".to_string();
test.cursor_position = 5;
let indent = App::get_next_line_indent(&test);
assert_eq!(indent, None);
}
#[test]
fn test_get_next_line_indent_empty_next_line() {
let mut test = TypingTest::new(Mode::Code, "python".to_string(), 60);
test.target_text = "line1\nline2".to_string();
test.cursor_position = 5;
let indent = App::get_next_line_indent(&test);
assert_eq!(indent, Some("".to_string()));
}
#[test]
fn test_get_next_line_indent_tabs() {
let mut test = TypingTest::new(Mode::Code, "rust".to_string(), 60);
test.target_text = "fn main() {\n\tprintln!();\n}".to_string();
test.cursor_position = 11;
let indent = App::get_next_line_indent(&test);
assert_eq!(indent, Some("\t".to_string()));
}
}