From 9ac53ca89151d4757198e6ef5b38ad8c80dd8151 Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Wed, 16 Feb 2022 16:09:39 -0800 Subject: [PATCH] Preparing for v2.0 (#74) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .gitignore | 3 - .vscode/launch.json | 42 +++++ .vscode/settings.json | 3 + Cargo.lock | 332 +++++++++++++++++++++++++------------- Cargo.toml | 1 + LICENSE | 2 +- src/handlers/app.rs | 14 +- src/handlers/data.rs | 78 +++++---- src/main.rs | 12 +- src/terminal.rs | 61 +++++-- src/twitch.rs | 10 +- src/ui/mod.rs | 39 +---- src/ui/popups/channels.rs | 37 +++++ src/ui/popups/help.rs | 12 +- src/ui/popups/messages.rs | 142 ++++++++++++++++ src/ui/popups/mod.rs | 155 ++++++++++++++---- 16 files changed, 701 insertions(+), 242 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 src/ui/popups/channels.rs create mode 100644 src/ui/popups/messages.rs diff --git a/.gitignore b/.gitignore index b10a151..c3fbb48 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,6 @@ # JetBrains project config folder. .idea -# Visual Studio Code config folder. -.vscode - # Ignore Vim temporary and swap files. *.sw? *~ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8dfd61f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'twt'", + "cargo": { + "args": [ + "build", + "--bin=twt", + "--package=twitch-tui" + ], + "filter": { + "name": "twt", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'twt'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=twt", + "--package=twitch-tui" + ], + "filter": { + "name": "twt", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..99d72a1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.defaultFormatter": "matklad.rust-analyzer" +} diff --git a/Cargo.lock b/Cargo.lock index 31202e6..b9c88a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" @@ -63,9 +63,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "4.2.2" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed" +checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db" dependencies = [ "error-code", "str-buf", @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", @@ -290,20 +290,50 @@ dependencies = [ ] [[package]] -name = "error-code" -version = "2.3.0" +name = "errno" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" dependencies = [ "libc", "str-buf", ] [[package]] -name = "fd-lock" -version = "3.0.1" +name = "fastrand" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc110fe50727d46a428eed832df40affe9bf74d077cac1bf3f2718e823f14c5" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "fd-lock" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcef756dea9cf3db5ce73759cf0467330427a786b47711b8d6c97620d718ceb9" dependencies = [ "cfg-if", "libc", @@ -415,10 +445,19 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.2.3" +name = "fuzzy-matcher" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if", "libc", @@ -452,6 +491,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ef6787e7f0faedc040f95716bdd0e62bcfcf4ba93da053b62dea2691c13864" +dependencies = [ + "winapi", +] + [[package]] name = "irc" version = "0.15.0" @@ -527,9 +575,9 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memoffset" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] @@ -598,9 +646,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" dependencies = [ "bitflags", "cc", @@ -611,9 +659,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] @@ -639,9 +687,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -649,9 +697,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "openssl" @@ -669,15 +717,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.71" +version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ "autocfg", "cc", @@ -736,18 +784,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", @@ -756,9 +804,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -768,15 +816,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" - -[[package]] -name = "ppv-lite86" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "proc-macro-error" @@ -804,18 +846,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.32" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] @@ -830,46 +872,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] - -[[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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" version = "0.2.10" @@ -915,6 +917,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustix" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cee647393af53c750e15dcbf7781cdd2e550b246bde76e46c326e7ea3c73773" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "winapi", +] + [[package]] name = "rustyline" version = "9.1.2" @@ -957,9 +973,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "2.3.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", @@ -970,9 +986,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.4.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", @@ -1000,9 +1016,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" dependencies = [ "libc", "signal-hook-registry", @@ -1036,9 +1052,9 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "smawk" @@ -1094,9 +1110,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", @@ -1105,13 +1121,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", + "fastrand", "libc", - "rand", "redox_syscall", "remove_dir_all", "winapi", @@ -1157,6 +1173,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "time" version = "0.1.43" @@ -1264,6 +1289,7 @@ dependencies = [ "crossterm 0.20.0", "enum-iterator", "futures", + "fuzzy-matcher", "irc", "lazy_static", "rustyline", @@ -1358,9 +1384,35 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" +checksum = "030b7ff91626e57a05ca64a07c481973cbb2db774e4852c9c7ca342408c6a99a" +dependencies = [ + "windows_aarch64_msvc 0.28.0", + "windows_i686_gnu 0.28.0", + "windows_i686_msvc 0.28.0", + "windows_x86_64_gnu 0.28.0", + "windows_x86_64_msvc 0.28.0", +] + +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" dependencies = [ "windows_aarch64_msvc 0.28.0", "windows_i686_gnu 0.28.0", @@ -1384,9 +1436,9 @@ dependencies = [ [[package]] name = "windows_aarch64_msvc" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" +checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" [[package]] name = "windows_aarch64_msvc" @@ -1396,9 +1448,27 @@ checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" [[package]] name = "windows_i686_gnu" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" +checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" [[package]] name = "windows_i686_gnu" @@ -1408,9 +1478,15 @@ checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" [[package]] name = "windows_i686_msvc" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" +checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" [[package]] name = "windows_i686_msvc" @@ -1420,9 +1496,15 @@ checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" [[package]] name = "windows_x86_64_gnu" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" +checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" [[package]] name = "windows_x86_64_gnu" @@ -1432,7 +1514,31 @@ checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" [[package]] name = "windows_x86_64_msvc" -version = "0.28.0" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" diff --git a/Cargo.toml b/Cargo.toml index 2f2d781..d5f2654 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ rustyline = "9.1.2" lazy_static = "1.4.0" structopt = "0.3.26" enum-iterator = "0.7.0" +fuzzy-matcher = "0.3.7" [[bin]] bench = false diff --git a/LICENSE b/LICENSE index d817ddd..8288895 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Xithrius +Copyright (c) 2022 Xithrius Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/handlers/app.rs b/src/handlers/app.rs index 47e47c5..fc396a3 100644 --- a/src/handlers/app.rs +++ b/src/handlers/app.rs @@ -11,12 +11,14 @@ pub enum State { Input, Help, ChannelSwitch, + Search, } #[derive(PartialEq, std::cmp::Eq, std::hash::Hash, IntoEnumIterator)] pub enum BufferName { Chat, Channel, + MessageSearch, } pub struct App { @@ -32,6 +34,10 @@ pub struct App { pub table_constraints: Option>, /// The titles of the columns within the table of the terminal pub column_titles: Option>, + /// Scrolling offset for windows + pub scroll_offset: usize, + /// A temporary snapshot of current messages + pub messages_snapshot: VecDeque, } impl App { @@ -49,10 +55,16 @@ impl App { input_buffers: input_buffers_map, table_constraints: None, column_titles: None, + scroll_offset: 0, + messages_snapshot: VecDeque::with_capacity(config.terminal.maximum_messages), } } - pub fn get_buffer(&mut self) -> &mut LineBuffer { + pub fn current_buffer(&self) -> &LineBuffer { + return self.input_buffers.get(&self.selected_buffer).unwrap(); + } + + pub fn current_buffer_mut(&mut self) -> &mut LineBuffer { return self.input_buffers.get_mut(&self.selected_buffer).unwrap(); } } diff --git a/src/handlers/data.rs b/src/handlers/data.rs index 2edbb0d..9ede675 100644 --- a/src/handlers/data.rs +++ b/src/handlers/data.rs @@ -9,6 +9,13 @@ use crate::{ utils::{colors::hsl_to_rgb, styles, text::align_text}, }; +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub enum PayLoad { + Message(String), + Err(String), +} + #[derive(Debug, Copy, Clone)] pub struct DataBuilder<'conf> { pub date_format: &'conf str, @@ -24,7 +31,7 @@ impl<'conf> DataBuilder<'conf> { time_sent: Local::now().format(self.date_format).to_string(), author: user, system: false, - message, + payload: PayLoad::Message(message), } } @@ -33,7 +40,7 @@ impl<'conf> DataBuilder<'conf> { time_sent: Local::now().format(self.date_format).to_string(), author: "System".to_string(), system: true, - message, + payload: PayLoad::Message(message), } } @@ -42,7 +49,7 @@ impl<'conf> DataBuilder<'conf> { time_sent: Local::now().format(self.date_format).to_string(), author: "Twitch".to_string(), system: true, - message, + payload: PayLoad::Message(message), } } } @@ -51,8 +58,8 @@ impl<'conf> DataBuilder<'conf> { pub struct Data { pub time_sent: String, pub author: String, - pub message: String, pub system: bool, + pub payload: PayLoad, } impl Data { @@ -77,38 +84,41 @@ impl Data { } pub fn to_row(&self, frontend_config: &FrontendConfig, limit: &usize) -> (u16, Row) { - let message = textwrap::fill(self.message.as_str(), *limit); + if let PayLoad::Message(m) = &self.payload { + let message = textwrap::fill(m.as_str(), *limit); - let style; - if self.system { - style = styles::SYSTEM_CHAT; + let style = if self.system { + styles::SYSTEM_CHAT + } else { + Style::default().fg(self.hash_username(&frontend_config.palette)) + }; + + let mut row_vector = vec![ + Cell::from(align_text( + &self.author, + frontend_config.username_alignment.as_str(), + frontend_config.maximum_username_length, + )) + .style(style), + Cell::from(message.to_string()), + ]; + + if frontend_config.date_shown { + row_vector.insert(0, Cell::from(self.time_sent.to_string())); + } + + let msg_height = message.split('\n').count() as u16; + + let mut row = Row::new(row_vector).style(styles::CHAT); + + if msg_height > 1 { + row = row.height(msg_height); + } + + (msg_height, row) } else { - style = Style::default().fg(self.hash_username(&frontend_config.palette)); + panic!("Data.to_row() can only take message payloads.") } - - let mut row_vector = vec![ - Cell::from(align_text( - &self.author, - frontend_config.username_alignment.as_str(), - frontend_config.maximum_username_length, - )) - .style(style), - Cell::from(message.to_string()), - ]; - - if frontend_config.date_shown { - row_vector.insert(0, Cell::from(self.time_sent.to_string())); - } - - let msg_height = message.split('\n').count() as u16; - - let mut row = Row::new(row_vector).style(styles::CHAT); - - if msg_height > 1 { - row = row.height(msg_height); - } - - (msg_height, row) } } @@ -123,8 +133,8 @@ mod tests { Data { time_sent: Local::now().format("%c").to_string(), author: "human".to_string(), - message: "beep boop".to_string(), system: false, + payload: PayLoad::Message("beep boop".to_string()), } } diff --git a/src/main.rs b/src/main.rs index 13e82bc..9795552 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,9 @@ +mod handlers; +mod terminal; +mod twitch; +mod ui; +mod utils; + use anyhow::Result; use structopt::StructOpt; use tokio::sync::mpsc; @@ -9,12 +15,6 @@ use handlers::{ use crate::handlers::app::App; -mod handlers; -mod terminal; -mod twitch; -mod ui; -mod utils; - #[tokio::main] async fn main() -> Result<()> { match CompleteConfig::new() { diff --git a/src/terminal.rs b/src/terminal.rs index 2d8eec5..22d48aa 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -18,7 +18,7 @@ use crate::{ handlers::{ app::{App, BufferName, State}, config::CompleteConfig, - data::{Data, DataBuilder}, + data::{Data, DataBuilder, PayLoad}, event::{Config, Event, Events, Key}, }, twitch::Action, @@ -96,24 +96,47 @@ pub async fn ui_driver( }; 'outer: loop { + if let Ok(info) = rx.try_recv() { + match info.payload { + PayLoad::Message(_) => app.messages.push_front(info), + + // If something such as a keypress failed, fallback to the normal state of the application. + PayLoad::Err(err) => { + app.state = State::Normal; + app.selected_buffer = BufferName::Chat; + + app.messages.push_front(data_builder.system(err)); + } + } + } + terminal .draw(|frame| draw_ui(frame, &mut app, &config)) .unwrap(); - if let Ok(info) = rx.try_recv() { - app.messages.push_front(info); - } - if let Some(Event::Input(key)) = events.next().await { match app.state { - State::Input | State::ChannelSwitch => { - let input_buffer = app.get_buffer(); + State::Input | State::ChannelSwitch | State::Search => { + let input_buffer = app.current_buffer_mut(); match key { - Key::Up => { - if let State::Input = app.state { + Key::Up => match app.state { + State::Input => { app.state = State::Normal; } + State::Search => { + if app.scroll_offset > 1 { + app.scroll_offset -= 1; + } + } + _ => {} + }, + Key::Down => { + if let State::Search = app.state { + if app.scroll_offset < app.messages_snapshot.len() { + app.scroll_offset += 1; + } + } } Key::Ctrl('f') | Key::Right => { input_buffer.move_forward(1); @@ -169,6 +192,7 @@ pub async fn ui_driver( .await .unwrap(); } + input_message.update("", 0); } BufferName::Channel => { @@ -184,11 +208,13 @@ pub async fn ui_driver( config.twitch.channel = input_message.to_string(); } + input_message.update("", 0); app.selected_buffer = BufferName::Chat; app.state = State::Normal; } + BufferName::MessageSearch => {} }, Key::Char(c) => { input_buffer.insert(c, 1); @@ -205,12 +231,20 @@ pub async fn ui_driver( app.state = State::Normal; app.selected_buffer = BufferName::Chat; } - Key::Char('?') => app.state = State::Help, - Key::Char('i') => app.state = State::Input, Key::Char('C') => { app.state = State::ChannelSwitch; app.selected_buffer = BufferName::Channel; } + Key::Char('?') => app.state = State::Help, + Key::Char('i') => { + app.state = State::Input; + app.selected_buffer = BufferName::Chat; + } + Key::Char('s') => { + app.state = State::Search; + app.selected_buffer = BufferName::MessageSearch; + app.messages_snapshot = app.messages.clone(); + } Key::Char('q') => { quitting(terminal); break 'outer; @@ -220,11 +254,10 @@ pub async fn ui_driver( quitting(terminal); break 'outer; } - State::ChannelSwitch => { - app.selected_buffer = BufferName::Chat; + _ => { app.state = State::Normal; + app.selected_buffer = BufferName::Chat; } - _ => app.state = State::Normal, }, _ => {} }, diff --git a/src/twitch.rs b/src/twitch.rs index d0f242b..24f3ab0 100644 --- a/src/twitch.rs +++ b/src/twitch.rs @@ -29,7 +29,9 @@ pub async fn twitch_irc(mut config: CompleteConfig, tx: Sender, mut rx: Re }; let mut client = Client::from_config(irc_config.clone()).await.unwrap(); + client.identify().unwrap(); + let mut stream = client.stream().unwrap(); let data_builder = DataBuilder::new(&config.frontend.date_format); let mut room_state_startup = false; @@ -57,17 +59,19 @@ pub async fn twitch_irc(mut config: CompleteConfig, tx: Sender, mut rx: Re biased; Some(action) = rx.recv() => { + let current_channel = format!("#{}", config.twitch.channel); + match action { Action::Privmsg(message) => { client - .send_privmsg(format!("#{}", config.twitch.channel), message) + .send_privmsg(current_channel, message) .unwrap(); } Action::Join(channel) => { let channel_list = format!("#{}", channel); // Leave previous channel - if let Err(err) = client.send_part(format!("#{}", config.twitch.channel)) { + if let Err(err) = client.send_part(current_channel) { tx.send(data_builder.twitch(err.to_string())).await.unwrap() } else { tx.send(data_builder.twitch(format!("Joined {}", channel_list))).await.unwrap(); @@ -85,7 +89,7 @@ pub async fn twitch_irc(mut config: CompleteConfig, tx: Sender, mut rx: Re } Some(_message) = stream.next() => { if let Ok(message) = _message { - let mut tags: HashMap<&str, &str> = std::collections::HashMap::new(); + let mut tags: HashMap<&str, &str> = HashMap::new(); if let Some(ref _tags) = message.tags { for tag in _tags { if let Some(ref tag_value) = tag.1 { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0e9691a..a7293e9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,7 +8,7 @@ use tui::{ style::{Color, Modifier, Style}, terminal::Frame, text::{Span, Spans}, - widgets::{Block, Borders, Clear, Paragraph, Row, Table}, + widgets::{Block, Borders, Paragraph, Row, Table}, }; use crate::{ @@ -16,10 +16,7 @@ use crate::{ app::{App, State}, config::CompleteConfig, }, - ui::{ - popups::{centered_popup, Centering}, - statics::COMMANDS, - }, + ui::statics::COMMANDS, utils::{styles, text::get_cursor_position}, }; @@ -122,7 +119,7 @@ pub fn draw_ui(frame: &mut Frame, app: &mut App, config: &Complet match app.state { State::Input => { - let input_buffer = app.get_buffer(); + let input_buffer = app.current_buffer(); if input_buffer.starts_with('/') { let suggested_commands = COMMANDS @@ -165,31 +162,9 @@ pub fn draw_ui(frame: &mut Frame, app: &mut App, config: &Complet vertical_chunks[vertical_chunk_constraints.len() - 1], ); } - State::Help => popups::help::keybinds(frame), - State::ChannelSwitch => { - let input_rect = centered_popup(Centering::Input(30, 10), frame.size()); - - let input_buffer = app.get_buffer(); - - let cursor_pos = get_cursor_position(input_buffer); - - frame.set_cursor( - (input_rect.x + cursor_pos as u16 + 1) - .min(input_rect.x + input_rect.width.saturating_sub(2)), - input_rect.y + 1, - ); - - let paragraph = Paragraph::new(input_buffer.as_str()) - .style(Style::default().fg(Color::Yellow)) - .block(Block::default().borders(Borders::ALL).title("[ Channel ]")) - .scroll(( - 0, - ((cursor_pos + 3) as u16).saturating_sub(input_rect.width), - )); - - frame.render_widget(Clear, input_rect); - frame.render_widget(paragraph, input_rect); - } - _ => (), + State::Help => popups::help::show_keybinds(frame), + State::ChannelSwitch => popups::channels::switch_channels(frame, app), + State::Search => popups::messages::search_messages(frame, app), + _ => {} } } diff --git a/src/ui/popups/channels.rs b/src/ui/popups/channels.rs new file mode 100644 index 0000000..8c38c5b --- /dev/null +++ b/src/ui/popups/channels.rs @@ -0,0 +1,37 @@ +use tui::{ + backend::Backend, + style::{Color, Style}, + terminal::Frame, + widgets::{Block, Borders, Clear, Paragraph}, +}; + +use crate::{ + handlers::app::App, + ui::popups::{centered_popup, WindowType}, + utils::text::get_cursor_position, +}; + +pub fn switch_channels(frame: &mut Frame, app: &mut App) { + let input_rect = centered_popup(WindowType::Input(frame.size().height), frame.size()); + + let input_buffer = app.current_buffer(); + + let cursor_pos = get_cursor_position(input_buffer); + + frame.set_cursor( + (input_rect.x + cursor_pos as u16 + 1) + .min(input_rect.x + input_rect.width.saturating_sub(2)), + input_rect.y + 1, + ); + + let paragraph = Paragraph::new(input_buffer.as_str()) + .style(Style::default().fg(Color::Yellow)) + .block(Block::default().borders(Borders::ALL).title("[ Channel ]")) + .scroll(( + 0, + ((cursor_pos + 3) as u16).saturating_sub(input_rect.width), + )); + + frame.render_widget(Clear, input_rect); + frame.render_widget(paragraph, input_rect); +} diff --git a/src/ui/popups/help.rs b/src/ui/popups/help.rs index e98fcbc..6baa1d4 100644 --- a/src/ui/popups/help.rs +++ b/src/ui/popups/help.rs @@ -7,13 +7,13 @@ use tui::{ use crate::{ ui::{ - popups::{centered_popup, Centering}, + popups::{centered_popup, Centering, WindowType}, statics::{HELP_COLUMN_TITLES, HELP_KEYBINDS}, }, utils::{styles, text::vector_column_max}, }; -pub fn keybinds(frame: &mut Frame) { +pub fn show_keybinds(frame: &mut Frame) { let table_widths = vector_column_max(&HELP_KEYBINDS, None) .into_iter() .map(Constraint::Min) @@ -26,7 +26,13 @@ pub fn keybinds(frame: &mut Frame) { .column_spacing(2) .style(styles::BORDER_NAME); - let area = centered_popup(Centering::Window(60, 50), frame.size()); + let area = centered_popup( + WindowType::Window( + Centering::Middle(frame.size().height), + HELP_KEYBINDS.len() as u16, + ), + frame.size(), + ); frame.render_widget(Clear, area); frame.render_widget(help_table, area); diff --git a/src/ui/popups/messages.rs b/src/ui/popups/messages.rs new file mode 100644 index 0000000..998b223 --- /dev/null +++ b/src/ui/popups/messages.rs @@ -0,0 +1,142 @@ +use std::collections::VecDeque; + +use tui::{ + backend::Backend, + layout::Constraint, + style::{Color, Modifier, Style}, + terminal::Frame, + text::{Span, Spans}, + widgets::{Block, Borders, Clear, Paragraph, Row, Table}, +}; + +use fuzzy_matcher::skim::SkimMatcherV2; +use lazy_static::lazy_static; + +use crate::{ + handlers::{app::App, data::PayLoad}, + ui::popups::{centered_popup, scroll_view, Centering, WindowType}, + utils::{styles, text::get_cursor_position}, +}; +use fuzzy_matcher::FuzzyMatcher; + +const MAX_MESSAGE_SEARCH: u16 = 10; + +lazy_static! { + pub static ref FUZZY_FINDER: SkimMatcherV2 = SkimMatcherV2::default(); +} + +pub fn search_messages(frame: &mut Frame, app: &mut App) { + let input_rect = centered_popup(WindowType::Input(frame.size().height), frame.size()); + let window_rect = centered_popup( + WindowType::Window(Centering::Height(frame.size().height), MAX_MESSAGE_SEARCH), + frame.size(), + ); + + let input_buffer = app.current_buffer(); + + let cursor_pos = get_cursor_position(input_buffer); + + frame.set_cursor( + (input_rect.x + cursor_pos as u16 + 1) + .min(input_rect.x + input_rect.width.saturating_sub(2)), + input_rect.y + 1, + ); + + let input_text = &input_buffer.as_str(); + + let input_paragraph = Paragraph::new(*input_text) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title("[ Message Search ]"), + ) + .scroll(( + 0, + ((cursor_pos + 3) as u16).saturating_sub(input_rect.width), + )); + + frame.render_widget(Clear, input_rect); + frame.render_widget(input_paragraph, input_rect); + + let all_messages = app + .messages_snapshot + .iter() + .flat_map(|f| match &f.payload { + PayLoad::Message(m) => Some(m.as_str()), + _ => None, + }) + .collect::>(); + + if all_messages.is_empty() { + let window_paragraph = Table::new(vec![]) + .block(Block::default().borders(Borders::ALL).title("[ Results ]")) + .column_spacing(2) + .style(styles::BORDER_NAME); + + frame.render_widget(Clear, window_rect); + frame.render_widget(window_paragraph, window_rect); + + return; + } + + let maximum_message_length = *all_messages + .iter() + .map(|v| v.len()) + .collect::>() + .iter() + .max() + .unwrap() as u16; + + let table_widths = all_messages + .iter() + .map(|_| Constraint::Min(maximum_message_length)) + .collect::>(); + + let render_messages = scroll_view(all_messages, app.scroll_offset, MAX_MESSAGE_SEARCH as usize); + + let rows = if input_text.is_empty() { + render_messages + .iter() + .map(|&v| Row::new(vec![v])) + .collect::>() + } else { + render_messages + .iter() + .flat_map(|&f| { + let chars = f.chars(); + + if let Some((_, indices)) = FUZZY_FINDER.fuzzy_indices(f, input_text) { + Some(Row::new(vec![Spans::from( + chars + .enumerate() + .map(|(i, s)| { + if indices.contains(&i) { + Span::styled( + s.to_string(), + Style::default() + .fg(Color::Red) + .add_modifier(Modifier::BOLD), + ) + } else { + Span::raw(s.to_string()) + } + }) + .collect::>(), + )])) + } else { + None + } + }) + .collect::>() + }; + + let window_paragraph = Table::new(rows) + .block(Block::default().borders(Borders::ALL).title("[ Results ]")) + .column_spacing(2) + .widths(&table_widths) + .style(styles::BORDER_NAME); + + frame.render_widget(Clear, window_rect); + frame.render_widget(window_paragraph, window_rect); +} diff --git a/src/ui/popups/mod.rs b/src/ui/popups/mod.rs index c53d3ce..f8ff0c8 100644 --- a/src/ui/popups/mod.rs +++ b/src/ui/popups/mod.rs @@ -1,22 +1,40 @@ +pub mod channels; pub mod help; +pub mod messages; + +use std::collections::VecDeque; use tui::layout::{Constraint, Direction, Layout, Rect}; +const HORIZONTAL_CONSTRAINTS: [Constraint; 3] = [ + Constraint::Percentage(15), + Constraint::Percentage(70), + Constraint::Percentage(15), +]; + pub enum Centering { - Input(u16, u16), - Window(u16, u16), + Height(u16), + Middle(u16), } -pub fn centered_popup(c: Centering, size: Rect) -> Rect { +pub enum WindowType { + /// An input window, with the integer representing the height of the terminal + Input(u16), + /// A window containing either some specified terminal height, or in the middle, + /// with an integer describing the amount of vertically stored items + Window(Centering, u16), +} + +pub fn centered_popup(c: WindowType, size: Rect) -> Rect { match c { - Centering::Window(percent_x, percent_y) => { + WindowType::Input(v) => { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints( [ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), + Constraint::Length((v / 2) as u16 - 6), + Constraint::Length(3), + Constraint::Min(0), ] .as_ref(), ) @@ -24,40 +42,113 @@ pub fn centered_popup(c: Centering, size: Rect) -> Rect { Layout::default() .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ] - .as_ref(), - ) + .constraints(HORIZONTAL_CONSTRAINTS.as_ref()) .split(popup_layout[1])[1] } - Centering::Input(percent_x, percent_y) => { + WindowType::Window(v, i) => { let popup_layout = Layout::default() .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Length(3), - Constraint::Percentage((100 - percent_y) / 2), - ] - .as_ref(), - ) + .constraints([ + Constraint::Length(match v { + Centering::Height(terminal_height) => (terminal_height / 2) as u16 - 3, + Centering::Middle(terminal_height) => ((terminal_height - i) / 2) as u16, + }), + Constraint::Length(i), + match v { + Centering::Height(_) => Constraint::Min(0), + Centering::Middle(terminal_height) => { + Constraint::Length(((terminal_height - i) / 2) as u16 - 3) + } + }, + ]) .split(size); Layout::default() .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ] - .as_ref(), - ) + .constraints(HORIZONTAL_CONSTRAINTS.as_ref()) .split(popup_layout[1])[1] } } } + +pub fn scroll_view( + v: VecDeque, + offset: usize, + amount: usize, +) -> VecDeque { + if offset > v.len() { + panic!( + "Scroll offset is {}, but length of VecDeque is {}.", + offset, + v.len() + ); + } + // If the offset is at 0 or at the bottom of the input, then there's no need to move. + else if (offset == 0 && amount == v.len()) || v.is_empty() { + v + } + // If there's no amount specified, return everything behind and including the offset index values, + // or when the offset and the amount wanted goes over the length of the VecDeque. + else if amount == 0 || offset + amount > v.len() { + v.range(offset..).copied().collect::>() + } else { + v.range(offset..offset + amount) + .copied() + .collect::>() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn setup() -> VecDeque { + VecDeque::from([1, 2, 3, 4, 5]) + } + + #[test] + #[should_panic(expected = "Scroll offset is 10, but length of VecDeque is 5.")] + fn test_offset_plus_amount_over_length() { + scroll_view(setup(), 10, 3); + } + + #[test] + #[should_panic(expected = "Scroll offset is 3, but length of VecDeque is 0.")] + fn test_zero_length_input_some_offset() { + scroll_view(vec![].iter().copied().collect::>(), 3, 0); + } + + #[test] + fn test_zero_length_input_no_offset() { + let empty_deq: VecDeque = scroll_view(VecDeque::from([]), 0, 3); + + assert_eq!(empty_deq, VecDeque::from([])); + } + + #[test] + fn test_no_offset_no_amount() { + let empty_deq: VecDeque = scroll_view(VecDeque::from([]), 0, 0); + + assert_eq!(empty_deq, VecDeque::from([])); + } + + #[test] + fn test_offset_1_all_elements() { + assert_eq!(scroll_view(setup(), 1, 0), VecDeque::from([2, 3, 4, 5])); + } + + #[test] + fn test_no_offset_some_amount() { + assert_eq!(scroll_view(setup(), 0, 3), VecDeque::from([1, 2, 3])); + } + + #[test] + fn test_some_offset_some_amount() { + assert_eq!(scroll_view(setup(), 2, 2), VecDeque::from([3, 4])); + } + + #[test] + fn test_offset_and_amount_centered() { + assert_eq!(scroll_view(setup(), 1, 3), VecDeque::from([2, 3, 4])); + } +}