diff --git a/README.md b/README.md index 6db4ba6..12e917c 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,14 @@ You can provide your own word list too (Note: the word list must meet [these ass toipe -f /path/to/word/list ``` +## Add punctuation to test + +By default, only lowercase words are shown. To add punctuation and sentence case, use the `-p` flag: + +``` +toipe -p +``` + # Platform support - toipe was only tested on Linux and Mac OS. If you find any problems, please [open an issue](https://github.com/Samyak2/toipe/issues). diff --git a/src/config.rs b/src/config.rs index 957e693..2ea82df 100644 --- a/src/config.rs +++ b/src/config.rs @@ -30,6 +30,9 @@ pub struct ToipeConfig { /// Number of words to show on each test. #[clap(short, long, default_value_t = 30)] pub num_words: usize, + /// Whether to include punctuation + #[clap(short, long)] + pub punctuation: bool, } impl ToipeConfig { diff --git a/src/lib.rs b/src/lib.rs index 77458c5..553994a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ use config::ToipeConfig; use results::ToipeResults; use termion::input::Keys; use termion::{color, event::Key, input::TermRead}; -use textgen::{RawWordSelector, WordSelector}; +use textgen::{PunctuatedWordSelector, RawWordSelector, WordSelector}; use tui::{Text, ToipeTui}; use wordlists::{BuiltInWordlist, OS_WORDLIST_PATH}; @@ -76,7 +76,7 @@ impl<'a> Toipe { /// Initializes the word selector. /// Also invokes [`Toipe::restart()`]. pub fn new(config: ToipeConfig) -> Result { - let word_selector: Box = if let Some(wordlist_path) = + let mut word_selector: Box = if let Some(wordlist_path) = config.wordlist_file.clone() { Box::new( @@ -105,6 +105,13 @@ impl<'a> Toipe { return Err(ToipeError::from("Undefined word list or path.".to_owned()))?; }; + if config.punctuation { + word_selector = Box::new(PunctuatedWordSelector::from_word_selector( + word_selector, + 0.15, + )) + } + let mut toipe = Toipe { tui: ToipeTui::new(), words: Vec::new(), diff --git a/src/textgen.rs b/src/textgen.rs index 385d9f6..e6e67e6 100644 --- a/src/textgen.rs +++ b/src/textgen.rs @@ -1,10 +1,12 @@ //! Utilities for generating/selecting new (random) words for the typing //! test. +use std::collections::VecDeque; use std::fs::File; use std::io::{self, BufRead, BufReader, Cursor, Seek, SeekFrom}; use std::path::PathBuf; +use rand::seq::SliceRandom; use rand::Rng; use bisection::bisect_right; @@ -248,3 +250,91 @@ impl WordSelector for RawWordSelector { Ok(word) } } + +/// Wraps another word selector, taking words from it and adding punctuation to the end of or +/// around words with a configurable chance. Will capitalize the next word when an end-of-sentence +/// punctuation mark is used. +pub struct PunctuatedWordSelector { + selector: Box, + next_is_capital: bool, + punctuation_chance: f64, +} + +enum PunctuationType { + Capitaizing(char), + Ending(char), + Surrounding(char, char), +} + +const PUNCTUATION: [PunctuationType; 12] = [ + PunctuationType::Capitaizing('!'), + PunctuationType::Capitaizing('?'), + PunctuationType::Capitaizing('.'), + PunctuationType::Ending(','), + PunctuationType::Ending(':'), + PunctuationType::Ending(';'), + PunctuationType::Surrounding('\'', '\''), + PunctuationType::Surrounding('"', '"'), + PunctuationType::Surrounding('(', ')'), + PunctuationType::Surrounding('{', '}'), + PunctuationType::Surrounding('<', '>'), + PunctuationType::Surrounding('[', ']'), +]; + +impl PunctuatedWordSelector { + /// Creates a PunctuatedWordSelector from another WordSelector, allowing the selection of the + /// chance of punctuation. + pub fn from_word_selector( + word_selector: Box, + punctuation_chance: f64, + ) -> Self { + Self { + selector: word_selector, + next_is_capital: true, + punctuation_chance, + } + } +} + +impl WordSelector for PunctuatedWordSelector { + fn new_word(&mut self) -> Result { + let mut rng = rand::thread_rng(); + + let mut word = self.selector.new_word()?; + + let will_punctuate = rng.gen_bool(self.punctuation_chance); + if will_punctuate || self.next_is_capital { + let mut chars: VecDeque = word.chars().collect(); + if self.next_is_capital { + // some unicode chars map to multiple chars when uppercased. + for c in chars + .pop_front() + .expect("got empty word") + .to_uppercase() + .rev() + { + chars.push_front(c) + } + self.next_is_capital = false; + } + if will_punctuate { + match PUNCTUATION + .choose(&mut rng) + .expect("only returns none if the slice is empty") + { + PunctuationType::Capitaizing(c) => { + self.next_is_capital = true; + chars.push_back(*c) + } + PunctuationType::Ending(c) => chars.push_back(*c), + PunctuationType::Surrounding(opening, closing) => { + chars.push_front(*opening); + chars.push_back(*closing); + } + } + } + word = chars.into_iter().collect(); + } + Ok(word) + } +}