From b25f575e41f078be3e47d843fda1827881f23646 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Tue, 29 Jul 2025 21:54:30 +0200 Subject: Add the exercises command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subcommands have been added to it which are similar to the ones on words on purpose. Signed-off-by: Miquel Sabaté Solà --- crates/cli/src/exercises.rs | 254 ++++++++++++++++++++++++++++++++++++++++++++ crates/cli/src/main.rs | 5 + crates/cli/src/words.rs | 4 +- 3 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 crates/cli/src/exercises.rs (limited to 'crates/cli') diff --git a/crates/cli/src/exercises.rs b/crates/cli/src/exercises.rs new file mode 100644 index 0000000..a4bff78 --- /dev/null +++ b/crates/cli/src/exercises.rs @@ -0,0 +1,254 @@ +use inquire::{Confirm, Editor, Select, Text}; +use mihi::{Exercise, ExerciseKind}; +use std::vec::IntoIter; + +// Show the help message. +fn help(msg: Option<&str>) { + if msg.is_some() { + println!("{}.\n", msg.unwrap()); + } + + println!("mihi exercises: Manage exercises.\n"); + println!("usage: mihi exercises [OPTIONS] \n"); + + println!("Options:"); + println!(" -h, --help\t\tPrint this message."); + + println!("\nSubcommands:"); + println!(" create\t\tCreate a new exercise."); + println!(" edit\t\t\tEdit information from an exercise."); + println!(" ls\t\t\tList exercises from the database."); + println!(" rm\t\t\tRemove an exercises from the database."); +} + +// Interactively ask the user to fill up an exercise based on the given +// `exercise` object. +fn ask_for_exercise_based_on(exercise: Exercise) -> Result { + let Ok(title) = Text::new("Title:") + .with_initial_value(&exercise.title) + .prompt() + else { + return Err("abort!".to_string()); + }; + if title.trim().is_empty() { + return Err("the title is required".to_string()); + } + + let kinds = vec![ + ExerciseKind::Pensum, + ExerciseKind::Translation, + ExerciseKind::Transformation, + ExerciseKind::Numerical, + ]; + let Ok(kind) = Select::new("Kind:", kinds) + .with_starting_cursor(exercise.kind as usize) + .prompt() + else { + return Err("abort!".to_string()); + }; + + let Ok(enunciate) = Editor::new("Enunciate:") + .with_predefined_text(&exercise.enunciate) + .with_file_extension(".md") + .prompt() + else { + return Err("abort!".to_string()); + }; + let enunciate = enunciate.trim().to_string(); + if enunciate.is_empty() { + return Err("the enunciate is required".to_string()); + } + + let Ok(solution) = Editor::new("Solution:") + .with_predefined_text(&exercise.solution) + .with_file_extension(".md") + .prompt() + else { + return Err("abort!".to_string()); + }; + let solution = solution.trim().to_string(); + if solution.trim().is_empty() { + return Err("the solution is required".to_string()); + } + + let Ok(lessons) = Editor::new("Lessons:") + .with_predefined_text(&exercise.lessons) + .with_file_extension(".md") + .prompt() + else { + return Err("abort!".to_string()); + }; + let lessons = lessons.trim().to_string(); + + Ok(Exercise { + id: exercise.id, + title, + enunciate, + solution, + lessons, + kind, + }) +} + +fn create(args: IntoIter) -> i32 { + if args.len() > 0 { + help(Some( + "error: exercises: no arguments were expected for this command", + )); + return 1; + } + + let exercise = match ask_for_exercise_based_on(Exercise::default()) { + Ok(ex) => ex, + Err(e) => { + println!("error: exercises: {e}"); + return 1; + } + }; + + let title = exercise.title.clone(); + match mihi::create_exercise(exercise) { + Ok(_) => { + println!("Exercise '{title}' has been successfully created!"); + 0 + } + Err(e) => { + println!("error: exercises: {e}"); + 1 + } + } +} + +fn select_single_exercise(search: Option) -> Result { + let exercises = mihi::select_by_title(search)?; + + let title = match exercises.len() { + 0 => return Err("not found".to_string()), + 1 => exercises.first().unwrap().to_owned(), + _ => match Select::new("Which exercise?", exercises) + .with_page_size(20) + .prompt() + { + Ok(choice) => choice, + Err(_) => return Err("abort!".to_string()), + }, + }; + + mihi::find_exercise_by_title(title.as_str()) +} + +fn edit(mut args: IntoIter) -> i32 { + if args.len() > 1 { + help(Some("error: exercises: too many filters")); + return 1; + } + + let exercise = match select_single_exercise(args.next()) { + Ok(exercise) => exercise, + Err(e) => { + println!("error: exercises: {e}"); + return 1; + } + }; + + let exercise = match ask_for_exercise_based_on(exercise) { + Ok(ex) => ex, + Err(e) => { + println!("error: exercises: {e}"); + return 1; + } + }; + + let title = exercise.title.clone(); + match mihi::update_exercise(exercise) { + Ok(_) => { + println!("Exercise '{title}' has been successfully updated!"); + 0 + } + Err(e) => { + println!("error: exercises: {e}"); + 1 + } + } +} + +fn rm(mut args: IntoIter) -> i32 { + if args.len() > 1 { + help(Some("error: exercises: too many filters")); + return 1; + } + + let exercise = match select_single_exercise(args.next()) { + Ok(exercise) => exercise, + Err(e) => { + println!("error: words: {e}"); + return 1; + } + }; + let selection = exercise.title.as_str(); + + let ans = Confirm::new( + format!("Do you really want to remove '{selection}' from the database?",).as_str(), + ) + .with_default(false) + .prompt(); + + match ans { + Ok(true) => match mihi::delete_exercise(selection) { + Ok(_) => println!("Removed '{}' from the database!", selection), + Err(e) => { + println!("error: words: {e}"); + return 1; + } + }, + Ok(false) => { + println!("Doing nothing..."); + } + Err(_) => return 1, + } + + 0 +} + +pub fn run(args: Vec) { + if args.is_empty() { + help(Some( + "error: exercises: you have to provide at least a subcommand", + )); + std::process::exit(1); + } + + let mut it = args.into_iter(); + + match it.next() { + Some(first) => match first.as_str() { + "-h" | "--help" => { + help(None); + std::process::exit(0); + } + "create" => { + std::process::exit(create(it)); + } + "edit" => { + std::process::exit(edit(it)); + } + "rm" => { + std::process::exit(rm(it)); + } + _ => { + help(Some( + format!("error: exercises: unknown flag or command '{first}'").as_str(), + )); + std::process::exit(1); + } + }, + None => { + help(Some( + "error: exercises: you need to provide a command" + .to_string() + .as_str(), + )); + std::process::exit(1); + } + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index db1de33..ffbcdae 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,3 +1,4 @@ +mod exercises; mod init; mod nuke; mod run; @@ -48,6 +49,10 @@ fn main() { let rest: Vec = args.collect(); init::run(rest); } + "exercises" => { + let rest: Vec = args.collect(); + exercises::run(rest); + } "nuke" => { let rest: Vec = args.collect(); nuke::run(rest); diff --git a/crates/cli/src/words.rs b/crates/cli/src/words.rs index 72748ae..7db84a2 100644 --- a/crates/cli/src/words.rs +++ b/crates/cli/src/words.rs @@ -166,7 +166,7 @@ fn trim_flags(given: String) -> String { for line in given.lines() { let trimmed = line.trim(); - if !line.trim().starts_with('#') { + if !trimmed.starts_with('#') { res.push_str(trimmed); } } @@ -510,7 +510,7 @@ fn edit(mut args: IntoIter) -> i32 { } fn show(mut _args: IntoIter) -> i32 { - 0 + todo!() } fn rm(mut args: IntoIter) -> i32 { -- cgit v1.2.3