diff --git a/src/bin/book.rs b/src/bin/book.rs new file mode 100644 index 0000000..f521942 --- /dev/null +++ b/src/bin/book.rs @@ -0,0 +1,12 @@ +use toipe::book::BookSelector; +use toipe::textgen::WordSelector; + +fn main() { + let mut word_selector = + BookSelector::from_string("This is a test.\nhello world!".to_string()).unwrap(); + + let mut word = word_selector.new_word().unwrap(); + println!("{}", word); + word = word_selector.new_word().unwrap(); + println!("{}", word); +} diff --git a/src/book.rs b/src/book.rs new file mode 100644 index 0000000..bdb3025 --- /dev/null +++ b/src/book.rs @@ -0,0 +1,79 @@ +use std::{ + fs::File, + io::{self, BufRead, BufReader, Cursor, Seek, SeekFrom}, + path::PathBuf, +}; + +use crate::textgen::WordSelector; +#[derive(Debug)] +pub struct BookSelector { + reader: BufReader, + offset: u64, +} + +impl BookSelector { + pub fn new(reader: BufReader) -> Result { + let book_selector = Self { reader, offset: 0 }; + Ok(book_selector) + } +} + +impl Iterator for BookSelector { + type Item = Result; + + fn next(&mut self) -> Option { + let mut buffer = vec![]; + if let Err(e) = self.reader.seek(SeekFrom::Start(self.offset)) { + return Some(Err(e)); + } + match self.reader.read_until(b' ', &mut buffer) { + Ok(len) => { + if len == 0 { + return None; + } + self.offset += len as u64; + Some(Ok(String::from_utf8(buffer).unwrap())) + } + Err(e) => Some(Err(e)), + } + } +} + +impl BookSelector { + pub fn from_path(file_path: PathBuf) -> Result { + let file = File::open(file_path)?; + + let reader = BufReader::new(file); + + Self::new(reader) + } +} + +impl BookSelector> { + pub fn from_string(word_list: String) -> Result { + let cursor = Cursor::new(word_list); + let reader = BufReader::new(cursor); + + BookSelector::new(reader) + } +} + +impl WordSelector for BookSelector { + fn new_word(&mut self) -> Result { + loop { + match self.next() { + Some(word) => { + if let Ok(mut w) = word { + w = w.replace("\n", " "); + if w.trim() != "" && w.is_ascii() { + return Ok(w.trim().to_string()); + } + } + } + None => { + return Err(io::Error::from(io::ErrorKind::UnexpectedEof)); + } + } + } + } +} diff --git a/src/config.rs b/src/config.rs index 957e693..c32fe09 100644 --- a/src/config.rs +++ b/src/config.rs @@ -30,6 +30,11 @@ pub struct ToipeConfig { /// Number of words to show on each test. #[clap(short, long, default_value_t = 30)] pub num_words: usize, + /// Path to custom book file + /// + /// This argument cannot be used along with `-w`/`--wordlist` + #[clap(short = 'b', long = "bookfile", conflicts_with = "wordlist")] + pub book_file: Option, } impl ToipeConfig { diff --git a/src/lib.rs b/src/lib.rs index a99774e..ede7047 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ //! See [`RawWordSelector`] if you're looking for the word selection //! algorithm. +pub mod book; pub mod config; pub mod results; pub mod textgen; @@ -28,6 +29,8 @@ use textgen::{RawWordSelector, WordSelector}; use tui::{Text, ToipeTui}; use wordlists::{BuiltInWordlist, OS_WORDLIST_PATH}; +use crate::book::BookSelector; + /// Typing test terminal UI and logic. pub struct Toipe { tui: ToipeTui, @@ -79,6 +82,8 @@ impl<'a> Toipe { let word_selector: Box = if let Some(wordlist_path) = config.wordlist_file.clone() { Box::new(RawWordSelector::from_path(PathBuf::from(wordlist_path))?) + } else if let Some(bookfile_path) = config.book_file.clone() { + Box::new(BookSelector::from_path(PathBuf::from(bookfile_path))?) } else if let Some(word_list) = config.wordlist.contents() { Box::new(RawWordSelector::from_string(word_list.to_string())?) } else if let BuiltInWordlist::OS = config.wordlist {