diff options
| author | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-07-29 21:54:30 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-07-29 21:54:30 +0200 |
| commit | b25f575e41f078be3e47d843fda1827881f23646 (patch) | |
| tree | caedf8553e1237e37260e1f970f417c2a64af089 /crates/cli/src | |
| parent | e012aa80006a0de8135929243e1311958b791048 (diff) | |
| download | mihi-b25f575e41f078be3e47d843fda1827881f23646.tar.gz mihi-b25f575e41f078be3e47d843fda1827881f23646.zip | |
Add the exercises command
Subcommands have been added to it which are similar to the ones on words
on purpose.
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'crates/cli/src')
| -rw-r--r-- | crates/cli/src/exercises.rs | 254 | ||||
| -rw-r--r-- | crates/cli/src/main.rs | 5 | ||||
| -rw-r--r-- | crates/cli/src/words.rs | 4 |
3 files changed, 261 insertions, 2 deletions
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] <subcommand>\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<Exercise, String> { + 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<String>) -> 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<String>) -> Result<Exercise, String> { + 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<String>) -> 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<String>) -> 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<String>) { + 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<String> = args.collect(); init::run(rest); } + "exercises" => { + let rest: Vec<String> = args.collect(); + exercises::run(rest); + } "nuke" => { let rest: Vec<String> = 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<String>) -> i32 { } fn show(mut _args: IntoIter<String>) -> i32 { - 0 + todo!() } fn rm(mut args: IntoIter<String>) -> i32 { |
