use std::{ error::Error, io::{stdout, Stdout, Write}, time::Duration, }; use crossterm::{ cursor::MoveTo, style::{Color, Print, SetForegroundColor}, terminal::{ self, disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, }, ExecutableCommand, QueueableCommand, }; use tokio::{ spawn, sync::mpsc::{channel, Receiver, Sender}, time::Instant, }; use crate::{ event::{handle_input, Event}, state::State, }; pub const TICK_RATE: u64 = 1000 / 20; pub struct App { stdout: Stdout, pub event_tx: Sender, event_rx: Receiver, running: bool, quote: Vec, state: State, should_render: bool, start: Option, completed: bool, } impl App { pub fn new(quote: String) -> App { let (event_tx, event_rx): (Sender, Receiver) = channel(10); App { stdout: stdout(), quote: quote .split_whitespace() .filter(|s| !s.is_empty()) .map(|s| s.to_string()) .collect(), event_rx, event_tx, running: false, state: State::default(), should_render: true, start: None, completed: false, } } pub async fn run(&mut self) -> Result<(), Box> { self.stdout.execute(EnterAlternateScreen)?; enable_raw_mode()?; let (input_ks_tx, input_ks_rx): (Sender<()>, Receiver<()>) = channel(1); let (tick_ks_tx, tick_ks_rx): (Sender<()>, Receiver<()>) = channel(1); spawn(start_input_handler(self.event_tx.clone(), input_ks_rx)); spawn(start_tick_generator(self.event_tx.clone(), tick_ks_rx)); self.running = true; self.start = Some(Instant::now()); while self.running { self.process().await?; } let time = self.start.unwrap().elapsed().as_millis(); let total_chars = self .quote .iter() .map(|w| w.chars().count() as f64) .sum::() + self.quote.len() as f64 - 1.0; let wpm = total_chars / 5.0 * 60000.0 / time as f64; input_ks_tx.send(()).await?; tick_ks_tx.send(()).await?; disable_raw_mode()?; self.stdout.execute(LeaveAlternateScreen)?; if self.completed { println!("Your WPM: {}", wpm.round()); } return Ok(()); } async fn process(&mut self) -> Result<(), Box> { let event = self.event_rx.recv().await.unwrap(); match event { Event::Terminate => { self.running = false; } Event::KeyPress(k) => self.handle_keypress(k).await?, Event::Backspace => self.handle_backspace().await, Event::Render => self.render().await?, Event::ForceRender => { self.should_render = true; self.render().await?; } } if event != Event::Render { self.should_render = true; } return Ok(()); } async fn handle_keypress(&mut self, k: char) -> Result<(), Box> { self.state.buffer.push(k); let is_word_completed = self .state .buffer .chars() .take(self.state.buffer.chars().count() - 1) .collect::() == self.quote[self.state.current]; let is_text_completed = self.state.buffer == self.quote[self.state.current] && self.state.current == self.quote.len() - 1; if is_word_completed && k == ' ' { self.state.buffer.clear(); self.state.current += 1; } else if is_text_completed { self.running = false; self.completed = true; } return Ok(()); } async fn handle_backspace(&mut self) { if !self.state.buffer.is_empty() { self.state.buffer.pop(); } } async fn render(&mut self) -> Result<(), Box> { if !self.should_render { return Ok(()); } let (col, _) = terminal::size()?; self.stdout .queue(Clear(ClearType::All)) .unwrap() .queue(SetForegroundColor(Color::Green)) .unwrap() .queue(MoveTo(0, 0)) .unwrap(); let done = self.quote[..self.state.current].join(" "); self.stdout.queue(Print(&done))?; if done.chars().count() > 0 { self.stdout.queue(Print(" "))?; } let mut cur_loc = done.chars().count() + self.state.buffer.chars().count(); if self.state.current > 0 { cur_loc += 1; } for i in 0..self.state.buffer.len() { if i >= self.quote[self.state.current].chars().count() { break; } let c = self.quote[self.state.current].chars().nth(i).unwrap(); if self.state.buffer.chars().nth(i).unwrap() == c { self.stdout.queue(SetForegroundColor(Color::Green)).unwrap(); } else { self.stdout.queue(SetForegroundColor(Color::Red)).unwrap(); } self.stdout.queue(Print(&c))?; } match ( self.state.buffer.chars().count(), self.quote[self.state.current].chars().count(), ) { (a, b) if a < b => { self.stdout.queue(SetForegroundColor(Color::Reset)).unwrap(); let v = &self.quote[self.state.current] .chars() .skip(self.state.buffer.chars().count()) .collect::(); self.stdout.queue(Print(&v))?; } (a, b) if a > b => { self.stdout .queue(SetForegroundColor(Color::Yellow)) .unwrap(); let v = &self .state .buffer .chars() .skip(self.quote[self.state.current].chars().count()) .collect::(); self.stdout.queue(Print(&v))?; } _ => (), } self.stdout.queue(Print(" "))?; self.stdout.queue(SetForegroundColor(Color::Reset)).unwrap(); let to_do = self.quote[self.state.current + 1..].join(" "); self.stdout.queue(Print(&to_do))?.queue(Print("\n"))?; self.stdout .queue(MoveTo(cur_loc as u16 % col, cur_loc as u16 / col))?; self.stdout.flush()?; self.should_render = false; return Ok(()); } } async fn start_tick_generator(ev: Sender, mut kill_switch: Receiver<()>) { loop { tokio::select! { _ = async { tokio::time::sleep(Duration::from_millis(TICK_RATE)).await; ev.send(Event::Render).await } => (), _ = kill_switch.recv() => return, } } } async fn start_input_handler(ev: Sender, mut kill_switch: Receiver<()>) { loop { tokio::select! { _ = handle_input(&ev) => (), _ = kill_switch.recv() => return, } } }