Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

55 changed files with 1327 additions and 3480 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
/target
/target
Cargo.lock

747
Cargo.lock generated
View File

@ -1,747 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cancellation"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7a879c84c21f354f13535f87ad119ac3be22ebb9097b552a0af6a78f86628c4"
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c911b090850d79fc64fe9ea01e28e465f65e821e08813ced95bced72f7a8a9b"
dependencies = [
"bitflags",
"clap_derive",
"clap_lex",
"is-terminal",
"once_cell",
"strsim",
"termcolor",
]
[[package]]
name = "clap_derive"
version = "4.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a932373bab67b984c790ddf2c9ca295d8e3af3b7ef92de5a5bacdccdee4b09b"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.10",
]
[[package]]
name = "clap_lex"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "crossterm"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
dependencies = [
"bitflags",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
dependencies = [
"winapi",
]
[[package]]
name = "ctrlc"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639"
dependencies = [
"nix",
"windows-sys",
]
[[package]]
name = "derive_more"
version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn 1.0.109",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "downcast"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
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 = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "fragile"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
[[package]]
name = "getrandom"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "io-lifetimes"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "is-terminal"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e"
dependencies = [
"hermit-abi",
"io-lifetimes",
"rustix",
"windows-sys",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "lock_api"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "mio"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
]
[[package]]
name = "mockall"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326"
dependencies = [
"cfg-if",
"downcast",
"fragile",
"lazy_static",
"mockall_derive",
"predicates",
"predicates-tree",
]
[[package]]
name = "mockall_derive"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "nix"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"static_assertions",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "os_str_bytes"
version = "6.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
dependencies = [
"cfg-if",
"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 = "predicates"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
dependencies = [
"difflib",
"float-cmp",
"itertools",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
[[package]]
name = "predicates-tree"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro2"
version = "1.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
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 = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.36.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
[[package]]
name = "signal-hook"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[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",
]
[[package]]
name = "syn"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "termtree"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
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",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[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_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[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_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "wipe"
version = "2.1.1"
dependencies = [
"approx",
"cancellation",
"clap",
"crossterm",
"ctrlc",
"derive_more",
"mockall",
"rand",
]

View File

@ -1,20 +1,17 @@
# Don't forget to update PKGBUILD
[package]
name = "wipe"
version = "2.1.1"
version = "1.0.0"
edition = "2021"
description = "Wipe the content of your terminal."
license = "MIT"
description = "Wipe your terminal with a random animation."
repository = "https://github.com/ricoriedel/wipe"
authors = ["Rico Riedel <rico.riedel@protonmail.ch>"]
authors = ["Rico Riedel"]
[dependencies]
clap = { version = "4.0", features = ["derive"] }
crossterm = "0.26"
ctrlc = "3.2"
cancellation = "0.1"
derive_more = "0.99"
anyhow = "1.0"
clap = { version = "3.1", features = ["derive"]}
crossterm = "0.23"
rand = "0.8"
[dev-dependencies]
mockall = "0.11"
approx = "0.5"
mockall = "0.11"

View File

@ -1,42 +1,18 @@
# wipe
Wipe the content of your terminal with a random animation.
# Wipe
Wipe your terminal with a smooth animation.
This is a fancy alternative to the `clear` command.
It plays randomly generated beautiful animations.
This is the perfect program for you, if you like `clear` but want to add an unnecessary animation.
### Build & install
Building this project requires Rust and Cargo to be installed.
```shell
cargo build --release
```
```shell
cp ./target/release/wipe /usr/local/bin
```
Download options can be found in the [release tab](https://github.com/ricoriedel/wipe/releases).
### Shell Integration
There are scripts for different shells which can be sourced to replace `clear` and `CTRL+L` with this program.
The scripts are located in `misc/shell/`.
## Configuration
All configuration is done using command line parameters.
If you want a persistent solution, you can add an alias to your `.bashrc` equivalent.
| Shell | Script |
|:------|:-------------|
| ZSH | `wipe.zsh` |
| Fish | `wipe.fish` |
### Arch Linux
There is an [AUR package](https://aur.archlinux.org/packages/wipe-term) called `wipe-term`.
The scripts can be integrated as follows:
#### ZSH
Put this into your `.zshrc`:
```shell
source /usr/share/zsh/plugins/wipe/wipe.zsh
```
#### Fish
The package will place the script under `/usr/share/fish/vendor_conf.d/`
which will be sourced by `fish` with no further configuration required.
For a list of parameters, execute `wipe -h`.
Note that some parameters like `--color` can be specified multiple times with different values.
## Showcase
[![Animation 1](misc/res/rec-1.gif)]()
[![Animation 2](misc/res/rec-2.gif)]()
[![Animation 3](misc/res/rec-3.gif)]()
[![Circle](doc/circle.gif)]()
[![Rhombus](doc/rhombus.gif)]()
[![Rotation](doc/rotation.gif)]()

28
dist/PKGBUILD vendored Normal file
View File

@ -0,0 +1,28 @@
pkgname='terminal-wipe-git'
pkgver='1.0.0'
pkgrel='2'
pkgdesc='Wipe your terminal with a random animation.'
arch=('x86_64')
url='https://github.com/ricoriedel/wipe'
license=('MIT')
makedepends=('rust')
conflicts=('wipe')
source=('git+https://www.github.com/ricoriedel/wipe.git')
sha256sums=('SKIP')
build() {
cd wipe
cargo build --release
}
check() {
cd wipe
cargo test --release
}
package() {
mkdir -p "$pkgdir/usr/bin"
mkdir -p "$pkgdir/usr/share/licenses/$pkgname"
mv 'wipe/target/release/wipe' "$pkgdir/usr/bin"
mv 'wipe/LICENSE' "$pkgdir/usr/share/licenses/$pkgname"
}

BIN
doc/circle.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
doc/rhombus.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

BIN
doc/rotation.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

View File

@ -1,23 +0,0 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Sapien faucibus et molestie ac.
Arcu odio ut sem nulla pharetra. Varius vel pharetra vel turpis nunc eget lorem
dolor. Vitae elementum curabitur vitae nunc sed velit dignissim sodales. Felis
donec et odio pellentesque diam volutpat commodo sed egestas. Nam at lectus
urna duis convallis. Ac turpis egestas sed tempus urna et. Diam quis enim
lobortis scelerisque fermentum dui faucibus. Et egestas quis ipsum suspendisse
ultrices gravida. Nec dui nunc mattis enim ut. Fermentum posuere urna nec
tincidunt praesent semper feugiat nibh sed.
Aliquet enim tortor at auctor urna nunc. Vulputate enim nulla aliquet porttitor
lacus luctus accumsan tortor. Est velit egestas dui id ornare arcu. Luctus
accumsan tortor posuere ac ut. At in tellus integer feugiat scelerisque varius
morbi enim. Et netus et malesuada fames ac turpis egestas. Aliquet nec
ullamcorper sit amet risus nullam eget. Sit amet facilisis magna etiam tempor
orci eu. Amet cursus sit amet dictum sit amet justo. Nec ullamcorper sit amet
risus nullam eget. Justo eget magna fermentum iaculis. Libero justo laoreet sit
amet cursus. Egestas maecenas pharetra convallis posuere. Quis hendrerit dolor
magna eget est. Cursus eget nunc scelerisque viverra mauris in aliquam sem
fringilla. Amet est placerat in egestas erat imperdiet sed euismod nisi. In
aliquam sem fringilla ut morbi tincidunt augue interdum. Mollis aliquam ut
porttitor leo. Venenatis urna cursus eget nunc scelerisque viverra mauris in.
Egestas quis ipsum suspendisse ultrices gravida dictum fusce ut.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -1,16 +0,0 @@
#!/usr/bin/env sh
cat placeholder.txt
sleep 1
wipe \
--char-pattern circle \
--char-invert false \
--char-segments 3 \
--char-shrink 2 \
--char-swap false \
--color-pattern wheel \
--color-segments 2 \
--color-invert false \
--color-shift true \
--color-swap false \
--colors rainbow

View File

@ -1,16 +0,0 @@
#!/usr/bin/env sh
cat placeholder.txt
sleep 1
wipe \
--char-pattern wheel \
--char-invert false \
--char-segments 2 \
--char-shrink 2 \
--char-swap false \
--color-pattern circle \
--color-segments 4 \
--color-invert false \
--color-shift false \
--color-swap false \
--colors dark-magenta

View File

@ -1,16 +0,0 @@
#!/usr/bin/env sh
cat placeholder.txt
sleep 1
wipe \
--char-pattern rhombus \
--char-invert true \
--char-segments 2 \
--char-shrink 2 \
--char-swap false \
--color-pattern wheel \
--color-segments 2 \
--color-invert true \
--color-shift true \
--color-swap false \
--colors cyan

View File

@ -1,7 +0,0 @@
#!/usr/bin/env fish
function clear
command wipe
end
bind \cl 'wipe; commandline -f repaint'

View File

@ -1,8 +0,0 @@
alias clear='wipe'
_wipe() {
wipe
zle reset-prompt
}
zle -N _wipe
bindkey '^l' _wipe

51
src/animation/circle.rs Normal file
View File

@ -0,0 +1,51 @@
use crate::animation::Animation;
use crate::vec::Vector;
const THICKNESS: f32 = 0.2;
const FINAL_RADIUS: f32 = 1.0 + THICKNESS * 2.0;
pub struct CircleAnimation {
center: Vector,
thickness: f32,
final_radius: f32,
}
impl CircleAnimation {
pub fn new(size: Vector) -> Self {
let center = size.center();
let distance = center.length();
Self {
center,
thickness: distance * THICKNESS,
final_radius: distance * FINAL_RADIUS,
}
}
}
impl Animation for CircleAnimation {
fn sample(&self, step: f32, pos: Vector) -> f32 {
let radius = self.final_radius * step - self.thickness;
let distance = (pos - self.center).length();
(distance - radius) / self.thickness
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn sample() {
let anim = CircleAnimation::new(Vector::new(10.0, 20.0));
let sample_1 = anim.sample(0.5, Vector::new(17.0, 5.0));
let sample_2 = anim.sample(0.8, Vector::new(11.0, 8.0));
let sample_3 = anim.sample(0.2, Vector::new(7.0, 10.0));
assert!(3.3 < sample_1 && sample_1 < 3.4);
assert!(-1.8 < sample_2 && sample_2 < -1.7);
assert!(0.4 < sample_3 && sample_3 < 0.5);
}
}

12
src/animation/mod.rs Normal file
View File

@ -0,0 +1,12 @@
pub mod circle;
pub mod rotation;
pub mod rhombus;
use crate::vec::Vector;
use mockall::automock;
#[automock]
pub trait Animation {
fn sample(&self, step: f32, pos: Vector) -> f32;
}

51
src/animation/rhombus.rs Normal file
View File

@ -0,0 +1,51 @@
use crate::animation::Animation;
use crate::vec::Vector;
const THICKNESS: f32 = 0.2;
const FINAL_DISTANCE: f32 = 1.0 + THICKNESS * 2.0;
pub struct RhombusAnimation {
center: Vector,
thickness: f32,
final_distance: f32,
}
impl RhombusAnimation {
pub fn new(size: Vector) -> Self {
let center = size.center();
let distance = center.sum();
Self {
center,
thickness: distance * THICKNESS,
final_distance: distance * FINAL_DISTANCE,
}
}
}
impl Animation for RhombusAnimation {
fn sample(&self, step: f32, pos: Vector) -> f32 {
let dist = self.final_distance * step - self.thickness;
let pos_dist = (self.center - pos).abs().sum();
(pos_dist - dist) / self.thickness
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn sample() {
let anim = RhombusAnimation::new(Vector::new(30.0, 10.0));
let sample_1 = anim.sample(0.2, Vector::new(5.0, 16.0));
let sample_2 = anim.sample(0.7, Vector::new(22.0, 2.0));
let sample_3 = anim.sample(0.5, Vector::new(4.0, 7.0));
assert!(4.8 < sample_1 && sample_1 < 4.9);
assert!(-1.5 < sample_2 && sample_2 < -1.4);
assert!(0.7 < sample_3 && sample_3 < 0.8);
}
}

46
src/animation/rotation.rs Normal file
View File

@ -0,0 +1,46 @@
use std::f32::consts::PI;
use crate::animation::Animation;
use crate::vec::Vector;
const TWO_PI: f32 = PI * 2.0;
const THICKNESS: f32 = TWO_PI * 0.1;
const FULL_ROTATION: f32 = TWO_PI + THICKNESS * 2.0;
pub struct RotationAnimation {
center: Vector
}
impl RotationAnimation {
pub fn new(size: Vector) -> Self {
Self {
center: size.center()
}
}
}
impl Animation for RotationAnimation {
fn sample(&self, step: f32, pos: Vector) -> f32 {
let angle = FULL_ROTATION * step - PI - THICKNESS;
let pos_angle = (pos - self.center).angle();
(pos_angle - angle) / THICKNESS
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn sample() {
let anim = RotationAnimation::new(Vector::new(30.0, 10.0));
let sample_1 = anim.sample(0.2, Vector::new(5.0, 16.0));
let sample_2 = anim.sample(0.7, Vector::new(22.0, 2.0));
let sample_3 = anim.sample(0.5, Vector::new(4.0, 7.0));
assert!(2.4 < sample_1 && sample_1 < 2.5);
assert!(0.7 < sample_2 && sample_2 < 0.8);
assert!(-2.3 < sample_3 && sample_3 < -2.2);
}
}

98
src/array.rs Normal file
View File

@ -0,0 +1,98 @@
use std::ops::{Index, IndexMut};
/// A two dimensional statically size array.
pub struct Array2D<T> {
width: usize,
height: usize,
values: Vec<T>
}
impl<T: Default + Copy> Array2D<T> {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
values: vec![T::default(); width * height]
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
/// Calculates the physical index of the given position.
///
/// # Panics
/// Panics if the position is out of bounds.
fn index_of(&self, pos: (usize, usize)) -> usize {
assert!(pos.0 < self.width);
assert!(pos.1 < self.height);
pos.0 + pos.1 * self.width
}
}
impl<T: Default + Copy> Index<(usize, usize)> for Array2D<T> {
type Output = T;
fn index(&self, pos: (usize, usize)) -> &Self::Output {
unsafe { self.values.get_unchecked(self.index_of(pos)) }
}
}
impl<T: Default + Copy> IndexMut<(usize, usize)> for Array2D<T> {
fn index_mut(&mut self, pos: (usize, usize)) -> &mut Self::Output {
let i = self.index_of(pos);
unsafe { self.values.get_unchecked_mut(i) }
}
}
#[cfg(test)]
mod test {
use crate::array::Array2D;
#[test]
fn width() {
let array = Array2D::<()>::new(10, 4);
assert_eq!(10, array.width());
}
#[test]
fn height() {
let array = Array2D::<()>::new(2, 5);
assert_eq!(5, array.height());
}
#[test]
fn index() {
let mut array = Array2D::new(4, 4);
array[(1, 2)] = 3;
array[(3, 3)] = 7;
assert_eq!(3, array[(1, 2)]);
assert_eq!(7, array[(3, 3)]);
}
#[test]
#[should_panic]
fn index_oob_width() {
let array = Array2D::<()>::new(5, 10);
array[(8, 2)];
}
#[test]
#[should_panic]
fn index_oob_height() {
let array = Array2D::<()>::new(10, 5);
array[(3, 7)];
}
}

58
src/char.rs Normal file
View File

@ -0,0 +1,58 @@
use mockall::automock;
/// Used to get a character with a given brightness.
#[automock]
pub trait CharSampler {
/// Gets a character with the given brightness.
/// # Arguments
/// * `level`: `0 <= level` and `level < 1`
fn sample(&self, level: f32) -> char;
}
pub struct SimpleCharSampler {
chars: String
}
impl SimpleCharSampler {
/// # Arguments
/// * `chars`: The characters ordered by brightness.
pub fn new(chars: String) -> Self {
Self { chars }
}
}
impl CharSampler for SimpleCharSampler {
fn sample(&self, level: f32) -> char {
assert!(0.0 <= level && level < 1.0);
let index = level * self.chars.chars().count() as f32;
self.chars.chars().nth(index as usize).unwrap()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn sample() {
let sampler = SimpleCharSampler::new("abc".to_string());
assert_eq!('a', sampler.sample(0.1));
assert_eq!('b', sampler.sample(0.4));
assert_eq!('c', sampler.sample(0.7));
}
#[test]
#[should_panic]
fn sample_index_negative() {
SimpleCharSampler::new("abc".to_string()).sample(-0.1);
}
#[test]
#[should_panic]
fn sample_index_equals_one() {
SimpleCharSampler::new("abc".to_string()).sample(1.0);
}
}

61
src/choose.rs Normal file
View File

@ -0,0 +1,61 @@
use rand::prelude::IteratorRandom;
use rand::Rng;
pub trait Options {
fn all() -> Vec<Self> where Self: Sized;
}
pub struct Chooser<TRng> {
rng: TRng
}
impl<TRng: Rng> Chooser<TRng> {
pub fn new(rng: TRng) -> Self {
Self { rng }
}
pub fn choose<TValue: Options>(&mut self, selection: Vec<TValue>) -> TValue {
let options = if selection.is_empty() {
TValue::all()
} else {
selection
};
options.into_iter().choose_stable(&mut self.rng).unwrap()
}
}
#[cfg(test)]
mod test {
use rand::rngs::mock::StepRng;
use crate::{Chooser, Options};
enum MockOptions {
First,
Second,
Third
}
impl Options for MockOptions {
fn all() -> Vec<Self> where Self: Sized {
use MockOptions::*;
vec![First, Second, Third]
}
}
#[test]
fn choose() {
let rng = StepRng::new(0, 1);
let mut chooser = Chooser::new(rng);
assert!(matches!(chooser.choose(vec![MockOptions::First, MockOptions::Second]), MockOptions::Second));
}
#[test]
fn choose_empty() {
let rng = StepRng::new(0, 1);
let mut chooser = Chooser::new(rng);
assert!(matches!(chooser.choose(Vec::new()), MockOptions::Third));
}
}

58
src/color.rs Normal file
View File

@ -0,0 +1,58 @@
use crossterm::style::Color;
use mockall::automock;
/// A collection of colors.
#[automock]
pub trait ColorSampler {
/// Gets a color for the given fill.
/// # Arguments
/// * `fill`: `0 <= fill` and `fill < 1`
fn sample(&self, fill: f32) -> Color;
}
pub struct SimpleColorSampler {
values: Vec<Color>
}
impl SimpleColorSampler {
pub fn new(values: Vec<Color>) -> Self {
Self { values }
}
}
impl ColorSampler for SimpleColorSampler {
fn sample(&self, fill: f32) -> Color {
assert!(0.0 <= fill && fill < 1.0);
let index = self.values.len() as f32 * fill;
self.values[index as usize]
}
}
#[cfg(test)]
mod test {
use crossterm::style::Color::*;
use super::*;
#[test]
fn sample() {
let sampler = SimpleColorSampler::new(vec![Red, Yellow, Green]);
assert_eq!(Red, sampler.sample(0.1));
assert_eq!(Yellow, sampler.sample(0.4));
assert_eq!(Green, sampler.sample(0.7));
}
#[test]
#[should_panic]
fn sample_index_negative() {
SimpleColorSampler::new(Vec::new()).sample(-0.1);
}
#[test]
#[should_panic]
fn sample_index_equals_one() {
SimpleColorSampler::new(Vec::new()).sample(1.0);
}
}

View File

@ -1,74 +0,0 @@
/// A sample for a terminal cell.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum CharSample {
/// Keep the char.
Keep,
/// Override the char.
Draw(char),
/// Clear the char.
Clear,
}
/// A trait to convert a sample to a [CharSample].
#[cfg_attr(test, mockall::automock)]
pub trait CharConverter {
fn convert(&self, level: f32) -> CharSample;
}
/// The implementation of [CharConverter].
pub struct CharConverterImpl {
chars: Vec<char>,
}
impl CharConverterImpl {
/// The chars used for mapping.
pub fn new(chars: String) -> Self {
let chars = chars.chars().collect();
Self { chars }
}
}
impl CharConverter for CharConverterImpl {
fn convert(&self, level: f32) -> CharSample {
if level < 0.0 {
CharSample::Clear
} else if level < 1.0 {
let len = self.chars.len() as f32;
let index = (level * len) as usize;
CharSample::Draw(self.chars[index])
} else {
CharSample::Keep
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn convert_clear() {
let converter = CharConverterImpl::new("abc".to_string());
assert_eq!(CharSample::Clear, converter.convert(-0.1));
}
#[test]
fn convert_draw() {
let converter = CharConverterImpl::new("xyz".to_string());
assert_eq!(CharSample::Draw('x'), converter.convert(0.0));
assert_eq!(CharSample::Draw('y'), converter.convert(0.5));
assert_eq!(CharSample::Draw('z'), converter.convert(0.9));
}
#[test]
fn convert_keep() {
let converter = CharConverterImpl::new("123".to_string());
assert_eq!(CharSample::Keep, converter.convert(1.0));
assert_eq!(CharSample::Keep, converter.convert(1.5));
}
}

View File

@ -1,69 +0,0 @@
use crossterm::style::Color;
/// A trait to convert a sample to a [Color].
#[cfg_attr(test, mockall::automock)]
pub trait ColorConverter {
fn convert(&self, level: f32) -> Color;
}
/// The implementation of [ColorConverter].
pub struct ColorConverterImpl {
colors: Vec<Color>,
}
impl ColorConverterImpl {
/// The colors used for mapping.
pub fn new(colors: Vec<Color>) -> Self {
Self { colors }
}
}
impl ColorConverter for ColorConverterImpl {
fn convert(&self, level: f32) -> Color {
let len = self.colors.len() as f32;
let index = (level * len).rem_euclid(len) as usize;
self.colors[index]
}
}
#[cfg(test)]
mod test {
use super::*;
use crossterm::style::Color::*;
#[test]
fn convert_negative_index() {
let converter = ColorConverterImpl::new(vec![Red, Green, Blue]);
assert_eq!(Blue, converter.convert(-0.2));
}
#[test]
fn convert_index_zero() {
let converter = ColorConverterImpl::new(vec![Red, Green, Blue]);
assert_eq!(Red, converter.convert(0.0));
}
#[test]
fn convert() {
let converter = ColorConverterImpl::new(vec![Red, Green, Blue]);
assert_eq!(Green, converter.convert(0.5));
}
#[test]
fn convert_index_one() {
let converter = ColorConverterImpl::new(vec![Red, Green, Blue]);
assert_eq!(Red, converter.convert(1.0));
}
#[test]
fn convert_index_above_one() {
let converter = ColorConverterImpl::new(vec![Red, Green, Blue]);
assert_eq!(Green, converter.convert(1.5));
}
}

View File

@ -1,72 +0,0 @@
//! Contains structs for converting samples to concrete types.
mod char;
mod color;
pub use crate::convert::char::*;
pub use crate::convert::color::*;
use crossterm::style::Color;
/// A trait to convert samples to concrete types.
#[cfg_attr(test, mockall::automock)]
pub trait Converter {
/// Converts a sample to a [CharSample].
fn char(&self, level: f32) -> CharSample;
/// Converts a sample to a [Color].
fn color(&self, level: f32) -> Color;
}
/// The implementation of [Converter].
#[derive(derive_more::Constructor)]
pub struct ConverterImpl<T1, T2> {
char: T1,
color: T2,
}
impl<T1: CharConverter, T2: ColorConverter> Converter for ConverterImpl<T1, T2> {
fn char(&self, level: f32) -> CharSample {
self.char.convert(level)
}
fn color(&self, level: f32) -> Color {
self.color.convert(level)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::convert::MockCharConverter;
use crate::convert::MockColorConverter;
use mockall::predicate::*;
#[test]
fn char() {
let mut char = MockCharConverter::new();
let color = MockColorConverter::new();
char.expect_convert()
.with(eq(4.0))
.return_const(CharSample::Draw('M'));
let converter = ConverterImpl::new(char, color);
assert_eq!(CharSample::Draw('M'), converter.char(4.0));
}
#[test]
fn color() {
let char = MockCharConverter::new();
let mut color = MockColorConverter::new();
color
.expect_convert()
.with(eq(2.0))
.return_const(Color::Yellow);
let converter = ConverterImpl::new(char, color);
assert_eq!(Color::Yellow, converter.color(2.0));
}
}

View File

@ -1,49 +0,0 @@
use std::fmt::{Debug, Formatter};
/// The error type.
pub struct Error(String);
impl Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error(err.to_string())
}
}
impl From<&str> for Error {
fn from(msg: &str) -> Self {
Error(msg.to_string())
}
}
impl From<ctrlc::Error> for Error {
fn from(err: ctrlc::Error) -> Self {
Error(err.to_string())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn from_std_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::Other, "123");
let msg = io_err.to_string();
let err: Error = io_err.into();
assert_eq!(msg, format!("{:?}", err));
}
#[test]
fn from_ref_str() {
let err: Error = "123".into();
assert_eq!("123", format!("{:?}", err));
}
}

View File

@ -1,196 +0,0 @@
use crate::Error;
use crate::Renderer;
use cancellation::CancellationToken;
use std::thread;
use std::time::{Duration, Instant};
/// A stub for the system clock.
#[cfg_attr(test, mockall::automock)]
pub trait Clock {
/// Returns the current time.
fn now(&self) -> Instant;
/// Sleep for the given duration.
fn sleep(&self, duration: Duration);
}
/// The implementation of [Clock].
#[derive(derive_more::Constructor)]
pub struct ClockImpl;
impl Clock for ClockImpl {
fn now(&self) -> Instant {
Instant::now()
}
fn sleep(&self, duration: Duration) {
thread::sleep(duration)
}
}
/// A timer for rendering.
#[derive(derive_more::Constructor)]
pub struct Executor<T> {
clock: T,
duration: Duration,
delay: Duration,
}
impl<T: Clock> Executor<T> {
/// Runs the animation main loop.
pub fn run(&self, mut renderer: impl Renderer, token: &CancellationToken) -> Result<(), Error> {
let start = self.clock.now();
let mut tick = start;
while !token.is_canceled() && tick.duration_since(start) < self.duration {
let step = tick.duration_since(start).as_secs_f32() / self.duration.as_secs_f32();
renderer.render(step)?;
tick = self.delay(tick);
}
Ok(())
}
/// Sleeps until the next frame starts.
/// Returns the current time.
fn delay(&self, begin: Instant) -> Instant {
let end = self.clock.now();
if self.delay > end.duration_since(begin) {
self.clock.sleep(self.delay - end.duration_since(begin));
}
self.clock.now()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MockRenderer;
use cancellation::CancellationTokenSource;
use mockall::predicate::eq;
use mockall::Sequence;
#[test]
fn run_steps_correct() {
let mut clock = MockClock::new();
let clock_seq = &mut Sequence::new();
let begin = Instant::now();
clock
.expect_now()
.once()
.return_const(begin)
.in_sequence(clock_seq);
clock
.expect_now()
.times(2)
.return_const(begin + Duration::from_secs(10))
.in_sequence(clock_seq);
clock
.expect_now()
.times(2)
.return_const(begin + Duration::from_secs(20))
.in_sequence(clock_seq);
let timer = Executor::new(clock, Duration::from_secs(20), Duration::from_secs(10));
let mut renderer = MockRenderer::new();
let renderer_seq = &mut Sequence::new();
renderer
.expect_render()
.with(eq(0.0))
.once()
.returning(|_| Ok(()))
.in_sequence(renderer_seq);
renderer
.expect_render()
.with(eq(0.5))
.once()
.returning(|_| Ok(()))
.in_sequence(renderer_seq);
timer.run(renderer, CancellationToken::none()).unwrap();
}
#[test]
fn run_sleep_duration_correct() {
let mut clock = MockClock::new();
let clock_seq = &mut Sequence::new();
let begin = Instant::now();
clock
.expect_now()
.once()
.return_const(begin)
.in_sequence(clock_seq);
clock
.expect_now()
.once()
.return_const(begin + Duration::from_secs(4))
.in_sequence(clock_seq);
clock
.expect_sleep()
.once()
.with(eq(Duration::from_secs(6)))
.return_const(())
.in_sequence(clock_seq);
clock
.expect_now()
.once()
.return_const(begin + Duration::from_secs(10))
.in_sequence(clock_seq);
let timer = Executor::new(clock, Duration::from_secs(10), Duration::from_secs(10));
let mut renderer = MockRenderer::new();
renderer.expect_render().returning(|_| Ok(()));
timer.run(renderer, CancellationToken::none()).unwrap();
}
#[test]
fn run_delay_exceeded_does_not_sleep() {
let mut clock = MockClock::new();
let clock_seq = &mut Sequence::new();
let begin = Instant::now();
clock
.expect_now()
.once()
.return_const(begin)
.in_sequence(clock_seq);
clock
.expect_now()
.times(2)
.return_const(begin + Duration::from_secs(12))
.in_sequence(clock_seq);
let timer = Executor::new(clock, Duration::from_secs(10), Duration::from_secs(10));
let mut renderer = MockRenderer::new();
renderer.expect_render().returning(|_| Ok(()));
timer.run(renderer, CancellationToken::none()).unwrap();
}
#[test]
fn run_canceled_aborts() {
let mut clock = MockClock::new();
let mut renderer = MockRenderer::new();
let src = CancellationTokenSource::new();
let token = src.token().clone();
clock.expect_now().return_const(Instant::now());
clock.expect_sleep().once().return_const(());
renderer.expect_render().once().returning(move |_| {
src.cancel();
Ok(())
});
let timer = Executor::new(clock, Duration::from_secs(10), Duration::from_secs(1));
timer.run(renderer, &token).unwrap();
}
}

40
src/fill/circle.rs Normal file
View File

@ -0,0 +1,40 @@
use crate::fill::FillMode;
use crate::vec::Vector;
const INTERVAL: f32 = 4.0;
pub struct CircleFillMode {
center: Vector,
interval: f32
}
impl CircleFillMode {
pub fn new(size: Vector) -> Self {
Self {
center: size.center(),
interval: size.smaller() / INTERVAL,
}
}
}
impl FillMode for CircleFillMode {
fn sample(&self, _: f32, pos: Vector) -> f32 {
((pos - self.center).length() % self.interval) / self.interval
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn sample() {
let fill = CircleFillMode::new(Vector::new(10.0, 8.0));
let sample_1 = fill.sample(0.0, Vector::new(5.0, 3.0));
let sample_2 = fill.sample(0.0, Vector::new(8.5, 4.0));
assert!(0.4 < sample_1 && sample_1 < 0.6);
assert!(0.7 < sample_2 && sample_2 < 0.8);
}
}

29
src/fill/level.rs Normal file
View File

@ -0,0 +1,29 @@
use crate::fill::FillMode;
use crate::vec::Vector;
pub struct LevelFillMode;
impl LevelFillMode {
pub fn new() -> Self {
Self { }
}
}
impl FillMode for LevelFillMode {
fn sample(&self, level: f32, _: Vector) -> f32 {
level
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn sample() {
let mode = LevelFillMode::new();
assert_eq!(0.3, mode.sample(0.3, Vector::new(0.0, 0.0)));
assert_eq!(0.7, mode.sample(0.7, Vector::new(0.1, 0.2)));
}
}

14
src/fill/mod.rs Normal file
View File

@ -0,0 +1,14 @@
pub mod level;
pub mod circle;
pub mod stripes;
use crate::vec::Vector;
use mockall::automock;
/// Used to choose the colors of characters.
#[automock]
pub trait FillMode {
/// Gets the color for this character.
fn sample(&self, level: f32, pos: Vector) -> f32;
}

35
src/fill/stripes.rs Normal file
View File

@ -0,0 +1,35 @@
use crate::FillMode;
use crate::vec::Vector;
const INTERVAL: f32 = 4.0;
pub struct StripesFillMode {
interval: f32
}
impl StripesFillMode {
pub fn new(size: Vector) -> Self {
Self {
interval: size.smaller() / INTERVAL
}
}
}
impl FillMode for StripesFillMode {
fn sample(&self, _: f32, pos: Vector) -> f32 {
(pos.sum() % self.interval) / self.interval
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn sample() {
let mode = StripesFillMode::new(Vector::new(8.0, 4.0));
assert_eq!(0.25, mode.sample(0.0, Vector::new(1.5, 0.75)));
assert_eq!(0.5, mode.sample(0.0, Vector::new(4.0, 2.5)));
}
}

View File

@ -1,453 +1,151 @@
pub mod convert;
pub mod pattern;
pub mod transform;
mod error;
mod exec;
mod printer;
mod renderer;
mod term;
mod vec;
pub use error::*;
pub use exec::*;
pub use printer::*;
pub use renderer::*;
pub use term::*;
pub use vec::*;
use crate::convert::*;
use crate::pattern::*;
use crate::transform::*;
use cancellation::CancellationTokenSource;
use clap::builder::NonEmptyStringValueParser;
use clap::{value_parser, Parser, ValueEnum};
use crossterm::style::Color;
use crossterm::style::Color::*;
use rand::prelude::*;
use std::io::stdout;
use std::time::Duration;
use anyhow::Error;
use clap::Parser;
use clap::ArgEnum;
use rand::rngs::OsRng;
use crate::animation::Animation;
use crate::animation::circle::CircleAnimation;
use crate::animation::rhombus::RhombusAnimation;
use crate::animation::rotation::RotationAnimation;
use crate::char::SimpleCharSampler;
use crate::choose::{Chooser, Options};
use crate::color::{ColorSampler, SimpleColorSampler};
use crate::fill::circle::CircleFillMode;
use crate::fill::FillMode;
use crate::fill::level::LevelFillMode;
use crate::fill::stripes::StripesFillMode;
use crate::render::{Renderer, SamplerRenderer};
use crate::runner::Runner;
use crate::sampler::ComposedSampler;
use crate::surface::WriteSurface;
use crate::timer::SimpleTimer;
use crate::vec::Vector;
/// The command line arguments.
#[derive(Parser, Default)]
#[command(
author = env!("CARGO_PKG_AUTHORS"),
version = env!("CARGO_PKG_VERSION"),
about = env!("CARGO_PKG_DESCRIPTION"),
)]
struct Args {
/// Set the animation duration as milliseconds
#[arg(
long,
default_value_t = 2000,
value_parser = value_parser!(u64).range(0..=60_000),
help = "Set the animation duration [milliseconds]"
)]
duration: u64,
/// Set the frames per second
#[arg(long, default_value_t = 60, value_parser = value_parser!(u64).range(1..=480))]
fps: u64,
/// Choose the chars used to draw the pattern
#[arg(long, default_value = ".:+#", value_parser = NonEmptyStringValueParser::new())]
chars: String,
/// Choose the pattern
#[arg(long, value_enum)]
char_pattern: Option<PatternEnum>,
/// Choose whether to invert the pattern
#[arg(long)]
char_invert: Option<bool>,
/// Choose whether to swap the x-axis and y-axis of the pattern
#[arg(long)]
char_swap: Option<bool>,
/// Choose the segment count of the pattern [default: 1-4]
#[arg(long, value_parser = value_parser!(u8).range(1..255))]
char_segments: Option<u8>,
/// Choose the factor by which to shrink the pattern [default: 1-4]
#[arg(long, value_parser = value_parser!(u8).range(1..255))]
char_shrink: Option<u8>,
/// Choose the colors used for the pattern
#[arg(long, value_enum)]
colors: Option<PalletEnum>,
/// Choose the fill pattern
#[arg(long, value_enum)]
color_pattern: Option<PatternEnum>,
/// Choose whether the fill pattern should move
#[arg(long)]
color_shift: Option<bool>,
/// Choose whether to invert the fill pattern
#[arg(long)]
color_invert: Option<bool>,
/// Choose whether to swap the x-axis and y-axis of the fill pattern
#[arg(long)]
color_swap: Option<bool>,
/// Choose the segment count of the fill pattern [default: 1-4]
#[arg(long, value_parser = value_parser!(u8).range(1..255))]
color_segments: Option<u8>,
mod color;
mod char;
mod fill;
mod vec;
mod array;
mod surface;
mod animation;
mod sampler;
mod render;
mod timer;
mod runner;
mod choose;
macro_rules! options {
($name:ident { $($opt:ident,)* }) => {
#[derive(Copy, Clone, ArgEnum)]
enum $name {
$($opt,)*
}
impl Options for $name {
fn all() -> Vec<Self> {
vec![$($name::$opt,)*]
}
}
}
}
/// All color pallets.
#[derive(ValueEnum, Copy, Clone)]
enum PalletEnum {
options!(AnimationType {
Circle,
Rhombus,
Rotation,
});
options!(ColorType {
Red,
Yellow,
Green,
Blue,
Magenta,
Cyan,
LightRed,
LightGreen,
LightBlue,
Grey,
Rainbow,
});
DarkRed,
DarkYellow,
DarkGreen,
DarkBlue,
DarkMagenta,
DarkCyan,
DarkRainbow,
RedYellow,
YellowGreen,
GreenBlue,
BlueCyan,
CyanMagenta,
MagentaRed,
Gray,
}
/// All possible [Pattern]s.
#[derive(ValueEnum, Copy, Clone, PartialEq, Debug)]
enum PatternEnum {
options!(FillModeType {
Circle,
Line,
Rhombus,
Wheel,
}
Level,
Stripes,
});
/// A configuration for a composed [Pattern].
#[derive(derive_more::Constructor)]
struct PatternConfig {
pattern: PatternEnum,
shift: bool,
invert: bool,
swap: bool,
segments: f32,
shrink: f32,
}
impl Args {
/// Returns the configuration for the char [Pattern].
fn char_config(&self, rng: &mut impl Rng) -> PatternConfig {
PatternConfig::new(
choose(self.char_pattern, rng),
true,
self.char_invert.unwrap_or(rng.gen()),
self.char_swap.unwrap_or(rng.gen()),
self.char_segments.unwrap_or(rng.gen_range(1..=4)) as f32,
self.char_shrink.unwrap_or(rng.gen_range(1..=4)) as f32,
)
}
/// Returns the configuration for the color [Pattern].
fn color_config(&self, rng: &mut impl Rng) -> PatternConfig {
PatternConfig::new(
choose(self.color_pattern, rng),
self.color_shift.unwrap_or(rng.gen()),
self.color_invert.unwrap_or(rng.gen()),
self.color_swap.unwrap_or(rng.gen()),
self.color_segments.unwrap_or(rng.gen_range(1..=4)) as f32,
1.0,
)
}
/// Returns the colors for the [ColorConverter].
fn pallet(&self, rng: &mut impl Rng) -> Vec<Color> {
match choose(self.colors, rng) {
PalletEnum::Red => vec![DarkRed, Red, White],
PalletEnum::Yellow => vec![DarkYellow, Yellow, White],
PalletEnum::Green => vec![DarkGreen, Green, White],
PalletEnum::Blue => vec![DarkBlue, Blue, White],
PalletEnum::Magenta => vec![DarkMagenta, Magenta, White],
PalletEnum::Cyan => vec![DarkCyan, Cyan, White],
PalletEnum::Rainbow => vec![Red, Yellow, Green, Blue, Cyan, Magenta],
PalletEnum::DarkRed => vec![Black, DarkRed, Red],
PalletEnum::DarkYellow => vec![Black, DarkYellow, Yellow],
PalletEnum::DarkGreen => vec![Black, DarkGreen, Green],
PalletEnum::DarkBlue => vec![Black, DarkBlue, Blue],
PalletEnum::DarkMagenta => vec![Black, DarkMagenta, Magenta],
PalletEnum::DarkCyan => vec![Black, DarkCyan, Cyan],
PalletEnum::DarkRainbow => vec![
DarkRed,
DarkYellow,
DarkGreen,
DarkBlue,
DarkCyan,
DarkMagenta,
],
PalletEnum::RedYellow => vec![Red, DarkRed, DarkYellow, Yellow],
PalletEnum::YellowGreen => vec![Yellow, DarkYellow, DarkGreen, Green],
PalletEnum::GreenBlue => vec![Green, DarkGreen, DarkBlue, Blue],
PalletEnum::BlueCyan => vec![Blue, DarkBlue, DarkCyan, Cyan],
PalletEnum::CyanMagenta => vec![Cyan, DarkCyan, DarkMagenta, Magenta],
PalletEnum::MagentaRed => vec![Magenta, DarkMagenta, DarkRed, Red],
PalletEnum::Gray => vec![Black, DarkGrey, Grey, White],
}
}
/// Returns the duration for the [Timer].
fn duration(&self) -> Duration {
Duration::from_millis(self.duration)
}
/// Returns the delay for the [Timer].
fn delay(&self) -> Duration {
Duration::from_nanos(1_000_000_000 / self.fps)
}
}
impl PatternConfig {
/// Creates a new base [Pattern].
fn create_base(&self) -> Box<dyn PatternFactory> {
match self.pattern {
PatternEnum::Circle => Box::new(CircleFactory::new()),
PatternEnum::Line => Box::new(LineFactory::new()),
PatternEnum::Rhombus => Box::new(RhombusFactory::new()),
PatternEnum::Wheel => Box::new(WheelFactory::new()),
}
}
/// Creates a new composed [Pattern].
fn create(&self) -> Box<dyn PatternFactory> {
let mut pattern = self.create_base();
if self.shift {
pattern = Box::new(ShiftFactory::new(pattern))
}
if self.invert {
pattern = Box::new(InvertFactory::new(pattern))
}
if self.swap {
pattern = Box::new(SwapFactory::new(pattern))
}
if self.segments != 1.0 {
pattern = Box::new(SegmentsFactory::new(pattern, self.segments));
}
if self.shrink != 1.0 {
pattern = Box::new(ShrinkFactory::new(pattern, self.shrink));
}
pattern
}
}
/// Returns the value of the [Option] or a random enum variant.
fn choose<TValue: ValueEnum, TRand: Rng>(opt: Option<TValue>, rng: &mut TRand) -> TValue {
match opt {
Some(value) => value.clone(),
None => TValue::value_variants().iter().choose(rng).unwrap().clone(),
}
#[derive(Parser)]
#[clap(author = env ! ("CARGO_PKG_AUTHORS"), version = env ! ("CARGO_PKG_VERSION"), about = env ! ("CARGO_PKG_DESCRIPTION"))]
struct Args {
#[clap(short, long, help = "Add animation", arg_enum)]
animation: Vec<AnimationType>,
#[clap(short, long, help = "Add fill mode", arg_enum)]
fill: Vec<FillModeType>,
#[clap(short, long, help = "Add color pallet", arg_enum)]
color: Vec<ColorType>,
#[clap(long, default_value = ".-+%#", help = "Set chars")]
chars: String,
#[clap(long, default_value = "30", help = "Set frames per second")]
fps: u64,
#[clap(long, default_value = "1000", help = "Set duration [milliseconds]")]
duration: u64,
#[clap(long, help = "Set width [default: terminal width]")]
width: Option<usize>,
#[clap(long, help = "Set height [default: terminal height]")]
height: Option<usize>,
}
fn main() -> Result<(), Error> {
let args = Args::parse();
let rand = &mut thread_rng();
let mut chooser = Chooser::new(OsRng::default());
let char = args.char_config(rand).create();
let color = args.color_config(rand).create();
let pallet = args.pallet(rand);
let duration = args.duration();
let delay = args.delay();
let terminal = crossterm::terminal::size()?;
let width = args.width.unwrap_or(terminal.0 as usize);
let height = args.height.unwrap_or(terminal.1 as usize);
let size = Vector::from_terminal(width, height);
let delay = Duration::from_micros(1_000_000 / args.fps);
let duration = Duration::from_millis(args.duration);
let sampler = SamplerFactoryImpl::new(char, color);
let char_converter = CharConverterImpl::new(args.chars);
let color_converter = ColorConverterImpl::new(pallet);
let converter = ConverterImpl::new(char_converter, color_converter);
let term = TerminalImpl::new(stdout());
let printer = PrinterImpl::new(term)?;
let renderer = RendererImpl::new(sampler, converter, printer)?;
let animation = create_animation(chooser.choose(args.animation), size);
let fill = create_fill(chooser.choose(args.fill), size);
let color = create_color(chooser.choose(args.color));
let char = Box::new(SimpleCharSampler::new(args.chars));
let clock = ClockImpl::new();
let executor = Executor::new(clock, duration, delay);
let sampler = ComposedSampler::new(animation, fill, color, char);
let surface = WriteSurface::new(stdout(), width, height);
let src = CancellationTokenSource::new();
let token = src.token().clone();
let renderer = SamplerRenderer::new(surface, sampler);
let timer = SimpleTimer::new(delay);
let runner = Runner::new(duration, timer, renderer);
ctrlc::set_handler(move || {
src.cancel();
})?;
executor.run(renderer, &token)
runner.run()
}
#[cfg(test)]
mod test {
use super::*;
use approx::*;
use rand::rngs::mock::StepRng;
#[test]
fn args_pallet_all_defined() {
let rand = &mut StepRng::new(1, 1);
for value in PalletEnum::value_variants() {
let args = Args {
colors: Some(*value),
..Args::default()
};
assert!(args.pallet(rand).len() > 0);
}
}
#[test]
fn duration() {
let args = Args {
duration: 3500,
..Args::default()
};
assert_eq!(Duration::from_millis(3500), args.duration());
}
#[test]
fn delay() {
let args = Args {
fps: 20,
..Args::default()
};
assert_eq!(Duration::from_millis(50), args.delay());
}
#[test]
fn char_config_pattern() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
char_pattern: Some(PatternEnum::Line),
..Args::default()
};
assert_eq!(PatternEnum::Line, args.char_config(rng).pattern);
}
#[test]
fn char_config_invert() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
char_invert: Some(false),
..Args::default()
};
assert_eq!(false, args.char_config(rng).invert);
}
#[test]
fn char_config_shift() {
let rng = &mut StepRng::new(1, 1);
let args = Args::default();
assert_eq!(true, args.char_config(rng).shift);
}
#[test]
fn char_config_swap() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
char_swap: Some(true),
..Args::default()
};
assert_eq!(true, args.char_config(rng).swap);
}
#[test]
fn char_config_segments() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
char_segments: Some(12),
..Args::default()
};
assert_abs_diff_eq!(12.0, args.char_config(rng).segments);
}
#[test]
fn char_config_shrink() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
char_shrink: Some(42),
..Args::default()
};
assert_abs_diff_eq!(42.0, args.char_config(rng).shrink);
}
#[test]
fn color_config_pattern() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
color_pattern: Some(PatternEnum::Circle),
..Args::default()
};
assert_eq!(PatternEnum::Circle, args.color_config(rng).pattern);
}
#[test]
fn color_config_invert() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
color_invert: Some(true),
..Args::default()
};
assert_eq!(true, args.color_config(rng).invert);
}
#[test]
fn color_config_shift() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
color_shift: Some(false),
..Args::default()
};
assert_eq!(false, args.color_config(rng).shift);
}
#[test]
fn color_config_swap() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
color_swap: Some(true),
..Args::default()
};
assert_eq!(true, args.color_config(rng).swap);
}
#[test]
fn color_config_segments() {
let rng = &mut StepRng::new(1, 1);
let args = Args {
color_segments: Some(23),
..Args::default()
};
assert_abs_diff_eq!(23.0, args.color_config(rng).segments);
}
#[test]
fn color_config_shrink() {
let rng = &mut StepRng::new(1, 1);
let args = Args::default();
assert_abs_diff_eq!(1.0, args.color_config(rng).shrink);
}
#[test]
fn pattern_config_all_defined() {
for value in PatternEnum::value_variants() {
let config = PatternConfig {
pattern: *value,
shift: true,
invert: true,
swap: true,
segments: 3.0,
shrink: 2.0,
};
config
.create()
.create(&Config::default())
.sample(Vector::default());
}
fn create_animation(animation: AnimationType, size: Vector) -> Box<dyn Animation> {
match animation {
AnimationType::Circle => Box::new(CircleAnimation::new(size)),
AnimationType::Rhombus => Box::new(RhombusAnimation::new(size)),
AnimationType::Rotation => Box::new(RotationAnimation::new(size)),
}
}
fn create_fill(fill: FillModeType, size: Vector) -> Box<dyn FillMode> {
match fill {
FillModeType::Circle => Box::new(CircleFillMode::new(size)),
FillModeType::Level => Box::new(LevelFillMode::new()),
FillModeType::Stripes => Box::new(StripesFillMode::new(size))
}
}
fn create_color(color: ColorType) -> Box<dyn ColorSampler> {
use crossterm::style::Color::*;
match color {
ColorType::Red => Box::new(SimpleColorSampler::new(vec![Yellow, DarkYellow, Red])),
ColorType::Green => Box::new(SimpleColorSampler::new(vec![Cyan, DarkGreen, Green])),
ColorType::Blue => Box::new(SimpleColorSampler::new(vec![Magenta, DarkBlue, Blue])),
ColorType::LightRed => Box::new(SimpleColorSampler::new(vec![White, Yellow, Red])),
ColorType::LightGreen => Box::new(SimpleColorSampler::new(vec![White, Cyan, Green])),
ColorType::LightBlue => Box::new(SimpleColorSampler::new(vec![White, Blue, Magenta])),
ColorType::Grey => Box::new(SimpleColorSampler::new(vec![Black, Grey, White])),
ColorType::Rainbow => Box::new(SimpleColorSampler::new(vec![Magenta, Blue, Green, Yellow, Red]))
}
}

View File

@ -1,51 +0,0 @@
use crate::pattern::*;
use crate::Vector;
/// A factory for [Circle].
#[derive(derive_more::Constructor)]
pub struct CircleFactory;
/// A circular [Pattern].
pub struct Circle {
center: Vector,
radius: f32,
}
impl PatternFactory for CircleFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
Box::new(Circle::new(config))
}
}
impl Circle {
pub fn new(config: &Config) -> Self {
let center = config.size.center();
let radius = center.len();
Self { center, radius }
}
}
impl Pattern for Circle {
fn sample(&self, pos: Vector) -> f32 {
(pos - self.center).len() / self.radius
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::*;
#[test]
fn sample() {
let config = Config {
size: Vector::new(10.0, 20.0),
..Config::default()
};
let pattern = CircleFactory::new().create(&config);
assert_abs_diff_eq!(1.0, pattern.sample(Vector::new(0.0, 0.0)), epsilon = 0.1);
assert_abs_diff_eq!(0.0, pattern.sample(Vector::new(5.0, 10.0)), epsilon = 0.1);
assert_abs_diff_eq!(0.5, pattern.sample(Vector::new(7.5, 15.0)), epsilon = 0.1);
}
}

View File

@ -1,50 +0,0 @@
use crate::pattern::*;
use crate::Vector;
/// A factory for [Line].
#[derive(derive_more::Constructor)]
pub struct LineFactory;
/// A horizontal line [Pattern].
pub struct Line {
width: f32,
}
impl PatternFactory for LineFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
Box::new(Line::new(config))
}
}
impl Line {
pub fn new(config: &Config) -> Self {
let width = config.size.x;
Self { width }
}
}
impl Pattern for Line {
fn sample(&self, pos: Vector) -> f32 {
pos.x / self.width
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::*;
#[test]
fn sample() {
let config = Config {
size: Vector::new(20.0, 0.0),
..Config::default()
};
let pattern = LineFactory::new().create(&config);
assert_abs_diff_eq!(0.0, pattern.sample(Vector::new(0.0, 4.0)), epsilon = 0.1);
assert_abs_diff_eq!(0.4, pattern.sample(Vector::new(8.0, 8.0)), epsilon = 0.1);
assert_abs_diff_eq!(0.8, pattern.sample(Vector::new(16.0, 7.0)), epsilon = 0.1);
assert_abs_diff_eq!(1.0, pattern.sample(Vector::new(20.0, 3.0)), epsilon = 0.1);
}
}

View File

@ -1,157 +0,0 @@
//! Contains all pattern traits and base patterns.
mod circle;
mod line;
mod rhombus;
mod wheel;
pub use circle::*;
pub use line::*;
pub use rhombus::*;
pub use wheel::*;
use crate::Vector;
/// A configuration for a [Pattern].
#[derive(Copy, Clone, Default, PartialEq, Debug)]
pub struct Config {
/// The size of the terminal.
pub size: Vector,
/// The current state of the animation.
pub step: f32,
}
/// A factory to create a [Pattern].
#[cfg_attr(test, mockall::automock)]
pub trait PatternFactory {
/// Creates a new [Pattern] with the given configuration.
fn create(&self, config: &Config) -> Box<dyn Pattern>;
}
/// A pattern for an animation.
#[cfg_attr(test, mockall::automock)]
pub trait Pattern {
/// Returns the level for a given coordinate.
/// If it is a base pattern, the start position of the
/// animation should by zero and the end position should be one.
fn sample(&self, pos: Vector) -> f32;
}
/// A factor for a [Sampler].
#[cfg_attr(test, mockall::automock(type Sampler = MockSampler;))]
pub trait SamplerFactory {
/// The type of the [Sampler].
type Sampler: Sampler;
/// Creates a new [Sampler].
fn create(&self, config: &Config) -> Self::Sampler;
}
/// A sampler for multiple values.
#[cfg_attr(test, mockall::automock)]
pub trait Sampler {
/// Returns the char level for a given position.
fn char(&self, pos: Vector) -> f32;
/// Returns the color level for a given position.
fn color(&self, pos: Vector) -> f32;
}
/// The implementation of [SamplerFactory].
#[derive(derive_more::Constructor)]
pub struct SamplerFactoryImpl {
char: Box<dyn PatternFactory>,
color: Box<dyn PatternFactory>,
}
/// The implementation of [Sampler].
#[derive(derive_more::Constructor)]
pub struct SamplerImpl {
char: Box<dyn Pattern>,
color: Box<dyn Pattern>,
}
impl SamplerFactory for SamplerFactoryImpl {
type Sampler = SamplerImpl;
fn create(&self, config: &Config) -> Self::Sampler {
SamplerImpl::new(self.char.create(config), self.color.create(config))
}
}
impl Sampler for SamplerImpl {
fn char(&self, pos: Vector) -> f32 {
self.char.sample(pos)
}
fn color(&self, pos: Vector) -> f32 {
self.color.sample(pos)
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::*;
use mockall::predicate::eq;
#[test]
fn char() {
let mut char = MockPattern::new();
let color = MockPattern::new();
char.expect_sample()
.with(eq(Vector::new(2.0, 5.0)))
.return_const(2.5);
let sampler = SamplerImpl::new(Box::new(char), Box::new(color));
assert_abs_diff_eq!(2.5, sampler.char(Vector::new(2.0, 5.0)));
}
#[test]
fn color() {
let char = MockPattern::new();
let mut color = MockPattern::new();
color
.expect_sample()
.with(eq(Vector::new(4.0, 2.0)))
.return_const(3.2);
let sampler = SamplerImpl::new(Box::new(char), Box::new(color));
assert_abs_diff_eq!(3.2, sampler.color(Vector::new(4.0, 2.0)));
}
#[test]
fn factory() {
let mut char = MockPatternFactory::new();
let mut color = MockPatternFactory::new();
let config = Config {
size: Vector::new(2.0, 3.0),
step: 0.6,
};
char.expect_create().with(eq(config)).once().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(3.0);
Box::new(sampler)
});
color
.expect_create()
.with(eq(config))
.once()
.returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(5.0);
Box::new(sampler)
});
let factory = SamplerFactoryImpl::new(Box::new(char), Box::new(color));
let sampler = factory.create(&config);
assert_abs_diff_eq!(3.0, sampler.char(Vector::default()));
assert_abs_diff_eq!(5.0, sampler.color(Vector::default()));
}
}

