aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock87
-rw-r--r--crates/cli/src/run.rs142
-rw-r--r--lib/mihi/Cargo.toml2
-rw-r--r--lib/mihi/src/lib.rs216
5 files changed, 430 insertions, 19 deletions
diff --git a/.gitignore b/.gitignore
index e5099bc..56767f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
/target
*.sqlite3
+tmp/
+TODO
diff --git a/Cargo.lock b/Cargo.lock
index cf0e5c6..8130300 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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()?;