aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-06-03 21:49:25 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-06-03 21:49:25 +0200
commit28c011c7b3ebd8f5aae111551222ed3748937b33 (patch)
treef6310107b9fbdabeda1cce46e2feb2cb85d1ce1a
downloadmihi-28c011c7b3ebd8f5aae111551222ed3748937b33.tar.gz
mihi-28c011c7b3ebd8f5aae111551222ed3748937b33.zip
Initial commit
Basic commands have been added, but most of the features for this project are still work in progress. Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock505
-rw-r--r--Cargo.toml16
-rw-r--r--crates/cli/Cargo.toml13
-rw-r--r--crates/cli/src/init.rs55
-rw-r--r--crates/cli/src/main.rs81
-rw-r--r--crates/cli/src/nuke.rs36
-rw-r--r--crates/cli/src/run.rs3
-rw-r--r--crates/cli/src/words.rs141
-rw-r--r--lib/mihi/Cargo.toml11
-rw-r--r--lib/mihi/src/lib.rs146
-rw-r--r--lib/mihi/src/migrate.rs42
12 files changed, 1051 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e5099bc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+*.sqlite3
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..cf0e5c6
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,505 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cc"
+version = "1.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cli"
+version = "0.1.0"
+dependencies = [
+ "inquire",
+ "mihi",
+]
+
+[[package]]
+name = "crossterm"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
+dependencies = [
+ "bitflags 1.3.2",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "fuzzy-matcher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
+dependencies = [
+ "thread_local",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "inquire"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
+dependencies = [
+ "bitflags 2.9.1",
+ "crossterm",
+ "dyn-clone",
+ "fuzzy-matcher",
+ "fxhash",
+ "newline-converter",
+ "once_cell",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "mihi"
+version = "0.1.0"
+dependencies = [
+ "rusqlite",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "newline-converter"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "rusqlite"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
+dependencies = [
+ "bitflags 2.9.1",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..d5d826d
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,16 @@
+[workspace]
+members = ["lib/*", "crates/*"]
+resolver = "2"
+
+[workspace.package]
+edition = "2021"
+# Everything under crates/* is GPLv3+; everything under lib/* is under LGPLv3+.
+license = "GPLv3+ AND LGPLv3+"
+authors = ["Miquel Sabaté Solà <mikisabate@gmail.com>"]
+
+[workspace.dependencies]
+mihi = { path = "./lib/mihi", version = "0.1.0" }
+
+[profile.release]
+codegen-units = 1
+lto = true
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
new file mode 100644
index 0000000..b35c369
--- /dev/null
+++ b/crates/cli/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "cli"
+version = "0.1.0"
+edition = "2021"
+authors = ["Miquel Sabaté Solà <mikisabate@gmail.com>"]
+
+[[bin]]
+name = "mihi"
+path = "src/main.rs"
+
+[dependencies]
+mihi.workspace = true
+inquire = "0.7.5"
diff --git a/crates/cli/src/init.rs b/crates/cli/src/init.rs
new file mode 100644
index 0000000..7de2e8c
--- /dev/null
+++ b/crates/cli/src/init.rs
@@ -0,0 +1,55 @@
+fn help() {
+ println!("mihi init: Initialize 'mihi' for a given language.\n");
+ println!("usage: mihi init [OPTIONS]\n");
+
+ println!("Options:");
+ println!(" -h, --help\t\tPrint this message.");
+ println!(" -l, --language\tThe language to be used.");
+}
+
+pub fn run(args: Vec<String>) {
+ let mut given_language: Option<String> = None;
+ let mut it = args.into_iter();
+
+ while let Some(arg) = it.next() {
+ match arg.as_str() {
+ "-h" | "--help" => {
+ help();
+ std::process::exit(0);
+ }
+ "-l" | "--language" => match it.next() {
+ Some(lang) => given_language = Some(lang),
+ None => {
+ println!(
+ "error: init: you have to provide a value for the '-l/--language' flag"
+ );
+ std::process::exit(1);
+ }
+ },
+ _ => {
+ println!("error: init: unknown flag: '{}'", arg.as_str());
+ std::process::exit(1);
+ }
+ }
+ }
+
+ // Assume latin for now. If we support more languages in the future, there
+ // should be a prompt or something.
+ let language = match given_language {
+ Some(lang) => lang,
+ None => String::from("latin"),
+ };
+
+ match init(language) {
+ Ok(_) => {}
+ Err(e) => {
+ println!("error: init: {}", e);
+ std::process::exit(1);
+ }
+ }
+}
+
+fn init(language: String) -> Result<(), String> {
+ mihi::add_language(language)?;
+ mihi::init_database()
+}
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
new file mode 100644
index 0000000..78d094d
--- /dev/null
+++ b/crates/cli/src/main.rs
@@ -0,0 +1,81 @@
+mod init;
+mod nuke;
+mod run;
+mod words;
+
+/// Version for this program.
+const VERSION: &str = "0.1.0";
+
+fn help() {
+ println!("Self-assessment tool for language learning.\n");
+ println!("usage: mihi [OPTIONS] [COMMAND] [COMMAND OPTIONS]\n");
+
+ println!("Options:");
+ println!(" -h, --help\t\tPrint this message.");
+ println!(" -v, --version\tPrint the version of this program.\n");
+
+ println!("Commands:");
+ println!(" init\t\t\tInitialize the configuration for this application.");
+ println!(" nuke\t\t\tRemove all files from this application and its database.");
+ println!(" run\t\t\tRun exercises. Default command if none was given.");
+ println!(" words\t\tManage the words for this application.");
+}
+
+fn main() {
+ let mut args = std::env::args();
+ let nargs = std::env::args().count();
+ let mut count = 1;
+
+ // Skip command name.
+ args.next();
+
+ // And iterate over the arguments.
+ while let Some(arg) = args.next() {
+ count += 1;
+
+ match arg.as_str() {
+ "-h" | "--help" => {
+ if nargs > count {
+ println!(
+ "warning: arguments passed the '{}' flag will be ignored.",
+ arg.as_str()
+ );
+ }
+ help();
+ std::process::exit(0);
+ }
+ "-v" | "--version" => {
+ if nargs > count {
+ println!(
+ "warning: arguments passed the '{}' flag will be ignored.",
+ arg.as_str()
+ );
+ }
+ println!("mihi {}", VERSION);
+ std::process::exit(0);
+ }
+ "init" => {
+ let rest: Vec<String> = args.collect();
+ return init::run(rest);
+ }
+ "nuke" => {
+ let rest: Vec<String> = args.collect();
+ return nuke::run(rest);
+ }
+ "words" => {
+ let rest: Vec<String> = args.collect();
+ return words::run(rest);
+ }
+ "run" => {
+ let rest: Vec<String> = args.collect();
+ return run::run(rest);
+ }
+ _ => {
+ println!("error: unknown flag or command: '{}'", arg.as_str());
+ std::process::exit(1);
+ }
+ }
+ }
+
+ run::run(Vec::new());
+}
diff --git a/crates/cli/src/nuke.rs b/crates/cli/src/nuke.rs
new file mode 100644
index 0000000..1ea6532
--- /dev/null
+++ b/crates/cli/src/nuke.rs
@@ -0,0 +1,36 @@
+fn help() {
+ println!("mihi nuke: Nuke the current installation.\n");
+ println!("usage: mihi nuke [OPTIONS]\n");
+
+ println!("Options:");
+ println!(" -h, --help\t\tPrint this message.");
+}
+
+pub fn run(args: Vec<String>) {
+ for arg in args {
+ match arg.as_str() {
+ "-h" | "--help" => {
+ help();
+ std::process::exit(0);
+ }
+ _ => {
+ println!("error: nuke: unknown flag: '{}'", arg.as_str());
+ std::process::exit(1);
+ }
+ }
+ }
+
+ match mihi::get_config_path() {
+ Ok(path) => match std::fs::remove_dir_all(path) {
+ Ok(_) => {}
+ Err(e) => {
+ println!("error: nuke: {}", e);
+ std::process::exit(1);
+ }
+ },
+ Err(e) => {
+ println!("error: nuke: {}", e);
+ std::process::exit(1);
+ }
+ }
+}
diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs
new file mode 100644
index 0000000..2288a17
--- /dev/null
+++ b/crates/cli/src/run.rs
@@ -0,0 +1,3 @@
+pub fn run(args: Vec<String>) {
+ println!("TBD: {:#?}", args);
+}
diff --git a/crates/cli/src/words.rs b/crates/cli/src/words.rs
new file mode 100644
index 0000000..bdf49b5
--- /dev/null
+++ b/crates/cli/src/words.rs
@@ -0,0 +1,141 @@
+use inquire::{Confirm, Select};
+use std::vec::IntoIter;
+
+use mihi::{delete_word, select_enunciated};
+
+fn help() {
+ println!("mihi words: Manage words.\n");
+ println!("usage: mihi words [OPTIONS] <subcommand>\n");
+
+ println!("Options:");
+ println!(" -h, --help\t\tPrint this message.");
+
+ println!("\nSubcommands:");
+ println!(" create\t\tCreate a new word.");
+ println!(" ls\t\t\tList the words from the database.");
+ println!(" rm\t\t\tRemove a word from the database.");
+ println!(" show\t\t\tShow information from a word.");
+}
+
+fn create(args: IntoIter<String>) -> i32 {
+ println!("TBD: {:#?}", args);
+
+ return 0;
+}
+
+fn show(args: IntoIter<String>) -> i32 {
+ println!("TBD: {:#?}", args);
+
+ return 0;
+}
+
+fn ls(mut args: IntoIter<String>) -> i32 {
+ if args.len() > 1 {
+ println!("error: words: too many filters");
+ return 1;
+ }
+
+ let words = match select_enunciated(args.next()) {
+ Ok(words) => words,
+ Err(e) => {
+ println!("error: words: {}", e);
+ return 1;
+ }
+ };
+
+ for enunciated in words {
+ println!("{}", enunciated);
+ }
+
+ return 0;
+}
+
+fn rm(mut args: IntoIter<String>) -> i32 {
+ if args.len() > 1 {
+ println!("error: words: too many filters");
+ return 1;
+ }
+
+ let words = match select_enunciated(args.next()) {
+ Ok(words) => words,
+ Err(e) => {
+ println!("error: words: {}", e);
+ return 1;
+ }
+ };
+
+ let selection: String = match words.len() {
+ 0 => {
+ println!("errors: words: not found!");
+ return 1;
+ }
+ 1 => words.first().unwrap().to_owned(),
+ _ => match Select::new("Which word?", words).prompt() {
+ Ok(choice) => choice,
+ Err(_) => return 1,
+ },
+ };
+
+ let ans = Confirm::new(
+ format!(
+ "Do you really want to remove '{}' from the database?",
+ selection
+ )
+ .as_str(),
+ )
+ .with_default(true)
+ .prompt();
+
+ match ans {
+ Ok(true) => match delete_word(&selection) {
+ Ok(_) => println!("Removed '{}' from the database!", selection),
+ Err(e) => {
+ println!("error: words: {}", e);
+ return 1;
+ }
+ },
+ Ok(false) => {
+ println!("Doing nothing...");
+ }
+ Err(_) => return 1,
+ }
+
+ return 0;
+}
+
+pub fn run(args: Vec<String>) {
+ if args.is_empty() {
+ println!("error: words: you have to provide at least a subcommand");
+ std::process::exit(1);
+ }
+
+ let mut it = args.into_iter();
+
+ while let Some(first) = it.next() {
+ match first.as_str() {
+ "-h" | "--help" => {
+ help();
+ std::process::exit(0);
+ }
+ "create" => {
+ std::process::exit(create(it));
+ }
+ "ls" => {
+ std::process::exit(ls(it));
+ }
+ "rm" => {
+ std::process::exit(rm(it));
+ }
+ "show" => {
+ std::process::exit(show(it));
+ }
+ _ => {
+ println!(
+ "error: words: unknown flag or command: '{}'",
+ first.as_str()
+ );
+ std::process::exit(1);
+ }
+ }
+ }
+}
diff --git a/lib/mihi/Cargo.toml b/lib/mihi/Cargo.toml
new file mode 100644
index 0000000..33f3439
--- /dev/null
+++ b/lib/mihi/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "mihi"
+version = "0.1.0"
+description = "Library for the 'mihi' application"
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+
+[dependencies]
+rusqlite = { version = "0.35.0", features = ["bundled"] }
diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs
new file mode 100644
index 0000000..906953d
--- /dev/null
+++ b/lib/mihi/src/lib.rs
@@ -0,0 +1,146 @@
+use std::fs;
+use std::fs::File;
+use std::io::prelude::*;
+use std::path::{Path, PathBuf};
+
+use rusqlite::{params, Connection};
+
+mod migrate;
+
+/// 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> {
+ let dir = match &std::env::var("XDG_CONFIG_HOME") {
+ Ok(path) => PathBuf::from(path),
+ Err(_) => match &std::env::var("HOME") {
+ Ok(path) => Path::new(path).join(".config"),
+ Err(_) => {
+ return Err(String::from(
+ "cannot find a suitable path for the configuration",
+ ))
+ }
+ },
+ }
+ .join("mihi");
+
+ match fs::create_dir_all(&dir) {
+ Ok(_) => {}
+ Err(e) => return Err(e.to_string()),
+ };
+
+ Ok(dir)
+}
+
+/// Add the given language into the configuration of this application.
+pub fn add_language(language: String) -> Result<(), String> {
+ if language.as_str() != "latin" {
+ return Err(String::from("only 'latin' is allowed for a language"));
+ }
+
+ let path = get_config_path()?;
+ let cfg = path.join("languages.txt");
+
+ if cfg.exists() {
+ return Ok(());
+ }
+
+ let mut file = match File::create(cfg) {
+ Ok(f) => f,
+ Err(e) => return Err(format!("could not create file: {}", e)),
+ };
+ match file.write_all(language.as_bytes()) {
+ Ok(_) => Ok(()),
+ Err(e) => Err(format!("could not save language '{}': {}", language, e)),
+ }
+}
+
+/// Ensure that in the config path there is a fully initialized database.
+pub fn init_database() -> Result<(), String> {
+ let path = get_config_path()?.join("database.sqlite3");
+ let conn = match Connection::open(path) {
+ Ok(handle) => handle,
+ Err(e) => {
+ return Err(format!(
+ "could not initialize the database: {}",
+ e.to_string()
+ ))
+ }
+ };
+
+ match migrate::init(conn) {
+ Ok(_) => Ok(()),
+ Err(e) => Err(format!("bad database schema file: {}", e.to_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
+// }
+
+pub fn select_enunciated(filter: Option<String>) -> Result<Vec<String>, String> {
+ let conn = get_connection()?;
+
+ let mut stmt;
+ let mut it = match filter {
+ Some(filter) => {
+ stmt = conn
+ .prepare(
+ "SELECT enunciated FROM words WHERE enunciated LIKE ('%' || ?1 || '%') ORDER BY enunciated",
+ )
+ .unwrap();
+ stmt.query([filter.as_str()]).unwrap()
+ }
+ None => {
+ stmt = conn
+ .prepare("SELECT enunciated FROM words ORDER BY enunciated")
+ .unwrap();
+ stmt.query([]).unwrap()
+ }
+ };
+
+ let mut res = vec![];
+ while let Some(row) = it.next().unwrap() {
+ res.push(row.get::<usize, String>(0).unwrap());
+ }
+ Ok(res)
+}
+
+pub fn delete_word(enunciated: &String) -> Result<(), String> {
+ let conn = get_connection()?;
+
+ match conn.execute(
+ "DELETE FROM words WHERE enunciated = ?1",
+ params![enunciated.as_str()],
+ ) {
+ Ok(_) => Ok(()),
+ Err(e) => return Err(format!("could not remove '{}': {}", enunciated, e)),
+ }
+}
+
+fn get_connection() -> Result<rusqlite::Connection, String> {
+ let path = get_config_path()?.join("database.sqlite3");
+ match Connection::open(path) {
+ Ok(handle) => Ok(handle),
+ Err(_) => {
+ return Err(
+ "could not fetch the database. Ensure that you have called 'init' first"
+ .to_string(),
+ )
+ }
+ }
+}
diff --git a/lib/mihi/src/migrate.rs b/lib/mihi/src/migrate.rs
new file mode 100644
index 0000000..48c078e
--- /dev/null
+++ b/lib/mihi/src/migrate.rs
@@ -0,0 +1,42 @@
+use rusqlite::{Connection, Result};
+
+/// Use the given `connection` in order to initialize the database.
+pub fn init(connection: Connection) -> Result<usize> {
+ connection.execute(
+ r#"
+CREATE TABLE IF NOT EXISTS "words" (
+ "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ "particle" varchar,
+ "enunciated" varchar,
+ "declension_id" integer,
+ "conjugation_id" integer,
+ "kind" varchar,
+ "category" integer,
+ "regular" boolean DEFAULT 1,
+ "locative" boolean DEFAULT 0,
+ "gender" integer,
+ "created_at" datetime(6) NOT NULL,
+ "updated_at" datetime(6) NOT NULL,
+ "last_asked_at" datetime(6) NOT NULL,
+ "suffix" varchar,
+ "language_id" integer,
+ "translation" json,
+ "pending" boolean DEFAULT 0,
+ "flags" jsonb DEFAULT '{}',
+
+ FOREIGN KEY ("conjugation_id") REFERENCES "conjugations" ("id"),
+ FOREIGN KEY ("declension_id") REFERENCES "declensions" ("id")
+);
+"#,
+ (),
+ )?;
+
+ connection.execute(
+ r#"
+CREATE UNIQUE INDEX IF NOT EXISTS "index_words_on_enunciated" ON "words" ("enunciated");
+"#,
+ (),
+ )?;
+
+ Ok(0)
+}