View File

@ -1,52 +0,0 @@
use crate::pattern::*;
use crate::Vector;
/// A factory for [Rhombus].
#[derive(derive_more::Constructor)]
pub struct RhombusFactory;
/// A rhombus shaped [Pattern].
pub struct Rhombus {
center: Vector,
distance: f32,
}
impl PatternFactory for RhombusFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
Box::new(Rhombus::new(config))
}
}
impl Rhombus {
pub fn new(config: &Config) -> Rhombus {
let center = config.size.center();
let distance = center.sum();
Self { center, distance }
}
}
impl Pattern for Rhombus {
fn sample(&self, pos: Vector) -> f32 {
(pos - self.center).abs().sum() / self.distance
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::*;
#[test]
fn sample() {
let config = Config {
size: Vector::new(10.0, 5.0),
..Config::default()
};
let pattern = RhombusFactory::new().create(&config);
assert_abs_diff_eq!(1.0, pattern.sample(Vector::new(0.0, 0.0)), epsilon = 0.1);
assert_abs_diff_eq!(1.0, pattern.sample(Vector::new(10.0, 5.0)), epsilon = 0.1);
assert_abs_diff_eq!(0.0, pattern.sample(Vector::new(5.0, 2.5)), epsilon = 0.1);
assert_abs_diff_eq!(0.5, pattern.sample(Vector::new(7.0, 0.5)), epsilon = 0.1);
}
}

View File

@ -1,51 +0,0 @@
use crate::pattern::*;
use crate::Vector;
use std::f32::consts::PI;
/// A factory for [Wheel].
#[derive(derive_more::Constructor)]
pub struct WheelFactory;
/// A fortune wheel [Pattern].
pub struct Wheel {
center: Vector,
}
impl PatternFactory for WheelFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
Box::new(Wheel::new(config))
}
}
impl Wheel {
pub fn new(config: &Config) -> Self {
let center = config.size.center();
Self { center }
}
}
impl Pattern for Wheel {
fn sample(&self, pos: Vector) -> f32 {
((pos - self.center).angle() + PI) / PI / 2.0
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::*;
#[test]
fn sample() {
let config = Config {
size: Vector::new(10.0, 20.0),
..Config::default()
};
let pattern = WheelFactory::new().create(&config);
assert_abs_diff_eq!(0.0, pattern.sample(Vector::new(0.0, 9.0)), epsilon = 0.1);
assert_abs_diff_eq!(1.0, pattern.sample(Vector::new(0.0, 10.0)), epsilon = 0.1);
assert_abs_diff_eq!(0.5, pattern.sample(Vector::new(10.0, 10.0)), epsilon = 0.1);
assert_abs_diff_eq!(0.75, pattern.sample(Vector::new(5.0, 20.0)), epsilon = 0.1);
}
}

View File

@ -1,335 +0,0 @@
use crate::Error;
use crate::Terminal;
use crossterm::cursor::*;
use crossterm::style::*;
use crossterm::terminal::*;
/// A trait for performance optimized terminal output.
///
/// All commands are queue and have to be executed using [Printer::flush].
#[cfg_attr(test, mockall::automock)]
pub trait Printer {
/// Shows the cursor if it isn't visible.
fn show_cursor(&mut self) -> Result<(), Error>;
/// Hides the cursor if it is visible.
fn hide_cursor(&mut self) -> Result<(), Error>;
/// Prints a character.
/// # Panics
/// Panics if the character is a special character like `ESC`, `DEL` or `NEWLINE`.
fn print(&mut self, char: char) -> Result<(), Error>;
/// Moves the cursor to the specified position if it isn't there already.
fn move_to(&mut self, x: u16, y: u16) -> Result<(), Error>;
/// Returns the size of the terminal.
fn size(&self) -> Result<(u16, u16), Error>;
/// Sets the foreground color of the terminal.
fn set_foreground(&mut self, color: Color) -> Result<(), Error>;
/// Clears the terminal content.
fn clear(&mut self) -> Result<(), Error>;
/// Flushes all queue commands.
fn flush(&mut self) -> Result<(), Error>;
}
/// The implementation of [Printer].
pub struct PrinterImpl<T> {
term: T,
position: (u16, u16),
cursor: Option<bool>,
foreground: Option<Color>,
}
impl<T: Terminal> PrinterImpl<T> {
pub fn new(term: T) -> Result<Self, Error> {
let position = term.position()?;
Ok(Self {
term,
position,
cursor: None,
foreground: None,
})
}
}
impl<T: Terminal> Printer for PrinterImpl<T> {
fn show_cursor(&mut self) -> Result<(), Error> {
if self.cursor != Some(true) {
self.cursor = Some(true);
self.term.queue(Show)?;
}
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), Error> {
if self.cursor != Some(false) {
self.cursor = Some(false);
self.term.queue(Hide)?;
}
Ok(())
}
fn print(&mut self, char: char) -> Result<(), Error> {
if char < '\u{20}' || char == '\u{7F}' {
return Err("Special chars can't be printed.".into());
}
self.position.0 += 1;
self.term.queue(Print(char))?;
Ok(())
}
fn move_to(&mut self, x: u16, y: u16) -> Result<(), Error> {
if self.position != (x, y) {
self.position = (x, y);
self.term.queue(MoveTo(x, y))?;
}
Ok(())
}
fn size(&self) -> Result<(u16, u16), Error> {
self.term.size()
}
fn set_foreground(&mut self, color: Color) -> Result<(), Error> {
if self.foreground != Some(color) {
self.foreground = Some(color);
self.term.queue(SetForegroundColor(color))?;
}
Ok(())
}
fn clear(&mut self) -> Result<(), Error> {
self.term.queue(Clear(ClearType::Purge))?;
Ok(())
}
fn flush(&mut self) -> Result<(), Error> {
self.term.flush()?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MockTerminal;
use mockall::predicate::eq;
#[test]
fn show_cursor() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(Show))
.once()
.returning(|_| Ok(()));
PrinterImpl::new(mock).unwrap().show_cursor().unwrap();
}
#[test]
fn show_cursor_twice_queues_once() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(Show))
.once()
.returning(|_| Ok(()));
let mut printer = PrinterImpl::new(mock).unwrap();
printer.show_cursor().unwrap();
printer.show_cursor().unwrap();
}
#[test]
fn show_cursor_after_hiding_queues_show() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(Show))
.once()
.returning(|_| Ok(()));
mock.expect_queue().with(eq(Hide)).returning(|_| Ok(()));
let mut printer = PrinterImpl::new(mock).unwrap();
printer.hide_cursor().unwrap();
printer.show_cursor().unwrap();
}
#[test]
fn hide_cursor() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(Hide))
.once()
.returning(|_| Ok(()));
PrinterImpl::new(mock).unwrap().hide_cursor().unwrap();
}
#[test]
fn hide_cursor_twice_queues_once() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(Hide))
.once()
.returning(|_| Ok(()));
let mut printer = PrinterImpl::new(mock).unwrap();
printer.hide_cursor().unwrap();
printer.hide_cursor().unwrap();
}
#[test]
fn hide_cursor_after_showing_queues_hide() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(Show))
.once()
.returning(|_| Ok(()));
mock.expect_queue().with(eq(Hide)).returning(|_| Ok(()));
let mut printer = PrinterImpl::new(mock).unwrap();
printer.show_cursor().unwrap();
printer.hide_cursor().unwrap();
}
#[test]
fn set_foreground() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(SetForegroundColor(Color::Blue)))
.once()
.returning(|_| Ok(()));
PrinterImpl::new(mock)
.unwrap()
.set_foreground(Color::Blue)
.unwrap();
}
#[test]
fn set_foreground_twice_queues_once() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.once()
.returning(|_: SetForegroundColor| Ok(()));
let mut printer = PrinterImpl::new(mock).unwrap();
printer.set_foreground(Color::Red).unwrap();
printer.set_foreground(Color::Red).unwrap();
}
#[test]
fn set_foreground_different_color_queues() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.times(3)
.returning(|_: SetForegroundColor| Ok(()));
let mut printer = PrinterImpl::new(mock).unwrap();
printer.set_foreground(Color::Red).unwrap();
printer.set_foreground(Color::Blue).unwrap();
printer.set_foreground(Color::Red).unwrap();
}
#[test]
fn print() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(Print('R')))
.once()
.returning(|_| Ok(()));
PrinterImpl::new(mock).unwrap().print('R').unwrap();
}
#[test]
fn print_moves_cursor() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((2, 4)));
mock.expect_queue()
.times(3)
.returning(|_: Print<char>| Ok(()));
let mut printer = PrinterImpl::new(mock).unwrap();
printer.print('A').unwrap();
printer.print('B').unwrap();
printer.print('C').unwrap();
printer.move_to(5, 4).unwrap();
}
#[test]
fn print_special_char_fails() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((2, 4)));
let mut printer = PrinterImpl::new(mock).unwrap();
assert!(printer.print('\u{0}').is_err());
assert!(printer.print('\u{1F}').is_err());
assert!(printer.print('\u{7F}').is_err());
}
#[test]
fn move_to_different_position_queues() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((7, 2)));
mock.expect_queue()
.with(eq(MoveTo(5, 4)))
.once()
.returning(|_| Ok(()));
PrinterImpl::new(mock).unwrap().move_to(5, 4).unwrap();
}
#[test]
fn move_to_same_position_does_not_queue() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((3, 13)));
PrinterImpl::new(mock).unwrap().move_to(3, 13).unwrap();
}
#[test]
fn size() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_size().returning(|| Ok((14, 76)));
assert_eq!((14, 76), PrinterImpl::new(mock).unwrap().size().unwrap());
}
#[test]
fn clear() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_queue()
.with(eq(Clear(ClearType::Purge)))
.once()
.returning(|_| Ok(()));
PrinterImpl::new(mock).unwrap().clear().unwrap();
}
#[test]
fn flush() {
let mut mock = MockTerminal::new();
mock.expect_position().returning(|| Ok((0, 0)));
mock.expect_flush().once().returning(|| Ok(()));
PrinterImpl::new(mock).unwrap().flush().unwrap();
}
}

