aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock74
-rw-r--r--crates/cli/Cargo.toml4
-rw-r--r--crates/cli/src/words.rs352
-rw-r--r--lib/mihi/src/lib.rs59
4 files changed, 467 insertions, 22 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 8130300..3590d94 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -47,6 +47,8 @@ version = "0.1.0"
dependencies = [
"inquire",
"mihi",
+ "serde",
+ "serde_json",
]
[[package]]
@@ -81,6 +83,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
+name = "errno"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -93,6 +105,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -117,6 +135,17 @@ dependencies = [
]
[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -147,6 +176,7 @@ dependencies = [
"fxhash",
"newline-converter",
"once_cell",
+ "tempfile",
"unicode-segmentation",
"unicode-width",
]
@@ -175,6 +205,12 @@ dependencies = [
]
[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
name = "lock_api"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -214,7 +250,7 @@ dependencies = [
"libc",
"log",
"wasi",
- "windows-sys",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -303,6 +339,19 @@ dependencies = [
]
[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.9.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -400,6 +449,20 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -471,6 +534,15 @@ dependencies = [
]
[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index b35c369..ab5a406 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -10,4 +10,6 @@ path = "src/main.rs"
[dependencies]
mihi.workspace = true
-inquire = "0.7.5"
+inquire = { version = "0.7.5", features = ["editor"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
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);
}
}
diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs
index 0a3c2f7..8325af8 100644
--- a/lib/mihi/src/lib.rs
+++ b/lib/mihi/src/lib.rs
@@ -221,10 +221,68 @@ pub struct Word {
pub gender: Gender,
pub suffix: Option<String>,
pub translation: Value,
+ pub flags: Value,
pub succeeded: usize,
pub steps: usize,
}
+const DECLENSIONS_WITH_KINDS: &[&[&str]] = &[
+ &["a"],
+ &["us", "um", "ius", "er/ir"],
+ &["is", "istem", "pureistem", "one", "onenonistem", "two", "three", "visvis", "sussuis", "bosbovis", "iuppiteriovis"],
+ &["fus", "domusdomus"],
+ &["ies", "es"],
+ &["indeclinable"],
+];
+
+const ADJECTIVE_KINDS: &[&[&str]] = &[
+ &["us", "er/ir"],
+ &[],
+ &["one", "onenonistem", "two", "three", "unusnauta", "unusnautaer/ir", "duo", "tres", "mille"],
+];
+
+/// Creates the given word into the database.
+pub fn create_word(word: Word) -> Result<(), String> {
+ match word.category {
+ Category::Noun => {
+ match word.declension_id {
+ Some(id @ 1..7) => {
+ if !DECLENSIONS_WITH_KINDS[id - 1].contains(&word.kind.as_str()) {
+ return Err(format!("bad kind for declension '{}'", id));
+ }
+ }
+ Some(val) => return Err(format!("the declension ID '{}' is not valid for nouns", val)),
+ None => return Err(String::from("you have to provide the declension ID for this noun")),
+ }
+ },
+ Category::Adjective => {
+ match word.declension_id {
+ Some(id @ (1 | 3)) => {
+ if !ADJECTIVE_KINDS[id - 1].contains(&word.kind.as_str()) {
+ return Err(format!("bad kind for declension '{}'", id));
+ }
+ }
+ Some(val) => return Err(format!("the declension ID '{}' is not valid for adjectives", val)),
+ None => return Err(String::from("you have to provide the declension ID for this adjective")),
+ }
+ },
+ // TODO
+ _ => return Err(format!("you cannot create a word from the '{}' category", word.category)),
+ }
+
+ let conn = get_connection()?;
+ match conn.execute(
+ "INSERT INTO words (enunciated, particle, language_id, declension_id, conjugation_id, kind, category, regular, locative, gender, suffix, flags, translation, updated_at, created_at) \
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, datetime('now'), datetime('now'))",
+ params![word.enunciated, word.particle, word.language as usize,
+ word.declension_id, word.conjugation_id, word.kind, word.category as usize,
+ word.regular, word.locative, word.gender as usize, word.suffix,
+ serde_json::to_string(&word.flags).unwrap(), serde_json::to_string(&word.translation).unwrap()]) {
+ Ok(_) => Ok(()),
+ Err(e) => Err(format!("could not create '{}': {}", word.enunciated, e)),
+ }
+}
+
pub fn select_enunciated(filter: Option<String>) -> Result<Vec<String>, String> {
let conn = get_connection()?;
@@ -286,6 +344,7 @@ pub fn select_random_words(category: Category, number: usize) -> Result<Vec<Word
translation: serde_json::from_str(&row.get::<usize, String>(12).unwrap()).unwrap(),
succeeded: row.get(13).unwrap(),
steps: row.get(14).unwrap(),
+ flags: serde_json::from_str("").unwrap(),
});
}
Ok(res)