aboutsummaryrefslogtreecommitdiff
path: root/crates/cli/src/words.rs
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-07-25 15:57:35 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-07-25 15:57:35 +0200
commit9310bf5d60810c1f911a41cccfeef3c8563211ef (patch)
tree260f7c997d82379e483a1638d4d53d5a8f5de2da /crates/cli/src/words.rs
parent9549858c7525d3a330d49bd1f8e353d8a9d6eb96 (diff)
downloadmihi-9310bf5d60810c1f911a41cccfeef3c8563211ef.tar.gz
mihi-9310bf5d60810c1f911a41cccfeef3c8563211ef.zip
Implement creation of words
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'crates/cli/src/words.rs')
-rw-r--r--crates/cli/src/words.rs352
1 files changed, 332 insertions, 20 deletions
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] <subcommand>\n");
@@ -17,21 +73,278 @@ fn help() {
println!(" show\t\t\tShow information from a word.");
}
-fn create(args: IntoIter<String>) -> 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::<Vec<_>>();
+
+ 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<String>) -> 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::<usize>() 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<String>) -> 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<String>) -> 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<String>) -> 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<String>) -> i32 {
fn rm(mut args: IntoIter<String>) -> 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<String>) -> i32 {
pub fn run(args: Vec<String>) {
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<String>) {
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<String>) {
"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);
}
}