use clap::{Parser, Subcommand}; use styles::Style; use std::{env, process::Command}; use text_io::read; mod config; mod state; mod styles; #[derive(Parser)] #[command(version, about, long_about = None)] struct Args { #[command(subcommand)] command: Option } #[derive(Subcommand)] enum Commands { /// Print the current problem View, /// Print the current problem's hint Hint, /// Check the solution of, or answer the current problem Check, /// Reset progress to the first problem Reset } fn print_problem(config: config::Config, state: state::State) { let problem = config.problems.get(state.problem_index).expect("Should be able to get current problem"); let name = env::args().next().unwrap_or_else(|| "captain".to_owned()); println!("{}\n{}\n\n{}", Style::Header.as_style().paint(format!("Problem {}:", state.problem_index)), problem.text, Style::Hint.as_style().paint(format!("Run `{} check` once complete", name))) } fn print_hint(config: config::Config, state: state::State) { let problem = config.problems.get(state.problem_index).expect("Should be able to get current problem"); let name = env::args().next().unwrap_or_else(|| "captain".to_owned()); match problem.hint.clone() { Some(hint) => println!("{}\n{}\n\n{}", Style::Header.as_style().paint(format!("Problem {} (Hint):", state.problem_index)), hint, Style::Hint.as_style().paint(format!("Run `{} check` once complete", name))), None => println!("{}", Style::Warning.as_style().paint("There is no hint available for this problem")) } } fn reset_state(mut state: state::State) { state.problem_index = 0; state.save(); println!("{}", Style::Good.as_style().paint("State has been reset!")) } fn run_command(cmd: String) -> Result { Command::new("bash").arg("-c").arg(cmd).output() } fn ask(expected: String) -> bool { const PROMPT: &str = "What is the answer? "; for _ in 1..4 { print!("{}", Style::Prompt.as_style().paint(PROMPT)); let actual: String = read!("{}\n"); if expected.trim() == actual.trim() { return true; } } false } fn check(config: config::Config, mut state: state::State) { let problem = config.problems.get(state.problem_index).expect("Should be able to get current problem"); println!("{}\n{}\n", Style::Header.as_style().paint(format!("Problem {}:", state.problem_index)), problem.text); let ok = match problem.check_type.as_str() { "cmd" => run_command(problem.check.clone()).expect("").status.success(), "ask_cmd" => { let cmd = run_command(problem.check.clone()).expect("Should be able to run check command successfully"); let expected = std::str::from_utf8(&cmd.stdout).expect("Should be able to parse check command output"); ask(expected.to_owned()) }, "ask" => { ask(problem.check.clone()) } _ => panic!("Check type {} is not implemented!", problem.check_type) }; if !ok { println!("{}", Style::Bad.as_style().paint("That's not quite right, try again.")); return; } if state.problem_index + 1 >= config.problems.len() { println!("{}", Style::Good.as_style().paint(config.success_message)); return; } let name = env::args().next().unwrap_or_else(|| "captain".to_owned()); println!("{}", Style::Good.as_style().paint(format!("Great! Run `{}` for the next problem.", name))); state.problem_index += 1; state.save(); } fn main() { let args = Args::parse(); let config = config::Config::load(); let state = state::State::load(); match &args.command { Some(Commands::View) | None => print_problem(config, state), Some(Commands::Hint) => print_hint(config, state), Some(Commands::Reset) => reset_state(state), Some(Commands::Check) => check(config, state) } }