Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better errors using anyhow #40

Merged
merged 8 commits into from
Feb 4, 2024
Merged
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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
rand = "0.8.4"
termion = "1.5.6"
include-flate = {version ="0.1.4", features=["stable"]}
78 changes: 46 additions & 32 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<std::io::Error> 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
}
}

Expand All @@ -62,34 +60,50 @@ impl From<String> 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.
///
/// See [`ToipeConfig`] for configuration options.
///
/// Initializes the word selector.
/// Also invokes [`Toipe::restart()`].
pub fn new(config: ToipeConfig) -> Result<Self, ToipeError> {
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(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<Self> {
let word_selector: Box<dyn WordSelector> = 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(),
Expand All @@ -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)?;
Expand All @@ -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(())
}
Expand All @@ -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::<char>::new();
let original_text = self
.text
Expand Down Expand Up @@ -174,7 +188,7 @@ impl<'a> Toipe {
}
}

let mut process_key = |key: Key| -> Result<TestStatus, ToipeError> {
let mut process_key = |key: Key| -> Result<TestStatus> {
match key {
Key::Ctrl('c') => {
return Ok(TestStatus::Quit);
Expand Down Expand Up @@ -287,7 +301,7 @@ impl<'a> Toipe {
&mut self,
results: ToipeResults,
mut keys: Keys<StdinLock>,
) -> Result<bool, ToipeError> {
) -> Result<bool> {
self.tui.reset_screen()?;

self.tui.display_lines::<&[Text], _>(&[
Expand Down
5 changes: 3 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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)?;
Expand Down
9 changes: 6 additions & 3 deletions src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use termion::{
};

use crate::ToipeError;
use anyhow::Result;

const MIN_LINE_WIDTH: usize = 50;

Expand Down Expand Up @@ -228,7 +229,7 @@ pub struct ToipeTui {
bottom_lines_len: usize,
}

type MaybeError<T = ()> = Result<T, ToipeError>;
type MaybeError<T = ()> = Result<T>;

impl ToipeTui {
/// Initializes stdout in raw mode for the TUI.
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/wordlists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading