use inquire::{Confirm, Select, Text, Editor}; use std::vec::IntoIter; use mihi::{create_word, delete_word, select_enunciated, Category, Gender, Language, Word}; static NEW_MESSAGE: &str = "New word"; static NEXT_MESSAGE: &str = "Skip this one!"; static QUIT_MESSAGE: &str = "Quit!"; static FLAGS_TEXT: &str = r#"# Write a JSON blob with the following allowed keys. # # => Boolean # # deponent: This is a Latin deponent verb. # onlysingular: It only has singular forms. # onlyplural: It only has plural forms. # contracted_root: The root contracts for certain forms (e.g. '_liber_' vs '_libr_ī'). # nonpositive: This is a non-positive word. # compsup_prefix: Comparative and superlative forms require a prefix. # indeclinable: It cannot be declined :-) # irregularsup: The superlative is irregular. # nopassive: Verb has no passive form. # nosupine: Verb has no supine form. # noperfect: Verb has no perfect forms. # nogerundive: Verb has no gerundive. # impersonal: Verb is impersonal (only third person available). # impersonalpassive: Verb is impersonal only on its passive forms. # noimperative: Verb has no imperative forms. # noinfinitive: Verb has no infinitive forms. # shortimperative: The imperative form is a short version. # onlythirdpassive: Verb has only forms on the third person of the passive voice. # enclitic: This is simply an enclitic. # notcomparable: There cannot be a comparable version for this word # onlyperfect: Only perfect forms are available. # semideponent: This is a Latin semi-deponent verb. # contracted_vocative The vocative contracts the root by one character. # # => More complex flags # # adds: There are some cases that are to be added to existing ones. # sets: There are some cases which need to replace the existing ones. # # For example: # # { # "onlysingular": true, # "sets": { # "accusative": { # "singular": ["im"] # } # } # } { } "#; fn help(msg: Option<&str>) { if msg.is_some() { println!("{}.\n", msg.unwrap()); } println!("mihi words: Manage words.\n"); println!("usage: mihi words [OPTIONS] \n"); println!("Options:"); println!(" -h, --help\t\tPrint this message."); println!("\nSubcommands:"); println!(" create\t\tCreate a new word."); println!(" ls\t\t\tList the words from the database."); println!(" rm\t\t\tRemove a word from the database."); println!(" show\t\t\tShow information from a word."); } #[derive(Default)] struct Guess { particle: String, category: Category, inflection_id: usize, gender: Gender, kind: String, } fn get_initial_guess(value: &str) -> Guess { let parts = value.trim().split(',').collect::>(); if parts.len() == 2 { let first = parts.first().unwrap(); let second = parts.last().unwrap(); if first.ends_with('a') && second.ends_with("ae") { return Guess { particle: first[0..first.len() - 1].to_string(), category: Category::Noun, inflection_id: 1, gender: Gender::Feminine, kind: "a".to_string(), }; } else if first.ends_with("us") && second.ends_with("ī") { return Guess { particle: first[0..first.len() - 2].to_string(), category: Category::Noun, inflection_id: 2, gender: Gender::Masculine, kind: "us".to_string(), }; } else if first.ends_with("um") && second.ends_with("ī") { return Guess { particle: first[0..first.len() - 2].to_string(), category: Category::Noun, inflection_id: 2, gender: Gender::Neuter, kind: "um".to_string(), }; } else if first.ends_with("us") && second.ends_with("ūs") { return Guess { particle: first[0..first.len() - 2].to_string(), category: Category::Noun, inflection_id: 4, gender: Gender::Masculine, kind: "fus".to_string(), }; } else if first.ends_with("ū") && second.ends_with("ūs") { return Guess { particle: first[0..first.len() - 1].to_string(), category: Category::Noun, inflection_id: 4, gender: Gender::Masculine, kind: "fus".to_string(), }; } else if first.ends_with("iēs") && second.ends_with("ēī") { return Guess { particle: first[0..first.len() - 3].to_string(), category: Category::Noun, inflection_id: 5, gender: Gender::Masculine, kind: "ies".to_string(), }; } else if first.ends_with("ēs") && second.ends_with("eī") { return Guess { particle: first[0..first.len() - 2].to_string(), category: Category::Noun, inflection_id: 5, gender: Gender::Masculine, kind: "es".to_string(), }; } else if second.ends_with("is") { return Guess { particle: second[0..second.len() - 2].to_string(), category: Category::Noun, inflection_id: 5, gender: Gender::Masculine, kind: "es".to_string(), }; } } Guess::default() } // Remove comments from the "flags" text that was provided. fn trim_flags(given: String) -> String { let mut res = String::new(); for line in given.lines() { let trimmed = line.trim(); if !line.trim().starts_with('#') { res.push_str(trimmed); } } res } fn do_create(enunciated: String) -> Result<(), String> { let guess = get_initial_guess(enunciated.as_str()); let Ok(particle) = Text::new("Particle:") .with_initial_value(&guess.particle) .prompt() else { return Err("abort!".to_string()); }; let categories = vec![ Category::Unknown, Category::Noun, Category::Adjective, Category::Verb, Category::Pronoun, Category::Adverb, Category::Preposition, Category::Conjunction, Category::Interjection, Category::Determiner, ]; let Ok(category) = Select::new("Category:", categories) .with_starting_cursor(guess.category as usize) .prompt() else { return Err("abort!".to_string()); }; let genders = vec![ Gender::Masculine, Gender::Feminine, Gender::MasculineOrFeminine, Gender::Neuter, Gender::None, ]; let gender = match category { Category::Noun => { match Select::new("Gender:", genders) .with_starting_cursor(guess.gender as usize) .prompt() { Ok(selection) => selection, Err(_) => return Err("abort!".to_string()), } } _ => Gender::None, }; let Ok(inflection) = Text::new("Inflection:") .with_initial_value(&guess.inflection_id.to_string()) .prompt() else { return Err("abort!".to_string()); }; let Ok(inflection_id) = inflection.parse::() else { return Err(format!("bad value for inflection ID '{}'", inflection)); }; let Ok(kind) = Text::new("Kind:").with_initial_value(&guess.kind).prompt() else { return Err("abort!".to_string()); }; let Ok(regular) = Confirm::new("Regular:").with_default(true).prompt() else { return Err("abort!".to_string()); }; let Ok(locative) = Confirm::new("Locative:").with_default(false).prompt() else { return Err("abort!".to_string()); }; let Ok(flags) = Editor::new("Flags:").with_predefined_text(FLAGS_TEXT).prompt() else { return Err("abort!".to_string()); }; let trimmed_flags = trim_flags(flags); let Ok(translation_en) = Text::new("Translation (english):").prompt() else { return Err("abort!".to_string()); }; let Ok(translation_ca) = Text::new("Translation (catalan):").prompt() else { return Err("abort!".to_string()); }; let word = Word{ id: 0, enunciated: enunciated.clone(), particle, language: Language::Latin, declension_id: if matches!(category, Category::Verb) { None } else { Some(inflection_id) }, conjugation_id: if matches!(category, Category::Verb) { Some(inflection_id) } else { None }, kind, category, regular, locative, gender, suffix: None, translation: serde_json::from_str(format!("{{\"en\":\"{}\", \"ca\":\"{}\"}}", translation_en.trim(), translation_ca.trim()).as_str()).unwrap(), flags: serde_json::from_str(&trimmed_flags).unwrap(), succeeded: 0, steps: 0, }; match create_word(word) { Ok(_) => { println!("Word '{}' has been successfully created!", enunciated); Ok(()) }, Err(e) => Err(e), } } fn create(args: IntoIter) -> i32 { if args.len() > 0 { help(Some( "error: words: no arguments were expected for this command", )); return 1; } loop { // Grab the enunciate from the word that we want to create. let Ok(enunciated) = Text::new("Enunciated:").prompt() else { return 1; }; if enunciated.trim().is_empty() { return 0; } // Now we try to fetch whether the word already existed, by doing a // general search on the database. let mut words = match select_enunciated(Some(enunciated.clone())) { Ok(words) => words, Err(e) => { println!("error: words: {}", e); return 1; } }; words.push(NEW_MESSAGE.to_string()); words.push(NEXT_MESSAGE.to_string()); words.push(QUIT_MESSAGE.to_string()); match words.len() { // Seems confusing, but we fill the "words" list with three default // "messages" which are part of the interface. Hence, if only three // "words" exist, then it's just the interface and we can go right // into creating the word. 3 => { if let Err(e) = do_create(enunciated) { println!("error: words: {}", e); return 1; } } _ => match Select::new("Is your word on this list?", words).prompt() { Ok(choice) => { if choice == QUIT_MESSAGE { return 0; } else if choice == NEW_MESSAGE { if let Err(e) = do_create(enunciated) { println!("error: words: {}", e); return 1; } } } Err(_) => return 1, }, }; } } fn ls(mut args: IntoIter) -> i32 { if args.len() > 1 { help(Some("error: words: too many filters")); return 1; } let words = match select_enunciated(args.next()) { Ok(words) => words, Err(e) => { println!("error: words: {}", e); return 1; } }; // TODO: not just the enunciated, but being able to edit for enunciated in words { println!("{}", enunciated); } return 0; } fn rm(mut args: IntoIter) -> i32 { if args.len() > 1 { help(Some("error: words: too many filters")); return 1; } let words = match select_enunciated(args.next()) { Ok(words) => words, Err(e) => { println!("error: words: {}", e); return 1; } }; let selection: String = match words.len() { 0 => { println!("errors: words: not found!"); return 1; } 1 => words.first().unwrap().to_owned(), _ => match Select::new("Which word?", words).prompt() { Ok(choice) => choice, Err(_) => return 1, }, }; let ans = Confirm::new( format!( "Do you really want to remove '{}' from the database?", selection ) .as_str(), ) .with_default(true) .prompt(); match ans { Ok(true) => match delete_word(&selection) { Ok(_) => println!("Removed '{}' from the database!", selection), Err(e) => { println!("error: words: {}", e); return 1; } }, Ok(false) => { println!("Doing nothing..."); } Err(_) => return 1, } return 0; } pub fn run(args: Vec) { if args.is_empty() { help(Some( "error: words: you have to provide at least a subcommand", )); std::process::exit(1); } let mut it = args.into_iter(); while let Some(first) = it.next() { match first.as_str() { "-h" | "--help" => { help(None); std::process::exit(0); } "create" => { std::process::exit(create(it)); } "ls" => { std::process::exit(ls(it)); } "rm" => { std::process::exit(rm(it)); } _ => { help(Some( format!("error: words: unknown flag or command '{}'", first).as_str(), )); std::process::exit(1); } } } }