From 7f445921fcd6db5b99cc3a6aca50f3ba723e35e2 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Mon, 5 Jun 2023 09:51:34 -0700 Subject: [PATCH 01/12] start v2 rewrite --- .vscode/settings.json | 3 + Cargo.lock | 615 ++++++++++++++++++------------------------ Cargo.toml | 26 +- build.rs | 54 ---- src/config.rs | 295 -------------------- src/main.rs | 292 ++------------------ src/test/contents.rs | 61 +++++ src/test/mod.rs | 133 +-------- src/test/results.rs | 145 ---------- src/ui.rs | 339 ----------------------- 10 files changed, 355 insertions(+), 1608 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 build.rs delete mode 100644 src/config.rs create mode 100644 src/test/contents.rs delete mode 100644 src/test/results.rs delete mode 100644 src/ui.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..aa0c5c3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "ansible.python.interpreterPath": "c:\\Program Files\\Python311\\python.exe" +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 87aff5d..924f081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,23 +3,52 @@ version = 3 [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstream" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", ] [[package]] -name = "atty" -version = "0.2.14" +name = "anstyle" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 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 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", ] [[package]] @@ -34,21 +63,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[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.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -57,33 +83,57 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.34.0" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" dependencies = [ - "ansi_term", - "atty", + "anstream", + "anstyle", "bitflags", + "clap_lex", "strsim", - "textwrap", - "unicode-width", - "vec_map", ] [[package]] -name = "cpufeatures" -version = "0.2.7" +name = "clap_derive" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" dependencies = [ - "libc", + "heck", + "proc-macro2", + "quote", + "syn", ] +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "crossterm" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" dependencies = [ "bitflags", "crossterm_winapi", @@ -105,95 +155,72 @@ dependencies = [ ] [[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "4.0.0" +name = "errno" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ - "dirs-sys", + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", ] [[package]] -name = "dirs-sys" -version = "0.3.7" +name = "errno-dragonfly" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ + "cc", "libc", - "redox_users", - "winapi", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] -name = "getrandom" -version = "0.2.9" +name = "hermit-abi" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] -name = "heck" -version = "0.3.3" +name = "io-lifetimes" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "unicode-segmentation", + "hermit-abi", + "libc", + "windows-sys 0.48.0", ] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "is-terminal" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ - "libc", + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "libc" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] -name = "libc" -version = "0.2.142" +name = "linux-raw-sys" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" @@ -207,25 +234,51 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" + +[[package]] +name = "miette" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a236ff270093b0b67451bc50a509bd1bad302cb1d3c7d37d5efe931238581fa9" dependencies = [ - "cfg-if", + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "once_cell" +version = "1.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" + [[package]] name = "parking_lot" version = "0.12.1" @@ -246,85 +299,38 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "windows-sys 0.45.0", ] [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] [[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" +name = "ratatui" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "ce841e0486e7c2412c3740168ede33adeba8e154a15107b879d8162d77c7174e" dependencies = [ - "getrandom", + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -337,57 +343,17 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - -[[package]] -name = "rust-embed" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b68543d5527e158213414a92832d2aab11a84d2571a5eb021ebe22c43aab066" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4e0f0ced47ded9a68374ac145edd65a6c1fa13a96447b873660b2a568a0fd7" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 1.0.109", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "7.5.0" +name = "rustix" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ - "sha2", - "walkdir", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", ] [[package]] @@ -396,37 +362,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "serde" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.160" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", -] - -[[package]] -name = "sha2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "signal-hook" version = "0.3.15" @@ -465,65 +400,21 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.40" @@ -541,56 +432,25 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", + "syn", ] [[package]] name = "ttyper" version = "1.2.0" dependencies = [ + "clap", "crossterm", - "dirs", - "rand", - "rust-embed", - "serde", - "structopt", - "toml", - "tui", -] - -[[package]] -name = "tui" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags", - "cassowary", - "crossterm", + "miette", + "ratatui", "unicode-segmentation", - "unicode-width", ] -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-segmentation" @@ -605,26 +465,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.4" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "wasi" @@ -648,15 +492,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -669,7 +504,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[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.0", ] [[package]] @@ -678,13 +522,28 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 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", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -693,38 +552,80 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml index 706d585..0f04e12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,24 +10,8 @@ authors = ["Max Niederman "] edition = "2018" [dependencies] -structopt = "0.3" -dirs = "4.0" -crossterm = "0.25" -rust-embed = "6.4" -toml = "0.5" - -[dependencies.tui] -version = "0.19" -default-features = false -features = ["crossterm"] - -[dependencies.rand] -version = "0.8" -features = ["alloc"] - -[dependencies.serde] -version = "1.0" -features = ["derive"] - -[build-dependencies] -dirs = "4.0" +crossterm = "0.26" +clap = { version = "4.3", features = ["derive"] } +miette = "5.9" +ratatui = "0.21" +unicode-segmentation = "1.10" \ No newline at end of file diff --git a/build.rs b/build.rs deleted file mode 100644 index b1bcabb..0000000 --- a/build.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; - -fn copy, V: AsRef>(from: U, to: V) -> std::io::Result<()> { - let mut stack = vec![PathBuf::from(from.as_ref())]; - - let output_root = PathBuf::from(to.as_ref()); - let input_root = PathBuf::from(from.as_ref()).components().count(); - - while let Some(working_path) = stack.pop() { - // Generate a relative path - let src: PathBuf = working_path.components().skip(input_root).collect(); - - // Create a destination if missing - let dest = if src.components().count() == 0 { - output_root.clone() - } else { - output_root.join(&src) - }; - if fs::metadata(&dest).is_err() { - fs::create_dir_all(&dest)?; - } - - for entry in fs::read_dir(working_path)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - stack.push(path); - } else if let Some(filename) = path.file_name() { - let dest_path = dest.join(filename); - fs::copy(&path, &dest_path)?; - } - } - } - - Ok(()) -} - -#[allow(unused_must_use)] -fn main() -> std::io::Result<()> { - let install_path = dirs::config_dir() - .expect("Couldn't find a configuration directory to install to.") - .join("ttyper"); - fs::create_dir_all(&install_path); - - let resources_path = env::current_dir() - .expect("Couldn't find the source directory.") - .join("resources") - .join("runtime"); - copy(&resources_path, &install_path); - - Ok(()) -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 1b2d8a4..0000000 --- a/src/config.rs +++ /dev/null @@ -1,295 +0,0 @@ -use serde::{ - de::{self, IntoDeserializer}, - Deserialize, -}; -use tui::style::{Color, Modifier, Style}; - -#[derive(Debug, Deserialize)] -#[serde(default)] -pub struct Config { - pub default_language: String, - pub theme: Theme, -} - -impl Default for Config { - fn default() -> Self { - Self { - default_language: "english200".into(), - theme: Theme::default(), - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(default)] -pub struct Theme { - #[serde(deserialize_with = "deserialize_style")] - pub default: Style, - #[serde(deserialize_with = "deserialize_style")] - pub title: Style, - - // test widget - #[serde(deserialize_with = "deserialize_style")] - pub input_border: Style, - #[serde(deserialize_with = "deserialize_style")] - pub prompt_border: Style, - - #[serde(deserialize_with = "deserialize_style")] - pub prompt_correct: Style, - #[serde(deserialize_with = "deserialize_style")] - pub prompt_incorrect: Style, - #[serde(deserialize_with = "deserialize_style")] - pub prompt_untyped: Style, - - #[serde(deserialize_with = "deserialize_style")] - pub prompt_current_correct: Style, - #[serde(deserialize_with = "deserialize_style")] - pub prompt_current_incorrect: Style, - #[serde(deserialize_with = "deserialize_style")] - pub prompt_current_untyped: Style, - - #[serde(deserialize_with = "deserialize_style")] - pub prompt_cursor: Style, - - // results widget - #[serde(deserialize_with = "deserialize_style")] - pub results_overview: Style, - #[serde(deserialize_with = "deserialize_style")] - pub results_overview_border: Style, - - #[serde(deserialize_with = "deserialize_style")] - pub results_worst_keys: Style, - #[serde(deserialize_with = "deserialize_style")] - pub results_worst_keys_border: Style, - - #[serde(deserialize_with = "deserialize_style")] - pub results_chart: Style, - #[serde(deserialize_with = "deserialize_style")] - pub results_chart_x: Style, - #[serde(deserialize_with = "deserialize_style")] - pub results_chart_y: Style, - - #[serde(deserialize_with = "deserialize_style")] - pub results_restart_prompt: Style, -} - -impl Default for Theme { - fn default() -> Self { - Self { - default: Style::default(), - - title: Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), - - input_border: Style::default().fg(Color::Cyan), - prompt_border: Style::default().fg(Color::Green), - - prompt_correct: Style::default().fg(Color::Green), - prompt_incorrect: Style::default().fg(Color::Red), - prompt_untyped: Style::default().fg(Color::Gray), - - prompt_current_correct: Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - prompt_current_incorrect: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - prompt_current_untyped: Style::default() - .fg(Color::Blue) - .add_modifier(Modifier::BOLD), - - prompt_cursor: Style::default().add_modifier(Modifier::UNDERLINED), - - results_overview: Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - results_overview_border: Style::default().fg(Color::Cyan), - - results_worst_keys: Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - results_worst_keys_border: Style::default().fg(Color::Cyan), - - results_chart: Style::default().fg(Color::Cyan), - results_chart_x: Style::default().fg(Color::Cyan), - results_chart_y: Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::BOLD), - - results_restart_prompt: Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::ITALIC), - } - } -} - -fn deserialize_style<'de, D>(deserializer: D) -> Result -where - D: de::Deserializer<'de>, -{ - struct StyleVisitor; - impl<'de> de::Visitor<'de> for StyleVisitor { - type Value = Style; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string describing a text style") - } - - fn visit_str(self, value: &str) -> Result { - let (colors, modifiers) = value.split_once(';').unwrap_or((value, "")); - let (fg, bg) = colors.split_once(':').unwrap_or((colors, "none")); - - let mut style = Style { - fg: match fg { - "none" | "" => None, - _ => Some(deserialize_color(fg.into_deserializer())?), - }, - bg: match bg { - "none" | "" => None, - _ => Some(deserialize_color(bg.into_deserializer())?), - }, - ..Default::default() - }; - - for modifier in modifiers.split_terminator(';') { - style = style.add_modifier(match modifier { - "bold" => Modifier::BOLD, - "crossed_out" => Modifier::CROSSED_OUT, - "dim" => Modifier::DIM, - "hidden" => Modifier::HIDDEN, - "italic" => Modifier::ITALIC, - "rapid_blink" => Modifier::RAPID_BLINK, - "slow_blink" => Modifier::SLOW_BLINK, - "reversed" => Modifier::REVERSED, - "underlined" => Modifier::UNDERLINED, - _ => { - return Err(E::invalid_value( - de::Unexpected::Str(modifier), - &"a style modifier", - )) - } - }); - } - - Ok(style) - } - } - - deserializer.deserialize_str(StyleVisitor) -} - -fn deserialize_color<'de, D>(deserializer: D) -> Result -where - D: de::Deserializer<'de>, -{ - struct ColorVisitor; - impl<'de> de::Visitor<'de> for ColorVisitor { - type Value = Color; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("a color name or hexadecimal color code") - } - - fn visit_str(self, value: &str) -> Result { - match value { - "reset" => Ok(Color::Reset), - "black" => Ok(Color::Black), - "white" => Ok(Color::White), - "red" => Ok(Color::Red), - "green" => Ok(Color::Green), - "yellow" => Ok(Color::Yellow), - "blue" => Ok(Color::Blue), - "magenta" => Ok(Color::Magenta), - "cyan" => Ok(Color::Cyan), - "gray" => Ok(Color::Gray), - "darkgray" => Ok(Color::DarkGray), - "lightred" => Ok(Color::LightRed), - "lightgreen" => Ok(Color::LightGreen), - "lightyellow" => Ok(Color::LightYellow), - "lightblue" => Ok(Color::LightBlue), - "lightmagenta" => Ok(Color::LightMagenta), - "lightcyan" => Ok(Color::LightCyan), - _ => { - if value.len() == 6 { - let parse_error = |_| E::custom("color code was not valid hexadecimal"); - - Ok(Color::Rgb( - u8::from_str_radix(&value[0..2], 16).map_err(parse_error)?, - u8::from_str_radix(&value[2..4], 16).map_err(parse_error)?, - u8::from_str_radix(&value[4..6], 16).map_err(parse_error)?, - )) - } else { - Err(E::invalid_value( - de::Unexpected::Str(value), - &"a color name or hexadecimal color code", - )) - } - } - } - } - } - - deserializer.deserialize_str(ColorVisitor) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn deserializes_basic_colors() { - fn color(string: &str) -> Color { - deserialize_color(de::IntoDeserializer::::into_deserializer( - string, - )) - .expect("failed to deserialize color") - } - - assert_eq!(color("black"), Color::Black); - assert_eq!(color("000000"), Color::Rgb(0, 0, 0)); - assert_eq!(color("ffffff"), Color::Rgb(0xff, 0xff, 0xff)); - assert_eq!(color("FFFFFF"), Color::Rgb(0xff, 0xff, 0xff)); - } - - #[test] - fn deserializes_styles() { - fn style(string: &str) -> Style { - deserialize_style(de::IntoDeserializer::::into_deserializer( - string, - )) - .expect("failed to deserialize style") - } - - assert_eq!(style("none"), Style::default()); - assert_eq!(style("none:none"), Style::default()); - assert_eq!(style("none:none;"), Style::default()); - - assert_eq!(style("black"), Style::default().fg(Color::Black)); - assert_eq!( - style("black:white"), - Style::default().fg(Color::Black).bg(Color::White) - ); - - assert_eq!( - style("none;bold"), - Style::default().add_modifier(Modifier::BOLD) - ); - assert_eq!( - style("none;bold;italic;underlined;"), - Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::ITALIC) - .add_modifier(Modifier::UNDERLINED) - ); - - assert_eq!( - style("00ff00:000000;bold;dim;italic;slow_blink"), - Style::default() - .fg(Color::Rgb(0, 0xff, 0)) - .bg(Color::Rgb(0, 0, 0)) - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::DIM) - .add_modifier(Modifier::ITALIC) - .add_modifier(Modifier::SLOW_BLINK) - ); - } -} diff --git a/src/main.rs b/src/main.rs index 6921358..08a2105 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,278 +1,40 @@ -mod config; mod test; -mod ui; -use config::Config; -use test::{results::Results, Test}; +use std::{num::{self, NonZeroUsize}, path::PathBuf}; -use crossterm::{ - self, cursor, - event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, - execute, terminal, -}; -use rand::{seq::SliceRandom, thread_rng}; -use rust_embed::RustEmbed; -use std::{ - ffi::OsString, - fs, - io::{self, BufRead}, - num, - path::PathBuf, - str, -}; -use structopt::StructOpt; -use tui::{backend::CrosstermBackend, terminal::Terminal}; - -#[derive(RustEmbed)] -#[folder = "resources/runtime"] -struct Resources; - -#[derive(Debug, StructOpt)] -#[structopt(name = "ttyper", about = "Terminal-based typing test.")] +#[derive(Debug, clap::Parser)] struct Opt { - #[structopt(parse(from_os_str))] - contents: Option, + #[command(subcommand)] + command: Command, - #[structopt(short, long)] + #[clap(long)] debug: bool, - - /// Specify word count - #[structopt(short, long, default_value = "50")] - words: num::NonZeroUsize, - - /// Use config file - #[structopt(short, long)] - config: Option, - - /// Specify test language in file - #[structopt(long, parse(from_os_str))] - language_file: Option, - - /// Specify test language - #[structopt(short, long)] - language: Option, - - /// List installed languages - #[structopt(long)] - list_languages: bool, -} - -impl Opt { - fn gen_contents(&self) -> Option> { - match &self.contents { - Some(path) => { - let lines: Vec = if path.as_os_str() == "-" { - std::io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect() - } else { - let file = fs::File::open(path).expect("Error reading language file."); - io::BufReader::new(file) - .lines() - .filter_map(Result::ok) - .collect() - }; - - Some(lines.iter().map(String::from).collect()) - } - None => { - let lang_name = self - .language - .clone() - .unwrap_or_else(|| self.config().default_language); - - let bytes: Vec = self - .language_file - .as_ref() - .map(fs::read) - .and_then(Result::ok) - .or_else(|| fs::read(self.language_dir().join(&lang_name)).ok()) - .or_else(|| { - Resources::get(&format!("language/{}", &lang_name)) - .map(|f| f.data.into_owned()) - })?; - - let mut rng = thread_rng(); - - let mut language: Vec<&str> = str::from_utf8(&bytes) - .expect("Language file had non-utf8 encoding.") - .lines() - .collect(); - language.shuffle(&mut rng); - - let mut contents: Vec<_> = language - .into_iter() - .cycle() - .take(self.words.get()) - .map(ToOwned::to_owned) - .collect(); - contents.shuffle(&mut rng); - - Some(contents) - } - } - } - - /// Configuration - fn config(&self) -> Config { - fs::read( - self.config - .clone() - .unwrap_or_else(|| self.config_dir().join("config.toml")), - ) - .map(|bytes| toml::from_slice(&bytes).expect("Configuration was ill-formed.")) - .unwrap_or_default() - } - - /// Installed languages under config directory - fn languages(&self) -> io::Result> { - Ok(self - .language_dir() - .read_dir()? - .filter_map(Result::ok) - .map(|e| e.file_name()) - .collect()) - } - - /// Config directory - fn config_dir(&self) -> PathBuf { - dirs::config_dir() - .expect("Failed to find config directory.") - .join("ttyper") - } - - /// Language directory under config directory - fn language_dir(&self) -> PathBuf { - self.config_dir().join("language") - } } -enum State { - Test(Test), - Results(Results), -} - -impl State { - fn render_into( - &self, - terminal: &mut Terminal, - config: &Config, - ) -> crossterm::Result<()> { - match self { - State::Test(test) => { - terminal.draw(|f| { - f.render_widget(config.theme.apply_to(test), f.size()); - })?; - } - State::Results(results) => { - terminal.draw(|f| { - f.render_widget(config.theme.apply_to(results), f.size()); - })?; - } - } - Ok(()) +#[derive(Debug, clap::Parser)] +enum Command { + /// Reads test contents from a file. + File { + path: PathBuf, + }, + /// Generates random words for test contents. + Words { + /// Number of words to generate. + count: num::NonZeroUsize, + + /// Language to sample words from. + #[clap(short, long)] + language: Option, + + /// Take first N words from the language while sampling. + #[clap(short = 'c', long)] + language_cutoff: Option, } } -fn main() -> crossterm::Result<()> { - let opt = Opt::from_args(); +fn main() { + let opt = ::parse(); if opt.debug { - dbg!(&opt); + dbg!(opt); } - - let config = opt.config(); - if opt.debug { - dbg!(&config); - } - - if opt.list_languages { - opt.languages() - .expect("Couldn't get installed languages under config directory. Make sure the config directory exists.") - .iter() - .for_each(|name| println!("{}", name.to_str().expect("Ill-formatted language name."))); - return Ok(()); - } - - let backend = CrosstermBackend::new(io::stdout()); - let mut terminal = Terminal::new(backend)?; - - terminal::enable_raw_mode()?; - execute!( - io::stdout(), - cursor::Hide, - cursor::SavePosition, - terminal::EnterAlternateScreen, - )?; - terminal.clear()?; - - let mut state = State::Test(Test::new(opt.gen_contents().expect( - "Couldn't get test contents. Make sure the specified language actually exists.", - ))); - - state.render_into(&mut terminal, &config)?; - loop { - let event = event::read()?; - - // handle exit controls - match event { - Event::Key(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - .. - }) => break, - Event::Key(KeyEvent { - code: KeyCode::Esc, - modifiers: KeyModifiers::NONE, - .. - }) => match state { - State::Test(ref test) => { - state = State::Results(Results::from(test)); - } - State::Results(_) => break, - }, - _ => {} - } - - match state { - State::Test(ref mut test) => { - if let Event::Key(key) = event { - test.handle_key(key); - if test.complete { - state = State::Results(Results::from(&*test)); - } - } - } - State::Results(_) => match event { - Event::Key(KeyEvent { - code: KeyCode::Char('r'), - modifiers: KeyModifiers::NONE, - .. - }) => { - state = State::Test(Test::new(opt.gen_contents().expect( - "Couldn't get test contents. Make sure the specified language actually exists.", - ))); - } - Event::Key(KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::NONE, - .. - }) => break, - _ => {} - }, - } - - state.render_into(&mut terminal, &config)?; - } - - terminal::disable_raw_mode()?; - execute!( - io::stdout(), - cursor::RestorePosition, - cursor::Show, - terminal::LeaveAlternateScreen, - )?; - - Ok(()) -} +} \ No newline at end of file diff --git a/src/test/contents.rs b/src/test/contents.rs new file mode 100644 index 0000000..7c6a470 --- /dev/null +++ b/src/test/contents.rs @@ -0,0 +1,61 @@ +use std::str::FromStr; + +/// A trait for types that can be used as test contents. +/// +/// The iterator should yield the smallest chunks of the +/// test that should not be split across line breaks. +pub trait Contents: Iterator + Sized { + /// Returns the next, restarted test, if possible. + fn restart(self) -> Option; +} + +pub struct Lexed> { + inner: C, + language: LexerLanguage, +} + +impl Iterator for Lexed +where + C: Contents +{ + type Item = String; + + fn next(&mut self) -> Option { + todo!() + } +} + +impl Contents for Lexed +where + C: Contents +{ + fn restart(self) -> Option { + Some(Self { + inner: self.inner.restart()?, + language: self.language, + }) + } +} + +pub enum LexerLanguage { + English, + ExtendedGraphemeClusters +} + +impl Default for LexerLanguage { + fn default() -> Self { + Self::ExtendedGraphemeClusters + } +} + +impl FromStr for LexerLanguage { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "english" => Ok(Self::English), + "extended-grapheme-clusters" => Ok(Self::ExtendedGraphemeClusters), + _ => Err("invalid lexer language"), + } + } +} diff --git a/src/test/mod.rs b/src/test/mod.rs index daab25e..431dc24 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,132 +1 @@ -pub mod results; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use std::fmt; -use std::time::Instant; - -pub struct TestEvent { - pub time: Instant, - pub key: KeyEvent, - pub correct: Option, -} - -impl fmt::Debug for TestEvent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TestEvent") - .field("time", &String::from("Instant { ... }")) - .field("key", &self.key) - .finish() - } -} - -#[derive(Debug)] -pub struct TestWord { - pub text: String, - pub progress: String, - pub events: Vec, -} - -impl From for TestWord { - fn from(string: String) -> Self { - TestWord { - text: string, - progress: String::new(), - events: Vec::new(), - } - } -} - -#[derive(Debug)] -pub struct Test { - pub words: Vec, - pub current_word: usize, - pub complete: bool, -} - -impl Test { - pub fn new(words: Vec) -> Self { - Self { - words: words.into_iter().map(TestWord::from).collect(), - current_word: 0, - complete: false, - } - } - - pub fn handle_key(&mut self, key: KeyEvent) { - let word = &mut self.words[self.current_word]; - match key.code { - KeyCode::Char(' ') | KeyCode::Enter => { - if word.text.chars().nth(word.progress.len()) == Some(' ') { - word.progress.push(' '); - word.events.push(TestEvent { - time: Instant::now(), - correct: Some(true), - key, - }) - } else if !word.progress.is_empty() || word.text.is_empty() { - word.events.push(TestEvent { - time: Instant::now(), - correct: Some(word.text == word.progress), - key, - }); - self.next_word(); - } - } - KeyCode::Backspace => { - if word.progress.is_empty() { - self.last_word(); - } else { - word.events.push(TestEvent { - time: Instant::now(), - correct: Some(!word.text.starts_with(&word.progress[..])), - key, - }); - word.progress.pop(); - } - } - // CTRL-BackSpace - KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) => { - if self.words[self.current_word].progress.is_empty() { - self.last_word(); - } - - let word = &mut self.words[self.current_word]; - - word.events.push(TestEvent { - time: Instant::now(), - correct: None, - key, - }); - word.progress.clear(); - } - KeyCode::Char(c) => { - word.progress.push(c); - word.events.push(TestEvent { - time: Instant::now(), - correct: Some(word.text.starts_with(&word.progress[..])), - key, - }); - if word.progress == word.text && self.current_word == self.words.len() - 1 { - self.complete = true; - self.current_word = 0; - } - } - _ => {} - }; - } - - fn last_word(&mut self) { - if self.current_word != 0 { - self.current_word -= 1; - } - } - - fn next_word(&mut self) { - if self.current_word == self.words.len() - 1 { - self.complete = true; - self.current_word = 0; - } else { - self.current_word += 1; - } - } -} +pub mod contents; diff --git a/src/test/results.rs b/src/test/results.rs deleted file mode 100644 index 8d7cebd..0000000 --- a/src/test/results.rs +++ /dev/null @@ -1,145 +0,0 @@ -use super::Test; - -use crossterm::event::KeyEvent; -use std::collections::HashMap; -use std::{cmp, fmt}; - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Fraction { - pub numerator: usize, - pub denominator: usize, -} - -impl Fraction { - pub const fn new(numerator: usize, denominator: usize) -> Self { - Self { - numerator, - denominator, - } - } -} - -impl From for f64 { - fn from(f: Fraction) -> Self { - f.numerator as f64 / f.denominator as f64 - } -} - -impl cmp::Ord for Fraction { - fn cmp(&self, other: &Self) -> cmp::Ordering { - f64::from(*self).partial_cmp(&f64::from(*other)).unwrap() - } -} - -impl PartialOrd for Fraction { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl fmt::Display for Fraction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}/{}", self.numerator, self.denominator) - } -} - -pub trait PartialResults { - fn progress(&self) -> Fraction; -} - -impl PartialResults for Test { - fn progress(&self) -> Fraction { - Fraction { - numerator: self.current_word + 1, - denominator: self.words.len(), - } - } -} - -pub struct TimingData { - // Instead of storing WPM, we store CPS (clicks per second) - pub overall_cps: f64, - pub per_event: Vec, - pub per_key: HashMap, -} - -pub struct AccuracyData { - pub overall: Fraction, - pub per_key: HashMap, -} - -pub struct Results { - pub timing: TimingData, - pub accuracy: AccuracyData, -} - -impl From<&Test> for Results { - fn from(test: &Test) -> Self { - let events: Vec<&super::TestEvent> = - test.words.iter().flat_map(|w| w.events.iter()).collect(); - - Self { - timing: { - let mut timing = TimingData { - overall_cps: -1.0, - per_event: Vec::new(), - per_key: HashMap::new(), - }; - - // map of keys to a two-tuple (total time, clicks) for counting average - let mut keys: HashMap = HashMap::new(); - - for win in events.windows(2) { - let event_dur = win[1] - .time - .checked_duration_since(win[0].time) - .map(|d| d.as_secs_f64()); - - if let Some(event_dur) = event_dur { - timing.per_event.push(event_dur); - - let key = keys.entry(win[1].key).or_insert((0.0, 0)); - key.0 += event_dur; - key.1 += 1; - } - } - - timing.per_key = keys - .into_iter() - .map(|(key, (total, count))| (key, total / count as f64)) - .collect(); - - timing.overall_cps = - timing.per_event.len() as f64 / timing.per_event.iter().sum::(); - - timing - }, - accuracy: { - let mut acc = AccuracyData { - overall: Fraction::new(0, 0), - per_key: HashMap::new(), - }; - - events - .iter() - .filter(|event| event.correct.is_some()) - .for_each(|event| { - let key = acc - .per_key - .entry(event.key) - .or_insert_with(|| Fraction::new(0, 0)); - - acc.overall.denominator += 1; - key.denominator += 1; - - if event.correct.unwrap() { - acc.overall.numerator += 1; - key.numerator += 1; - } - }); - - acc - }, - } - } -} diff --git a/src/ui.rs b/src/ui.rs deleted file mode 100644 index ca5022a..0000000 --- a/src/ui.rs +++ /dev/null @@ -1,339 +0,0 @@ -use crate::config::Theme; - -use super::test::{results, Test}; - -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; -use results::Fraction; -use std::iter; -use tui::{ - buffer::Buffer, - layout::{Constraint, Direction, Layout, Rect}, - symbols::Marker, - text::{Span, Spans, Text}, - widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType, Paragraph, Widget}, -}; - -// Convert CPS to WPM (clicks per second) -const WPM_PER_CPS: f64 = 12.0; - -// Width of the moving average window for the WPM chart -const WPM_SMA_WIDTH: usize = 10; - -#[derive(Clone)] -struct SizedBlock<'a> { - block: Block<'a>, - area: Rect, -} - -impl SizedBlock<'_> { - fn render(self, buf: &mut Buffer) { - self.block.render(self.area, buf) - } -} - -trait UsedWidget: Widget {} -impl UsedWidget for Paragraph<'_> {} - -trait DrawInner { - fn draw_inner(&self, content: T, buf: &mut Buffer); -} - -impl DrawInner<&Spans<'_>> for SizedBlock<'_> { - fn draw_inner(&self, content: &Spans, buf: &mut Buffer) { - let inner = self.block.inner(self.area); - buf.set_spans(inner.x, inner.y, content, inner.width); - } -} - -impl DrawInner for SizedBlock<'_> { - fn draw_inner(&self, content: T, buf: &mut Buffer) { - let inner = self.block.inner(self.area); - content.render(inner, buf); - } -} - -pub trait ThemedWidget { - fn render(self, area: Rect, buf: &mut Buffer, theme: &Theme); -} - -pub struct Themed<'t, W: ?Sized> { - theme: &'t Theme, - widget: W, -} -impl<'t, W: ThemedWidget> Widget for Themed<'t, W> { - fn render(self, area: Rect, buf: &mut Buffer) { - self.widget.render(area, buf, self.theme) - } -} -impl Theme { - pub fn apply_to(&self, widget: W) -> Themed<'_, W> { - Themed { - theme: self, - widget, - } - } -} - -impl ThemedWidget for &Test { - fn render(self, area: Rect, buf: &mut Buffer, theme: &Theme) { - buf.set_style(area, theme.default); - - // Chunks - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Length(6)]) - .split(area); - - // Sections - let input = SizedBlock { - block: Block::default() - .title(Spans::from(vec![Span::styled("Input", theme.title)])) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(theme.input_border), - area: chunks[0], - }; - input.draw_inner( - &Spans::from(self.words[self.current_word].progress.clone()), - buf, - ); - input.render(buf); - - let target_lines: Vec = { - let words = iter::empty::>() - // already typed words - .chain(self.words[..self.current_word].iter().map(|w| { - vec![Span::styled( - w.text.clone() + " ", - if w.progress == w.text { - theme.prompt_correct - } else { - theme.prompt_incorrect - }, - )] - })) - // current word - .chain({ - let progress_ind = self.words[self.current_word] - .progress - .len() - .min(self.words[self.current_word].text.len()); - - let correct = self.words[self.current_word] - .text - .starts_with(&self.words[self.current_word].progress[..]); - - let (typed, untyped) = - self.words[self.current_word] - .text - .split_at(ceil_char_boundary( - &self.words[self.current_word].text, - progress_ind, - )); - - let mut remaining = untyped.chars().chain(iter::once(' ')); - let cursor = remaining.next().unwrap(); - - iter::once(vec![ - Span::styled( - typed, - if correct { - theme.prompt_current_correct - } else { - theme.prompt_current_incorrect - }, - ), - Span::styled( - cursor.to_string(), - theme.prompt_current_untyped.patch(theme.prompt_cursor), - ), - Span::styled(remaining.collect::(), theme.prompt_current_untyped), - ]) - }) - // remaining words - .chain( - self.words[self.current_word + 1..] - .iter() - .map(|w| vec![Span::styled(w.text.clone() + " ", theme.prompt_untyped)]), - ); - - let mut lines: Vec = Vec::new(); - let mut current_line: Vec = Vec::new(); - let mut current_width = 0; - for word in words { - let word_width: usize = word.iter().map(|s| s.width()).sum(); - - if current_width + word_width > chunks[1].width as usize - 2 { - current_line.push(Span::raw("\n")); - lines.push(Spans::from(current_line.clone())); - current_line.clear(); - current_width = 0; - } - - current_line.extend(word); - current_width += word_width; - } - lines.push(Spans::from(current_line)); - - lines - }; - let target = Paragraph::new(target_lines).block( - Block::default() - .title(Span::styled("Prompt", theme.title)) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(theme.prompt_border), - ); - target.render(chunks[1], buf); - } -} - -impl ThemedWidget for &results::Results { - fn render(self, area: Rect, buf: &mut Buffer, theme: &Theme) { - buf.set_style(area, theme.default); - - // Chunks - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(1), Constraint::Length(1)]) - .split(area); - let res_chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) // Graph looks tremendously better with just a little margin - .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]) - .split(chunks[0]); - let info_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) - .split(res_chunks[0]); - - let exit = Span::styled( - "Press 'q' to quit or 'r' for another test.", - theme.results_restart_prompt, - ); - buf.set_span(chunks[1].x, chunks[1].y, &exit, chunks[1].width); - - // Sections - let mut overview_text = Text::styled("", theme.results_overview); - overview_text.extend([ - Spans::from(format!( - "Adjusted WPM: {:.1}", - self.timing.overall_cps * WPM_PER_CPS * f64::from(self.accuracy.overall) - )), - Spans::from(format!( - "Accuracy: {:.1}%", - f64::from(self.accuracy.overall) * 100f64 - )), - Spans::from(format!( - "Raw WPM: {:.1}", - self.timing.overall_cps * WPM_PER_CPS - )), - Spans::from(format!("Correct Keypresses: {}", self.accuracy.overall)), - ]); - let overview = Paragraph::new(overview_text).block( - Block::default() - .title(Span::styled("Overview", theme.title)) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(theme.results_overview_border), - ); - overview.render(info_chunks[0], buf); - - let mut worst_keys: Vec<(&KeyEvent, &Fraction)> = self - .accuracy - .per_key - .iter() - .filter(|(key, _)| matches!(key.code, KeyCode::Char(_))) - .collect(); - worst_keys.sort_unstable_by_key(|x| x.1); - - let mut worst_text = Text::styled("", theme.results_worst_keys); - worst_text.extend( - worst_keys - .iter() - .take(5) - .filter_map(|(key, acc)| { - if let KeyCode::Char(character) = key.code { - Some(format!( - "- {} at {:.1}% accuracy", - character, - f64::from(**acc) * 100.0, - )) - } else { - None - } - }) - .map(Spans::from), - ); - let worst = Paragraph::new(worst_text).block( - Block::default() - .title(Span::styled("Worst Keys", theme.title)) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(theme.results_worst_keys_border), - ); - worst.render(info_chunks[1], buf); - - let wpm_sma: Vec<(f64, f64)> = self - .timing - .per_event - .windows(WPM_SMA_WIDTH) - .enumerate() - .map(|(i, window)| { - ( - (i + WPM_SMA_WIDTH) as f64, - window.len() as f64 / window.iter().copied().sum::() * WPM_PER_CPS, - ) - }) - .collect(); - - let wpm_sma_min = wpm_sma - .iter() - .map(|(_, x)| x) - .fold(f64::INFINITY, |a, &b| a.min(b)); - let wpm_sma_max = wpm_sma - .iter() - .map(|(_, x)| x) - .fold(f64::NEG_INFINITY, |a, &b| a.max(b)); - - let wpm_datasets = vec![Dataset::default() - .name("WPM") - .marker(Marker::Braille) - .graph_type(GraphType::Line) - .style(theme.results_chart) - .data(&wpm_sma)]; - - let wpm_chart = Chart::new(wpm_datasets) - .block(Block::default().title(vec![Span::styled("Chart", theme.title)])) - .x_axis( - Axis::default() - .title(Span::styled("Keypresses", theme.results_chart_x)) - .bounds([0.0, self.timing.per_event.len() as f64]), - ) - .y_axis( - Axis::default() - .title(Span::styled( - "WPM (10-keypress rolling average)", - theme.results_chart_y, - )) - .bounds([wpm_sma_min, wpm_sma_max]) - .labels( - (wpm_sma_min as u16..wpm_sma_max as u16) - .step_by(5) - .map(|n| Span::raw(format!("{}", n))) - .collect(), - ), - ); - wpm_chart.render(res_chunks[1], buf); - } -} - -// FIXME: replace with `str::ceil_char_boundary` when stable -fn ceil_char_boundary(string: &str, index: usize) -> usize { - if string.is_char_boundary(index) { - index - } else { - ceil_char_boundary(string, index + 1) - } -} From 0b09ef06ae1ce3b448cc44fc4ab162c7c5a392a3 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Mon, 5 Jun 2023 10:01:45 -0700 Subject: [PATCH 02/12] refactor(test/contents): simplify content trait --- src/main.rs | 16 +++++++++++++--- src/test/contents.rs | 22 +++++++++++++--------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index 08a2105..f7fb34a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,11 @@ mod test; -use std::{num::{self, NonZeroUsize}, path::PathBuf}; +use std::{ + num::{self, NonZeroUsize}, + path::PathBuf, +}; + +use test::contents::LexerLanguage; #[derive(Debug, clap::Parser)] struct Opt { @@ -15,7 +20,12 @@ struct Opt { enum Command { /// Reads test contents from a file. File { + /// Path to the file. path: PathBuf, + + /// Language with which to lex the file. + #[clap(short, long, default_value = "extended-grapheme-clusters")] + lexer_language: LexerLanguage, }, /// Generates random words for test contents. Words { @@ -29,7 +39,7 @@ enum Command { /// Take first N words from the language while sampling. #[clap(short = 'c', long)] language_cutoff: Option, - } + }, } fn main() { @@ -37,4 +47,4 @@ fn main() { if opt.debug { dbg!(opt); } -} \ No newline at end of file +} diff --git a/src/test/contents.rs b/src/test/contents.rs index 7c6a470..f184a30 100644 --- a/src/test/contents.rs +++ b/src/test/contents.rs @@ -4,19 +4,22 @@ use std::str::FromStr; /// /// The iterator should yield the smallest chunks of the /// test that should not be split across line breaks. -pub trait Contents: Iterator + Sized { - /// Returns the next, restarted test, if possible. +pub trait Contents: Iterator + MaybeRestartable {} + +pub trait MaybeRestartable: Sized { + /// Returns a restarted value, if possible. fn restart(self) -> Option; } -pub struct Lexed> { - inner: C, +#[derive(Debug, Clone, Copy)] +pub struct Lexed { + inner: I, language: LexerLanguage, } -impl Iterator for Lexed +impl Iterator for Lexed where - C: Contents + I: Iterator, { type Item = String; @@ -25,9 +28,9 @@ where } } -impl Contents for Lexed +impl MaybeRestartable for Lexed where - C: Contents + I: MaybeRestartable, { fn restart(self) -> Option { Some(Self { @@ -37,9 +40,10 @@ where } } +#[derive(Debug, Clone, Copy)] pub enum LexerLanguage { English, - ExtendedGraphemeClusters + ExtendedGraphemeClusters, } impl Default for LexerLanguage { From 895f65c1952d61ddc291fff046f1afbd00216ad5 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Wed, 7 Jun 2023 09:53:09 -0700 Subject: [PATCH 03/12] refactor(test/contents): only lex for files --- src/main.rs | 4 ++-- src/test/contents.rs | 43 +++++++++---------------------------------- 2 files changed, 11 insertions(+), 36 deletions(-) diff --git a/src/main.rs b/src/main.rs index f7fb34a..ce8437f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use std::{ path::PathBuf, }; -use test::contents::LexerLanguage; +use test::contents::Lexer; #[derive(Debug, clap::Parser)] struct Opt { @@ -25,7 +25,7 @@ enum Command { /// Language with which to lex the file. #[clap(short, long, default_value = "extended-grapheme-clusters")] - lexer_language: LexerLanguage, + lexer: Lexer, }, /// Generates random words for test contents. Words { diff --git a/src/test/contents.rs b/src/test/contents.rs index f184a30..c32c590 100644 --- a/src/test/contents.rs +++ b/src/test/contents.rs @@ -4,55 +4,30 @@ use std::str::FromStr; /// /// The iterator should yield the smallest chunks of the /// test that should not be split across line breaks. -pub trait Contents: Iterator + MaybeRestartable {} - -pub trait MaybeRestartable: Sized { - /// Returns a restarted value, if possible. +pub trait Contents: Iterator + Sized { + /// Returns the contents of the next, restarted test, if possible. fn restart(self) -> Option; } #[derive(Debug, Clone, Copy)] -pub struct Lexed { - inner: I, - language: LexerLanguage, +pub enum Lexer { + ExtendedGraphemeClusters, + English, } -impl Iterator for Lexed -where - I: Iterator, -{ - type Item = String; - - fn next(&mut self) -> Option { +impl Lexer { + fn consume_lexeme(&self, bytes: impl Iterator) -> String { todo!() } } -impl MaybeRestartable for Lexed -where - I: MaybeRestartable, -{ - fn restart(self) -> Option { - Some(Self { - inner: self.inner.restart()?, - language: self.language, - }) - } -} - -#[derive(Debug, Clone, Copy)] -pub enum LexerLanguage { - English, - ExtendedGraphemeClusters, -} - -impl Default for LexerLanguage { +impl Default for Lexer { fn default() -> Self { Self::ExtendedGraphemeClusters } } -impl FromStr for LexerLanguage { +impl FromStr for Lexer { type Err = &'static str; fn from_str(s: &str) -> Result { From e4dde6c3747e63df308d74a8c2169a8faaa61c57 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Mon, 12 Jun 2023 09:33:34 -0700 Subject: [PATCH 04/12] feat(test/contents): add char and grapheme iterators --- Cargo.lock | 8 +++++++- Cargo.toml | 2 +- src/test/contents.rs | 48 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 924f081..17773c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,12 @@ dependencies = [ "libc", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "heck" version = "0.4.1" @@ -441,9 +447,9 @@ version = "1.2.0" dependencies = [ "clap", "crossterm", + "finl_unicode", "miette", "ratatui", - "unicode-segmentation", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0f04e12..b40dd53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,4 @@ crossterm = "0.26" clap = { version = "4.3", features = ["derive"] } miette = "5.9" ratatui = "0.21" -unicode-segmentation = "1.10" \ No newline at end of file +finl_unicode = { version = "1.2", features = ["grapheme_clusters"] } \ No newline at end of file diff --git a/src/test/contents.rs b/src/test/contents.rs index c32c590..01d50ad 100644 --- a/src/test/contents.rs +++ b/src/test/contents.rs @@ -1,4 +1,5 @@ use std::str::FromStr; +use finl_unicode::grapheme_clusters::Graphemes; /// A trait for types that can be used as test contents. /// @@ -15,12 +16,6 @@ pub enum Lexer { English, } -impl Lexer { - fn consume_lexeme(&self, bytes: impl Iterator) -> String { - todo!() - } -} - impl Default for Lexer { fn default() -> Self { Self::ExtendedGraphemeClusters @@ -38,3 +33,44 @@ impl FromStr for Lexer { } } } + +struct Utf8Chars { + bytes: I, +} + +impl Iterator for Utf8Chars +where + I: Iterator, +{ + type Item = char; + + fn next(&mut self) -> Option { + const CONTINUATION_MASK: u8 = 0b0011_1111; + + let first = self.bytes.next()?; + match first.leading_ones() { + 0 => Some(char::from(first)), + 1 => { + let first = first & 0b0001_1111; + let second = self.bytes.next()? & CONTINUATION_MASK; + char::from_u32((first as u32) << 6 | second as u32) + } + 2 => { + let first = first & 0b0000_1111; + let second = self.bytes.next()? & CONTINUATION_MASK; + let third = self.bytes.next()? & CONTINUATION_MASK; + char::from_u32((first as u32) << 12 | (second as u32) << 6 | third as u32) + } + 3 => { + let first = first & 0b0000_0111; + let second = self.bytes.next()? & CONTINUATION_MASK; + let third = self.bytes.next()? & CONTINUATION_MASK; + let fourth = self.bytes.next()? & CONTINUATION_MASK; + char::from_u32( + (first as u32) << 18 | (second as u32) << 12 | (third as u32) << 6 | fourth as u32, + ) + } + _ => panic!("invalid UTF-8"), + } + } +} \ No newline at end of file From 6ffe1bb161b39d52d55ad62c54c7507fe477c54a Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Mon, 17 Jul 2023 14:48:52 -0700 Subject: [PATCH 05/12] feat(contents): implement basic test content gen --- Cargo.lock | 199 ++++++++++++++++++++++++++++++++++++-- Cargo.toml | 6 +- src/config.rs | 221 +++++++++++++++++++++++++++++++++++++++++++ src/contents.rs | 140 +++++++++++++++++++++++++++ src/main.rs | 67 ++++++------- src/opt.rs | 59 ++++++++++++ src/test/contents.rs | 76 --------------- src/test/mod.rs | 1 - 8 files changed, 643 insertions(+), 126 deletions(-) create mode 100644 src/config.rs create mode 100644 src/contents.rs create mode 100644 src/opt.rs delete mode 100644 src/test/contents.rs delete mode 100644 src/test/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 17773c8..0310ec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.1" @@ -176,10 +203,21 @@ dependencies = [ ] [[package]] -name = "finl_unicode" -version = "1.2.0" +name = "getrandom" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" [[package]] name = "heck" @@ -193,6 +231,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -244,6 +292,12 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "miette" version = "5.9.0" @@ -285,6 +339,12 @@ version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -308,11 +368,17 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" dependencies = [ "unicode-ident", ] @@ -326,6 +392,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ratatui" version = "0.21.0" @@ -348,6 +444,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "rustix" version = "0.37.19" @@ -368,6 +475,35 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "serde" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + [[package]] name = "signal-hook" version = "0.3.15" @@ -412,9 +548,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.18" +version = "2.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" dependencies = [ "proc-macro2", "quote", @@ -441,15 +577,53 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "ttyper" version = "1.2.0" dependencies = [ "clap", "crossterm", - "finl_unicode", + "dirs", "miette", + "rand", "ratatui", + "serde", + "toml", + "unicode-segmentation", ] [[package]] @@ -635,3 +809,12 @@ name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index b40dd53..1b5c642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,8 @@ crossterm = "0.26" clap = { version = "4.3", features = ["derive"] } miette = "5.9" ratatui = "0.21" -finl_unicode = { version = "1.2", features = ["grapheme_clusters"] } \ No newline at end of file +unicode-segmentation = "1.10" +rand = "0.8.5" +serde = { version = "1.0.171", features = ["derive"] } +dirs = "5.0.1" +toml = "0.7.6" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d476db7 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,221 @@ +use std::path::PathBuf; + +use ratatui::style::{Color, Modifier, Style}; +use serde::{ + de::{self, IntoDeserializer}, + Deserialize, +}; + +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct Config { + pub default_language: PathBuf, + pub default_lexer: String, + + pub theme: Theme, +} + +impl Default for Config { + fn default() -> Self { + Self { + default_language: "english200".into(), + default_lexer: "extended-grapheme-clusters".into(), // TODO: this should be unicode words probably + + theme: Theme::default(), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct Theme {} + +impl Default for Theme { + fn default() -> Self { + Self {} + } +} + +fn deserialize_style<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + struct StyleVisitor; + impl<'de> de::Visitor<'de> for StyleVisitor { + type Value = Style; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string describing a text style") + } + + fn visit_str(self, value: &str) -> Result { + let (colors, modifiers) = value.split_once(';').unwrap_or((value, "")); + let (fg, bg) = colors.split_once(':').unwrap_or((colors, "none")); + + let mut style = Style { + fg: match fg { + "none" | "" => None, + _ => Some(deserialize_color(fg.into_deserializer())?), + }, + bg: match bg { + "none" | "" => None, + _ => Some(deserialize_color(bg.into_deserializer())?), + }, + ..Default::default() + }; + + for modifier in modifiers.split_terminator(';') { + style = style.add_modifier(match modifier { + "bold" => Modifier::BOLD, + "crossed_out" => Modifier::CROSSED_OUT, + "dim" => Modifier::DIM, + "hidden" => Modifier::HIDDEN, + "italic" => Modifier::ITALIC, + "rapid_blink" => Modifier::RAPID_BLINK, + "slow_blink" => Modifier::SLOW_BLINK, + "reversed" => Modifier::REVERSED, + "underlined" => Modifier::UNDERLINED, + _ => { + return Err(E::invalid_value( + de::Unexpected::Str(modifier), + &"a style modifier", + )) + } + }); + } + + Ok(style) + } + } + + deserializer.deserialize_str(StyleVisitor) +} + +pub fn default_config_file_path() -> std::path::PathBuf { + dirs::config_dir().unwrap().join("config.toml") +} + +pub fn load(opt: &crate::Opt) -> Config { + if let Ok(config_raw) = std::fs::read_to_string(&opt.config_file) { + toml::from_str(&config_raw).unwrap() + } else { + Config::default() + } +} + +fn deserialize_color<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + struct ColorVisitor; + impl<'de> de::Visitor<'de> for ColorVisitor { + type Value = Color; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a color name or hexadecimal color code") + } + + fn visit_str(self, value: &str) -> Result { + match value { + "reset" => Ok(Color::Reset), + "black" => Ok(Color::Black), + "white" => Ok(Color::White), + "red" => Ok(Color::Red), + "green" => Ok(Color::Green), + "yellow" => Ok(Color::Yellow), + "blue" => Ok(Color::Blue), + "magenta" => Ok(Color::Magenta), + "cyan" => Ok(Color::Cyan), + "gray" => Ok(Color::Gray), + "darkgray" => Ok(Color::DarkGray), + "lightred" => Ok(Color::LightRed), + "lightgreen" => Ok(Color::LightGreen), + "lightyellow" => Ok(Color::LightYellow), + "lightblue" => Ok(Color::LightBlue), + "lightmagenta" => Ok(Color::LightMagenta), + "lightcyan" => Ok(Color::LightCyan), + _ => { + if value.len() == 6 { + let parse_error = |_| E::custom("color code was not valid hexadecimal"); + + Ok(Color::Rgb( + u8::from_str_radix(&value[0..2], 16).map_err(parse_error)?, + u8::from_str_radix(&value[2..4], 16).map_err(parse_error)?, + u8::from_str_radix(&value[4..6], 16).map_err(parse_error)?, + )) + } else { + Err(E::invalid_value( + de::Unexpected::Str(value), + &"a color name or hexadecimal color code", + )) + } + } + } + } + } + + deserializer.deserialize_str(ColorVisitor) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserializes_basic_colors() { + fn color(string: &str) -> Color { + deserialize_color(de::IntoDeserializer::::into_deserializer( + string, + )) + .expect("failed to deserialize color") + } + + assert_eq!(color("black"), Color::Black); + assert_eq!(color("000000"), Color::Rgb(0, 0, 0)); + assert_eq!(color("ffffff"), Color::Rgb(0xff, 0xff, 0xff)); + assert_eq!(color("FFFFFF"), Color::Rgb(0xff, 0xff, 0xff)); + } + + #[test] + fn deserializes_styles() { + fn style(string: &str) -> Style { + deserialize_style(de::IntoDeserializer::::into_deserializer( + string, + )) + .expect("failed to deserialize style") + } + + assert_eq!(style("none"), Style::default()); + assert_eq!(style("none:none"), Style::default()); + assert_eq!(style("none:none;"), Style::default()); + + assert_eq!(style("black"), Style::default().fg(Color::Black)); + assert_eq!( + style("black:white"), + Style::default().fg(Color::Black).bg(Color::White) + ); + + assert_eq!( + style("none;bold"), + Style::default().add_modifier(Modifier::BOLD) + ); + assert_eq!( + style("none;bold;italic;underlined;"), + Style::default() + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::ITALIC) + .add_modifier(Modifier::UNDERLINED) + ); + + assert_eq!( + style("00ff00:000000;bold;dim;italic;slow_blink"), + Style::default() + .fg(Color::Rgb(0, 0xff, 0)) + .bg(Color::Rgb(0, 0, 0)) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::DIM) + .add_modifier(Modifier::ITALIC) + .add_modifier(Modifier::SLOW_BLINK) + ); + } +} diff --git a/src/contents.rs b/src/contents.rs new file mode 100644 index 0000000..e59c121 --- /dev/null +++ b/src/contents.rs @@ -0,0 +1,140 @@ +use std::{ + fs::File, + io::{BufRead, BufReader}, + num::NonZeroUsize, +}; + +use rand::{ + distributions::{DistIter, Uniform}, + prelude::*, +}; +use unicode_segmentation::GraphemeCursor; + +use crate::opt::{Command, FileLexer}; + +/// A trait for types that can be used as test contents. +/// +/// The iterator should yield "atoms," the smallest chunks +/// of the test that should not be split across line breaks. +pub trait Contents: Iterator { + fn restart(&mut self); +} + +pub fn generate(env: &crate::Env) -> Box { + match &env.opt.command { + Command::File { path, lexer } => { + let raw = std::fs::read_to_string(path).unwrap(); + match lexer { + FileLexer::ExtendedGraphemeClusters => Box::new(ExtendedGraphemeClusters::new(raw)), + } + } + Command::Words { + count, + language: language_name, + language_cutoff, + } => { + let language_name = language_name + .clone() + .unwrap_or(env.config.default_language.clone()); + + let language: Vec<_> = if language_name.is_file() { + BufReader::new(File::open(language_name).unwrap()) + .lines() + .take(language_cutoff.unwrap_or(NonZeroUsize::MAX).get()) + .map(Result::unwrap) + .collect() + } else { + todo!("builtin languages") + }; + + let contents = Uniform::from(0..language.len()) + .map(move |i| language[i].clone()) + .sample_iter(thread_rng()); + + let contents: Box = if let Some(count) = count { + Box::new(Take::new(contents, count.get())) + } else { + Box::new(contents) + }; + + contents + } + } +} + +struct Take { + contents: C, + count: usize, + remaining: usize, +} + +impl Take { + pub fn new(inner: C, count: usize) -> Self { + Self { + count, + remaining: count, + contents: inner, + } + } +} + +impl Iterator for Take { + type Item = C::Item; + + fn next(&mut self) -> Option { + if self.remaining == 0 { + None + } else { + self.remaining -= 1; + self.contents.next() + } + } +} + +impl Contents for Take { + fn restart(&mut self) { + self.remaining = self.count; + self.contents.restart(); + } +} + +impl Contents for DistIter +where + D: Distribution, + R: Rng, +{ + fn restart(&mut self) {} +} + +struct ExtendedGraphemeClusters = String> { + string: S, + cursor: unicode_segmentation::GraphemeCursor, +} + +impl> ExtendedGraphemeClusters { + pub fn new(string: S) -> Self { + Self { + cursor: GraphemeCursor::new(0, string.as_ref().len(), true), + string, + } + } +} + +impl> Iterator for ExtendedGraphemeClusters { + type Item = String; + + fn next(&mut self) -> Option { + let start = self.cursor.cur_cursor(); + let end = self + .cursor + .next_boundary(self.string.as_ref(), 0) + .unwrap()?; + Some(self.string.as_ref()[start..end].to_owned()) + } +} + +impl> Contents for ExtendedGraphemeClusters { + fn restart(&mut self) { + self.cursor.set_cursor(0); + } +} diff --git a/src/main.rs b/src/main.rs index ce8437f..288a811 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,50 +1,37 @@ -mod test; +pub mod config; +pub mod contents; +pub mod opt; -use std::{ - num::{self, NonZeroUsize}, - path::PathBuf, -}; +use config::Config; +use opt::Opt; -use test::contents::Lexer; - -#[derive(Debug, clap::Parser)] -struct Opt { - #[command(subcommand)] - command: Command, - - #[clap(long)] - debug: bool, +#[derive(Debug)] +pub struct Env { + pub config: Config, + pub opt: Opt, } -#[derive(Debug, clap::Parser)] -enum Command { - /// Reads test contents from a file. - File { - /// Path to the file. - path: PathBuf, +fn main() { + let opt = ::parse(); + if opt.debug { + dbg!(&opt); + } - /// Language with which to lex the file. - #[clap(short, long, default_value = "extended-grapheme-clusters")] - lexer: Lexer, - }, - /// Generates random words for test contents. - Words { - /// Number of words to generate. - count: num::NonZeroUsize, + let config = config::load(&opt); + if opt.debug { + dbg!(&config); + } - /// Language to sample words from. - #[clap(short, long)] - language: Option, + let env = Env { config, opt }; - /// Take first N words from the language while sampling. - #[clap(short = 'c', long)] - language_cutoff: Option, - }, -} + let mut contents = contents::generate(&env); -fn main() { - let opt = ::parse(); - if opt.debug { - dbg!(opt); + for line in &mut *contents { + println!("{}", line); + } + println!(":: restart ::"); + contents.restart(); + for line in &mut *contents { + println!("{}", line); } } diff --git a/src/opt.rs b/src/opt.rs new file mode 100644 index 0000000..bc8a1d4 --- /dev/null +++ b/src/opt.rs @@ -0,0 +1,59 @@ +use std::{ + num::{self, NonZeroUsize}, + path::PathBuf, + str::FromStr, +}; + +#[derive(Debug, clap::Parser)] +pub struct Opt { + #[command(subcommand)] + pub command: Command, + + #[clap(long, default_value_os_t = crate::config::default_config_file_path())] + pub config_file: PathBuf, + + #[clap(long)] + pub debug: bool, +} + +#[derive(Debug, clap::Parser)] +pub enum Command { + /// Reads test contents from a file. + File { + /// Path to the file. + path: PathBuf, + + /// Lexer with which to read the file. + #[clap(short, long, default_value = "extended-grapheme-clusters")] + lexer: FileLexer, + }, + /// Generates random words for test contents. + Words { + /// Number of words to generate. + count: Option, + + /// Language to sample words from. + #[clap(short, long)] + language: Option, + + /// Take first N words from the language while sampling. + #[clap(short = 'c', long)] + language_cutoff: Option, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileLexer { + ExtendedGraphemeClusters, +} + +impl FromStr for FileLexer { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "extended-grapheme-clusters" => Ok(Self::ExtendedGraphemeClusters), + _ => Err(format!("unknown lexer: {}", s)), + } + } +} diff --git a/src/test/contents.rs b/src/test/contents.rs deleted file mode 100644 index 01d50ad..0000000 --- a/src/test/contents.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::str::FromStr; -use finl_unicode::grapheme_clusters::Graphemes; - -/// A trait for types that can be used as test contents. -/// -/// The iterator should yield the smallest chunks of the -/// test that should not be split across line breaks. -pub trait Contents: Iterator + Sized { - /// Returns the contents of the next, restarted test, if possible. - fn restart(self) -> Option; -} - -#[derive(Debug, Clone, Copy)] -pub enum Lexer { - ExtendedGraphemeClusters, - English, -} - -impl Default for Lexer { - fn default() -> Self { - Self::ExtendedGraphemeClusters - } -} - -impl FromStr for Lexer { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "english" => Ok(Self::English), - "extended-grapheme-clusters" => Ok(Self::ExtendedGraphemeClusters), - _ => Err("invalid lexer language"), - } - } -} - -struct Utf8Chars { - bytes: I, -} - -impl Iterator for Utf8Chars -where - I: Iterator, -{ - type Item = char; - - fn next(&mut self) -> Option { - const CONTINUATION_MASK: u8 = 0b0011_1111; - - let first = self.bytes.next()?; - match first.leading_ones() { - 0 => Some(char::from(first)), - 1 => { - let first = first & 0b0001_1111; - let second = self.bytes.next()? & CONTINUATION_MASK; - char::from_u32((first as u32) << 6 | second as u32) - } - 2 => { - let first = first & 0b0000_1111; - let second = self.bytes.next()? & CONTINUATION_MASK; - let third = self.bytes.next()? & CONTINUATION_MASK; - char::from_u32((first as u32) << 12 | (second as u32) << 6 | third as u32) - } - 3 => { - let first = first & 0b0000_0111; - let second = self.bytes.next()? & CONTINUATION_MASK; - let third = self.bytes.next()? & CONTINUATION_MASK; - let fourth = self.bytes.next()? & CONTINUATION_MASK; - char::from_u32( - (first as u32) << 18 | (second as u32) << 12 | (third as u32) << 6 | fourth as u32, - ) - } - _ => panic!("invalid UTF-8"), - } - } -} \ No newline at end of file diff --git a/src/test/mod.rs b/src/test/mod.rs deleted file mode 100644 index 431dc24..0000000 --- a/src/test/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod contents; From 9dc2abf5245e23eae0cabbea1acbb97c94616ce0 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Tue, 26 Sep 2023 19:36:11 -0700 Subject: [PATCH 06/12] begin history implementation --- Cargo.lock | 24 ---- Cargo.toml | 1 - src/contents.rs | 2 +- src/main.rs | 7 +- src/trial/history.rs | 323 +++++++++++++++++++++++++++++++++++++++++++ src/trial/mod.rs | 15 ++ 6 files changed, 343 insertions(+), 29 deletions(-) create mode 100644 src/trial/history.rs create mode 100644 src/trial/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 0310ec1..88900bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,29 +298,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" -[[package]] -name = "miette" -version = "5.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a236ff270093b0b67451bc50a509bd1bad302cb1d3c7d37d5efe931238581fa9" -dependencies = [ - "miette-derive", - "once_cell", - "thiserror", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "5.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "mio" version = "0.8.8" @@ -618,7 +595,6 @@ dependencies = [ "clap", "crossterm", "dirs", - "miette", "rand", "ratatui", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1b5c642..d42874c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ edition = "2018" [dependencies] crossterm = "0.26" clap = { version = "4.3", features = ["derive"] } -miette = "5.9" ratatui = "0.21" unicode-segmentation = "1.10" rand = "0.8.5" diff --git a/src/contents.rs b/src/contents.rs index e59c121..f9ccfe2 100644 --- a/src/contents.rs +++ b/src/contents.rs @@ -12,7 +12,7 @@ use unicode_segmentation::GraphemeCursor; use crate::opt::{Command, FileLexer}; -/// A trait for types that can be used as test contents. +/// A trait for types that can be used as trial contents. /// /// The iterator should yield "atoms," the smallest chunks /// of the test that should not be split across line breaks. diff --git a/src/main.rs b/src/main.rs index 288a811..490aafa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -pub mod config; -pub mod contents; -pub mod opt; +mod config; +mod contents; +mod opt; +mod trial; use config::Config; use opt::Opt; diff --git a/src/trial/history.rs b/src/trial/history.rs new file mode 100644 index 0000000..041fc9a --- /dev/null +++ b/src/trial/history.rs @@ -0,0 +1,323 @@ +use std::{iter, ops::Range}; + +use unicode_segmentation::UnicodeSegmentation; + +/// Stores the history of typed grapheme clusters, +/// and evaluates mistakes using a modified version +/// of the Needleman-Wunsch algorithm for sequence alignment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct History { + /// The maximum misalignment allowed between the typed and reference strings. + /// + /// By only allowing some misalignment, we get O(p min{n, m}) space complexity + /// and O(p) time complexity for each push, where p is the max misalignment. + max_misalignment: usize, + + /// The m typed grapheme clusters. + typed: String, + + /// The starting indices of the typed grapheme clusters. + typed_indices: Vec, + + /// The n reference grapheme clusters. + reference: String, + + /// The starting indices of the reference grapheme clusters. + reference_indices: Vec, + + /// Tracked entries in the NW matrix. + /// + /// This is a flattened array of m rows, with each containing + /// the 2p + 1 tracked entries in the corresponding row of the NW matrix. + /// Note that the first p rows have entries outside the matrix, + /// which are set to [`NWEntry::Invalid`]. + nw_entries: Vec, +} + +/// An "action" in the Needleman-Wunsch algorithm. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum NWAction { + /// The typed and reference grapheme clusters match. + Match, + + /// The typed and reference grapheme clusters do not match. + Mismatch, + + /// The typed grapheme cluster was inserted. + /// I.e., the reference grapheme cluster was deleted. + Insertion, + + /// The typed grapheme cluster was deleted. + /// I.e., the reference grapheme cluster was inserted. + Deletion, + + /// The entry is invalid, because it is outside the matrix. + Invalid, +} + +impl History { + /// Creates a new history, with empty typed and reference strings. + pub fn new(max_misalignment: usize) -> Self { + let mut nw_entries = vec![u32::MAX; 2 * max_misalignment + 1]; + + // the 1,1 entry is always 0 + nw_entries[max_misalignment] = 0; + + Self { + max_misalignment, + typed: String::new(), + typed_indices: Vec::new(), + reference: String::new(), + reference_indices: Vec::new(), + nw_entries, + } + } + + /// Pushes a reference grapheme cluster to the history. + pub fn push_reference(&mut self, reference: &str) { + debug_assert_eq!(reference.graphemes(true).count(), 1); + + // Push the index of the cluster. + self.reference_indices.push(self.reference.len()); + + // Push to the string. + self.reference.push_str(reference); + + // The index of the new column. + let col = self.reference_indices.len(); + + // The index of the row being updated. + let mut row = col.saturating_sub(self.max_misalignment); + + // None of the entries in the row are tracked. + if row > self.typed_indices.len() { + return; + } + + // The index in the tracked entry array of the current entry. + let mut idx = { + let row_start = row * self.tracked_entries_per_row(); + let offset = col + self.max_misalignment - row; + row_start + offset + }; + // The value of the top neighbor. + // I.e., the last entry entered. + let mut top = u32::MAX; + // The value of the top-left neighbor. + // I.e., the last entry's left neighbor. + let mut top_left = idx + .checked_sub(self.tracked_entries_per_row()) + .map(|i| self.nw_entries[i]) + .unwrap_or(u32::MAX); + + loop { + let replacement_cost = + top_left.saturating_add(self.replacement_cost(row, col).unwrap_or(u32::MAX)); + let deletion_cost = top.saturating_add(Self::DELETION_COST); + + // This is the last tracked entry. + if col + self.max_misalignment == row { + self.nw_entries[idx] = *[replacement_cost, deletion_cost].iter().min().unwrap(); + break; + } else { + let left = self.nw_entries[idx - 1]; + let insertion_cost = left.saturating_add(Self::INSERTION_COST); + + let val = *[deletion_cost, replacement_cost, insertion_cost] + .iter() + .min() + .unwrap(); + self.nw_entries[idx] = val; + + row += 1; + idx += self.tracked_entries_per_row() - 1; + + if idx >= self.nw_entries.len() { + break; + } + + top = val; + top_left = left; + } + } + } + + /// Push a typed grapheme cluster to the history. + fn push_typed(&mut self, typed: &str) { + debug_assert_eq!(typed.graphemes(true).count(), 1); + + // Push the index of the cluster. + self.typed_indices.push(self.typed.len()); + + // Push to the string. + self.typed.push_str(typed); + + // The index of the new row. + let row = self.typed_indices.len(); + + // None of the row's entries are tracked. + if row > self.reference_indices.len() + self.max_misalignment { + return; + } + + // The current column. + // Very high values are outside the reference string. + let mut col = row.wrapping_sub(self.max_misalignment); + + // A peekable iterator over indices of the previous row. + // Used to get the top and top-left neighbors. + let mut prev_row_indices = self.row_indices(row - 1).peekable(); + // The value of the left neighbor. + let mut left = u32::MAX; + + loop { + let top_left = match prev_row_indices.next() { + Some(i) => self.nw_entries[i], + None => break, + }; + + let top = prev_row_indices + .peek() + .map(|&i| self.nw_entries[i]) + .unwrap_or(u32::MAX); + + let replacement_cost = + top_left.saturating_add(self.replacement_cost(row, col).unwrap_or(u32::MAX)); + let insertion_cost = top.saturating_add(Self::DELETION_COST); + let deletion_cost = left.saturating_add(Self::INSERTION_COST); + + let val = *[replacement_cost, insertion_cost, deletion_cost] + .iter() + .min() + .unwrap(); + + self.nw_entries.push(val); + + col = col.wrapping_add(1); + + left = val; + } + } + + // A match is free. + const MATCH_COST: u32 = 0; + // A mismatch requires two keystrokes, + // one to delete the typed grapheme cluster + // and one to insert the reference grapheme cluster. + const MISMATCH_COST: u32 = 2; + // An insertion or deletion requires one keystroke. + const INSERTION_COST: u32 = 1; + const DELETION_COST: u32 = 1; + + fn tracked_entries_per_row(&self) -> usize { + 2 * self.max_misalignment + 1 + } + + fn row_indices(&self, row: usize) -> Range { + let row_start = row * self.tracked_entries_per_row(); + row_start..row_start + self.tracked_entries_per_row() + } + + fn col_indices(&self, col: usize) -> impl Iterator { + let tracked_entries_per_row = self.tracked_entries_per_row(); + let nw_entries_len = self.nw_entries.len(); + + let first = + (col.saturating_sub(self.max_misalignment) + 1) * self.tracked_entries_per_row() - 1; + + iter::successors(Some(first), move |&i| { + i.checked_add(tracked_entries_per_row - 1) + }) + .take_while(move |&i| i < nw_entries_len) + .take(tracked_entries_per_row) + } + + fn replacement_cost(&self, row: usize, col: usize) -> Option { + let typed_bidx = *self.typed_indices.get(row.checked_sub(1)?)?; + let reference_bidx = *self.reference_indices.get(col.checked_sub(1)?)?; + + let typed = first_grapheme_cluster(&self.typed[typed_bidx..]); + let reference = first_grapheme_cluster(&self.reference[reference_bidx..]); + + if typed == reference { + Some(Self::MATCH_COST) + } else { + Some(Self::MISMATCH_COST) + } + } +} + +fn first_grapheme_cluster(string: &str) -> &str { + string.graphemes(true).next().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new() { + let history = History::new(1); + + assert_eq!(history.max_misalignment, 1); + assert_eq!(history.typed, ""); + assert_eq!(history.typed_indices, vec![]); + assert_eq!(history.reference, ""); + assert_eq!(history.reference_indices, vec![]); + assert_eq!(history.nw_entries[1], 0); + } + + #[test] + fn push_saturday_sunday() { + let mut history = History::new(2); + + let nw_mat = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8], + [1, 0, 1, 2, 3, 4, 5, 6, 7], + [2, 1, 2, 3, 2, 3, 4, 5, 6], + [3, 2, 3, 4, 3, 4, 5, 6, 7], + [4, 3, 4, 5, 4, 5, 4, 5, 6], + [5, 4, 3, 4, 5, 6, 5, 4, 5], + [6, 5, 4, 5, 6, 7, 6, 5, 4], + ]; + + history.push_reference("s"); + check_tracked_rows(&history, &nw_mat); + + history.push_reference("a"); + check_tracked_rows(&history, &nw_mat); + + history.push_reference("t"); + check_tracked_rows(&history, &nw_mat); + + history.push_typed("s"); + check_tracked_rows(&history, &nw_mat); + + history.push_typed("u"); + check_tracked_rows(&history, &nw_mat); + } + + fn check_tracked_rows( + history: &History, + reference: &[[u32; N]; M], + ) { + let rows = history.typed_indices.len() + 1; + let cols = history.reference_indices.len() + 1; + + let mut tracked = vec![u32::MAX; rows * history.tracked_entries_per_row()]; + + for row in 0..rows { + for col in 0..cols { + let offset = match (col + history.max_misalignment).checked_sub(row) { + Some(offset) if offset < history.tracked_entries_per_row() => offset, + _ => continue, + }; + let tracked_idx = row * history.tracked_entries_per_row() + offset; + + tracked[tracked_idx] = reference[row][col]; + } + } + + assert_eq!(history.nw_entries, tracked); + } +} diff --git a/src/trial/mod.rs b/src/trial/mod.rs new file mode 100644 index 0000000..a91be96 --- /dev/null +++ b/src/trial/mod.rs @@ -0,0 +1,15 @@ +mod history; + +use history::History; + +/// The state of a trial. +pub struct Trial { + /// The history of typed grapheme clusters and reference grapheme clusters. + history: History, + + /// The current grapheme cluster being typed. + /// Once the cluster is finished, it is pushed to `evaluation`. + working_grapheme_cluster: String, +} + + From 7f6b2df719350f435ffdfef6c0346cf15c527526 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Tue, 26 Sep 2023 21:29:58 -0700 Subject: [PATCH 07/12] fix history implementation --- Cargo.lock | 1 + Cargo.toml | 3 + src/trial/history.rs | 190 ++++++++++++++++++++++++------------------- 3 files changed, 111 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88900bb..f84479f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -596,6 +596,7 @@ dependencies = [ "crossterm", "dirs", "rand", + "rand_chacha", "ratatui", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index d42874c..fddbe6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,6 @@ rand = "0.8.5" serde = { version = "1.0.171", features = ["derive"] } dirs = "5.0.1" toml = "0.7.6" + +[dev-dependencies] +rand_chacha = "0.3.1" diff --git a/src/trial/history.rs b/src/trial/history.rs index 041fc9a..6381ea9 100644 --- a/src/trial/history.rs +++ b/src/trial/history.rs @@ -1,4 +1,4 @@ -use std::{iter, ops::Range}; +use std::{collections::btree_map::Entry, convert::TryFrom, iter, ops::Range}; use unicode_segmentation::UnicodeSegmentation; @@ -86,59 +86,38 @@ impl History { // The index of the new column. let col = self.reference_indices.len(); - // The index of the row being updated. - let mut row = col.saturating_sub(self.max_misalignment); + // A peekable iterator over indices of the previous column. + // Used to get the left and top-left neighbors. + let mut prev_col_indices = self.col_indices(col - 1).peekable(); - // None of the entries in the row are tracked. - if row > self.typed_indices.len() { - return; - } - - // The index in the tracked entry array of the current entry. - let mut idx = { - let row_start = row * self.tracked_entries_per_row(); - let offset = col + self.max_misalignment - row; - row_start + offset - }; // The value of the top neighbor. // I.e., the last entry entered. let mut top = u32::MAX; - // The value of the top-left neighbor. - // I.e., the last entry's left neighbor. - let mut top_left = idx - .checked_sub(self.tracked_entries_per_row()) - .map(|i| self.nw_entries[i]) - .unwrap_or(u32::MAX); - - loop { - let replacement_cost = - top_left.saturating_add(self.replacement_cost(row, col).unwrap_or(u32::MAX)); - let deletion_cost = top.saturating_add(Self::DELETION_COST); - - // This is the last tracked entry. - if col + self.max_misalignment == row { - self.nw_entries[idx] = *[replacement_cost, deletion_cost].iter().min().unwrap(); - break; + + for EntryIndices { row, buf: idx, .. } in self.col_indices(col) { + let top_left = if row <= 0 { + u32::MAX } else { - let left = self.nw_entries[idx - 1]; - let insertion_cost = left.saturating_add(Self::INSERTION_COST); + self.nw_entries[prev_col_indices.next().unwrap().buf] + }; + let left = prev_col_indices + .peek() + .map(|&i| self.nw_entries[i.buf]) + .unwrap_or(u32::MAX); - let val = *[deletion_cost, replacement_cost, insertion_cost] - .iter() - .min() - .unwrap(); - self.nw_entries[idx] = val; + let replacement_cost = top_left + .saturating_add(self.replacement_cost(row, col as isize).unwrap_or(u32::MAX)); + let insertion_cost = top.saturating_add(Self::INSERTION_COST); + let deletion_cost = left.saturating_add(Self::DELETION_COST); - row += 1; - idx += self.tracked_entries_per_row() - 1; + let val = *[replacement_cost, insertion_cost, deletion_cost] + .iter() + .min() + .unwrap(); - if idx >= self.nw_entries.len() { - break; - } + self.nw_entries[idx] = val; - top = val; - top_left = left; - } + top = val; } } @@ -160,29 +139,33 @@ impl History { return; } - // The current column. - // Very high values are outside the reference string. - let mut col = row.wrapping_sub(self.max_misalignment); - // A peekable iterator over indices of the previous row. // Used to get the top and top-left neighbors. let mut prev_row_indices = self.row_indices(row - 1).peekable(); // The value of the left neighbor. let mut left = u32::MAX; - loop { + self.nw_entries.resize( + self.nw_entries.len() + self.tracked_entries_per_row(), + u32::MAX, + ); + + for EntryIndices { col, buf: idx, .. } in self.row_indices(row).filter({ + let cols = self.reference_indices.len() + 1; + move |&i| i.col < cols as isize + }) { let top_left = match prev_row_indices.next() { - Some(i) => self.nw_entries[i], + Some(i) => self.nw_entries[i.buf], None => break, }; let top = prev_row_indices .peek() - .map(|&i| self.nw_entries[i]) + .map(|&i| self.nw_entries[i.buf]) .unwrap_or(u32::MAX); - let replacement_cost = - top_left.saturating_add(self.replacement_cost(row, col).unwrap_or(u32::MAX)); + let replacement_cost = top_left + .saturating_add(self.replacement_cost(row as isize, col).unwrap_or(u32::MAX)); let insertion_cost = top.saturating_add(Self::DELETION_COST); let deletion_cost = left.saturating_add(Self::INSERTION_COST); @@ -191,9 +174,7 @@ impl History { .min() .unwrap(); - self.nw_entries.push(val); - - col = col.wrapping_add(1); + self.nw_entries[idx] = val; left = val; } @@ -213,28 +194,52 @@ impl History { 2 * self.max_misalignment + 1 } - fn row_indices(&self, row: usize) -> Range { - let row_start = row * self.tracked_entries_per_row(); - row_start..row_start + self.tracked_entries_per_row() + fn row_indices(&self, row: usize) -> impl Iterator { + let first_col = row as isize - self.max_misalignment as isize; + + let first_buf = row * self.tracked_entries_per_row(); + let buf_range = first_buf..first_buf + self.tracked_entries_per_row(); + + buf_range + .zip(first_col..) + .map(move |(buf, col)| EntryIndices { + row: row as isize, + col, + buf, + }) } - fn col_indices(&self, col: usize) -> impl Iterator { + fn col_indices(&self, col: usize) -> impl Iterator { let tracked_entries_per_row = self.tracked_entries_per_row(); let nw_entries_len = self.nw_entries.len(); - let first = - (col.saturating_sub(self.max_misalignment) + 1) * self.tracked_entries_per_row() - 1; + let first_row = col.saturating_sub(self.max_misalignment) as isize; + let buf = if first_row == 0 { + self.max_misalignment + col as usize + } else { + (first_row as usize + 1) * tracked_entries_per_row - 1 + }; + + let first = EntryIndices { + row: first_row, + col: col as isize, + buf, + }; - iter::successors(Some(first), move |&i| { - i.checked_add(tracked_entries_per_row - 1) + iter::successors(Some(first), move |&EntryIndices { row, col, buf }| { + Some(EntryIndices { + row: row + 1, + col, + buf: buf + tracked_entries_per_row - 1, + }) }) - .take_while(move |&i| i < nw_entries_len) + .take_while(move |&idx| idx.buf < nw_entries_len) .take(tracked_entries_per_row) } - fn replacement_cost(&self, row: usize, col: usize) -> Option { - let typed_bidx = *self.typed_indices.get(row.checked_sub(1)?)?; - let reference_bidx = *self.reference_indices.get(col.checked_sub(1)?)?; + fn replacement_cost(&self, row: isize, col: isize) -> Option { + let typed_bidx = *self.typed_indices.get(usize::try_from(row - 1).ok()?)?; + let reference_bidx = *self.reference_indices.get(usize::try_from(col - 1).ok()?)?; let typed = first_grapheme_cluster(&self.typed[typed_bidx..]); let reference = first_grapheme_cluster(&self.reference[reference_bidx..]); @@ -247,12 +252,22 @@ impl History { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct EntryIndices { + row: isize, + col: isize, + buf: usize, +} + fn first_grapheme_cluster(string: &str) -> &str { string.graphemes(true).next().unwrap() } #[cfg(test)] mod tests { + use rand::{seq::SliceRandom, SeedableRng}; + use rand_chacha::ChaCha20Rng; + use super::*; #[test] @@ -268,10 +283,8 @@ mod tests { } #[test] - fn push_saturday_sunday() { - let mut history = History::new(2); - - let nw_mat = [ + fn replicates_needleman_wunsch_with_high_max_misalignment() { + const NW_MAT: [[u32; 9]; 7] = [ [0, 1, 2, 3, 4, 5, 6, 7, 8], [1, 0, 1, 2, 3, 4, 5, 6, 7], [2, 1, 2, 3, 2, 3, 4, 5, 6], @@ -280,21 +293,32 @@ mod tests { [5, 4, 3, 4, 5, 6, 5, 4, 5], [6, 5, 4, 5, 6, 7, 6, 5, 4], ]; + const REFERENCE: &'static str = "saturday"; + const TYPED: &'static str = "sunday"; + const PERMUTATIONS: usize = 1_000; + + let mut rng = ChaCha20Rng::seed_from_u64(42); - history.push_reference("s"); - check_tracked_rows(&history, &nw_mat); + let mut push_order = [vec![true; 6], vec![false; 8]].concat(); - history.push_reference("a"); - check_tracked_rows(&history, &nw_mat); + for _ in 0..PERMUTATIONS { + push_order.shuffle(&mut rng); - history.push_reference("t"); - check_tracked_rows(&history, &nw_mat); + let mut history = History::new(8); - history.push_typed("s"); - check_tracked_rows(&history, &nw_mat); + let mut typed = TYPED.graphemes(true); + let mut reference = REFERENCE.graphemes(true); - history.push_typed("u"); - check_tracked_rows(&history, &nw_mat); + for &push_typed in &push_order { + if push_typed { + history.push_typed(typed.next().unwrap()); + } else { + history.push_reference(reference.next().unwrap()); + } + + check_tracked_rows(&history, &NW_MAT); + } + } } fn check_tracked_rows( From 89d6b6c6291bdd70ce8d41cfb65dd002c400c9a9 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Thu, 28 Sep 2023 20:29:42 -0700 Subject: [PATCH 08/12] chore(trial): remove dead code --- src/trial/history.rs | 25 ++----------------------- src/trial/mod.rs | 2 +- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/trial/history.rs b/src/trial/history.rs index 6381ea9..18344f0 100644 --- a/src/trial/history.rs +++ b/src/trial/history.rs @@ -1,4 +1,4 @@ -use std::{collections::btree_map::Entry, convert::TryFrom, iter, ops::Range}; +use std::{convert::TryFrom, iter}; use unicode_segmentation::UnicodeSegmentation; @@ -34,27 +34,6 @@ pub struct History { nw_entries: Vec, } -/// An "action" in the Needleman-Wunsch algorithm. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum NWAction { - /// The typed and reference grapheme clusters match. - Match, - - /// The typed and reference grapheme clusters do not match. - Mismatch, - - /// The typed grapheme cluster was inserted. - /// I.e., the reference grapheme cluster was deleted. - Insertion, - - /// The typed grapheme cluster was deleted. - /// I.e., the reference grapheme cluster was inserted. - Deletion, - - /// The entry is invalid, because it is outside the matrix. - Invalid, -} - impl History { /// Creates a new history, with empty typed and reference strings. pub fn new(max_misalignment: usize) -> Self { @@ -283,7 +262,7 @@ mod tests { } #[test] - fn replicates_needleman_wunsch_with_high_max_misalignment() { + fn reproduces_needleman_wunsch_with_high_max_misalignment() { const NW_MAT: [[u32; 9]; 7] = [ [0, 1, 2, 3, 4, 5, 6, 7, 8], [1, 0, 1, 2, 3, 4, 5, 6, 7], diff --git a/src/trial/mod.rs b/src/trial/mod.rs index a91be96..6da8029 100644 --- a/src/trial/mod.rs +++ b/src/trial/mod.rs @@ -8,7 +8,7 @@ pub struct Trial { history: History, /// The current grapheme cluster being typed. - /// Once the cluster is finished, it is pushed to `evaluation`. + /// Once the cluster is finished, it is pushed to `history`. working_grapheme_cluster: String, } From 5e39cf01ae21a8017472f21db6f7611c758a5787 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Sat, 30 Sep 2023 13:09:47 -0700 Subject: [PATCH 09/12] build: remove old Nix flake It wasn't being used for anything by anyone, as far as I'm aware. --- flake.lock | 114 ----------------------------------------------------- flake.nix | 45 --------------------- 2 files changed, 159 deletions(-) delete mode 100644 flake.lock delete mode 100644 flake.nix diff --git a/flake.lock b/flake.lock deleted file mode 100644 index de3f602..0000000 --- a/flake.lock +++ /dev/null @@ -1,114 +0,0 @@ -{ - "nodes": { - "fenix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1639808710, - "narHash": "sha256-OKDHt4D14puuqfVHptQ6EvjIR9RaHXyPzMh8Rjo8vzA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "9b391fc1831ece6c245a4eafe7b52f5c806df28c", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "flake-utils": { - "locked": { - "lastModified": 1638122382, - "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "naersk": { - "inputs": { - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1639731602, - "narHash": "sha256-5u/J/7KrY/fYL/OeBLxP4E9NdNdQrriHpOyPBleKp5I=", - "owner": "nmattia", - "repo": "naersk", - "rev": "5415c7045366bb53db1a33cf7d975942b5553a28", - "type": "github" - }, - "original": { - "owner": "nmattia", - "repo": "naersk", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1639837534, - "narHash": "sha256-zAZoVtvVfrs41e+kEEumyptQ4DOkcXQIYgxmaJ51+hs=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e1f9e754a40b645a39f6592d45df943cb5f59dcf", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "type": "indirect" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1639699734, - "narHash": "sha256-tlX6WebGmiHb2Hmniff+ltYp+7dRfdsBxw9YczLsP60=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "03ec468b14067729a285c2c7cfa7b9434a04816c", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-unstable", - "type": "indirect" - } - }, - "root": { - "inputs": { - "fenix": "fenix", - "flake-utils": "flake-utils", - "naersk": "naersk", - "nixpkgs": "nixpkgs_2" - } - }, - "rust-analyzer-src": { - "flake": false, - "locked": { - "lastModified": 1639175515, - "narHash": "sha256-Yj38u9BpKfyGrcSEaoSEnOns885xn/Ask6lR5rsxS8k=", - "owner": "rust-analyzer", - "repo": "rust-analyzer", - "rev": "d03397fe1173eaeb2e04c9e55ac223289e7e08ee", - "type": "github" - }, - "original": { - "owner": "rust-analyzer", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index aa0389a..0000000 --- a/flake.nix +++ /dev/null @@ -1,45 +0,0 @@ -{ - inputs = { - flake-utils.url = "github:numtide/flake-utils"; - - nixpkgs.url = "nixpkgs/nixos-unstable"; - fenix = { - url = "github:nix-community/fenix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - naersk.url = "github:nmattia/naersk"; - }; - - outputs = { self, nixpkgs, flake-utils, fenix, naersk }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - rust = with fenix.packages.${system}; stable; - naersk-lib = naersk.lib.${system}.override { - inherit (rust) rustc cargo; - }; - in - rec { - # `nix build` - packages.ttyper = naersk-lib.buildPackage rec { - pname = "ttyper"; - root = ./.; - }; - defaultPackage = packages.ttyper; - - # `nix run` - apps.ttyper = flake-utils.lib.mkApp { - drv = packages.ttyper; - }; - defaultApp = apps.ttyper; - - # `nix develop` - devShell = pkgs.mkShell { - nativeBuildInputs = with pkgs; [ - (rust.withComponents [ "rustc" "cargo" "rust-src" "rustfmt" "clippy" ]) - rust-analyzer - ]; - }; - } - ); -} From 34f4ca3ac5886dc5ccdd512b0b7f91ebc0e1dd21 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Sat, 30 Sep 2023 13:11:36 -0700 Subject: [PATCH 10/12] chore: remove .envrc --- .envrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .envrc diff --git a/.envrc b/.envrc deleted file mode 100644 index 8392d15..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use flake \ No newline at end of file From 5a41982b4c893f5ce66fe72384313caef91c9807 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Mon, 10 Jun 2024 22:48:37 -0700 Subject: [PATCH 11/12] feat: scaffold out Trial::input API --- Cargo.lock | 416 +++++++++++++++++++++++++++++++++-------------- Cargo.toml | 14 +- src/config.rs | 4 + src/contents.rs | 2 +- src/trial/mod.rs | 36 ++++ 5 files changed, 346 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f84479f..6ad475d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,36 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", + "is_terminal_polyfill", "utf8parse", ] @@ -43,12 +61,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -63,6 +81,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "cassowary" version = "0.3.0" @@ -70,10 +94,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] -name = "cc" -version = "1.0.79" +name = "castaway" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] [[package]] name = "cfg-if" @@ -83,33 +110,31 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.3.1" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.1" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", - "bitflags", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.1" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck", "proc-macro2", @@ -119,9 +144,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" @@ -129,13 +154,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "crossterm" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags", + "bitflags 2.5.0", "crossterm_winapi", "libc", "mio", @@ -147,9 +185,9 @@ dependencies = [ [[package]] name = "crossterm_winapi" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] @@ -176,31 +214,16 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.1" +name = "either" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "getrandom" @@ -218,18 +241,16 @@ name = "hashbrown" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" @@ -242,39 +263,31 @@ dependencies = [ ] [[package]] -name = "io-lifetimes" -version = "1.0.11" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] -name = "is-terminal" -version = "0.4.7" +name = "itertools" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", + "either", ] [[package]] -name = "libc" -version = "0.2.144" +name = "itoa" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] -name = "linux-raw-sys" -version = "0.3.8" +name = "libc" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "lock_api" @@ -292,6 +305,15 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.5.0" @@ -312,9 +334,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.2" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "option-ext" @@ -345,6 +367,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -353,18 +381,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -401,14 +429,21 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.21.0" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce841e0486e7c2412c3740168ede33adeba8e154a15107b879d8162d77c7174e" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ - "bitflags", + "bitflags 2.5.0", "cassowary", + "compact_str", "crossterm", + "itertools", + "lru", + "paste", + "stability", + "strum", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] @@ -418,7 +453,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -433,18 +468,16 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.37.19" +name = "rustversion" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.48.0", -] +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scopeguard" @@ -454,18 +487,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -474,18 +507,18 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -517,17 +550,55 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] [[package]] name = "syn" -version = "2.0.25" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -556,9 +627,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.6" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", @@ -568,18 +639,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.12" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", @@ -590,7 +661,7 @@ dependencies = [ [[package]] name = "ttyper" -version = "1.2.0" +version = "2.0.0-pre.1" dependencies = [ "clap", "crossterm", @@ -611,9 +682,19 @@ checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools", + "unicode-width", +] [[package]] name = "unicode-width" @@ -627,6 +708,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -673,6 +760,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -703,6 +799,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -715,6 +827,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -727,6 +845,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -739,6 +863,18 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -751,6 +887,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -763,6 +905,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -775,6 +923,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -787,11 +941,37 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + [[package]] name = "winnow" -version = "0.4.9" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index fddbe6f..340eee6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ttyper" description = "Terminal-based typing test." -version = "1.2.0" +version = "2.0.0-pre.1" readme = "README.md" repository = "https://github.com/max-niederman/ttyper.git" homepage = "https://github.com/max-niederman/ttyper" @@ -10,14 +10,14 @@ authors = ["Max Niederman "] edition = "2018" [dependencies] -crossterm = "0.26" -clap = { version = "4.3", features = ["derive"] } -ratatui = "0.21" -unicode-segmentation = "1.10" +crossterm = "0.27.0" +clap = { version = "4.5.7", features = ["derive"] } +ratatui = "0.26.3" +unicode-segmentation = "1.11.0" rand = "0.8.5" -serde = { version = "1.0.171", features = ["derive"] } +serde = { version = "1.0.203", features = ["derive"] } dirs = "5.0.1" -toml = "0.7.6" +toml = "0.8.14" [dev-dependencies] rand_chacha = "0.3.1" diff --git a/src/config.rs b/src/config.rs index d476db7..7c0039d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,8 @@ pub struct Config { pub default_language: PathBuf, pub default_lexer: String, + pub max_misalignment: usize, + pub theme: Theme, } @@ -21,6 +23,8 @@ impl Default for Config { default_language: "english200".into(), default_lexer: "extended-grapheme-clusters".into(), // TODO: this should be unicode words probably + max_misalignment: 8, + theme: Theme::default(), } } diff --git a/src/contents.rs b/src/contents.rs index f9ccfe2..ae01248 100644 --- a/src/contents.rs +++ b/src/contents.rs @@ -14,7 +14,7 @@ use crate::opt::{Command, FileLexer}; /// A trait for types that can be used as trial contents. /// -/// The iterator should yield "atoms," the smallest chunks +/// The iterator should yield "words," i.e. the smallest chunks /// of the test that should not be split across line breaks. pub trait Contents: Iterator { fn restart(&mut self); diff --git a/src/trial/mod.rs b/src/trial/mod.rs index 6da8029..cc22d1b 100644 --- a/src/trial/mod.rs +++ b/src/trial/mod.rs @@ -12,4 +12,40 @@ pub struct Trial { working_grapheme_cluster: String, } +/// A user input to a trial. +pub enum Input { + /// Type a Unicode scalar value. + TypeScalar(char), + /// Delete the last typed grapheme cluster. + DeleteGraphemeCluster, + /// Delete the "last word". + DeleteWord, +} + +impl Trial { + /// Create a new trial. + pub fn new(env: &crate::Env) -> Self { + Self { + history: History::new(env.config.max_misalignment), + working_grapheme_cluster: String::new(), + } + } + /// Process a user input. + pub fn process(&mut self, input: Input) { + match input { + Input::TypeScalar(c) => { + self.working_grapheme_cluster.push(c); + } + Input::DeleteGraphemeCluster if self.working_grapheme_cluster.is_empty() => { + todo!() + } + Input::DeleteGraphemeCluster => { + self.working_grapheme_cluster.clear(); + } + Input::DeleteWord => { + todo!() + } + } + } +} From df19af66db814027d2ec4abf15d97868247e9d64 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Mon, 10 Jun 2024 23:24:50 -0700 Subject: [PATCH 12/12] feat(trial): implement grapheme clustered input --- src/trial/history.rs | 2 +- src/trial/mod.rs | 66 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/trial/history.rs b/src/trial/history.rs index 18344f0..ee314d9 100644 --- a/src/trial/history.rs +++ b/src/trial/history.rs @@ -101,7 +101,7 @@ impl History { } /// Push a typed grapheme cluster to the history. - fn push_typed(&mut self, typed: &str) { + pub fn push_typed(&mut self, typed: &str) { debug_assert_eq!(typed.graphemes(true).count(), 1); // Push the index of the cluster. diff --git a/src/trial/mod.rs b/src/trial/mod.rs index cc22d1b..f4d153e 100644 --- a/src/trial/mod.rs +++ b/src/trial/mod.rs @@ -2,6 +2,8 @@ mod history; use history::History; +use unicode_segmentation::UnicodeSegmentation; + /// The state of a trial. pub struct Trial { /// The history of typed grapheme clusters and reference grapheme clusters. @@ -35,7 +37,9 @@ impl Trial { pub fn process(&mut self, input: Input) { match input { Input::TypeScalar(c) => { - self.working_grapheme_cluster.push(c); + if let Some(finished_cluster) = push_to_working_cluster(&mut self.working_grapheme_cluster, c) { + self.history.push_typed(&finished_cluster); + } } Input::DeleteGraphemeCluster if self.working_grapheme_cluster.is_empty() => { todo!() @@ -49,3 +53,63 @@ impl Trial { } } } + +/// Push a scalar value to the working grapheme cluster, returning the previous cluster if it was finished. +fn push_to_working_cluster(working_grapheme_cluster: &mut String, scalar: char) -> Option { + working_grapheme_cluster.push(scalar); + + let mut clusters = working_grapheme_cluster.graphemes(true); + let first_cluster = clusters.next().unwrap(); + + if let Some(new_cluster) = clusters.next() { + let first_cluster = first_cluster.to_owned(); + let new_cluster = new_cluster.to_owned(); + + *working_grapheme_cluster = new_cluster; + + Some(first_cluster) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn push_to_working_cluster_handles_latin() { + let mut working_grapheme_cluster = String::new(); + + assert_eq!( + push_to_working_cluster(&mut working_grapheme_cluster, 'a'), + None + ); + assert_eq!( + push_to_working_cluster(&mut working_grapheme_cluster, 'a'), + Some("a".to_owned()) + ); + + assert_eq!( + push_to_working_cluster(&mut working_grapheme_cluster, 'é'), + Some("a".to_owned()) + ); + assert_eq!( + push_to_working_cluster(&mut working_grapheme_cluster, 'a'), + Some("é".to_owned()) + ); + + assert_eq!( + push_to_working_cluster(&mut working_grapheme_cluster, 'a'), + Some("a".to_owned()) + ); + assert_eq!( + push_to_working_cluster(&mut working_grapheme_cluster, '\u{0302}'), // combining circumflex accent + None + ); + assert_eq!( + push_to_working_cluster(&mut working_grapheme_cluster, 'a'), + Some("a\u{0302}".to_owned()) + ); + } +}