use clap::{Parser, Subcommand};
use read_input::prelude::*;
use std::ffi::OsString;
use std::path::Path;
use wr::{ExerciseCollection, ExerciseDefinition, ExercisesConfig, OpenedExercise};
use yansi::Paint;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Command {
#[arg(long)]
pub no_skip: bool,
#[arg(long)]
pub verbose: bool,
#[arg(long)]
pub keep_going: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
Open {
#[arg(long)]
chapter: String,
#[arg(long)]
exercise: String,
},
}
fn main() -> Result<(), anyhow::Error> {
let command = Command::parse();
if !use_ansi_colours() {
Paint::disable();
}
let configuration = ExercisesConfig::load()?;
let mut exercises = ExerciseCollection::new(configuration.exercises_dir().to_path_buf())?;
if let Some(command) = command.command {
match command {
Commands::Open { chapter, exercise } => {
enum Selector {
FullName(String),
Number(u16),
}
impl Selector {
fn new(s: String) -> Self {
match s.parse::<u16>() {
Ok(number) => Selector::Number(number),
Err(_) => Selector::FullName(s),
}
}
fn matches(&self, name: &str, number: u16) -> bool {
match self {
Selector::FullName(s) => s == name,
Selector::Number(n) => *n == number,
}
}
}
impl std::fmt::Display for Selector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Selector::FullName(s) => write!(f, "{}", s),
Selector::Number(n) => write!(f, "{}", n),
}
}
}
let chapter_selector = Selector::new(chapter);
let exercise_selector = Selector::new(exercise);
let exercise = exercises.iter().find(|k| {
chapter_selector.matches(&k.chapter(), k.chapter_number())
&& exercise_selector.matches(&k.exercise(), k.exercise_number())
}).ok_or_else(|| {
anyhow::anyhow!("There is no exercise matching `--chapter {chapter_selector} -- exercise {exercise_selector}`")
})?.to_owned();
exercises.open(&exercise)?;
print_opened_message(&exercise, exercises.exercises_dir());
}
}
return Ok(());
}
if let TestOutcome::Failure { details } = seek_the_path(
&exercises,
command.no_skip,
configuration.verification_command(),
command.verbose,
)? {
print_failure_message(&details);
std::process::exit(1);
};
while let Some(next_exercise) = exercises.next()? {
if command.keep_going {
let next_exercise = exercises
.open_next()
.expect("Failed to open the next exercise");
let exercise_outcome = verify(
&exercises,
&next_exercise,
configuration.verification_command(),
command.verbose,
)?;
if let TestOutcome::Failure { details } = exercise_outcome {
print_failure_message(&details);
std::process::exit(1);
};
continue;
} else {
println!(
"\t{}\n",
info_style().paint(
"Eternity lies ahead of us, and behind. Your path is not yet finished. 🍂"
)
);
let open_next = input::<String>()
.repeat_msg(format!(
"Do you want to open the next exercise, {}? [y/n] ",
next_exercise
))
.err("Please answer either yes or no.")
.add_test(|s| parse_bool(s).is_some())
.get();
let open_next = parse_bool(&open_next).unwrap();
if open_next {
let next_exercise = exercises
.open_next()
.expect("Failed to open the next exercise");
print_opened_message(&next_exercise, exercises.exercises_dir());
}
return Ok(());
}
}
println!(
"{}\n\t{}\n",
success_style().paint("\n\tThere will be no more tasks."),
info_style().paint("What is the sound of one hand clapping (for you)? 🌟")
);
Ok(())
}
fn parse_bool(s: &str) -> Option<bool> {
match s.to_ascii_lowercase().as_str() {
"yes" | "y" => Some(true),
"no" | "n" => Some(false),
_ => None,
}
}
fn seek_the_path(
exercises: &ExerciseCollection,
no_skip: bool,
verification_cmd: Option<&str>,
verbose: bool,
) -> Result<TestOutcome, anyhow::Error> {
println!(" \n\n{}", info_style().dimmed().paint("Running tests...\n"));
for exercise in exercises.opened()? {
let OpenedExercise { definition, solved } = &exercise;
if *solved && !no_skip {
println!(
"{}",
info_style().paint(format!("\t✅ {} (Skipped)", definition))
);
continue;
}
let exercise_outcome = verify(exercises, &definition, verification_cmd, verbose)?;
if let TestOutcome::Failure { details } = exercise_outcome {
return Ok(TestOutcome::Failure { details });
}
}
Ok(TestOutcome::Success)
}
fn verify(
exercises: &ExerciseCollection,
definition: &ExerciseDefinition,
verification_cmd: Option<&str>,
verbose: bool,
) -> Result<TestOutcome, anyhow::Error> {
let exercise_outcome = _verify(
&definition.manifest_path(exercises.exercises_dir()),
verification_cmd,
verbose,
);
match &exercise_outcome {
TestOutcome::Success => {
println!("{}", success_style().paint(format!("\t🚀 {}", definition)));
exercises.mark_as_solved(&definition)?;
}
TestOutcome::Failure { .. } => {
println!("{}", failure_style().paint(format!("\t❌ {}", definition)));
exercises.mark_as_unsolved(&definition)?;
}
}
Ok(exercise_outcome)
}
fn _verify(manifest_path: &Path, verification_cmd: Option<&str>, verbose: bool) -> TestOutcome {
let color_option = if use_ansi_colours() {
"always"
} else {
"never"
};
{
let mut cmd = std::process::Command::new("cargo");
cmd.arg("build");
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
cmd.arg("--all-targets");
cmd.arg("--color");
cmd.arg(color_option);
if !verbose {
cmd.arg("-q");
}
if verbose {
cmd.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
}
let output = cmd.output().expect("Failed to run tests");
if !output.status.success() {
return TestOutcome::Failure {
details: [output.stderr, output.stdout].concat(),
};
}
}
{
let mut verification_cmd = match verification_cmd {
None => {
let mut args: Vec<OsString> =
vec!["test".into(), "--color".into(), color_option.into()];
if !verbose {
args.push("-q".into());
}
let mut cmd = std::process::Command::new("cargo");
cmd.args(args);
cmd
}
Some(cmd) => std::process::Command::new(cmd),
};
verification_cmd.current_dir(
manifest_path
.parent()
.expect("Failed to get parent dir for manifest"),
);
let error_msg = format!(
"Failed to run the verification command: `{:?}`",
verification_cmd
);
let output = verification_cmd.output().expect(&error_msg);
if !output.status.success() {
return TestOutcome::Failure {
details: [output.stderr, output.stdout].concat(),
};
}
}
TestOutcome::Success
}
#[derive(PartialEq)]
enum TestOutcome {
Success,
Failure { details: Vec<u8> },
}
fn print_opened_message(exercise: &ExerciseDefinition, exercises_dir: &Path) {
println!(
"{} {}",
next_style().paint("\n\tAhead of you lies"),
next_style().bold().paint(format!("{exercise}")),
);
let relative_path = exercise.manifest_folder_path(exercises_dir);
let open_msg = format!(
"\n\tOpen {:?} in your editor and get started!\n\tRun `wr` again to compile the exercise and execute its tests.",
relative_path
);
println!("{}", next_style().paint(open_msg));
}
fn print_failure_message(details: &[u8]) {
println!(
"\n\t{}\n\n{}\n\n",
info_style()
.paint("Meditate on your approach and return. Mountains are merely mountains.\n\n"),
cargo_style().paint(&String::from_utf8_lossy(details).to_string())
);
}
pub fn info_style() -> yansi::Style {
yansi::Style::new(yansi::Color::Default)
}
pub fn cargo_style() -> yansi::Style {
yansi::Style::new(yansi::Color::Default).dimmed()
}
pub fn next_style() -> yansi::Style {
yansi::Style::new(yansi::Color::Yellow)
}
pub fn success_style() -> yansi::Style {
yansi::Style::new(yansi::Color::Green)
}
pub fn failure_style() -> yansi::Style {
yansi::Style::new(yansi::Color::Red)
}
pub fn use_ansi_colours() -> bool {
if cfg!(target_os = "windows") {
Paint::enable_windows_ascii()
} else {
true
}
}