From 9310bf5d60810c1f911a41cccfeef3c8563211ef Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Fri, 25 Jul 2025 15:57:35 +0200 Subject: Implement creation of words MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- crates/cli/src/words.rs | 352 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 332 insertions(+), 20 deletions(-) (limited to 'crates/cli/src') diff --git a/crates/cli/src/words.rs b/crates/cli/src/words.rs index bdf49b5..4e6e4b6 100644 --- a/crates/cli/src/words.rs +++ b/crates/cli/src/words.rs @@ -1,9 +1,65 @@ -use inquire::{Confirm, Select}; +use inquire::{Confirm, Select, Text, Editor}; use std::vec::IntoIter; -use mihi::{delete_word, select_enunciated}; +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()); + } -fn help() { println!("mihi words: Manage words.\n"); println!("usage: mihi words [OPTIONS] \n"); @@ -17,21 +73,278 @@ fn help() { println!(" show\t\t\tShow information from a word."); } -fn create(args: IntoIter) -> i32 { - println!("TBD: {:#?}", args); +#[derive(Default)] +struct Guess { + particle: String, + category: Category, + inflection_id: usize, + gender: Gender, + kind: String, +} - return 0; +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() } -fn show(args: IntoIter) -> i32 { - println!("TBD: {:#?}", args); +// Remove comments from the "flags" text that was provided. +fn trim_flags(given: String) -> String { + let mut res = String::new(); - return 0; + 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 { - println!("error: words: too many filters"); + help(Some("error: words: too many filters")); return 1; } @@ -43,6 +356,7 @@ fn ls(mut args: IntoIter) -> i32 { } }; + // TODO: not just the enunciated, but being able to edit for enunciated in words { println!("{}", enunciated); } @@ -52,7 +366,7 @@ fn ls(mut args: IntoIter) -> i32 { fn rm(mut args: IntoIter) -> i32 { if args.len() > 1 { - println!("error: words: too many filters"); + help(Some("error: words: too many filters")); return 1; } @@ -105,7 +419,9 @@ fn rm(mut args: IntoIter) -> i32 { pub fn run(args: Vec) { if args.is_empty() { - println!("error: words: you have to provide at least a subcommand"); + help(Some( + "error: words: you have to provide at least a subcommand", + )); std::process::exit(1); } @@ -114,7 +430,7 @@ pub fn run(args: Vec) { while let Some(first) = it.next() { match first.as_str() { "-h" | "--help" => { - help(); + help(None); std::process::exit(0); } "create" => { @@ -126,14 +442,10 @@ pub fn run(args: Vec) { "rm" => { std::process::exit(rm(it)); } - "show" => { - std::process::exit(show(it)); - } _ => { - println!( - "error: words: unknown flag or command: '{}'", - first.as_str() - ); + help(Some( + format!("error: words: unknown flag or command '{}'", first).as_str(), + )); std::process::exit(1); } } -- cgit v1.2.3