89
src/render.rs Normal file
View File

@ -0,0 +1,89 @@
use anyhow::Error;
use crate::sampler::{Sample, Sampler};
use crate::surface::Surface;
use crate::Vector;
use mockall::automock;
#[automock]
pub trait Renderer {
fn render(&mut self, step: f32);
fn present(&mut self) -> Result<(), Error>;
}
pub struct SamplerRenderer<TSurface, TSampler> {
surface: TSurface,
sampler: TSampler,
}
impl<T1, T2> SamplerRenderer<T1, T2> {
pub fn new(surface: T1, sampler: T2) -> Self {
Self { surface, sampler }
}
}
impl<T1: Surface, T2: Sampler> Renderer for SamplerRenderer<T1, T2> {
fn render(&mut self, step: f32) {
for x in 0..self.surface.width() {
for y in 0..self.surface.height() {
let pos = Vector::from_terminal(x, y);
let sample = self.sampler.sample(step, pos);
match sample {
Sample::Keep => (),
Sample::Draw { char, color } => self.surface.draw(x, y, char, color),
Sample::Clear => self.surface.clear(x, y),
}
}
}
}
fn present(&mut self) -> Result<(), Error> {
self.surface.present()
}
}
#[cfg(test)]
mod test {
use crossterm::style::*;
use mockall::predicate::*;
use super::*;
use crate::surface::MockSurface;
use crate::sampler::MockSampler;
#[test]
fn render() {
let mut surface = MockSurface::new();
let mut sampler = MockSampler::new();
sampler.expect_sample().withf(|_, pos| pos.x == 0.0 && pos.y == 0.0).returning(|_,_| Sample::Clear);
sampler.expect_sample().withf(|_, pos| pos.x == 1.0 && pos.y == 0.0).returning(|_,_| Sample::Keep);
sampler.expect_sample().withf(|_, pos| pos.x == 0.0 && pos.y == 2.0).returning(|_,_| Sample::Draw { char: 'a', color: Color::Red });
sampler.expect_sample().withf(|_, pos| pos.x == 1.0 && pos.y == 2.0).returning(|_,_| Sample::Keep);
sampler.expect_sample().withf(|_, pos| pos.x == 0.0 && pos.y == 4.0).returning(|_,_| Sample::Draw { char: 'x', color: Color::Yellow });
sampler.expect_sample().withf(|_, pos| pos.x == 1.0 && pos.y == 4.0).returning(|_,_| Sample::Clear);
surface.expect_width().return_const(2 as usize);
surface.expect_height().return_const(3 as usize);
surface.expect_clear().once().with(eq(0), eq(0)).return_const(());
surface.expect_draw().once().with(eq(0), eq(1), eq('a'), eq(Color::Red)).return_const(());
surface.expect_draw().once().with(eq(0), eq(2), eq('x'), eq(Color::Yellow)).return_const(());
surface.expect_clear().once().with(eq(1), eq(2)).return_const(());
let mut renderer = SamplerRenderer::new(surface, sampler);
renderer.render(0.5);
}
#[test]
fn present() {
let mut surface = MockSurface::new();
let sampler = MockSampler::new();
surface.expect_present().once().returning(|| Ok(()));
let mut renderer = SamplerRenderer::new(surface, sampler);
renderer.present().unwrap();
}
}

