diff --git a/Cargo.lock b/Cargo.lock index 9347aa9..b09ae0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + [[package]] name = "atty" version = "0.2.14" @@ -377,6 +383,7 @@ checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" name = "toipe" version = "0.4.1" dependencies = [ + "anyhow", "bisection", "clap", "include-flate", diff --git a/Cargo.toml b/Cargo.toml index 4d153c7..d46cfe5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,9 @@ strip = "debuginfo" [lib] [dependencies] -termion = "1.5.6" -rand = "0.8.4" +anyhow = "1.0" bisection = "0.1.0" clap = { version = "3.0.5", features = ["derive", "color", "suggestions"] } -include-flate = {version ="0.1.4", features=["stable"]} \ No newline at end of file +rand = "0.8.4" +termion = "1.5.6" +include-flate = {version ="0.1.4", features=["stable"]} diff --git a/src/lib.rs b/src/lib.rs index a99774e..77458c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,8 @@ use textgen::{RawWordSelector, WordSelector}; use tui::{Text, ToipeTui}; use wordlists::{BuiltInWordlist, OS_WORDLIST_PATH}; +use anyhow::{Context, Result}; + /// Typing test terminal UI and logic. pub struct Toipe { tui: ToipeTui, @@ -38,21 +40,17 @@ pub struct Toipe { } /// Represents any error caught in Toipe. +#[derive(Debug)] pub struct ToipeError { + /// Error message. Should not start with "error" or similar. pub msg: String, } -/// Converts [`std::io::Error`] to [`ToipeError`]. -/// -/// This keeps only the error message. -/// -/// TODO: there must be a better way to keep information from the -/// original error. -impl From for ToipeError { - fn from(error: std::io::Error) -> Self { - ToipeError { - msg: error.to_string(), - } +impl ToipeError { + /// Prefixes the message with a context + pub fn with_context(mut self, context: &str) -> Self { + self.msg = context.to_owned() + &self.msg; + self } } @@ -62,12 +60,14 @@ impl From for ToipeError { } } -impl std::fmt::Debug for ToipeError { +impl std::fmt::Display for ToipeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(format!("ToipeError: {}", self.msg).as_str()) } } +impl std::error::Error for ToipeError {} + impl<'a> Toipe { /// Initializes a new typing test on the standard output. /// @@ -75,21 +75,35 @@ 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) = config.wordlist_file.clone() { - Box::new(RawWordSelector::from_path(PathBuf::from(wordlist_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 { - Box::new(RawWordSelector::from_path(PathBuf::from(OS_WORDLIST_PATH))?) - } else { - // this should never happen! - // TODO: somehow enforce this at compile time? - return Err(ToipeError { - msg: "Undefined word list or path.".to_string(), - }); - }; + pub fn new(config: ToipeConfig) -> Result { + let word_selector: Box = if let Some(wordlist_path) = + config.wordlist_file.clone() + { + Box::new( + RawWordSelector::from_path(PathBuf::from(wordlist_path.clone())).with_context( + || format!("reading the word list from given path '{}'", wordlist_path), + )?, + ) + } else if let Some(word_list) = config.wordlist.contents() { + Box::new( + RawWordSelector::from_string(word_list.to_string()).with_context(|| { + format!("reading the built-in word list {:?}", config.wordlist) + })?, + ) + } else if let BuiltInWordlist::OS = config.wordlist { + Box::new( + RawWordSelector::from_path(PathBuf::from(OS_WORDLIST_PATH)).with_context(|| { + format!( + "reading from the OS wordlist at path '{}'. See https://en.wikipedia.org/wiki/Words_(Unix) for more info on this file and how it can be installed.", + OS_WORDLIST_PATH + ) + })?, + ) + } else { + // this should never happen! + // TODO: somehow enforce this at compile time? + return Err(ToipeError::from("Undefined word list or path.".to_owned()))?; + }; let mut toipe = Toipe { tui: ToipeTui::new(), @@ -108,7 +122,7 @@ impl<'a> Toipe { /// /// Clears the screen, generates new words and displays them on the /// UI. - pub fn restart(&mut self) -> Result<(), ToipeError> { + pub fn restart(&mut self) -> Result<()> { self.tui.reset_screen()?; self.words = self.word_selector.new_words(self.config.num_words)?; @@ -125,7 +139,7 @@ impl<'a> Toipe { Ok(()) } - fn show_words(&mut self) -> Result<(), ToipeError> { + fn show_words(&mut self) -> Result<()> { self.text = self.tui.display_words(&self.words)?; Ok(()) } @@ -137,7 +151,7 @@ impl<'a> Toipe { /// If the test completes successfully, returns a boolean indicating /// whether the user wants to do another test and the /// [`ToipeResults`] for this test. - pub fn test(&mut self, stdin: StdinLock<'a>) -> Result<(bool, ToipeResults), ToipeError> { + pub fn test(&mut self, stdin: StdinLock<'a>) -> Result<(bool, ToipeResults)> { let mut input = Vec::::new(); let original_text = self .text @@ -174,7 +188,7 @@ impl<'a> Toipe { } } - let mut process_key = |key: Key| -> Result { + let mut process_key = |key: Key| -> Result { match key { Key::Ctrl('c') => { return Ok(TestStatus::Quit); @@ -287,7 +301,7 @@ impl<'a> Toipe { &mut self, results: ToipeResults, mut keys: Keys, - ) -> Result { + ) -> Result { self.tui.reset_screen()?; self.tui.display_lines::<&[Text], _>(&[ diff --git a/src/main.rs b/src/main.rs index 4492bb0..23e1443 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ +use anyhow::Result; use clap::StructOpt; + use std::io::stdin; use toipe::config::ToipeConfig; use toipe::Toipe; -use toipe::ToipeError; -fn main() -> Result<(), ToipeError> { +fn main() -> Result<()> { let config = ToipeConfig::parse(); let mut toipe = Toipe::new(config)?; diff --git a/src/tui.rs b/src/tui.rs index b906b46..4b917d8 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -14,6 +14,7 @@ use termion::{ }; use crate::ToipeError; +use anyhow::Result; const MIN_LINE_WIDTH: usize = 50; @@ -228,7 +229,7 @@ pub struct ToipeTui { bottom_lines_len: usize, } -type MaybeError = Result; +type MaybeError = Result; impl ToipeTui { /// Initializes stdout in raw mode for the TUI. @@ -416,12 +417,14 @@ impl ToipeTui { "Terminal height is too short! Toipe requires at least {} lines, got {} lines", lines.len() + self.bottom_lines_len + 2, terminal_height, - ))); + )) + .into()); } else if max_word_len > terminal_width as usize { return Err(ToipeError::from(format!( "Terminal width is too low! Toipe requires at least {} columns, got {} columns", max_word_len, terminal_width, - ))); + )) + .into()); } self.track_lines = true; diff --git a/src/wordlists.rs b/src/wordlists.rs index d91d684..b4e7fb4 100644 --- a/src/wordlists.rs +++ b/src/wordlists.rs @@ -14,7 +14,7 @@ flate!(static TOP_MISSPELLED: str from "src/word_lists/commonly_misspelled"); /// Word lists with top English words. /// /// See [variants](#variants) for details on each word list. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum, Debug)] pub enum BuiltInWordlist { /// Source: [wordfrequency.info](https://www.wordfrequency.info/samples.asp) (top 60K lemmas sample). Top250,