Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/bin/book.rs
Original file line number Diff line number Diff line change
@@ -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);
}
79 changes: 79 additions & 0 deletions src/book.rs
Original file line number Diff line number Diff line change
@@ -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<T> {
reader: BufReader<T>,
offset: u64,
}

impl<T: Seek + io::Read> BookSelector<T> {
pub fn new(reader: BufReader<T>) -> Result<Self, io::Error> {
let book_selector = Self { reader, offset: 0 };
Ok(book_selector)
}
}

impl<T: Seek + io::Read> Iterator for BookSelector<T> {
type Item = Result<String, io::Error>;

fn next(&mut self) -> Option<Self::Item> {
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<File> {
pub fn from_path(file_path: PathBuf) -> Result<Self, io::Error> {
let file = File::open(file_path)?;

let reader = BufReader::new(file);

Self::new(reader)
}
}

impl BookSelector<Cursor<String>> {
pub fn from_string(word_list: String) -> Result<Self, io::Error> {
let cursor = Cursor::new(word_list);
let reader = BufReader::new(cursor);

BookSelector::new(reader)
}
}

impl<T: Seek + io::Read> WordSelector for BookSelector<T> {
fn new_word(&mut self) -> Result<String, io::Error> {
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));
}
}
}
}
}
5 changes: 5 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

impl ToipeConfig {
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -79,6 +82,8 @@ impl<'a> Toipe {
let word_selector: Box<dyn WordSelector> =
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 {
Expand Down