diff options
| author | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-06-03 21:49:25 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-06-03 21:49:25 +0200 |
| commit | 28c011c7b3ebd8f5aae111551222ed3748937b33 (patch) | |
| tree | f6310107b9fbdabeda1cce46e2feb2cb85d1ce1a | |
| download | mihi-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-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.lock | 505 | ||||
| -rw-r--r-- | Cargo.toml | 16 | ||||
| -rw-r--r-- | crates/cli/Cargo.toml | 13 | ||||
| -rw-r--r-- | crates/cli/src/init.rs | 55 | ||||
| -rw-r--r-- | crates/cli/src/main.rs | 81 | ||||
| -rw-r--r-- | crates/cli/src/nuke.rs | 36 | ||||
| -rw-r--r-- | crates/cli/src/run.rs | 3 | ||||
| -rw-r--r-- | crates/cli/src/words.rs | 141 | ||||
| -rw-r--r-- | lib/mihi/Cargo.toml | 11 | ||||
| -rw-r--r-- | lib/mihi/src/lib.rs | 146 | ||||
| -rw-r--r-- | lib/mihi/src/migrate.rs | 42 |
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) +} |