View File

@ -1,345 +0,0 @@
use crate::convert::{CharSample, Converter};
use crate::pattern::*;
use crate::Error;
use crate::Printer;
use crate::Vector;
use crossterm::style::Color;
/// A renderer for an animation.
#[cfg_attr(test, mockall::automock)]
pub trait Renderer {
/// Renders the current frame and flushes.
fn render(&mut self, step: f32) -> Result<(), Error>;
}
/// The implementation of [Renderer].
pub struct RendererImpl<T1, T2, T3: Printer> {
sampler: T1,
converter: T2,
printer: T3,
}
impl<T1, T2, T3: Printer> RendererImpl<T1, T2, T3> {
pub fn new(sampler: T1, converter: T2, mut printer: T3) -> Result<Self, Error> {
printer.hide_cursor()?;
Ok(Self {
sampler,
converter,
printer,
})
}
}
impl<T1: SamplerFactory, T2: Converter, T3: Printer> Renderer for RendererImpl<T1, T2, T3> {
fn render(&mut self, step: f32) -> Result<(), Error> {
let (width, height) = self.printer.size()?;
let config = Config {
step,
size: Vector::from_terminal(width, height),
};
let sampler = self.sampler.create(&config);
for y in 0..height {
for x in 0..width {
let pos = Vector::from_terminal(x, y);
match self.converter.char(sampler.char(pos)) {
CharSample::Draw(char) => {
let color = self.converter.color(sampler.color(pos));
self.printer.move_to(x, y)?;
self.printer.set_foreground(color)?;
self.printer.print(char)?;
}
CharSample::Clear => {
self.printer.move_to(x, y)?;
self.printer.print(' ')?;
}
CharSample::Keep => (),
}
}
}
self.printer.flush()
}
}
impl<T1, T2, T3: Printer> Drop for RendererImpl<T1, T2, T3> {
fn drop(&mut self) {
// Errors while dropping the renderer can be safely ignored.
self.printer.move_to(0, 0).ok();
self.printer.set_foreground(Color::Reset).ok();
self.printer.show_cursor().ok();
self.printer.clear().ok();
self.printer.flush().ok();
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::convert::MockConverter;
use crate::pattern::MockSampler;
use crate::pattern::MockSamplerFactory;
use crate::MockPrinter;
use crossterm::style::Color;
use mockall::predicate::eq;
use mockall::Sequence;
#[test]
fn new() {
let factory = MockSamplerFactory::new();
let converter = MockConverter::new();
let mut printer = MockPrinter::new();
// Constructor
printer.expect_hide_cursor().once().returning(|| Ok(()));
// Drop
printer.expect_move_to().returning(|_, _| Ok(()));
printer.expect_set_foreground().returning(|_| Ok(()));
printer.expect_show_cursor().returning(|| Ok(()));
printer.expect_clear().returning(|| Ok(()));
printer.expect_flush().returning(|| Ok(()));
drop(RendererImpl::new(factory, converter, printer));
}
#[test]
fn render_config_correct() {
let mut sampler = MockSamplerFactory::new();
let mut converter = MockConverter::new();
let mut printer = MockPrinter::new();
printer.expect_size().returning(|| Ok((2, 2)));
sampler.expect_create().returning(|_| {
let mut sampler = MockSampler::new();
sampler
.expect_char()
.with(eq(Vector::new(0.0, 0.0)))
.return_const(1.0);
sampler
.expect_char()
.with(eq(Vector::new(1.0, 0.0)))
.return_const(2.0);
sampler
.expect_char()
.with(eq(Vector::new(0.0, 2.0)))
.return_const(3.0);
sampler
.expect_char()
.with(eq(Vector::new(1.0, 2.0)))
.return_const(4.0);
sampler
.expect_color()
.with(eq(Vector::new(0.0, 0.0)))
.return_const(5.0);
sampler
.expect_color()
.with(eq(Vector::new(1.0, 0.0)))
.return_const(6.0);
sampler
.expect_color()
.with(eq(Vector::new(0.0, 2.0)))
.return_const(7.0);
sampler
.expect_color()
.with(eq(Vector::new(1.0, 2.0)))
.return_const(8.0);
sampler
});
converter
.expect_char()
.with(eq(1.0))
.return_const(CharSample::Keep);
converter
.expect_char()
.with(eq(2.0))
.return_const(CharSample::Clear);
converter
.expect_char()
.with(eq(3.0))
.return_const(CharSample::Draw('A'));
converter
.expect_char()
.with(eq(4.0))
.return_const(CharSample::Draw('X'));
converter
.expect_color()
.with(eq(7.0))
.return_const(Color::Red);
converter
.expect_color()
.with(eq(8.0))
.return_const(Color::Blue);
let seq = &mut Sequence::new();
// Constructor
printer
.expect_hide_cursor()
.once()
.returning(|| Ok(()))
.in_sequence(seq);
// Rendering
printer
.expect_move_to()
.once()
.with(eq(1), eq(0))
.returning(|_, _| Ok(()))
.in_sequence(seq);
printer
.expect_print()
.once()
.with(eq(' '))
.returning(|_| Ok(()))
.in_sequence(seq);
printer
.expect_move_to()
.once()
.with(eq(0), eq(1))
.returning(|_, _| Ok(()))
.in_sequence(seq);
printer
.expect_set_foreground()
.once()
.with(eq(Color::Red))
.returning(|_| Ok(()))
.in_sequence(seq);
printer
.expect_print()
.once()
.with(eq('A'))
.returning(|_| Ok(()))
.in_sequence(seq);
printer
.expect_move_to()
.once()
.with(eq(1), eq(1))
.returning(|_, _| Ok(()))
.in_sequence(seq);
printer
.expect_set_foreground()
.once()
.with(eq(Color::Blue))
.returning(|_| Ok(()))
.in_sequence(seq);
printer
.expect_print()
.once()
.with(eq('X'))
.returning(|_| Ok(()))
.in_sequence(seq);
printer
.expect_flush()
.once()
.returning(|| Ok(()))
.in_sequence(seq);
// Drop
printer
.expect_move_to()
.once()
.returning(|_, _| Ok(()))
.in_sequence(seq);
printer
.expect_set_foreground()
.with(eq(Color::Reset))
.once()
.returning(|_| Ok(()))
.in_sequence(seq);
printer
.expect_show_cursor()
.once()
.returning(|| Ok(()))
.in_sequence(seq);
printer
.expect_clear()
.once()
.returning(|| Ok(()))
.in_sequence(seq);
printer
.expect_flush()
.once()
.returning(|| Ok(()))
.in_sequence(seq);
let mut renderer = RendererImpl::new(sampler, converter, printer).unwrap();
renderer.render(0.0).unwrap();
}
#[test]
fn render() {
let mut sampler = MockSamplerFactory::new();
let mut converter = MockConverter::new();
let mut printer = MockPrinter::new();
sampler.expect_create().returning(|_| {
let mut sampler = MockSampler::new();
sampler.expect_char().return_const(0.0);
sampler
});
converter.expect_char().return_const(CharSample::Keep);
printer.expect_size().returning(|| Ok((3, 2)));
printer.expect_flush().returning(|| Ok(()));
// Constructor
printer.expect_hide_cursor().returning(|| Ok(()));
// Drop
printer.expect_move_to().returning(|_, _| Ok(()));
printer.expect_set_foreground().returning(|_| Ok(()));
printer.expect_show_cursor().returning(|| Ok(()));
printer.expect_clear().returning(|| Ok(()));
printer.expect_flush().returning(|| Ok(()));
let mut renderer = RendererImpl::new(sampler, converter, printer).unwrap();
renderer.render(0.8).unwrap();
}
#[test]
fn end() {
let factory = MockSamplerFactory::new();
let converter = MockConverter::new();
let mut printer = MockPrinter::new();
let seq = &mut Sequence::new();
// Constructor
printer.expect_hide_cursor().returning(|| Ok(()));
// Drop
printer
.expect_move_to()
.with(eq(0), eq(0))
.once()
.returning(|_, _| Ok(()))
.in_sequence(seq);
printer
.expect_set_foreground()
.with(eq(Color::Reset))
.once()
.returning(|_| Ok(()))
.in_sequence(seq);
printer
.expect_show_cursor()
.once()
.returning(|| Ok(()))
.in_sequence(seq);
printer
.expect_clear()
.once()
.returning(|| Ok(()))
.in_sequence(seq);
printer
.expect_flush()
.once()
.returning(|| Ok(()))
.in_sequence(seq);
drop(RendererImpl::new(factory, converter, printer));
}
}

66
src/runner.rs Normal file
View File

@ -0,0 +1,66 @@
use std::time::Duration;
use anyhow::Error;
use crate::Renderer;
use crate::timer::Timer;
pub struct Runner<TTimer, TRenderer> {
timer: TTimer,
ticks: u128,
renderer: TRenderer,
}
impl<T1: Timer, T2: Renderer> Runner<T1, T2> {
pub fn new(duration: Duration,
timer: T1,
renderer: T2) -> Self {
let ticks = duration.as_nanos() / timer.delay().as_nanos();
Self { timer, ticks, renderer }
}
pub fn run(mut self) -> Result<(), Error> {
for i in 0..=self.ticks {
let step = i as f32 / self.ticks as f32;
self.renderer.render(step);
self.renderer.present()?;
self.timer.sleep();
}
Ok(())
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use mockall::predicate::*;
use mockall::Sequence;
use crate::timer::MockTimer;
use crate::render::MockRenderer;
use super::*;
#[test]
fn run() {
let mut timer = MockTimer::new();
let mut renderer = MockRenderer::new();
let seq = &mut Sequence::new();
timer.expect_delay().return_const(Duration::from_secs(2));
renderer.expect_render().once().with(eq(0.0)).in_sequence(seq).return_const(());
renderer.expect_present().once().in_sequence(seq).returning(|| Ok(()));
timer.expect_sleep().once().in_sequence(seq).return_const(());
renderer.expect_render().once().with(eq(0.5)).in_sequence(seq).return_const(());
renderer.expect_present().once().in_sequence(seq).returning(|| Ok(()));
timer.expect_sleep().once().in_sequence(seq).return_const(());
renderer.expect_render().once().with(eq(1.0)).in_sequence(seq).return_const(());
renderer.expect_present().once().in_sequence(seq).returning(|| Ok(()));
timer.expect_sleep().once().in_sequence(seq).return_const(());
let runner = Runner::new(Duration::from_secs(4), timer, renderer);
runner.run().unwrap();
}
}

139
src/sampler.rs Normal file
View File

@ -0,0 +1,139 @@
use crossterm::style::Color;
use mockall::automock;
use crate::animation::Animation;
use crate::char::CharSampler;
use crate::color::ColorSampler;
use crate::fill::FillMode;
use crate::vec::Vector;
pub enum Sample {
Keep,
Draw { char: char, color: Color },
Clear,
}
#[automock]
pub trait Sampler {
fn sample(&self, step: f32, pos: Vector) -> Sample;
}
pub struct ComposedSampler {
animation: Box<dyn Animation>,
fill: Box<dyn FillMode>,
color: Box<dyn ColorSampler>,
char: Box<dyn CharSampler>,
}
impl ComposedSampler {
pub fn new(animation: Box<dyn Animation>,
fill: Box<dyn FillMode>,
color: Box<dyn ColorSampler>,
char: Box<dyn CharSampler>) -> Self {
Self { animation, fill, color, char }
}
}
impl Sampler for ComposedSampler {
fn sample(&self, step: f32, pos: Vector) -> Sample {
let level = self.animation.sample(step, pos);
if level >= 1.0 {
Sample::Keep
} else if level >= 0.0 {
let char = self.char.sample(level);
let fill = self.fill.sample(level, pos);
let color = self.color.sample(fill);
Sample::Draw { char, color }
} else {
Sample::Clear
}
}
}
#[cfg(test)]
mod test {
use mockall::predicate::{always, eq};
use super::*;
use crate::animation::MockAnimation;
use crate::fill::MockFillMode;
use crate::color::MockColorSampler;
use crate::char::MockCharSampler;
#[test]
fn sample_keep() {
let mut anim = Box::new(MockAnimation::new());
let fill = Box::new(MockFillMode::new());
let color = Box::new(MockColorSampler::new());
let char = Box::new(MockCharSampler::new());
anim.expect_sample().return_const(3.0);
let sampler = ComposedSampler::new(anim, fill, color, char);
assert!(matches!(sampler.sample(0.7, Vector::new(0.3, 0.1)), Sample::Keep));
}
#[test]
fn sample_draw() {
let mut anim = Box::new(MockAnimation::new());
let mut fill = Box::new(MockFillMode::new());
let mut color = Box::new(MockColorSampler::new());
let mut char = Box::new(MockCharSampler::new());
anim.expect_sample().once().with(eq(0.2), always()).return_const(0.3);
fill.expect_sample().once().with(eq(0.3), always()).return_const(0.8);
color.expect_sample().once().with(eq(0.8)).return_const(Color::Blue);
char.expect_sample().once().with(eq(0.3)).return_const('Z');
let sampler = ComposedSampler::new(anim, fill, color, char);
assert!(matches!(sampler.sample(0.2, Vector::new(0.3, 0.1)), Sample::Draw { char: 'Z', color: Color::Blue }));
}
#[test]
fn sample_clear() {
let mut anim = Box::new(MockAnimation::new());
let fill = Box::new(MockFillMode::new());
let color = Box::new(MockColorSampler::new());
let char = Box::new(MockCharSampler::new());
anim.expect_sample().return_const(-0.4);
let sampler = ComposedSampler::new(anim, fill, color, char);
assert!(matches!(sampler.sample(0.7, Vector::new(0.3, 0.1)), Sample::Clear));
}
#[test]
fn sample_almost_draw() {
let mut anim = Box::new(MockAnimation::new());
let fill = Box::new(MockFillMode::new());
let color = Box::new(MockColorSampler::new());
let char = Box::new(MockCharSampler::new());
anim.expect_sample().return_const(1.0);
let sampler = ComposedSampler::new(anim, fill, color, char);
assert!(matches!(sampler.sample(0.7, Vector::new(0.3, 0.1)), Sample::Keep));
}
#[test]
fn sample_almost_clear() {
let mut anim = Box::new(MockAnimation::new());
let mut fill = Box::new(MockFillMode::new());
let mut color = Box::new(MockColorSampler::new());
let mut char = Box::new(MockCharSampler::new());
anim.expect_sample().return_const(0.0);
fill.expect_sample().return_const(0.8);
color.expect_sample().return_const(Color::Blue);
char.expect_sample().return_const('a');
let sampler = ComposedSampler::new(anim, fill, color, char);
assert!(matches!(sampler.sample(0.7, Vector::new(0.3, 0.1)), Sample::Draw { .. }));
}
}

193
src/surface.rs Normal file
View File

@ -0,0 +1,193 @@
use anyhow::Error;
use crossterm::cursor::MoveTo;
use crossterm::{ExecutableCommand, QueueableCommand};
use crossterm::style::{Color, Print, SetForegroundColor};
use crossterm::terminal::{Clear, ClearType};
use mockall::automock;
use std::io::Write;
use crate::array::Array2D;
#[automock]
pub trait Surface {
fn width(&self) -> usize;
fn height(&self) -> usize;
fn draw(&mut self, x: usize, y: usize, char: char, color: Color);
fn clear(&mut self, x: usize, y: usize);
fn present(&mut self) -> Result<(), Error>;
}
pub struct WriteSurface<T: Write> {
out: T,
array: Array2D<Cell>,
}
#[derive(Copy, Clone)]
enum Cell {
Keep,
Draw { char: char, color: Color },
}
impl Default for Cell {
fn default() -> Self { Cell::Keep }
}
impl<T: Write> WriteSurface<T> {
pub fn new(out: T, width: usize, height: usize) -> Self {
Self {
out,
array: Array2D::new(width, height)
}
}
}
impl<T: Write> Surface for WriteSurface<T> {
fn width(&self) -> usize {
self.array.width()
}
fn height(&self) -> usize {
self.array.height()
}
fn draw(&mut self, x: usize, y: usize, char: char, color: Color) {
self.array[(x, y)] = Cell::Draw { char, color };
}
fn clear(&mut self, x: usize, y: usize) {
self.array[(x, y)] = Cell::Draw { char: ' ', color: Color::Reset };
}
fn present(&mut self) -> Result<(), Error> {
let mut needs_move;
let mut last_color = None;
for y in 0..self.array.height() {
needs_move = true;
for x in 0..self.array.width() {
match self.array[(x, y)] {
Cell::Keep => {
needs_move = true;
}
Cell::Draw { char, color } => {
if needs_move {
needs_move = false;
self.out.queue(MoveTo(x as u16, y as u16))?;
}
if last_color.is_none() || last_color.unwrap() != color {
last_color = Some(color);
self.out.queue(SetForegroundColor(color))?;
}
self.out.queue(Print(char))?;
}
}
}
}
self.out.queue(MoveTo(0, 0))?;
self.out.flush()?;
Ok(())
}
}
impl<T: Write> Drop for WriteSurface<T> {
fn drop(&mut self) {
if let Err(e) = self.out.execute(Clear(ClearType::Purge)) {
println!("{}", e);
}
}
}
#[cfg(test)]
mod test {
use std::cell::RefCell;
use std::rc::Rc;
use super::*;
#[derive(PartialEq, Debug)]
struct Data {
flushed: Vec<Vec<u8>>,
buffer: Vec<u8>
}
struct MockWrite {
data: Rc<RefCell<Data>>
}
impl Data {
fn new() -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
flushed: Vec::new(),
buffer: Vec::new()
}))
}
}
impl MockWrite {
fn new(data: Rc<RefCell<Data>>) -> Box<dyn Write> {
Box::new(Self { data })
}
}
impl Write for MockWrite {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.data.borrow_mut().buffer.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
let data = self.data.borrow_mut().buffer.drain(..).collect();
self.data.borrow_mut().flushed.push(data);
Ok(())
}
}
#[test]
fn width() {
let data = Data::new();
let mock = MockWrite::new(data);
let renderer = WriteSurface::new(mock, 10, 2);
assert_eq!(10, renderer.width());
}
#[test]
fn height() {
let data = Data::new();
let mock = MockWrite::new(data);
let renderer = WriteSurface::new(mock, 5, 8);
assert_eq!(8, renderer.height());
}
#[test]
fn present() {
// Execute
let data = Data::new();
let mock = MockWrite::new(data.clone());
let mut renderer = WriteSurface::new(mock, 3, 2);
renderer.draw(0, 0, 'A', Color::Green);
renderer.draw(1, 0, 'x', Color::Green);
renderer.clear(1, 1);
renderer.present().unwrap();
// Recreate expectation
let expected = Data::new();
let mut stream = MockWrite::new(expected.clone());
stream.queue(MoveTo(0, 0)).unwrap();
stream.queue(SetForegroundColor(Color::Green)).unwrap();
stream.queue(Print('A')).unwrap();
stream.queue(Print('x')).unwrap();
stream.queue(MoveTo(1, 1)).unwrap();
stream.queue(SetForegroundColor(Color::Reset)).unwrap();
stream.queue(Print(' ')).unwrap();
stream.queue(MoveTo(0, 0)).unwrap();
stream.flush().unwrap();
// Compare
assert_eq!(expected, data);
}
}

