diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.lock | 87 | ||||
| -rw-r--r-- | crates/cli/src/run.rs | 142 | ||||
| -rw-r--r-- | lib/mihi/Cargo.toml | 2 | ||||
| -rw-r--r-- | lib/mihi/src/lib.rs | 216 |
5 files changed, 430 insertions, 19 deletions
@@ -1,2 +1,4 @@ /target *.sqlite3 +tmp/ +TODO @@ -152,6 +152,12 @@ dependencies = [ ] [[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -185,10 +191,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] name = "mihi" version = "0.1.0" dependencies = [ "rusqlite", + "serde", + "serde_json", ] [[package]] @@ -248,6 +262,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] name = "redox_syscall" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -271,12 +303,50 @@ dependencies = [ ] [[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -319,6 +389,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] +name = "syn" +version = "2.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -329,6 +410,12 @@ dependencies = [ ] [[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs index 2288a17..fe193b9 100644 --- a/crates/cli/src/run.rs +++ b/crates/cli/src/run.rs @@ -1,3 +1,143 @@ +use inquire::Text; +use mihi::{select_random_words, update_success, Category, Word}; + +fn help(msg: Option<&str>) { + if msg.is_some() { + println!("{}.\n", msg.unwrap()); + } + + println!("mihi run: Run exercises. Default command if none was given.\n"); + println!("usage: mihi run [OPTIONS]\n"); + + println!("Options:"); + println!(" -h, --help\t\tPrint this message."); +} + +enum Locale { + English, + Catalan, +} + +impl Locale { + fn to_code(&self) -> &str { + match self { + Self::English => "en", + Self::Catalan => "ca", + } + } +} + +impl std::fmt::Display for Locale { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::English => write!(f, "english"), + Self::Catalan => write!(f, "català"), + } + } +} + +fn run_words(words: Vec<Word>, locale: Locale) -> i32 { + let mut errors = 0; + + for word in words { + // If the translation cannot be found, skip this word. + let Some(translation) = word.translation.get(locale.to_code()) else { + continue; + }; + + println!("Word: {}", word.enunciated); + + let Ok(raw) = Text::new(format!("Translation ({}):", locale).as_str()).prompt() else { + return 1; + }; + let answer = raw.trim(); + + let tr = translation.as_str().unwrap_or(""); + let found = !answer.is_empty() && tr.split(',').any(|tr| tr.trim().contains(&answer)); + + if found { + let _ = update_success(&word, word.succeeded + 1); + println!("\x1b[92m✓ {}\x1b[0m", tr); + } else { + if word.succeeded > 0 { + let _ = update_success(&word, word.succeeded - 1); + } + println!("\x1b[91m❌{}\x1b[0m", tr); + errors += 1; + } + } + + errors +} + +fn select_general_words() -> Result<Vec<Word>, String> { + let mut res = select_random_words(Category::Noun, 4)?; + res.append(&mut select_random_words(Category::Adjective, 2)?); + res.append(&mut select_random_words(Category::Verb, 4)?); + res.append(&mut select_random_words(Category::Pronoun, 1)?); + res.append(&mut select_random_words(Category::Adverb, 2)?); + res.append(&mut select_random_words(Category::Preposition, 1)?); + res.append(&mut select_random_words(Category::Conjunction, 1)?); + Ok(res) +} + pub fn run(args: Vec<String>) { - println!("TBD: {:#?}", args); + let mut it = args.into_iter(); + let mut category = None; + + while let Some(first) = it.next() { + match first.as_str() { + "-h" | "--help" => { + help(None); + std::process::exit(0); + } + "-c" | "--category" => { + if category.is_some() { + help(Some("error: run: you cannot provide multiple categories")); + } + match it.next() { + Some(cat) => { + category = match cat.trim().to_lowercase().as_str() { + "noun" => Some(Category::Noun), + "adjective" => Some(Category::Adjective), + "verb" => Some(Category::Verb), + "pronoun" => Some(Category::Pronoun), + "adverb" => Some(Category::Adverb), + "preposition" => Some(Category::Preposition), + "conjunction" => Some(Category::Conjunction), + "determiner" => Some(Category::Determiner), + _ => return help(Some("error: run: category not allowed")), + }; + } + None => help(Some("error: run: you have to provide a category")), + } + } + _ => { + help(Some( + format!("error: run: unknown flag or command '{}'", first).as_str(), + )); + std::process::exit(1); + } + } + } + + let raw_locale = std::env::var("LC_ALL").unwrap_or("en".to_string()); + let locale = if raw_locale.starts_with("ca") { + Locale::Catalan + } else { + Locale::English + }; + + let words = match category { + Some(cat) => select_random_words(cat, 15), + None => select_general_words(), + }; + + match words { + Ok(list) => std::process::exit(run_words(list, locale)), + Err(e) => { + println!("error: run: {}", e); + std::process::exit(1); + } + }; } diff --git a/lib/mihi/Cargo.toml b/lib/mihi/Cargo.toml index 33f3439..4b973d6 100644 --- a/lib/mihi/Cargo.toml +++ b/lib/mihi/Cargo.toml @@ -9,3 +9,5 @@ license.workspace = true [dependencies] rusqlite = { version = "0.35.0", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index 906953d..e464549 100644 --- a/lib/mihi/src/lib.rs +++ b/lib/mihi/src/lib.rs @@ -1,3 +1,4 @@ +use serde_json::Value; use std::fs; use std::fs::File; use std::io::prelude::*; @@ -7,6 +8,138 @@ use rusqlite::{params, Connection}; mod migrate; +#[derive(Debug)] +pub enum Category { + Unknown = 0, + Noun, + Adjective, + Verb, + Pronoun, + Adverb, + Preposition, + Conjunction, + Interjection, + Determiner, +} + +impl std::fmt::Display for Category { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Unknown => write!(f, "unknown"), + Self::Noun => write!(f, "noun"), + Self::Adjective => write!(f, "adjective"), + Self::Verb => write!(f, "verb"), + Self::Pronoun => write!(f, "pronoun"), + Self::Adverb => write!(f, "adverb"), + Self::Preposition => write!(f, "preposition"), + Self::Conjunction => write!(f, "conjunction"), + Self::Interjection => write!(f, "interjection"), + Self::Determiner => write!(f, "determiner"), + } + } +} + +impl std::default::Default for Category { + fn default() -> Self { + Category::Unknown + } +} + +impl TryFrom<usize> for Category { + type Error = &'static str; + + fn try_from(value: usize) -> Result<Self, Self::Error> { + match value { + 0 => Ok(Self::Unknown), + 1 => Ok(Self::Noun), + 2 => Ok(Self::Adjective), + 3 => Ok(Self::Verb), + 4 => Ok(Self::Pronoun), + 5 => Ok(Self::Adverb), + 6 => Ok(Self::Preposition), + 7 => Ok(Self::Conjunction), + 8 => Ok(Self::Interjection), + 9 => Ok(Self::Determiner), + _ => Err("unknonwn category!"), + } + } +} + +#[derive(Debug)] +pub enum Gender { + Masculine = 0, + Feminine, + MasculineOrFeminine, + Neuter, + None, +} + +impl TryFrom<usize> for Gender { + type Error = &'static str; + + fn try_from(value: usize) -> Result<Self, Self::Error> { + match value { + 0 => Ok(Self::Masculine), + 1 => Ok(Self::Feminine), + 2 => Ok(Self::MasculineOrFeminine), + 3 => Ok(Self::Neuter), + 4 => Ok(Self::None), + _ => Err("unknonwn gender!"), + } + } +} + +impl std::fmt::Display for Gender { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Masculine => write!(f, "masculine"), + Self::Feminine => write!(f, "feminine"), + Self::MasculineOrFeminine => write!(f, "masculine or feminine"), + Self::Neuter => write!(f, "neuter"), + Self::None => write!(f, "none"), + } + } +} + +impl std::default::Default for Gender { + fn default() -> Self { + Gender::None + } +} + +#[derive(Debug)] +pub enum Language { + Unknown = 0, + Latin, +} + +impl TryFrom<usize> for Language { + type Error = &'static str; + + fn try_from(value: usize) -> Result<Self, Self::Error> { + match value { + 0 => Ok(Self::Unknown), + 1 => Ok(Self::Latin), + _ => Err("unknonwn language!"), + } + } +} + +impl std::fmt::Display for Language { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Unknown => write!(f, "unknown"), + Self::Latin => write!(f, "latin"), + } + } +} + +impl std::default::Default for Language { + fn default() -> Self { + Language::Unknown + } +} + /// Returns the configuration path for the application, and it even creates it /// if it doesn't exist already. pub fn get_config_path() -> Result<PathBuf, String> { @@ -73,24 +206,23 @@ pub fn init_database() -> Result<(), String> { } } -// #[derive(Debug)] -// struct Word { -// id: i32, -// enunciated: String, -// particle: String, -// language_id: u64, -// declension_id: u64, -// conjugation_id: u64, -// kind: String, -// category: String, -// regular: bool, -// locative: bool, -// gender: u32, -// suffix: String, -// translation: String, -// // TODO: datetime -// // TODO: jsonb -// } +#[derive(Debug)] +pub struct Word { + pub id: i32, + pub enunciated: String, + pub particle: String, + pub language: Language, + pub declension_id: Option<usize>, + pub conjugation_id: Option<usize>, + pub kind: String, + pub category: Category, + pub regular: bool, + pub locative: bool, + pub gender: Gender, + pub suffix: Option<String>, + pub translation: Value, + pub succeeded: usize, +} pub fn select_enunciated(filter: Option<String>) -> Result<Vec<String>, String> { let conn = get_connection()?; @@ -120,6 +252,54 @@ pub fn select_enunciated(filter: Option<String>) -> Result<Vec<String>, String> Ok(res) } +pub fn select_random_words(category: Category, number: usize) -> Result<Vec<Word>, String> { + let conn = get_connection()?; + let mut stmt = conn + .prepare( + "SELECT id, enunciated, particle, language_id, declension_id, conjugation_id, \ + kind, category, regular, locative, gender, suffix, translation, succeeded \ + FROM words \ + WHERE category = ?1 AND translation != '{}' \ + ORDER BY succeeded ASC, updated_at DESC + LIMIT ?2", + ) + .unwrap(); + let mut it = stmt.query([category as usize, number]).unwrap(); + + let mut res = vec![]; + while let Some(row) = it.next().unwrap() { + res.push(Word { + id: row.get(0).unwrap(), + enunciated: row.get(1).unwrap(), + particle: row.get(2).unwrap(), + language: row.get::<usize, usize>(3).unwrap().try_into()?, + declension_id: row.get(4).unwrap(), + conjugation_id: row.get(5).unwrap(), + kind: row.get(6).unwrap(), + category: row.get::<usize, usize>(7).unwrap().try_into()?, + regular: row.get(8).unwrap(), + locative: row.get(9).unwrap(), + gender: row.get::<usize, usize>(10).unwrap().try_into()?, + suffix: row.get(11).unwrap(), + translation: serde_json::from_str(&row.get::<usize, String>(12).unwrap()).unwrap(), + succeeded: row.get(13).unwrap(), + }); + } + Ok(res) +} + +pub fn update_success(word: &Word, success: usize) -> Result<(), String> { + let conn = get_connection()?; + + match conn.execute( + "UPDATE words SET succeeded = ?1, updated_at = datetime('now') WHERE id = ?2", + params![success, word.id], + ) { + Ok(_) => Ok(()), + Err(e) => return Err(format!("could not update '{}': {}", word.enunciated, e)), + } +} + pub fn delete_word(enunciated: &String) -> Result<(), String> { let conn = get_connection()?; |
