diff options
| author | srdusr <trevorgray@srdusr.com> | 2025-09-26 13:39:28 +0200 |
|---|---|---|
| committer | srdusr <trevorgray@srdusr.com> | 2025-09-26 13:39:28 +0200 |
| commit | 8d60c7f93407988ee0232ea90980028f299cb0f3 (patch) | |
| tree | b343b691d1bce64fb3bc9b40324857486f2be244 /src | |
| parent | 76f0d0e902e6ed164704572bd81faa5e5e560cf3 (diff) | |
| download | typerpunk-8d60c7f93407988ee0232ea90980028f299cb0f3.tar.gz typerpunk-8d60c7f93407988ee0232ea90980028f299cb0f3.zip | |
Initial Commit
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 320 | ||||
| -rw-r--r-- | src/test.rs | 164 |
2 files changed, 0 insertions, 484 deletions
diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 9c529f0..0000000 --- a/src/main.rs +++ /dev/null @@ -1,320 +0,0 @@ -// Import necessary crates -use crossterm::{ - event::{self, KeyCode, KeyEvent}, - terminal::{disable_raw_mode, enable_raw_mode}, -}; -use rand::Rng; -use std::{ - fs, - io::{self}, - path::Path, - sync::{Arc, Mutex}, - time::Instant, -}; -use tui::{ - backend::{Backend, CrosstermBackend}, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Span, Spans}, - widgets::Paragraph, - Frame, Terminal, -}; - -// Define the possible states of the application -#[derive(Debug, Clone, Copy, PartialEq)] -enum State { - MainMenu, - TypingGame, - EndScreen, -} - -// Struct to hold the application state -struct App { - time_taken: u64, - input_string: String, - timer: Option<Instant>, - state: State, - should_exit: bool, - sentences: Vec<String>, - current_sentence_index: usize, -} - -impl App { - // Constructor to create a new instance of the application - fn new() -> Result<Self, io::Error> { - let sentences = read_sentences("sentences.txt")?; - let current_sentence_index = rand::thread_rng().gen_range(0..sentences.len()); - let app = App { - time_taken: 0, - input_string: String::new(), - timer: None, - state: State::MainMenu, - should_exit: false, - sentences, - current_sentence_index, - }; - Ok(app) - } - - // Reset the game to its initial state - fn reset(&mut self) { - let current_sentence_index = rand::thread_rng().gen_range(0..self.sentences.len()); - self.current_sentence_index = current_sentence_index; - self.time_taken = 0; - self.input_string.clear(); - self.timer = None; - self.state = State::TypingGame; - } - - // Get the current sentence the user needs to type - fn current_sentence(&self) -> &str { - if let Some(sentence) = self.sentences.get(self.current_sentence_index) { - sentence - } else { - "No sentence available" - } - } - - // Start the timer - fn start_timer(&mut self) { - if self.timer.is_none() { - self.timer = Some(Instant::now()); - } - } - - // Update the timer - fn update_timer(&mut self) { - if let Some(timer) = self.timer { - self.time_taken = timer.elapsed().as_secs(); - } - } - - // Calculate and return the current typing speed (Words Per Minute) - fn update_wpm(&self) -> f64 { - let time_elapsed = self.time_taken as f64; - if time_elapsed == 0.0 { - 0.0 - } else { - let wpm = (self.input_string.split_whitespace().count() as f64) / (time_elapsed / 60.0); - if wpm.is_nan() { - 0.0 - } else { - wpm - } - } - } -} - -// Function to read sentences from a file -fn read_sentences(filename: &str) -> Result<Vec<String>, io::Error> { - if !Path::new(filename).exists() { - return Err(io::Error::new(io::ErrorKind::NotFound, "File not found")); - } - - let contents = fs::read_to_string(filename)?; - let sentences: Vec<String> = contents.lines().map(|s| s.to_string()).collect(); - Ok(sentences) -} - -// Function to draw the typing game UI -fn draw_typing_game(f: &mut Frame<CrosstermBackend<std::io::Stdout>>, chunk: Rect, app: &mut App) { - let wpm = app.update_wpm(); - let time_used = app.time_taken as f64; - - let mut colored_text: Vec<Span> = Vec::new(); - let cursor_pos = app.input_string.len(); - - // Iterate over each character in the current sentence and color it based on user input - for (index, c) in app.current_sentence().chars().enumerate() { - let color = if let Some(input_char) = app.input_string.chars().nth(index) { - if c == input_char { - Color::Green - } else { - Color::Red - } - } else { - Color::Gray - }; - - // cursor - let span = if index == cursor_pos { - Span::styled( - c.to_string(), - Style::default().fg(color).add_modifier(Modifier::REVERSED), - ) - } else { - Span::styled(c.to_string(), Style::default().fg(color)) - }; - colored_text.push(span); - } - - let text = vec![ - Spans::from(Span::styled( - "Type the following sentence:", - Style::default().add_modifier(Modifier::BOLD), - )), - Spans::from(colored_text), - Spans::from(Span::styled(format!("WPM: {:.2}", wpm), Style::default())), - Spans::from(Span::styled( - format!("Time: {:.1} seconds", time_used), - Style::default(), - )), - ]; - - // Render the widget - f.render_widget(Paragraph::new(text).alignment(Alignment::Center), chunk); - - app.update_timer(); -} - -// Function to handle user input events -async fn input_handler(event: KeyEvent, app: &mut App, _event_tx: Arc<Mutex<()>>) { - match event.code { - KeyCode::Char(c) => { - if app.timer.is_none() { - app.timer = Some(Instant::now()); - } - app.input_string.push(c); - } - KeyCode::Backspace => { - app.input_string.pop(); - } - KeyCode::Esc => match app.state { - State::MainMenu => { - app.should_exit = true; - } - State::TypingGame | State::EndScreen => { - app.state = State::MainMenu; - app.input_string.clear(); - app.timer = None; - } - }, - KeyCode::Enter => match app.state { - State::MainMenu => { - app.state = State::TypingGame; - app.start_timer(); - app.input_string.clear(); - } - State::TypingGame => { - if app.input_string.trim() == app.current_sentence().trim() { - app.state = State::EndScreen; - app.update_timer(); - } - } - State::EndScreen => { - app.reset(); - } - }, - _ => {} - } -} - -// Include test module -#[cfg(test)] -mod test; - -// Main function -#[tokio::main] -async fn main() -> Result<(), io::Error> { - // Enable raw mode for terminal input - enable_raw_mode()?; - - // Create a new instance of the App - let mut app = App::new().unwrap(); - - // Initialize the terminal backend - let stdout = io::stdout(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // Clear the terminal and hide the cursor - terminal.clear()?; - terminal.hide_cursor()?; - - // Main event loop - loop { - // Get the terminal size - if let Ok(size) = terminal.backend().size() { - // Define layout for the UI - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints([ - Constraint::Min(3), - Constraint::Percentage(70), - Constraint::Min(3), - ]) - .split(size); - - // Draw UI based on app state - terminal.draw(|f| match app.state { - State::MainMenu => { - let main_menu = vec![ - Spans::from(Span::styled("Welcome to typerpunk!", Style::default())), - Spans::from(Span::styled("Press Enter to Start", Style::default())), - Spans::from(Span::styled("Press Esc to Quit", Style::default())), - ]; - f.render_widget( - Paragraph::new(main_menu).alignment(Alignment::Center), - chunks[0], - ); - } - State::TypingGame => { - draw_typing_game(f, chunks[1], &mut app); - } - State::EndScreen => { - let wpm = app.update_wpm(); - let time_taken = app.time_taken as f64; - let end_screen = vec![ - Spans::from(Span::styled("Game Over!", Style::default())), - Spans::from(Span::styled( - format!("Words Per Minute: {:.2}", wpm), - Style::default(), - )), - Spans::from(Span::styled( - format!("Time Taken: {:.1} seconds", time_taken), - Style::default(), - )), - Spans::from(Span::styled("Press Enter to Play Again", Style::default())), - Spans::from(Span::styled("Press Esc to Quit", Style::default())), - ]; - f.render_widget( - Paragraph::new(end_screen).alignment(Alignment::Center), - chunks[1], - ); - } - })?; - - // Handle input events - if let event::Event::Key(event) = event::read()? { - input_handler(event, &mut app, Arc::new(Mutex::new(()))).await; - } - - // Check if the app should exit - if app.should_exit { - break; - } - } - } - - // Cleanup: Show cursor, disable raw mode, and clear only the game UI - terminal.show_cursor()?; - disable_raw_mode()?; - let size = terminal.backend().size().unwrap(); - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints([ - Constraint::Min(3), - Constraint::Percentage(70), - Constraint::Min(3), - ]) - .split(size); - terminal - .draw(|f| f.render_widget(Paragraph::new("").alignment(Alignment::Center), chunks[1]))?; - - // Manually clear the terminal before exiting - println!("\x1B[2J\x1B[1;1H"); - - Ok(()) -} diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index 7f2728e..0000000 --- a/src/test.rs +++ /dev/null @@ -1,164 +0,0 @@ -// Import necessary items for testing -use crate::{draw_typing_game, input_handler, read_sentences, App, State}; -use crossterm::event::{KeyCode, KeyEvent}; -use std::{ - io, - sync::{Arc, Mutex}, -}; -use tui::layout::Rect; -use tui::{backend::CrosstermBackend, Terminal}; - -#[cfg(test)] -#[allow(unused_imports)] // Ignore unused import warnings within the test module -mod tests { - use super::*; - - #[test] - fn test_new_app() { - // Test that a new App initializes correctly - let app_result = App::new(); - assert!(app_result.is_ok()); - let app = app_result.unwrap(); - assert_eq!(app.input_string, ""); - assert_eq!(app.time_taken, 0); - assert_eq!(app.timer, None); - assert_eq!(app.state, State::MainMenu); - assert_eq!(app.should_exit, false); - assert!(!app.sentences.is_empty()); - assert!(app.current_sentence_index < app.sentences.len()); - } - - #[test] - fn test_reset_app() { - // Test the reset functionality of App - let mut app = App::new().unwrap(); - app.current_sentence_index = 0; // Set initial index to 0 - let old_index = app.current_sentence_index; - println!( - "Before reset: old_index = {}, current_sentence_index = {}", - old_index, app.current_sentence_index - ); - - app.time_taken = 100; - app.input_string = "Some input".to_string(); - app.reset(); - - println!( - "After reset: old_index = {}, current_sentence_index = {}", - old_index, app.current_sentence_index - ); - - // Check that current_sentence_index changes after reset - assert_ne!(app.current_sentence_index, old_index); - assert_eq!(app.time_taken, 0); - assert_eq!(app.input_string, ""); - assert_eq!(app.timer, None); - assert_eq!(app.state, State::TypingGame); - } - - #[test] - fn test_current_sentence() { - // Test the retrieval of the current sentence - let app = App::new().unwrap(); - let current_sentence = app.current_sentence(); - assert!(!current_sentence.is_empty()); - } - - #[test] - fn test_start_timer() { - // Test starting the timer - let mut app = App::new().unwrap(); - app.start_timer(); - assert!(app.timer.is_some()); - } - - #[test] - fn test_update_timer() { - // Test updating the timer - let mut app = App::new().unwrap(); - app.start_timer(); - let initial_time = app.time_taken; - std::thread::sleep(std::time::Duration::from_secs(1)); - app.update_timer(); - assert!(app.time_taken > initial_time); - } - - #[test] - fn test_update_wpm() { - // Test updating words per minute calculation - let mut app = App::new().unwrap(); - app.time_taken = 60; // 1 minute - app.input_string = "This is a test sentence".to_string(); - let wpm = app.update_wpm(); - assert_eq!(wpm, 5.0); // 5 words per minute for this sentence - } - - #[test] - fn test_read_sentences() { - // Test reading sentences from a file - let sentences_result = read_sentences("sentences.txt"); - assert!(sentences_result.is_ok()); - let sentences = sentences_result.unwrap(); - assert!(!sentences.is_empty()); - } - - #[test] - fn test_draw_typing_game() { - // Test drawing the typing game UI - let mut app = App::new().unwrap(); - let stdout = io::stdout(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend).unwrap(); - let size = Rect::default(); - draw_typing_game(&mut terminal.get_frame(), size, &mut app); - // No need to assert anything here since it's a rendering function - } - - #[tokio::test] - async fn test_input_handler() { - // Test the input handler function - let mut app = App::new().unwrap(); - - // Simulate typing 'a' - let event = KeyEvent::from(KeyCode::Char('a')); - input_handler(event, &mut app, Arc::new(Mutex::new(()))).await; - assert_eq!(app.input_string, "a"); - - // Simulate typing 'b' - let event = KeyEvent::from(KeyCode::Char('b')); - input_handler(event, &mut app, Arc::new(Mutex::new(()))).await; - assert_eq!(app.input_string, "ab"); - - // Simulate pressing Backspace - let event = KeyEvent::from(KeyCode::Backspace); - input_handler(event, &mut app, Arc::new(Mutex::new(()))).await; - assert_eq!(app.input_string, "a"); - - // Simulate pressing Enter in MainMenu state - let event = KeyEvent::from(KeyCode::Enter); - app.state = State::MainMenu; - input_handler(event, &mut app, Arc::new(Mutex::new(()))).await; - assert_eq!(app.state, State::TypingGame); - assert_eq!(app.input_string, ""); - - // Simulate typing 'T' in TypingGame state - let event = KeyEvent::from(KeyCode::Char('T')); - app.state = State::TypingGame; - input_handler(event, &mut app, Arc::new(Mutex::new(()))).await; - assert_eq!(app.input_string, "T"); - - // Simulate completing sentence and pressing Enter - let sentence = app.current_sentence().to_string(); - app.input_string = sentence.clone(); - let event = KeyEvent::from(KeyCode::Enter); - input_handler(event, &mut app, Arc::new(Mutex::new(()))).await; - assert_eq!(app.state, State::EndScreen); - assert_eq!(app.input_string.trim(), sentence.trim()); - - // Simulate pressing Esc in EndScreen state - let event = KeyEvent::from(KeyCode::Esc); - app.state = State::EndScreen; - input_handler(event, &mut app, Arc::new(Mutex::new(()))).await; - assert_eq!(app.should_exit, true); - } -} |