View File

@ -1,47 +0,0 @@
use crate::Error;
use crossterm::{Command, QueueableCommand};
use std::io::Write;
/// A stub for OS calls and crossterm functions.
#[cfg_attr(test, mockall::automock)]
pub trait Terminal {
/// Queues a command for execution.
fn queue<T: 'static + Command>(&mut self, cmd: T) -> Result<(), Error>;
/// Flushes all queued commands.
fn flush(&mut self) -> Result<(), Error>;
/// Returns the current size of the terminal.
fn size(&self) -> Result<(u16, u16), Error>;
/// Returns the current cursor position.
fn position(&self) -> Result<(u16, u16), Error>;
}
/// The implementation of [Terminal].
pub struct TerminalImpl<T> {
out: T,
}
impl<T> TerminalImpl<T> {
pub fn new(out: T) -> Self {
Self { out }
}
}
impl<T: Write> Terminal for TerminalImpl<T> {
fn queue<TCmd: Command>(&mut self, cmd: TCmd) -> Result<(), Error> {
self.out.queue(cmd)?;
Ok(())
}
fn flush(&mut self) -> Result<(), Error> {
self.out.flush()?;
Ok(())
}
fn size(&self) -> Result<(u16, u16), Error> {
Ok(crossterm::terminal::size()?)
}
fn position(&self) -> Result<(u16, u16), Error> {
Ok(crossterm::cursor::position()?)
}
}

