diff --git a/Cargo.lock b/Cargo.lock index 85d1005..b5acf3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,12 +35,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anyhow" -version = "1.0.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9" - [[package]] name = "argh" version = "0.1.8" @@ -238,11 +232,10 @@ dependencies = [ [[package]] name = "guess-that-lang" -version = "1.0.16" +version = "1.0.17" dependencies = [ "ansi_colours", "ansi_term", - "anyhow", "argh", "confy", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 9cb1a58..160a84b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "guess-that-lang" -version = "1.0.16" +version = "1.0.17" rust-version = "1.63" authors = ["Lioness100 "] edition = "2021" @@ -8,13 +8,13 @@ description = "CLI game to see how fast you can guess the language of a code blo repository = "https://github.com/Lioness100/guess-that-lang" license = "Apache-2.0" keywords = ["cli", "game", "cli-game", "fun"] +categories = ["command-line-utilities", "games"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] ansi_colours = "1.1.1" ansi_term = "0.12.1" -anyhow = "1.0.59" argh = "0.1.8" confy = "0.4.0" crossterm = "0.25.0" diff --git a/src/game.rs b/src/game.rs index 8a89ed1..5cc7648 100644 --- a/src/game.rs +++ b/src/game.rs @@ -20,7 +20,7 @@ use rand::{seq::SliceRandom, thread_rng}; use crate::{ github::{GistData, Github}, terminal::Terminal, - Config, CONFIG, + Config, Result, CONFIG, }; /// The prompt to be shown before the options in [`Terminal::print_round_info`]. @@ -99,7 +99,7 @@ impl Drop for Game { impl Game { /// Create new game. - pub fn new(client: Github) -> anyhow::Result { + pub fn new(client: Github) -> Result { let terminal = Terminal::new()?; Ok(Self { @@ -113,34 +113,32 @@ impl Game { /// Get the language options for a round. This will choose 3 random unique /// languages, push them to a vec along with the correct language, and /// shuffle the vec. - pub fn get_options(correct_language: &str) -> Vec<&str> { + #[must_use] + pub fn get_options(correct_language: &str) -> Option> { let mut options = Vec::<&str>::with_capacity(4); options.push(correct_language); let mut thread_rng = thread_rng(); while options.len() < 4 { - let random_language = LANGUAGES.choose(&mut thread_rng).unwrap(); + let random_language = LANGUAGES.choose(&mut thread_rng)?; if !options.contains(random_language) { options.push(random_language); } } options.shuffle(&mut thread_rng); - options + Some(options) } /// Start a new round, which is called in the main function with a for loop. - pub fn start_new_round( - &mut self, - preloader: Option>, - ) -> anyhow::Result> { + pub fn start_new_round(&mut self, preloader: Option>) -> Result> { if self.gist_data.is_empty() { self.gist_data = self.client.get_gists(&self.terminal.syntaxes)?; } - let gist = self.gist_data.pop().unwrap(); + let gist = self.gist_data.pop().ok_or("empty gist vec")?; let text = self.client.get_gist(&gist.url)?; - let width = Terminal::width(); + let width = Terminal::width()?; let highlighter = self.terminal.get_highlighter(&gist.extension); let code = match self.terminal.parse_code(&text, highlighter, &width) { @@ -149,14 +147,14 @@ impl Game { None => return self.start_new_round(preloader), }; - let options = Self::get_options(&gist.language); + let options = Self::get_options(&gist.language).ok_or("failed to get options")?; if let Some(preloader) = preloader { - preloader.recv().unwrap(); + let _ = preloader.recv(); } self.terminal - .print_round_info(&options, &code, &width, self.points); + .print_round_info(&options, &code, &width, self.points)?; let available_points = Mutex::new(100.0); @@ -167,47 +165,47 @@ impl Game { thread::scope(|s| { let display = s.spawn(|| { self.terminal - .start_showing_code(code, &available_points, receiver); + .start_showing_code(code, &available_points, receiver) }); let input = s.spawn(|| { - let input = Terminal::read_input_char(); + let input = Terminal::read_input_char()?; // Notifies [`Terminal::start_showing_code`] to not show the // next line. let sender = sender; - let _sent = sender.send(()); + let _ = sender.send(()); if input == 'q' || input == 'c' { Ok(ControlFlow::Break(())) } else { let result = self.terminal.process_input( - input.to_digit(10).unwrap(), + input.to_digit(10).ok_or("invalid input")?, &options, &gist.language, &available_points, &mut self.points, - )?; + ); // Let the user visually process the result. If they got it // correct, the timer is set after a thread is spawned to // preload the next round's gist. - if result == ControlFlow::Break(()) { + if let Ok(ControlFlow::Break(())) = result { thread::sleep(Duration::from_millis(1500)); } - Ok(result) + result } }); - display.join().unwrap(); + display.join().unwrap()?; input.join().unwrap() }) } - // Wait 1.5 seconds for the user to visually process they got the right - // answer while the next round is preloading, then start the next round. - pub fn start_next_round(&mut self) -> anyhow::Result> { + /// Wait 1.5 seconds for the user to visually process they got the right + /// answer while the next round is preloading, then start the next round. + pub fn start_next_round(&mut self) -> Result> { let (sender, receiver) = mpsc::channel(); thread::scope(|s| { @@ -218,8 +216,8 @@ impl Game { // Clear the screen and move to the top right corner. This is not // a method of [`Terminal`] because it would take a lot of work to // let the borrow checker let me use `self` again. - execute!(stdout().lock(), Clear(ClearType::All), MoveTo(0, 0)).unwrap(); - sender.send(()).unwrap(); + let _clear = execute!(stdout().lock(), Clear(ClearType::All), MoveTo(0, 0)); + let _ = sender.send(()); handle.join().unwrap() }) diff --git a/src/github.rs b/src/github.rs index c2d1088..2520885 100644 --- a/src/github.rs +++ b/src/github.rs @@ -1,13 +1,12 @@ use std::{collections::BTreeMap, path::Path}; -use anyhow::{bail, Context}; use rand::{seq::SliceRandom, thread_rng, Rng}; use regex::RegexBuilder; use serde::Deserialize; use syntect::parsing::SyntaxSet; use ureq::{Agent, AgentBuilder, Response}; -use crate::{game::LANGUAGES, Config, ARGS, CONFIG}; +use crate::{game::LANGUAGES, Config, Result, ARGS, CONFIG}; pub const GITHUB_BASE_URL: &str = "https://api.github.com"; @@ -33,9 +32,9 @@ pub struct GistData { } impl GistData { - /// Create a new GistData struct from a [`Gist`]. This will return [`None`] + /// Create a new [`GistData`] struct from a [`Gist`]. This will return [`None`] /// if none of the gist files use one of the supported languages. - pub fn from(gist: Gist, syntaxes: &SyntaxSet) -> Option { + pub fn from(gist: Gist, syntaxes: &SyntaxSet) -> Option { let file = gist.files.into_values().find(|file| { matches!(file.language.as_ref(), Some(language) if LANGUAGES.contains(&language.as_str())) })?; @@ -46,7 +45,7 @@ impl GistData { Some(Self { url: file.raw_url.to_string(), extension: extension.to_string(), - language: file.language.unwrap(), + language: file.language?, }) } } @@ -70,18 +69,20 @@ impl Default for Github { } impl Github { - pub fn new() -> anyhow::Result { + pub fn new() -> Result { let mut github = Self::default(); github.token = github.apply_token()?; Ok(github) } - pub fn apply_token(&mut self) -> anyhow::Result> { + /// If a token is found from arguments or the config: validate it and return + /// it. If it wasn't found from the config, store it in the config. + pub fn apply_token(&mut self) -> Result> { if let Some(token) = &ARGS.token { - Github::test_token_structure(token)?; + Self::test_token_structure(token)?; self.validate_token(token) - .context("Invalid personal access token")?; + .expect("Invalid personal access token"); confy::store( "guess-that-lang", @@ -92,7 +93,9 @@ impl Github { )?; return Ok(Some(token.to_string())); - } else if !CONFIG.token.is_empty() { + } + + if !CONFIG.token.is_empty() { let result = self.validate_token(&CONFIG.token); if result.is_err() { confy::store( @@ -103,7 +106,7 @@ impl Github { }, )?; - result.context("The token found in the config is invalid, so it has been removed. Please try again.")?; + panic!("The token found in the config is invalid, so it has been removed. Please try again.") } else { return Ok(Some(CONFIG.token.clone())); } @@ -115,23 +118,19 @@ impl Github { /// Test a Github personal access token via regex and return it if valid. The /// second step of validation is [`validate_token`] which requires querying the /// Github API asynchronously and thus can not be used with [`clap::value_parser`]. - pub fn test_token_structure(token: &str) -> anyhow::Result { + pub fn test_token_structure(token: &str) -> Result { let re = RegexBuilder::new(r"[\da-f]{40}|ghp_\w{36,251}") // This is an expensive regex, so the size limit needs to be increased. .size_limit(1 << 25) - .build() - .unwrap(); + .build()?; - if re.is_match(token) { - Ok(token.to_string()) - } else { - bail!("Invalid token format") - } + assert!(re.is_match(token), "Invalid token format"); + Ok(token.to_string()) } /// Queries the Github ratelimit API using the provided token to make sure it's /// valid. The ratelimit data itself isn't used. - pub fn validate_token(&self, token: &str) -> anyhow::Result { + pub fn validate_token(&self, token: &str) -> Result { self.agent .get(&format!("{GITHUB_BASE_URL}/rate_limit")) .set("Authorization", &format!("Bearer {token}")) @@ -141,7 +140,7 @@ impl Github { /// Get a vec of random valid gists on Github. This is used with the assumption /// that at least one valid gist will be found. - pub fn get_gists(&self, syntaxes: &SyntaxSet) -> anyhow::Result> { + pub fn get_gists(&self, syntaxes: &SyntaxSet) -> Result> { let mut request = ureq::get(&format!("{GITHUB_BASE_URL}/gists/public")) .query("page", &thread_rng().gen_range(0..=100).to_string()); @@ -162,7 +161,7 @@ impl Github { } /// Get single gist content. - pub fn get_gist(&self, url: &str) -> anyhow::Result { + pub fn get_gist(&self, url: &str) -> Result { let mut request = ureq::get(url); if let Some(token) = &self.token { diff --git a/src/main.rs b/src/main.rs index 0178058..43a0b1f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,13 @@ #![forbid(unsafe_code)] -#![warn(clippy::pedantic)] +#![warn(clippy::pedantic, clippy::cargo)] #![allow( - // Allowed to avoid breaking changes. - clippy::module_name_repetitions, - clippy::struct_excessive_bools, - clippy::unused_self, - // Allowed as they are too pedantic clippy::cast_possible_truncation, - clippy::unreadable_literal, - clippy::cast_possible_wrap, - clippy::wildcard_imports, clippy::cast_sign_loss, - clippy::too_many_lines, - clippy::doc_markdown, - clippy::cast_lossless, - clippy::must_use_candidate, - clippy::needless_pass_by_value, - // Document this later clippy::missing_errors_doc, - clippy::missing_panics_doc, + clippy::missing_panics_doc )] -use std::ops::ControlFlow; +use std::{error::Error, ops::ControlFlow, result}; use argh::FromArgs; use lazy_static::lazy_static; @@ -33,6 +19,8 @@ pub mod terminal; use crate::{game::Game, github::Github, terminal::ThemeStyle}; +pub type Result = result::Result>; + /// CLI game to see how fast you can guess the language of a code block! #[derive(FromArgs)] pub struct Args { @@ -66,7 +54,7 @@ lazy_static! { pub static ref CONFIG: Config = confy::load("guess-that-lang").unwrap(); } -pub fn main() -> anyhow::Result<()> { +pub fn main() -> Result<()> { let client = Github::new()?; let mut game = Game::new(client)?; diff --git a/src/terminal.rs b/src/terminal.rs index c260d46..cda144f 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -2,6 +2,7 @@ use std::{ env, io::{stdout, Stdout, Write}, ops::ControlFlow, + result, sync::{mpsc::Receiver, Mutex}, time::Duration, }; @@ -14,7 +15,6 @@ use ansi_term::{ ANSIStrings, Color::{self, Fixed, RGB}, }; -use anyhow::Context; use crossterm::{ cursor::{Hide, MoveTo, MoveToColumn, MoveUp, RestorePosition, SavePosition}, event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, @@ -32,7 +32,7 @@ use syntect::{ util::LinesWithEndings, }; -use crate::{game::PROMPT, Config, ARGS, CONFIG}; +use crate::{game::PROMPT, Config, Result, ARGS, CONFIG}; #[derive(Serialize, Deserialize, Clone)] pub enum ThemeStyle { @@ -43,7 +43,7 @@ pub enum ThemeStyle { impl TryFrom> for ThemeStyle { type Error = (); - fn try_from(opt: Option) -> Result { + fn try_from(opt: Option) -> result::Result { match opt { Some(string) if string == "dark" => Ok(Self::Dark), Some(string) if string == "light" => Ok(Self::Light), @@ -69,34 +69,32 @@ pub struct Terminal { } impl Terminal { - pub fn new() -> anyhow::Result { + pub fn new() -> Result { #[cfg(windows)] - let _ansi = enable_ansi_support(); + let _ = enable_ansi_support(); let themes: ThemeSet = dumps::from_binary(include_bytes!("../assets/dumps/themes.dump")); let syntaxes: SyntaxSet = - dumps::from_uncompressed_data(include_bytes!("../assets/dumps/syntaxes.dump")) - .context("Failed to load syntaxes") - .unwrap(); + dumps::from_uncompressed_data(include_bytes!("../assets/dumps/syntaxes.dump"))?; let mut stdout = stdout(); if !cfg!(test) { - let _hide = execute!(stdout, EnterAlternateScreen, Hide, MoveTo(0, 0)); + let _clear = execute!(stdout, EnterAlternateScreen, Hide, MoveTo(0, 0)); let _raw = enable_raw_mode(); } Ok(Self { syntaxes, stdout, - theme: themes.themes[Terminal::get_theme()?].clone(), - is_truecolor: Terminal::is_truecolor(), + theme: themes.themes[Self::get_theme()?].clone(), + is_truecolor: Self::is_truecolor(), }) } /// Highlight a line of code. - pub fn highlight_line(&self, code: String, highlighter: &mut HighlightLines) -> Option { - let ranges = highlighter.highlight_line(&code, &self.syntaxes).unwrap(); + pub fn highlight_line(&self, code: &str, highlighter: &mut HighlightLines) -> Option { + let ranges = highlighter.highlight_line(code, &self.syntaxes).ok()?; let mut colorized = Vec::with_capacity(ranges.len()); for (style, component) in ranges { @@ -116,8 +114,9 @@ impl Terminal { } /// Converts [`syntect::highlighting::Color`] to [`ansi_term::Color`]. The - /// implementation is taken from https://github.com/sharkdp/bat and relevant + /// implementation is taken from and relevant /// explanations of this functions can be found there. + #[must_use] pub fn to_ansi_color(color: highlighting::Color, true_color: bool) -> ansi_term::Color { if color.a == 0 { match color.r { @@ -139,14 +138,15 @@ impl Terminal { } /// Return true if the current running terminal support true color. + #[must_use] pub fn is_truecolor() -> bool { env::var("COLORTERM") .map(|colorterm| colorterm == "truecolor" || colorterm == "24bit") - .unwrap_or(false) + .unwrap_or_default() } /// Get light/dark mode specific theme. - pub fn get_theme() -> anyhow::Result<&'static str> { + pub fn get_theme() -> Result<&'static str> { if let Ok(theme) = ThemeStyle::try_from(ARGS.theme.clone()) { confy::store( "guess-that-lang", @@ -194,7 +194,7 @@ impl Terminal { line.to_string() }; - self.highlight_line(trimmed.clone(), &mut highlighter) + self.highlight_line(&trimmed, &mut highlighter) .map(|highlighted| (trimmed, highlighted)) }) .take_while(move |(line, _)| { @@ -242,7 +242,7 @@ impl Terminal { code_lines: &[(String, String)], width: &usize, total_points: u32, - ) { + ) -> Result<()> { let pipe = "│".white().dim(); let points = format!( @@ -294,7 +294,7 @@ impl Terminal { "{top}\r\n{points}\r\n{mid}\r\n{dotted_code}{bottom}\r\n\r\n{PROMPT}\r\n\r\n{option_text}\r\n{quit_option_text}" ); - execute!(self.stdout.lock(), Print(text)).unwrap(); + execute!(self.stdout.lock(), Print(text)).map_err(Into::into) } pub fn get_highlighter(&self, extension: &str) -> HighlightLines { @@ -313,7 +313,7 @@ impl Terminal { mut code_lines: Vec<(String, String)>, available_points: &Mutex, receiver: Receiver<()>, - ) { + ) -> Result<()> { if ARGS.shuffle { code_lines.shuffle(&mut thread_rng()); }; @@ -322,6 +322,9 @@ impl Terminal { // 0 because the lines could be shuffled. let mut is_first_line = true; + // Consume receiver. + let receiver = receiver; + for (idx, (raw, line)) in code_lines.iter().enumerate() { if raw == "\n" { continue; @@ -340,11 +343,11 @@ impl Terminal { // Move to the row index of the dotted code and replace it with the // real code. - queue!(stdout, SavePosition, MoveTo(9, idx as u16 + 5), Print(line)).unwrap(); + queue!(stdout, SavePosition, MoveTo(9, idx as u16 + 5), Print(line))?; // `available_points` should not be decreased on the first line. if idx != 0 { - let mut available_points = available_points.lock().unwrap(); + let mut available_points = available_points.lock().map_err(|_| "could not lock")?; *available_points -= 10.0; // https://stackoverflow.com/a/7947812/13721990 @@ -361,12 +364,13 @@ impl Terminal { "{} ", new_color.paint(available_points.to_string()) )) - ) - .unwrap(); + )?; } - execute!(stdout, RestorePosition).unwrap(); + execute!(stdout, RestorePosition)?; } + + Ok(()) } /// Responds to input from the user (1 | 2 | 3 | 4). @@ -378,7 +382,7 @@ impl Terminal { correct_language: &str, available_points: &Mutex, total_points: &mut u32, - ) -> anyhow::Result> { + ) -> Result> { // Locking the stdout will let any work that's being done in // [`Terminal::start_showing_code`] to finish before we continue. let mut stdout = self.stdout.lock(); @@ -386,10 +390,10 @@ impl Terminal { let correct_option_idx = options .iter() .position(|&option| option == correct_language) - .unwrap(); + .ok_or("correct language not found")?; let was_correct = (correct_option_idx + 1) as u32 == num; - let available_points = available_points.lock().unwrap(); + let available_points = available_points.lock().map_err(|_| "could not lock")?; let correct_option_name_text = if was_correct { format!("{correct_language} (+ {available_points})") @@ -439,24 +443,31 @@ impl Terminal { } /// Utility function to wait for a relevant char to be pressed. - pub fn read_input_char() -> char { + pub fn read_input_char() -> Result { + // Consume all ready-to-be-collected events to ensure that only future + // are collected. + while event::poll(Duration::from_millis(1))? { + event::read()?; + } + loop { - if let Ok(Event::Key(KeyEvent { + if let Event::Key(KeyEvent { code: KeyCode::Char(char @ ('1' | '2' | '3' | '4' | 'q' | 'c')), modifiers, .. - })) = event::read() + }) = event::read()? { if char == 'c' && modifiers != KeyModifiers::CONTROL { continue; } - return char; + return Ok(char); } } } /// Utility function to format an option. + #[must_use] pub fn format_option(key: &str, name: &str) -> String { format!( "{padding}[{key}] {name}", @@ -466,9 +477,11 @@ impl Terminal { ) } - /// Get terminal width - pub fn width() -> usize { - terminal::size().map(|(width, _)| width as usize).unwrap() + /// Get terminal width. + pub fn width() -> Result { + terminal::size() + .map(|(width, _)| width as usize) + .map_err(Into::into) } }