aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock729
-rw-r--r--Cargo.toml12
-rw-r--r--README.md4
-rwxr-xr-xbin/dev-server (renamed from bin/watch.sh)0
-rw-r--r--flake.lock36
-rw-r--r--flake.nix2
-rw-r--r--src/deck.rs145
-rw-r--r--src/gui/message.rs16
-rw-r--r--src/gui/mod.rs70
-rw-r--r--src/gui/question.rs188
-rw-r--r--src/main.rs38
-rw-r--r--src/model/difficulty.rs2
-rw-r--r--src/model/mod.rs2
-rw-r--r--src/sync.rs206
-rw-r--r--src/util/event.rs73
-rw-r--r--src/util/mod.rs1
-rw-r--r--src/util/serialization.rs18
17 files changed, 1003 insertions, 539 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d7bf371..bd06166 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,46 +4,96 @@ version = 3
[[package]]
name = "ahash"
-version = "0.7.6"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
dependencies = [
- "getrandom",
+ "cfg-if",
"once_cell",
"version_check",
+ "zerocopy",
]
[[package]]
-name = "ansi_term"
-version = "0.12.1"
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
- "winapi",
+ "libc",
]
[[package]]
-name = "anyhow"
-version = "1.0.45"
+name = "anstream"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
+checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
[[package]]
-name = "atty"
-version = "0.2.14"
+name = "anstyle"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
dependencies = [
- "hermit-abi",
- "libc",
- "winapi",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys",
]
[[package]]
+name = "anstyle-wincon"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+
+[[package]]
name = "autocfg"
-version = "1.0.1"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
@@ -52,12 +102,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
+name = "bitflags"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+
+[[package]]
+name = "bumpalo"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+
+[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -65,33 +136,102 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.19"
+version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
- "libc",
- "num-integer",
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
"num-traits",
- "time",
- "winapi",
+ "wasm-bindgen",
+ "windows-targets",
]
[[package]]
name = "clap"
-version = "2.34.0"
+version = "4.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272"
dependencies = [
- "ansi_term",
- "atty",
- "bitflags",
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
"strsim",
- "textwrap",
- "unicode-width",
- "vec_map",
]
[[package]]
+name = "clap_derive"
+version = "4.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "crossterm"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
+dependencies = [
+ "bitflags 2.4.1",
+ "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 = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+
+[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -109,248 +249,298 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
+ "clap",
+ "crossterm",
+ "ratatui",
"rusqlite",
"rusqlite_migration",
"serde",
"serde_json",
- "structopt",
- "termion",
- "tui",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi",
]
[[package]]
name = "hashbrown"
-version = "0.11.2"
+version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
dependencies = [
"ahash",
+ "allocator-api2",
]
[[package]]
name = "hashlink"
-version = "0.7.0"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown",
]
[[package]]
name = "heck"
-version = "0.3.3"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
- "unicode-segmentation",
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
]
[[package]]
-name = "hermit-abi"
-version = "0.1.19"
+name = "iana-time-zone-haiku"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
- "libc",
+ "cc",
+]
+
+[[package]]
+name = "indoc"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
+
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
]
[[package]]
name = "itoa"
-version = "0.4.8"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
-name = "lazy_static"
-version = "1.4.0"
+name = "js-sys"
+version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
+dependencies = [
+ "wasm-bindgen",
+]
[[package]]
name = "libc"
-version = "0.2.105"
+version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
+checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "libsqlite3-sys"
-version = "0.23.1"
+version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abd5850c449b40bacb498b2bbdfaff648b1b055630073ba8db499caf2d0ea9f2"
+checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
-name = "log"
-version = "0.4.14"
+name = "lock_api"
+version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
dependencies = [
- "cfg-if",
+ "autocfg",
+ "scopeguard",
]
[[package]]
-name = "memchr"
-version = "2.4.1"
+name = "log"
+version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
-name = "num-integer"
-version = "0.1.44"
+name = "lru"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+checksum = "2994eeba8ed550fd9b47a0b38f0242bc3344e496483c6180b69139cc2fa5d1d7"
dependencies = [
- "autocfg",
- "num-traits",
+ "hashbrown",
]
[[package]]
-name = "num-traits"
-version = "0.2.14"
+name = "mio"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
dependencies = [
- "autocfg",
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
]
[[package]]
-name = "numtoa"
-version = "0.1.0"
+name = "num-traits"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
+checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
+dependencies = [
+ "autocfg",
+]
[[package]]
name = "once_cell"
-version = "1.8.0"
+version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
-name = "pkg-config"
-version = "0.3.22"
+name = "parking_lot"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
+name = "parking_lot_core"
+version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn",
- "version_check",
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
]
[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
+name = "paste"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
-]
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "proc-macro2"
-version = "1.0.32"
+version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
+checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
dependencies = [
- "unicode-xid",
+ "unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.10"
+version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
-name = "redox_syscall"
-version = "0.2.10"
+name = "ratatui"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425"
dependencies = [
- "bitflags",
+ "bitflags 2.4.1",
+ "cassowary",
+ "crossterm",
+ "indoc",
+ "itertools",
+ "lru",
+ "paste",
+ "strum",
+ "unicode-segmentation",
+ "unicode-width",
]
[[package]]
-name = "redox_termios"
-version = "0.1.2"
+name = "redox_syscall"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
- "redox_syscall",
+ "bitflags 1.3.2",
]
[[package]]
name = "rusqlite"
-version = "0.26.1"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a82b0b91fad72160c56bf8da7a549b25d7c31109f52cc1437eac4c0ad2550a7"
+checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
- "bitflags",
+ "bitflags 2.4.1",
"chrono",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
- "memchr",
"smallvec",
]
[[package]]
name = "rusqlite_migration"
-version = "0.5.0"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2abaa6d2e015b59342d088590b0c0051ba1ed5508780f8c31315b2986ebbf5b6"
+checksum = "ef7dd29a4426624704d5966416682fb7ab3682f724986e9e3893eaca44accabc"
dependencies = [
"log",
"rusqlite",
]
[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
+[[package]]
name = "ryu"
-version = "1.0.5"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
-version = "1.0.130"
+version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.130"
+version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
@@ -359,9 +549,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.70"
+version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
+checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
"itoa",
"ryu",
@@ -369,137 +559,175 @@ dependencies = [
]
[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "smallvec"
-version = "1.7.0"
+version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
+checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "strsim"
-version = "0.8.0"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
-name = "structopt"
-version = "0.3.26"
+name = "strum"
+version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
- "clap",
- "lazy_static",
- "structopt-derive",
+ "strum_macros",
]
[[package]]
-name = "structopt-derive"
-version = "0.4.18"
+name = "strum_macros"
+version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
+checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
dependencies = [
"heck",
- "proc-macro-error",
"proc-macro2",
"quote",
+ "rustversion",
"syn",
]
[[package]]
name = "syn"
-version = "1.0.81"
+version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
+checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"proc-macro2",
"quote",
- "unicode-xid",
+ "unicode-ident",
]
[[package]]
-name = "termion"
-version = "1.5.6"
+name = "unicode-ident"
+version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
-dependencies = [
- "libc",
- "numtoa",
- "redox_syscall",
- "redox_termios",
-]
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
-name = "textwrap"
-version = "0.11.0"
+name = "unicode-segmentation"
+version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-dependencies = [
- "unicode-width",
-]
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
-name = "time"
-version = "0.1.43"
+name = "unicode-width"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
-dependencies = [
- "libc",
- "winapi",
-]
+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
-name = "tui"
-version = "0.16.0"
+name = "utf8parse"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23"
-dependencies = [
- "bitflags",
- "cassowary",
- "termion",
- "unicode-segmentation",
- "unicode-width",
-]
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
-name = "unicode-segmentation"
-version = "1.8.0"
+name = "vcpkg"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
-name = "unicode-width"
-version = "0.1.9"
+name = "version_check"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
-name = "unicode-xid"
-version = "0.2.2"
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
-name = "vcpkg"
-version = "0.2.15"
+name = "wasm-bindgen"
+version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
[[package]]
-name = "vec_map"
-version = "0.8.2"
+name = "wasm-bindgen-backend"
+version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
[[package]]
-name = "version_check"
-version = "0.9.3"
+name = "wasm-bindgen-macro"
+version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
[[package]]
-name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
+name = "wasm-bindgen-macro-support"
+version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "winapi"
@@ -522,3 +750,98 @@ 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-core"
+version = "0.51.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[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_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[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_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[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_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
index ce79a66..1f22eb6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,15 +2,15 @@
name = "flashcards"
version = "0.1.0"
authors = ["Joris GUYONVARCH"]
-edition = "2018"
+edition = "2021"
[dependencies]
anyhow = "1.0"
chrono = "0.4"
-rusqlite = { version = "0.26", features = [ "chrono" ] }
-rusqlite_migration = "0.5"
+clap = { version = "4.4", features = ["derive"] }
+crossterm = { version = "0.27" }
+rusqlite = { version = "0.29", features = [ "chrono" ] }
+rusqlite_migration = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-structopt = "0.3"
-termion = "1.5"
-tui = "0.16"
+tui = { package = "ratatui", version = "0.24", features = ["crossterm"] }
diff --git a/README.md b/README.md
index 1a20b8e..6941979 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Getting started
```bash
-nix develop --command bin/watch.sh
+nix develop --command bin/dev-server.sh
```
# Screenshot
@@ -22,7 +22,7 @@ Cards are created from a plain text `./deck` file:
- good moorning : bonjour
- alternative 1 | alternative 2 : choix 1 | choix 2
-- cat (indication) : chat
+- cat (indication) : chat [ʃa]
```
# Backlog
diff --git a/bin/watch.sh b/bin/dev-server
index e3a6c28..e3a6c28 100755
--- a/bin/watch.sh
+++ b/bin/dev-server
diff --git a/flake.lock b/flake.lock
index 6e669c0..4b02f9b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
"locked": {
- "lastModified": 1667395993,
- "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
+ "lastModified": 1694529238,
+ "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+ "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
@@ -17,11 +20,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1669040464,
- "narHash": "sha256-A+9mPkCdd6ei8EfbRX8butcneRuXbwiGpoyeh9TbAwg=",
+ "lastModified": 1701550882,
+ "narHash": "sha256-nt/o7lLaIkBpPkVviBqxp/HMEtEfVJzLl2eHzuJF9/I=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "328d723f89af95a280d3046e6124786b03b0e2bf",
+ "rev": "c3857011d76ef8d137ac9abb5b0a8c957961a471",
"type": "github"
},
"original": {
@@ -47,11 +50,11 @@
]
},
"locked": {
- "lastModified": 1668998422,
- "narHash": "sha256-G/BklIplCHZEeDIabaaxqgITdIXtMolRGlwxn9jG2/Q=",
+ "lastModified": 1701483183,
+ "narHash": "sha256-MDH3oUajqTaYClCiq1QK7jWVMtMFDJWxVBCFAnkt6J4=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "68ab029c93f8f8eed4cf3ce9a89a9fd4504b2d6e",
+ "rev": "47fe4578cb64a365f400e682a70e054657c42fa5",
"type": "github"
},
"original": {
@@ -59,6 +62,21 @@
"repo": "rust-overlay",
"type": "github"
}
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
}
},
"root": "root",
diff --git a/flake.nix b/flake.nix
index 80c0c8f..03b010c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -19,7 +19,7 @@
{
devShell = mkShell {
buildInputs = [
- rust-bin.stable."1.60.0".default
+ rust-bin.stable."1.74.0".default
watchexec
cargo-watch
sqlite
diff --git a/src/deck.rs b/src/deck.rs
index 0c302e1..4491d9b 100644
--- a/src/deck.rs
+++ b/src/deck.rs
@@ -23,51 +23,57 @@ impl std::error::Error for ParseError {
}
}
-pub fn read(deck: &str) -> Result<Vec<Line>> {
- let file = File::open(deck)?;
+pub fn read_file(path: &str) -> Result<Vec<Line>> {
+ let file = File::open(path)?;
let reader = BufReader::new(file);
let mut entries: Vec<Line> = Vec::new();
for (index, line) in reader.lines().enumerate() {
let line = line?;
- let line = line.trim();
- if !line.starts_with('#') && !line.is_empty() {
- if !line.starts_with('-') {
- return Err(Error::from(ParseError {
+ if let Some(line) = read_line(index, &line)? {
+ entries.push(line)
+ }
+ }
+
+ Ok(entries)
+}
+
+fn read_line(index: usize, line: &str) -> Result<Option<Line>> {
+ let line = line.trim();
+
+ if line.starts_with('#') || line.is_empty() {
+ Ok(None)
+ } else if !line.starts_with('-') {
+ Err(Error::from(ParseError {
+ line: index + 1,
+ message: "an entry should starts with “-”.".to_string(),
+ }))
+ } else {
+ let without_minus = line.split('-').skip(1).collect::<Vec<&str>>().join("-");
+ let without_comment = without_minus.split('#').collect::<Vec<&str>>()[0].trim();
+ let translation = without_comment.split(':').collect::<Vec<&str>>();
+ if translation.len() != 2 {
+ Err(Error::from(ParseError {
+ line: index + 1,
+ message: "an entry should contain two parts separated by “:”.".to_string(),
+ }))
+ } else {
+ let t1 = translation[0].trim();
+ let t2 = translation[1].trim();
+ if t1.is_empty() || t2.is_empty() {
+ Err(Error::from(ParseError {
line: index + 1,
- message: "an entry should starts with “-”.".to_string(),
- }));
+ message: "an entry should contain two parts separated by “:”.".to_string(),
+ }))
} else {
- let without_minus = line.split('-').skip(1).collect::<Vec<&str>>().join("-");
- let without_comment = without_minus.split('#').collect::<Vec<&str>>()[0].trim();
- let translation = without_comment.split(':').collect::<Vec<&str>>();
- if translation.len() != 2 {
- return Err(Error::from(ParseError {
- line: index + 1,
- message: "an entry should contain two parts separated by “:”.".to_string(),
- }));
- } else {
- let t1 = translation[0].trim();
- let t2 = translation[1].trim();
- if t1.is_empty() || t2.is_empty() {
- return Err(Error::from(ParseError {
- line: index + 1,
- message: "an entry should contain two parts separated by “:”."
- .to_string(),
- }));
- } else {
- entries.push(Line {
- part_1: serialization::line_to_words(t1),
- part_2: serialization::line_to_words(t2),
- })
- }
- }
+ Ok(Some(Line {
+ part_1: serialization::line_to_words(t1),
+ part_2: serialization::line_to_words(t2),
+ }))
}
}
}
-
- Ok(entries)
}
pub fn pp_from_path(path: &str) -> Option<String> {
@@ -83,3 +89,74 @@ fn capitalize(s: &str) -> String {
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
+
+#[cfg(test)]
+pub mod tests {
+
+ use crate::model::Line;
+ use anyhow::Result;
+
+ #[test]
+ fn errors() {
+ is_error("A : a");
+ is_error("- A");
+ is_error("- A -> a");
+ is_error("- A : B : C");
+ is_error("- : ");
+ is_error("- A : a\n-")
+ }
+
+ #[test]
+ fn ignored() {
+ check("", &[]);
+ check(" ", &[]);
+ check(" \n \n ", &[]);
+ check("# 1", &[]);
+ check("# 1\n\n # 2", &[]);
+ }
+
+ #[test]
+ fn card() {
+ check("- A : a", &[(&["A"], &["a"])]);
+ }
+
+ #[test]
+ fn cards() {
+ check("- A : a\n- B : b", &[(&["A"], &["a"]), (&["B"], &["b"])]);
+ }
+
+ #[test]
+ fn alternatives() {
+ check("- A : a1 | a2", &[(&["A"], &["a1", "a2"])]);
+ check("- A1 | A2 : a", &[(&["A1", "A2"], &["a"])]);
+ check("- A1 | A2 : a1 | a2", &[(&["A1", "A2"], &["a1", "a2"])]);
+ }
+
+ fn is_error(content: &str) {
+ assert!(read_string(content).is_err())
+ }
+
+ fn check(content: &str, res: &[(&[&str], &[&str])]) {
+ assert_eq!(
+ read_string(content).unwrap(),
+ res.iter()
+ .map(|(part_1, part_2)| Line {
+ part_1: part_1.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
+ part_2: part_2.iter().map(|x| x.to_string()).collect::<Vec<_>>()
+ })
+ .collect::<Vec<_>>()
+ )
+ }
+
+ pub fn read_string(content: &str) -> Result<Vec<Line>> {
+ let mut entries: Vec<Line> = Vec::new();
+
+ for (index, line) in content.lines().enumerate() {
+ if let Some(line) = super::read_line(index, line)? {
+ entries.push(line)
+ }
+ }
+
+ Ok(entries)
+ }
+}
diff --git a/src/gui/message.rs b/src/gui/message.rs
index 29b5d8a..61f57ba 100644
--- a/src/gui/message.rs
+++ b/src/gui/message.rs
@@ -1,7 +1,6 @@
use crate::gui::util;
-use crate::util::event::{Event, Events};
use anyhow::Result;
-use termion::event::Key;
+use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use tui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout},
@@ -11,7 +10,6 @@ use tui::{
pub fn show<B: Backend>(
terminal: &mut Terminal<B>,
- events: &Events,
title: &str,
message: &str,
wait: bool,
@@ -33,12 +31,12 @@ pub fn show<B: Backend>(
})?;
if wait {
- if let Event::Input(key) = events.next()? {
- match key {
- Key::Char('q') | Key::Ctrl('c') => {
- break;
- }
- _ => {}
+ if let Event::Key(key) = event::read()? {
+ if key.code == KeyCode::Char('q')
+ || key.code == KeyCode::Char('c')
+ && key.modifiers.contains(KeyModifiers::CONTROL)
+ {
+ break;
}
}
} else {
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
index 358e4b5..3abe238 100644
--- a/src/gui/mod.rs
+++ b/src/gui/mod.rs
@@ -2,34 +2,59 @@ pub mod message;
pub mod question;
pub mod util;
-use crate::{db, space_repetition, util::event::Events, util::time};
+use crate::sync;
+use crate::{db, space_repetition, util::time};
use anyhow::Result;
+use crossterm::terminal;
use rusqlite::Connection;
-use std::io;
-use termion::{raw::IntoRawMode, raw::RawTerminal, screen::AlternateScreen};
-use tui::{backend::TermionBackend, Terminal};
+use std::fs;
+use std::io::Stdout;
+use tui::{backend::CrosstermBackend, Terminal};
-pub type Term = Terminal<TermionBackend<AlternateScreen<RawTerminal<io::Stdout>>>>;
+pub type Term = Terminal<CrosstermBackend<Stdout>>;
-pub fn terminal() -> Result<Term> {
- let stdout = io::stdout().into_raw_mode()?;
- let stdout = AlternateScreen::from(stdout);
- let backend = TermionBackend::new(stdout);
+pub fn setup_terminal() -> Result<Term> {
+ terminal::enable_raw_mode()?;
+ let mut stdout = std::io::stdout();
+ crossterm::execute!(stdout, terminal::EnterAlternateScreen)?;
+ let backend = CrosstermBackend::new(stdout);
Ok(Terminal::new(backend)?)
}
-pub fn start(conn: &Connection, term: &mut Term, events: &Events, deck_name: &str) -> Result<()> {
- let mut answers = 0;
+pub fn restore_terminal(term: &mut Term) -> Result<()> {
+ terminal::disable_raw_mode()?;
+ crossterm::execute!(term.backend_mut(), terminal::LeaveAlternateScreen)?;
+ term.show_cursor()?;
+ Ok(())
+}
+pub fn start(
+ conn: &mut Connection,
+ term: &mut Term,
+ deck_path: &str,
+ deck_name: &str,
+ mut deck_last_sync: u64,
+ hide_remaining: bool,
+) -> Result<()> {
loop {
- let now = time::seconds_since_unix_epoch()?;
- let title = title(deck_name, answers, db::count_available(conn).unwrap_or(0));
+ // Synchronize deck if necessary
+ let deck_last_update =
+ time::seconds_since_unix_epoch_of(fs::metadata(deck_path)?.modified()?)?;
+ if deck_last_update > deck_last_sync {
+ sync::run(conn, deck_path)?;
+ deck_last_sync = time::seconds_since_unix_epoch()?;
+ }
+
+ let title = title(
+ deck_name,
+ db::count_available(conn).unwrap_or(0),
+ hide_remaining,
+ );
match db::pick_random_ready(conn) {
- Some(card) => match question::ask(term, events, &title, &card)? {
+ Some(card) => match question::ask(term, &title, &card)? {
question::Response::Aborted => break,
question::Response::Answered { difficulty } => {
- answers += 1;
db::update(
conn,
&card.question,
@@ -40,12 +65,13 @@ pub fn start(conn: &Connection, term: &mut Term, events: &Events, deck_name: &st
None => {
let message = match db::next_ready(conn) {
Some(ready) => {
+ let now = time::seconds_since_unix_epoch()?;
let duration = time::pp_duration(ready - now);
format!("Prochaine carte disponible dans {duration}.")
}
None => "Aucune carte n’est disponible. Votre deck est-il vide ?".to_string(),
};
- let _ = message::show(term, events, &title, &message, true);
+ let _ = message::show(term, &title, &message, true);
break;
}
}
@@ -54,16 +80,10 @@ pub fn start(conn: &Connection, term: &mut Term, events: &Events, deck_name: &st
Ok(())
}
-fn title(deck_name: &str, answers: i32, available_cards: i32) -> String {
- if answers == 0 && available_cards == 0 {
+fn title(deck_name: &str, available_cards: i32, hide_remaining: bool) -> String {
+ if available_cards == 0 || hide_remaining {
deck_name.to_string()
- } else if available_cards == 0 {
- let from = answers;
- let to = answers + available_cards;
- format!("{deck_name} ({from} / {to})")
} else {
- let from = answers + 1;
- let to = answers + available_cards;
- format!("{deck_name} ({from} / {to})")
+ format!("{deck_name} ({available_cards})")
}
}
diff --git a/src/gui/question.rs b/src/gui/question.rs
index 2aa6e65..512ca49 100644
--- a/src/gui/question.rs
+++ b/src/gui/question.rs
@@ -1,17 +1,16 @@
use crate::{
gui::util,
model::{difficulty, difficulty::Difficulty, Card},
- util::event::{Event, Events},
util::serialization,
};
use anyhow::Result;
-use termion::event::Key;
+use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use tui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
- text::{Span, Spans, Text},
- widgets::{Block, Borders, Paragraph, Wrap},
+ text::{Line, Span, Text},
+ widgets::{Paragraph, Wrap},
Terminal,
};
@@ -30,12 +29,7 @@ pub enum Response {
Answered { difficulty: Difficulty },
}
-pub fn ask<B: Backend>(
- terminal: &mut Terminal<B>,
- events: &Events,
- title: &str,
- card: &Card,
-) -> Result<Response> {
+pub fn ask<B: Backend>(terminal: &mut Terminal<B>, title: &str, card: &Card) -> Result<Response> {
let mut state = State {
input: String::new(),
answer: Answer::Write,
@@ -62,16 +56,6 @@ pub fn ask<B: Backend>(
f.render_widget(d1, chunks[0]);
let question = Paragraph::new(util::center_vertically(chunks[1], &card.question))
- .style(match state.answer {
- Answer::Write => {
- if state.input.trim().is_empty() {
- Style::default().fg(Color::Yellow)
- } else {
- Style::default()
- }
- }
- _ => Style::default(),
- })
.alignment(Alignment::Center);
f.render_widget(question, chunks[1]);
@@ -83,15 +67,15 @@ pub fn ask<B: Backend>(
.style(match state.answer {
Answer::Write => Style::default(),
Answer::Difficulty { difficulty: _ } => {
- if is_correct(&state.input, &card.responses) {
- Style::default().fg(Color::Green)
- } else {
- Style::default().fg(Color::Red)
+ match check_response(&state.input, &card.responses) {
+ CheckResponse::Correct { phonetics: _ } => {
+ Style::default().fg(Color::Green)
+ }
+ CheckResponse::Incorrect => Style::default().fg(Color::Red),
}
}
})
.alignment(Alignment::Center)
- .block(Block::default().borders(Borders::ALL).title("Réponse"))
.wrap(Wrap { trim: true });
f.render_widget(answer, chunks[2]);
@@ -99,12 +83,17 @@ pub fn ask<B: Backend>(
difficulty: selected,
} = state.answer
{
- if !is_correct(&state.input, &card.responses) {
- let paragraph = Paragraph::new(util::center_vertically(
- chunks[3],
- &serialization::words_to_line(&card.responses),
- ))
- .alignment(Alignment::Center);
+ let maybe_indication: Option<String> =
+ match check_response(&state.input, &card.responses) {
+ CheckResponse::Correct { phonetics } => phonetics,
+ CheckResponse::Incorrect => {
+ Some(serialization::words_to_line(&card.responses))
+ }
+ };
+
+ if let Some(indication) = maybe_indication {
+ let paragraph = Paragraph::new(util::center_vertically(chunks[3], &indication))
+ .alignment(Alignment::Center);
f.render_widget(paragraph, chunks[3]);
};
@@ -131,73 +120,84 @@ pub fn ask<B: Backend>(
})
.collect::<Vec<Vec<Span>>>()
.concat();
- let p = Paragraph::new(Text::from(Spans::from(tabs))).alignment(Alignment::Center);
+ let p = Paragraph::new(Text::from(Line::from(tabs))).alignment(Alignment::Center);
f.render_widget(p, chunks[4]);
}
})?;
- if let Event::Input(key) = events.next()? {
+ if let Event::Key(key) = event::read()? {
match state.answer {
- Answer::Write => match key {
- Key::Char('\n') => {
- let difficulty = if is_correct(&state.input, &card.responses) {
+ Answer::Write => match key.code {
+ KeyCode::Enter => {
+ let difficulty = if state.input.is_empty() {
+ // Encourage solving without typing by defaulting to good answer
Difficulty::Good
} else {
- Difficulty::Again
+ match check_response(&state.input, &card.responses) {
+ CheckResponse::Correct { phonetics: _ } => Difficulty::Good,
+ CheckResponse::Incorrect => Difficulty::Again,
+ }
};
state.answer = Answer::Difficulty { difficulty }
}
- Key::Char(c) => {
- state.input.push(c);
- if is_correct(&state.input, &card.responses) {
- state.answer = Answer::Difficulty {
- difficulty: Difficulty::Good,
+ KeyCode::Char(c) => {
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
+ if c == 'u' {
+ state.input.clear();
+ } else if c == 'w' {
+ let mut words =
+ state.input.split_whitespace().collect::<Vec<&str>>();
+ if !words.is_empty() {
+ words.truncate(words.len() - 1);
+ let joined_words = words.join(" ");
+ let space = if !words.is_empty() { " " } else { "" };
+ state.input = format!("{joined_words}{space}");
+ }
+ } else if c == 'c' {
+ return Ok(Response::Aborted);
+ }
+ } else {
+ state.input.push(c);
+ if let CheckResponse::Correct { phonetics: _ } =
+ check_response(&state.input, &card.responses)
+ {
+ state.answer = Answer::Difficulty {
+ difficulty: Difficulty::Good,
+ }
}
}
}
- Key::Backspace => {
+ KeyCode::Backspace => {
state.input.pop();
}
- Key::Ctrl('u') => {
- state.input.clear();
- }
- Key::Ctrl('w') => {
- let mut words = state.input.split_whitespace().collect::<Vec<&str>>();
- if !words.is_empty() {
- words.truncate(words.len() - 1);
- state.input = format!(
- "{}{}",
- words.join(" "),
- if !words.is_empty() { " " } else { "" }
- );
- }
- }
- Key::Ctrl('c') => {
- return Ok(Response::Aborted);
- }
_ => {}
},
Answer::Difficulty {
difficulty: selected,
- } => match key {
- Key::Left => {
- for d in relative_element(&card.state.difficulties(), &selected, -1).iter()
+ } => match key.code {
+ KeyCode::Left => {
+ if let Some(difficulty) =
+ relative_element(&card.state.difficulties(), &selected, -1)
{
- state.answer = Answer::Difficulty { difficulty: *d }
+ state.answer = Answer::Difficulty { difficulty }
}
}
- Key::Right => {
- for d in relative_element(&card.state.difficulties(), &selected, 1).iter() {
- state.answer = Answer::Difficulty { difficulty: *d }
+ KeyCode::Right => {
+ if let Some(difficulty) =
+ relative_element(&card.state.difficulties(), &selected, 1)
+ {
+ state.answer = Answer::Difficulty { difficulty }
}
}
- Key::Char('\n') => {
+ KeyCode::Enter => {
return Ok(Response::Answered {
difficulty: selected,
})
}
- Key::Ctrl('c') => {
- return Ok(Response::Aborted);
+ KeyCode::Char('c') => {
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
+ return Ok(Response::Aborted);
+ }
}
_ => {}
},
@@ -206,18 +206,50 @@ pub fn ask<B: Backend>(
}
}
-fn is_correct(input: &str, responses: &[String]) -> bool {
- // Remove whitespaces
- let input = input
+enum CheckResponse {
+ Incorrect,
+ Correct { phonetics: Option<String> },
+}
+
+fn check_response(input: &str, responses: &[String]) -> CheckResponse {
+ let input = remove_whitespaces(input);
+
+ responses
+ .iter()
+ .find(|r| remove_indications_and_phonetics(r) == input)
+ .map(|r| CheckResponse::Correct {
+ phonetics: extract_phonetics(r),
+ })
+ .unwrap_or(CheckResponse::Incorrect)
+}
+
+fn remove_whitespaces(input: &str) -> String {
+ input
.split_whitespace()
.map(|word| word.trim())
.collect::<Vec<&str>>()
- .join(" ");
+ .join(" ")
+}
- responses
- .iter()
- .map(|r| r.split('(').collect::<Vec<&str>>()[0].trim())
- .any(|x| x == input)
+fn remove_indications_and_phonetics(response: &str) -> &str {
+ response
+ .split(|c| c == '(' || c == '[')
+ .collect::<Vec<&str>>()[0]
+ .trim()
+}
+
+fn extract_phonetics(response: &str) -> Option<String> {
+ let s1 = response.split('[').collect::<Vec<&str>>();
+ if s1.len() == 2 {
+ let s2 = s1[1].split(']').collect::<Vec<&str>>();
+ if s2.len() > 1 {
+ Some(format!("[{}]", s2[0]))
+ } else {
+ None
+ }
+ } else {
+ None
+ }
}
fn relative_element<T: Clone + PartialEq>(xs: &[T], x: &T, ri: i32) -> Option<T> {
diff --git a/src/main.rs b/src/main.rs
index a791f29..b18cb1a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,35 +6,49 @@ mod space_repetition;
mod sync;
mod util;
-use crate::util::event::Events;
use anyhow::Result;
+use clap::Parser;
use std::path::PathBuf;
-use structopt::StructOpt;
-#[derive(StructOpt)]
-#[structopt()]
+#[derive(Parser)]
+#[clap()]
struct Opt {
- #[structopt(long, default_value = "deck.deck")]
+ /// Path to the deck
+ #[clap(long, default_value = "deck.deck")]
deck: String,
+
+ /// Hide remaining card counts
+ #[clap(long)]
+ hide_remaining: bool,
}
fn main() -> Result<()> {
- let deck_path = Opt::from_args().deck;
+ let args = Opt::parse();
+ let deck_path = args.deck;
let mut conn = db::init(db_path(&deck_path))?;
let deck_name = deck::pp_from_path(&deck_path).unwrap_or_else(|| "Deck".to_string());
sync::run(&mut conn, &deck_path)?;
+ let deck_last_sync = util::time::seconds_since_unix_epoch()?;
+
+ let mut term = gui::setup_terminal()?;
- let mut term = gui::terminal()?;
- let events = Events::new();
- match gui::start(&conn, &mut term, &events, &deck_name) {
- Ok(()) => Ok(()),
+ match gui::start(
+ &mut conn,
+ &mut term,
+ &deck_path,
+ &deck_name,
+ deck_last_sync,
+ args.hide_remaining,
+ ) {
+ Ok(()) => (),
Err(msg) => {
// Show errors in TUI, otherwise they are hidden
- gui::message::show(&mut term, &events, &deck_name, &format!("{msg}"), true)?;
- Err(msg)
+ gui::message::show(&mut term, &deck_name, &format!("{msg}"), true)?
}
}
+
+ gui::restore_terminal(&mut term)
}
fn db_path(deck_path: &str) -> String {
diff --git a/src/model/difficulty.rs b/src/model/difficulty.rs
index ea5a9ce..727ce4b 100644
--- a/src/model/difficulty.rs
+++ b/src/model/difficulty.rs
@@ -1,4 +1,4 @@
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Difficulty {
Again,
Hard,
diff --git a/src/model/mod.rs b/src/model/mod.rs
index 2dc1ab5..4df4c49 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -2,7 +2,7 @@ use crate::space_repetition;
pub mod difficulty;
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub struct Line {
pub part_1: Vec<String>,
pub part_2: Vec<String>,
diff --git a/src/sync.rs b/src/sync.rs
index 3911d55..6e3d84b 100644
--- a/src/sync.rs
+++ b/src/sync.rs
@@ -4,11 +4,12 @@ use crate::{
};
use anyhow::Result;
use rusqlite::Connection;
+use std::collections::HashMap;
use std::collections::HashSet;
pub fn run(conn: &mut Connection, deck_path: &str) -> Result<()> {
let db_entries = db::all(conn)?;
- let lines = deck::read(deck_path)?;
+ let lines = deck::read_file(deck_path)?;
let Diff {
new,
deleted,
@@ -29,29 +30,23 @@ struct Diff {
}
fn diff(db_entries: Vec<DbEntry>, lines: Vec<Line>) -> Diff {
- let mut file_questions: HashSet<Question> = HashSet::new();
- let mut db_questions_not_deleted: HashSet<Question> = HashSet::new();
- let mut db_questions_deleted: HashSet<Question> = HashSet::new();
+ let mut file_questions = HashMap::<String, Vec<String>>::new();
+ let mut db_questions_not_deleted = HashSet::<Question>::new();
+ let mut db_questions_deleted = HashSet::<Question>::new();
for Line { part_1, part_2 } in lines {
- for question in part_1.clone() {
- let mut responses = part_2.clone();
- responses.sort();
- file_questions.insert(Question {
- question,
- responses,
- });
- }
- for question in part_2 {
- let mut responses = part_1.clone();
- responses.sort();
- file_questions.insert(Question {
- question,
- responses,
- });
- }
+ insert(&mut file_questions, part_1.clone(), part_2.clone());
+ insert(&mut file_questions, part_2, part_1);
}
+ let file_questions: HashSet<Question> = file_questions
+ .iter()
+ .map(|(question, responses)| Question {
+ question: question.to_string(),
+ responses: responses.to_vec(),
+ })
+ .collect();
+
for DbEntry {
question,
mut responses,
@@ -97,77 +92,120 @@ fn diff(db_entries: Vec<DbEntry>, lines: Vec<Line>) -> Diff {
}
}
+fn insert(map: &mut HashMap<String, Vec<String>>, questions: Vec<String>, responses: Vec<String>) {
+ for question in questions {
+ let mut responses = responses.clone();
+ responses.sort();
+ match map.get_mut(&question) {
+ Some(existing_responses) => existing_responses.append(&mut responses),
+ None => {
+ map.insert(question, responses);
+ }
+ };
+ }
+}
+
#[cfg(test)]
mod tests {
- use super::*;
+ use super::{deck, DbEntry, Diff, Question};
+ use std::collections::HashSet;
#[test]
- fn sync() {
- let db_entries = vec![
- DbEntry {
- question: "A".to_string(),
- responses: vec!["A".to_string()],
- deleted: None,
- },
- DbEntry {
- question: "B".to_string(),
- responses: vec!["B".to_string()],
- deleted: None,
- },
- DbEntry {
- question: "C".to_string(),
- responses: vec!["C".to_string()],
- deleted: Some(0),
- },
- DbEntry {
- question: "D".to_string(),
- responses: vec!["D".to_string()],
- deleted: Some(0),
- },
- ];
-
- let lines = vec![
- Line {
- part_1: vec!["A".to_string()],
- part_2: vec!["A".to_string()],
- },
- Line {
- part_1: vec!["C".to_string()],
- part_2: vec!["C".to_string()],
- },
- Line {
- part_1: vec!["E".to_string()],
- part_2: vec!["E".to_string()],
- },
- ];
-
- let Res {
- new,
- deleted,
- undeleted,
- } = sync(db_entries, lines);
+ fn test_added() {
+ let diff = deck_diff("- A : a", "- A : a\n- B : b");
- assert_eq!(
- new,
- vec!(Question {
- question: "E".to_string(),
- responses: vec!("E".to_string())
- })
- );
- assert_eq!(
- deleted,
- vec!(Question {
- question: "B".to_string(),
- responses: vec!("B".to_string())
- })
+ has_questions(diff.new, vec![("B", vec!["b"]), ("b", vec!["B"])]);
+ assert!(diff.deleted.is_empty());
+ assert!(diff.undeleted.is_empty());
+ }
+
+ #[test]
+ fn test_updated() {
+ let diff = deck_diff("- A : a1", "- A : a2");
+
+ has_questions(diff.new, vec![("A", vec!["a2"]), ("a2", vec!["A"])]);
+ has_questions(diff.deleted, vec![("A", vec!["a1"]), ("a1", vec!["A"])]);
+ assert!(diff.undeleted.is_empty());
+ }
+
+ #[test]
+ fn test_deleted() {
+ let diff = deck_diff("- A : a", "");
+
+ assert!(diff.new.is_empty());
+ has_questions(diff.deleted, vec![("A", vec!["a"]), ("a", vec!["A"])]);
+ assert!(diff.undeleted.is_empty());
+ }
+
+ #[test]
+ fn test_undeleted() {
+ let db_entries = vec![DbEntry {
+ question: "A".to_string(),
+ responses: vec!["a".to_string()],
+ deleted: Some(0),
+ }];
+
+ let diff = super::diff(db_entries, deck::tests::read_string("- A : a").unwrap());
+
+ has_questions(diff.new, vec![("a", vec!["A"])]);
+ assert!(diff.deleted.is_empty());
+ has_questions(diff.undeleted, vec![("A", vec!["a"])]);
+ }
+ #[test]
+ fn regroup_same_question() {
+ let diff = deck_diff("", "- A : a\n- A | B : b");
+
+ has_questions(
+ diff.new,
+ vec![
+ ("A", vec!["a", "b"]),
+ ("B", vec!["b"]),
+ ("a", vec!["A"]),
+ ("b", vec!["A", "B"]),
+ ],
);
+ assert!(diff.deleted.is_empty());
+ assert!(diff.undeleted.is_empty());
+ }
+
+ fn deck_diff(from: &str, to: &str) -> Diff {
+ super::diff(db_entries(from), deck::tests::read_string(to).unwrap())
+ }
+
+ fn has_questions(questions: Vec<Question>, xs: Vec<(&str, Vec<&str>)>) {
assert_eq!(
- undeleted,
- vec!(Question {
- question: "C".to_string(),
- responses: vec!("C".to_string())
- })
- );
+ to_set(questions),
+ HashSet::from_iter(
+ xs.iter()
+ .map(|(y, ys)| Question {
+ question: y.to_string(),
+ responses: ys.iter().map(|z| z.to_string()).collect::<Vec<_>>()
+ })
+ .collect::<Vec<_>>()
+ )
+ )
+ }
+
+ fn to_set<A: std::cmp::Eq + std::hash::Hash + std::clone::Clone>(xs: Vec<A>) -> HashSet<A> {
+ xs.iter().cloned().collect()
+ }
+
+ fn db_entries(deck: &str) -> Vec<DbEntry> {
+ let lines = deck::tests::read_string(deck).unwrap();
+ let diff = super::diff(vec![], lines);
+ diff.new
+ .iter()
+ .map(
+ |Question {
+ question,
+ responses,
+ }| DbEntry {
+ question: question.to_string(),
+ responses: responses.to_vec(),
+ deleted: None,
+ },
+ )
+ .collect()
}
}
diff --git a/src/util/event.rs b/src/util/event.rs
deleted file mode 100644
index 379df99..0000000
--- a/src/util/event.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-use std::io;
-use std::sync::mpsc;
-use std::thread;
-use std::time::Duration;
-
-use termion::event::Key;
-use termion::input::TermRead;
-
-pub enum Event<I> {
- Input(I),
- Tick,
-}
-
-/// A small event handler that wrap termion input and tick events. Each event
-/// type is handled in its own thread and returned to a common `Receiver`
-pub struct Events {
- rx: mpsc::Receiver<Event<Key>>,
- input_handle: thread::JoinHandle<()>,
- tick_handle: thread::JoinHandle<()>,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct Config {
- pub tick_rate: Duration,
-}
-
-impl Default for Config {
- fn default() -> Config {
- Config {
- tick_rate: Duration::from_millis(250),
- }
- }
-}
-
-impl Events {
- pub fn new() -> Events {
- Events::with_config(Config::default())
- }
-
- pub fn with_config(config: Config) -> Events {
- let (tx, rx) = mpsc::channel();
- let input_handle = {
- let tx = tx.clone();
- thread::spawn(move || {
- let stdin = io::stdin();
- for key in stdin.keys().flatten() {
- if let Err(err) = tx.send(Event::Input(key)) {
- eprintln!("{err}");
- return;
- }
- }
- })
- };
- let tick_handle = {
- thread::spawn(move || loop {
- if let Err(err) = tx.send(Event::Tick) {
- eprintln!("{err}");
- break;
- }
- thread::sleep(config.tick_rate);
- })
- };
- Events {
- rx,
- input_handle,
- tick_handle,
- }
- }
-
- pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
- self.rx.recv()
- }
-}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index c866e61..3444389 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1,4 +1,3 @@
#[allow(dead_code)]
-pub mod event;
pub mod serialization;
pub mod time;
diff --git a/src/util/serialization.rs b/src/util/serialization.rs
index 61b3a83..189a41a 100644
--- a/src/util/serialization.rs
+++ b/src/util/serialization.rs
@@ -8,3 +8,21 @@ pub fn line_to_words(line: &str) -> Vec<String> {
pub fn words_to_line(words: &[String]) -> String {
words.join(" | ")
}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+
+ #[test]
+ fn test_line_to_words() {
+ assert_eq!(line_to_words("a"), vec!("a"));
+ assert_eq!(line_to_words("a | b | c"), vec!("a", "b", "c"));
+ }
+
+ #[test]
+ fn test_words_to_line() {
+ assert_eq!(words_to_line(&["a".to_string()]), "a");
+ assert_eq!(words_to_line(&["a".to_string(), "b".to_string(), "c".to_string()]), "a | b | c");
+ }
+}