39
src/timer.rs Normal file
View File

@ -0,0 +1,39 @@
use std::thread::sleep;
use std::time::{Duration, Instant};
use mockall::automock;
#[automock]
pub trait Timer {
fn sleep(&mut self);
fn delay(&self) -> Duration;
}
pub struct SimpleTimer {
delay: Duration,
last: Instant
}
impl SimpleTimer {
pub fn new(delay: Duration) -> Self {
Self {
last: Instant::now(),
delay,
}
}
}
impl Timer for SimpleTimer {
fn sleep(&mut self) {
let now = Instant::now();
if self.last + self.delay > now {
sleep(self.delay - (now - self.last));
}
self.last = Instant::now();
}
fn delay(&self) -> Duration {
self.delay
}
}

View File

@ -1,90 +0,0 @@
use crate::pattern::*;
use crate::Vector;
/// A factory for [Invert].
///
/// Inverts the time of the [Config] for the child [Pattern].
#[derive(derive_more::Constructor)]
pub struct InvertFactory {
child: Box<dyn PatternFactory>,
}
/// Inverts the level of the [Pattern].
#[derive(derive_more::Constructor)]
pub struct Invert {
child: Box<dyn Pattern>,
}
impl PatternFactory for InvertFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
let mut copy = config.clone();
copy.step = 1.0 - config.step;
Box::new(Invert::new(self.child.create(&copy)))
}
}
impl Pattern for Invert {
fn sample(&self, pos: Vector) -> f32 {
1.0 - self.child.sample(pos)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MockPatternFactory;
use approx::*;
use mockall::predicate::eq;
#[test]
fn create_config_correct() {
let input = Config {
size: Vector::new(4.0, 2.0),
step: 0.4,
};
let mut output = input.clone();
output.step = 0.6;
let mut child = MockPatternFactory::new();
child
.expect_create()
.with(eq(output))
.once()
.returning(|_| Box::new(MockPattern::new()));
InvertFactory::new(Box::new(child)).create(&input);
}
#[test]
fn sample_inverted() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.7);
Box::new(sampler)
});
let sampler = InvertFactory::new(Box::new(child)).create(&Config::default());
assert_abs_diff_eq!(0.3, sampler.sample(Vector::default()));
}
#[test]
fn sample_pos_correct() {
let mut child = MockPatternFactory::new();
child.expect_create().once().returning(|_| {
let mut sampler = MockPattern::new();
sampler
.expect_sample()
.with(eq(Vector::new(4.0, 2.0)))
.once()
.return_const(0.0);
Box::new(sampler)
});
let sampler = InvertFactory::new(Box::new(child)).create(&Config::default());
sampler.sample(Vector::new(4.0, 2.0));
}
}

