aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsrdusr <trevorgray@srdusr.com>2025-09-26 13:39:28 +0200
committersrdusr <trevorgray@srdusr.com>2025-09-26 13:39:28 +0200
commit8d60c7f93407988ee0232ea90980028f299cb0f3 (patch)
treeb343b691d1bce64fb3bc9b40324857486f2be244 /src
parent76f0d0e902e6ed164704572bd81faa5e5e560cf3 (diff)
downloadtyperpunk-8d60c7f93407988ee0232ea90980028f299cb0f3.tar.gz
typerpunk-8d60c7f93407988ee0232ea90980028f299cb0f3.zip
Initial Commit
Diffstat (limited to 'src')
-rw-r--r--src/main.rs320
-rw-r--r--src/test.rs164
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);
- }
-}