View File

@ -1,13 +0,0 @@
//! Contains transformations to apply on top of patterns.
mod invert;
mod segment;
mod shift;
mod shrink;
mod swap;
pub use invert::*;
pub use segment::*;
pub use shift::*;
pub use shrink::*;
pub use swap::*;

View File

@ -1,160 +0,0 @@
use crate::pattern::*;
use crate::Vector;
/// A factory for [Segments].
#[derive(derive_more::Constructor)]
pub struct SegmentsFactory {
child: Box<dyn PatternFactory>,
segments: f32,
}
/// Converts a pattern to `n` segments each starting with zero and ending with one.
#[derive(derive_more::Constructor)]
pub struct Segments {
child: Box<dyn Pattern>,
segments: f32,
}
impl PatternFactory for SegmentsFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
Box::new(Segments::new(self.child.create(config), self.segments))
}
}
impl Pattern for Segments {
fn sample(&self, pos: Vector) -> f32 {
let sample = self.child.sample(pos);
if 0.0 <= sample && sample < 1.0 {
sample * self.segments % 1.0
} else {
sample
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MockPatternFactory;
use approx::*;
use mockall::predicate::eq;
#[test]
fn create_config_correct() {
let config = Config {
size: Vector::new(6.0, 3.0),
step: 0.4,
};
let mut child = MockPatternFactory::new();
child
.expect_create()
.with(eq(config))
.once()
.returning(|_| Box::new(MockPattern::new()));
SegmentsFactory::new(Box::new(child), 2.0).create(&config);
}
#[test]
fn sample_above_one_untouched() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(1.1);
Box::new(sampler)
});
let sampler = SegmentsFactory::new(Box::new(child), 3.0).create(&Config::default());
assert_abs_diff_eq!(1.1, sampler.sample(Vector::default()));
}
#[test]
fn sample_below_zero_untouched() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(-0.1);
Box::new(sampler)
});
let sampler = SegmentsFactory::new(Box::new(child), 3.0).create(&Config::default());
assert_abs_diff_eq!(-0.1, sampler.sample(Vector::default()));
}
#[test]
fn sample_second_segment_begins_with_one() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.74);
Box::new(sampler)
});
let sampler = SegmentsFactory::new(Box::new(child), 4.0).create(&Config::default());
assert_abs_diff_eq!(0.96, sampler.sample(Vector::default()), epsilon = 0.01);
}
#[test]
fn sample_second_segment_ends_with_zero() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.5);
Box::new(sampler)
});
let sampler = SegmentsFactory::new(Box::new(child), 4.0).create(&Config::default());
assert_abs_diff_eq!(0.0, sampler.sample(Vector::default()));
}
#[test]
fn sample_last_segment_begins_with_one() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.24);
Box::new(sampler)
});
let sampler = SegmentsFactory::new(Box::new(child), 4.0).create(&Config::default());
assert_abs_diff_eq!(0.96, sampler.sample(Vector::default()));
}
#[test]
fn sample_last_segment_ends_with_zero() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.0);
Box::new(sampler)
});
let sampler = SegmentsFactory::new(Box::new(child), 4.0).create(&Config::default());
assert_abs_diff_eq!(0.0, sampler.sample(Vector::default()));
}
#[test]
fn sample_pos_correct() {
let mut child = MockPatternFactory::new();
child.expect_create().once().returning(|_| {
let mut sampler = MockPattern::new();
sampler
.expect_sample()
.with(eq(Vector::new(5.0, 1.0)))
.once()
.return_const(0.0);
Box::new(sampler)
});
let sampler = SegmentsFactory::new(Box::new(child), 3.0).create(&Config::default());
sampler.sample(Vector::new(5.0, 1.0));
}
}

View File

@ -1,87 +0,0 @@
use crate::pattern::*;
use crate::Vector;
/// A factory for [Shift].
#[derive(derive_more::Constructor)]
pub struct ShiftFactory {
child: Box<dyn PatternFactory>,
}
/// Offsets the [Pattern] out of screen, then moves it inside and finally outside the visible area.
#[derive(derive_more::Constructor)]
pub struct Shift {
child: Box<dyn Pattern>,
shift: f32,
}
impl PatternFactory for ShiftFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
Box::new(Shift::new(self.child.create(config), config.step))
}
}
impl Pattern for Shift {
fn sample(&self, pos: Vector) -> f32 {
self.child.sample(pos) + 1.0 - 2.0 * self.shift
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MockPatternFactory;
use approx::*;
use mockall::predicate::eq;
#[test]
fn create_config_correct() {
let config = Config {
size: Vector::new(4.0, 2.0),
step: 0.4,
};
let mut child = MockPatternFactory::new();
child
.expect_create()
.with(eq(config))
.once()
.returning(|_| Box::new(MockPattern::new()));
ShiftFactory::new(Box::new(child)).create(&config);
}
#[test]
fn sample_shifted() {
let config = Config {
step: 0.4,
..Config::default()
};
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.6);
Box::new(sampler)
});
let sampler = ShiftFactory::new(Box::new(child)).create(&config);
assert_abs_diff_eq!(0.8, sampler.sample(Vector::default()));
}
#[test]
fn sample_pos_correct() {
let mut child = MockPatternFactory::new();
child.expect_create().once().returning(|_| {
let mut sampler = MockPattern::new();
sampler
.expect_sample()
.with(eq(Vector::new(6.0, 7.0)))
.once()
.return_const(0.0);
Box::new(sampler)
});
let sampler = ShiftFactory::new(Box::new(child)).create(&Config::default());
sampler.sample(Vector::new(6.0, 7.0));
}
}

View File

@ -1,126 +0,0 @@
use crate::pattern::*;
use crate::Vector;
/// A factory for [Shrink].
pub struct ShrinkFactory {
child: Box<dyn PatternFactory>,
width: f32,
rest: f32,
}
/// Reduces the width of the child [Pattern] to one over `n`.
#[derive(derive_more::Constructor)]
pub struct Shrink {
child: Box<dyn Pattern>,
width: f32,
rest: f32,
}
impl ShrinkFactory {
pub fn new(child: Box<dyn PatternFactory>, factor: f32) -> Self {
let width = 1.0 / factor;
let rest = 1.0 - width;
Self { child, width, rest }
}
}
impl PatternFactory for ShrinkFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
Box::new(Shrink::new(
self.child.create(config),
self.width,
self.rest,
))
}
}
impl Pattern for Shrink {
fn sample(&self, pos: Vector) -> f32 {
(self.child.sample(pos) - self.rest) / self.width
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MockPatternFactory;
use approx::*;
use mockall::predicate::eq;
#[test]
fn create_config_correct() {
let config = Config {
size: Vector::new(7.0, 3.0),
step: 0.2,
};
let mut child = MockPatternFactory::new();
child
.expect_create()
.with(eq(config))
.once()
.returning(|_| Box::new(MockPattern::new()));
ShrinkFactory::new(Box::new(child), 4.0).create(&config);
}
#[test]
fn sample_starts_with_one() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(1.0);
Box::new(sampler)
});
let sampler = ShrinkFactory::new(Box::new(child), 4.0).create(&Config::default());
assert_abs_diff_eq!(1.0, sampler.sample(Vector::default()));
}
#[test]
fn sample_ends_with_zero() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.75);
Box::new(sampler)
});
let sampler = ShrinkFactory::new(Box::new(child), 4.0).create(&Config::default());
assert_abs_diff_eq!(0.0, sampler.sample(Vector::default()));
}
#[test]
fn sample_values_beyond_end_are_negative() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.5);
Box::new(sampler)
});
let sampler = ShrinkFactory::new(Box::new(child), 4.0).create(&Config::default());
assert!(sampler.sample(Vector::default()) < 0.0);
}
#[test]
fn sample_pos_correct() {
let mut child = MockPatternFactory::new();
child.expect_create().once().returning(|_| {
let mut sampler = MockPattern::new();
sampler
.expect_sample()
.with(eq(Vector::new(3.0, 5.0)))
.once()
.return_const(0.0);
Box::new(sampler)
});
let sampler = ShrinkFactory::new(Box::new(child), 3.0).create(&Config::default());
sampler.sample(Vector::new(3.0, 5.0));
}
}

View File

@ -1,90 +0,0 @@
use crate::pattern::*;
use crate::Vector;
/// A factory for [Swap].
///
/// Swaps the x-axis and y-axis of terminal size for the contained [Pattern].
#[derive(derive_more::Constructor)]
pub struct SwapFactory {
child: Box<dyn PatternFactory>,
}
/// Swaps the x-axis and y-axis.
#[derive(derive_more::Constructor)]
pub struct Swap {
child: Box<dyn Pattern>,
}
impl PatternFactory for SwapFactory {
fn create(&self, config: &Config) -> Box<dyn Pattern> {
let mut copy = config.clone();
copy.size = config.size.swap();
Box::new(Swap::new(self.child.create(&copy)))
}
}
impl Pattern for Swap {
fn sample(&self, pos: Vector) -> f32 {
self.child.sample(pos.swap())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MockPatternFactory;
use approx::*;
use mockall::predicate::eq;
#[test]
fn create_config_correct() {
let input = Config {
size: Vector::new(4.0, 2.0),
step: 0.4,
};
let mut output = input.clone();
output.size = Vector::new(2.0, 4.0);
let mut child = MockPatternFactory::new();
child
.expect_create()
.with(eq(output))
.once()
.returning(|_| Box::new(MockPattern::new()));
SwapFactory::new(Box::new(child)).create(&input);
}
#[test]
fn sample_value_correct() {
let mut child = MockPatternFactory::new();
child.expect_create().returning(|_| {
let mut sampler = MockPattern::new();
sampler.expect_sample().return_const(0.4);
Box::new(sampler)
});
let sampler = SwapFactory::new(Box::new(child)).create(&Config::default());
assert_abs_diff_eq!(0.4, sampler.sample(Vector::default()));
}
#[test]
fn sample_pos_correct() {
let mut child = MockPatternFactory::new();
child.expect_create().once().returning(|_| {
let mut sampler = MockPattern::new();
sampler
.expect_sample()
.with(eq(Vector::new(9.0, 5.0)))
.once()
.return_const(0.0);
Box::new(sampler)
});
let sampler = SwapFactory::new(Box::new(child)).create(&Config::default());
sampler.sample(Vector::new(5.0, 9.0));
}
}

View File

@ -1,91 +1,106 @@
/// A vector with a x-axis and y-axis.
#[derive(Copy, Clone, PartialEq, Debug, Default, derive_more::Sub)]
use std::ops::Sub;
/// A vector with a x and y axis.
#[derive(Copy, Clone)]
pub struct Vector {
pub x: f32,
pub y: f32,
pub y: f32
}
impl Vector {
/// Creates a new vector.
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
/// Creates a new vector from terminal coordinates.
pub fn from_terminal(x: u16, y: u16) -> Self {
Vector::new(x as f32, y as f32 * 2.0)
}
/// Returns the length.
pub fn len(&self) -> f32 {
(self.x * self.x + self.y * self.y).sqrt()
}
/// Returns a vector with absolute values.
pub fn abs(&self) -> Vector {
Self::new(self.x.abs(), self.y.abs())
}
/// Returns the sum of all axis.
pub fn sum(&self) -> f32 {
self.x + self.y
}
/// Returns the center.
pub fn center(&self) -> Vector {
pub fn center(self) -> Self {
Self::new(self.x / 2.0, self.y / 2.0)
}
/// Returns the angle.
pub fn angle(&self) -> f32 {
self.y.atan2(self.x)
pub fn length(self) -> f32 {
(self.x * self.x + self.y * self.y).sqrt()
}
/// Returns a vector with x and y swapped.
pub fn swap(&self) -> Vector {
Self::new(self.y, self.x)
pub fn angle(self) -> f32 {
self.x.atan2(self.y)
}
pub fn smaller(self) -> f32 {
self.x.min(self.y)
}
pub fn abs(self) -> Vector {
Self::new(self.x.abs(), self.y.abs())
}
pub fn sum(self) -> f32 {
self.x + self.y
}
/// Creates a vector with the on screen coordinates based on the terminal coordinates.
/// # Arguments
/// * `x`: The x axis of the terminal character.
/// * `y`: The y axis of the terminal character.
pub fn from_terminal(x: usize, y: usize) -> Self {
Self::new(x as f32, y as f32 * 2.0)
}
}
impl Sub for Vector {
type Output = Vector;
fn sub(self, rhs: Self) -> Self::Output {
Vector::new(self.x - rhs.x, self.y - rhs.y)
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::*;
#[test]
fn new() {
let v = Vector::new(4.0, 7.0);
assert_abs_diff_eq!(4.0, v.x);
assert_abs_diff_eq!(7.0, v.y);
}
let vec = Vector::new(3.0, 5.0);
#[test]
fn len() {
assert_abs_diff_eq!(8.5, Vector::new(3.0, 8.0).len(), epsilon = 0.1);
}
#[test]
fn abs() {
assert_eq!(Vector::new(3.0, 7.0), Vector::new(-3.0, -7.0).abs());
}
#[test]
fn sum() {
assert_abs_diff_eq!(11.0, Vector::new(3.0, 8.0).sum());
assert_eq!(3.0, vec.x);
assert_eq!(5.0, vec.y);
}
#[test]
fn center() {
assert_eq!(Vector::new(4.0, 9.0), Vector::new(8.0, 18.0).center());
let vec = Vector::new(3.0, 8.0);
assert_eq!(1.5, vec.center().x);
assert_eq!(4.0, vec.center().y);
}
#[test]
fn angle() {
assert_abs_diff_eq!(-1.5, Vector::new(2.0, -20.0).angle(), epsilon = 0.1);
fn length() {
let vec = Vector::new(3.0, 6.0);
assert!(6.7 < vec.length() && vec.length() < 6.8);
}
#[test]
fn swap() {
assert_eq!(Vector::new(7.0, 2.0), Vector::new(2.0, 7.0).swap());
fn smaller() {
assert_eq!(4.0, Vector::new(7.0, 4.0).smaller());
assert_eq!(2.0, Vector::new(2.0, 9.0).smaller());
}
}
#[test]
fn from_terminal() {
let vec = Vector::from_terminal(2, 4);
assert_eq!(2.0, vec.x);
assert_eq!(8.0, vec.y);
}
#[test]
fn sub() {
let left = Vector::new(8.0, 15.0);
let right = Vector::new(2.0, 4.0);
let result = left - right;
assert_eq!(6.0, result.x);
assert_eq!(11.0, result.y);
}
}