1
mirror of https://github.com/ricoriedel/wipe.git synced 2025-04-07 08:18:21 +00:00

Compare commits

...

76 Commits
v1.0.0 ... main

Author SHA1 Message Date
Rico Riedel
ab16aeaf71
refactor: upgrade dependencies 2023-03-26 18:40:48 +02:00
Rico Riedel
ce2e4d898f
feat: add fish configuration file 2023-03-26 18:31:27 +02:00
Rico Riedel
979a820200
Update lock file 2022-12-29 16:46:56 +01:00
Rico Riedel
5c589c0ab5
Update version number 2022-12-29 16:37:56 +01:00
Rico Riedel
5932d054f3
Update dependencies 2022-12-29 16:37:07 +01:00
Rico Riedel
46e8a68e75
Update Cargo.lock 2022-11-23 20:43:55 +01:00
Rico Riedel
5f1c8d67ba
Bump version number 2022-11-23 20:18:11 +01:00
Rico Riedel
4ad08ea42d
Rename slice to shrink 2022-11-23 20:16:41 +01:00
Rico Riedel
1e4403b5cb
Add cancel feature 2022-11-23 19:53:31 +01:00
Rico Riedel
19461dde26
Improve CLI help 2022-11-22 22:04:12 +01:00
Rico Riedel
edb429ed43
Adhere to RAII 2022-11-22 21:51:09 +01:00
Rico Riedel
1eddc2e3a2
Improve unit tests 2022-11-22 21:49:07 +01:00
Rico Riedel
3fcab89c51
Switch to approx 2022-08-06 21:55:37 +02:00
Rico Riedel
f8dd56ccb6
Fix gif speed 2022-08-06 21:49:19 +02:00
Rico Riedel
24bf3797f4
Add readme 2022-08-06 21:49:19 +02:00
Rico Riedel
4286d7055d
Add comments 2022-08-06 21:49:18 +02:00
Rico Riedel
e07d5bfb3e
Input validation 2022-08-06 21:49:18 +02:00
Rico Riedel
87175b6619
Refactoring 2022-08-06 21:49:18 +02:00
Rico Riedel
703fe1a62b
Refactoring 2022-08-06 21:49:17 +02:00
Rico Riedel
97aa376e3c
Add unit tests for main 2022-08-06 21:49:17 +02:00
Rico Riedel
9e8e9253bd
Replace default with new 2022-08-06 21:49:17 +02:00
Rico Riedel
402077e109
More unit tests 2022-08-06 21:49:17 +02:00
Rico Riedel
d965ba413a
Add more unit tests 2022-08-06 21:49:16 +02:00
Rico Riedel
7c13c8838a
Switch to approx 2022-08-06 21:49:16 +02:00
Rico Riedel
66a5ec78f3
Rename units to segments 2022-08-06 21:49:16 +02:00
Rico Riedel
5bf391e5ff
Add units unit tests 2022-08-06 21:49:16 +02:00
Rico Riedel
7c11b8e439
Add swap unit tests 2022-08-06 21:49:15 +02:00
Rico Riedel
dd5b215aaf
Add slice unit tests 2022-08-06 21:49:15 +02:00
Rico Riedel
726f482687
Add shift unit tests 2022-08-06 21:49:15 +02:00
Rico Riedel
cbb0c532e2
Add invert unit tests 2022-08-06 21:49:14 +02:00
Rico Riedel
6d60ef0088
Refactor cli 2022-08-06 21:49:14 +02:00
Rico Riedel
711ba4850c
Add units transformation 2022-08-06 21:49:14 +02:00
Rico Riedel
c80dac4c4e
Better slice implementation 2022-08-06 21:49:13 +02:00
Rico Riedel
3318db9a26
Add slice transformation 2022-08-06 21:49:13 +02:00
Rico Riedel
852d3e5bf5
Add swap transformation 2022-08-06 21:49:13 +02:00
Rico Riedel
91e80cc7c7
Add invert transformation 2022-08-06 21:49:13 +02:00
Rico Riedel
74e83cd543
Add shift transform 2022-08-06 21:49:12 +02:00
Rico Riedel
32cd72552e
Remove name function 2022-08-06 21:49:12 +02:00
Rico Riedel
dcbae98ed3
Implement main function 2022-08-06 21:49:12 +02:00
Rico Riedel
ba68f5e11e
Reworked imports 2022-08-06 21:49:12 +02:00
Rico Riedel
38bf1818e4
Add timer 2022-08-06 21:49:11 +02:00
Rico Riedel
2943a2514b
Fix char converter 2022-08-06 21:49:11 +02:00
Rico Riedel
393d3dd67e
Improve printer 2022-08-06 21:49:11 +02:00
Rico Riedel
ef4eaec34d
Add renderer 2022-08-06 21:49:10 +02:00
Rico Riedel
f5730be835
Replace sampler 2022-08-06 21:49:10 +02:00
Rico Riedel
8d75146744
Add sampler 2022-08-06 21:49:10 +02:00
Rico Riedel
56e630204a
Simplify tests 2022-08-06 21:49:10 +02:00
Rico Riedel
1f03a8fd13
Add converter 2022-08-06 21:49:09 +02:00
Rico Riedel
a42d141e6a
Make converter wrap 2022-08-06 21:49:09 +02:00
Rico Riedel
60ccd12e4b
Add sampler module 2022-08-06 21:49:09 +02:00
Rico Riedel
2bdd88a7e8
Add patterns 2022-08-06 21:49:08 +02:00
Rico Riedel
afe3dc4122
Add vector 2022-08-06 21:49:08 +02:00
Rico Riedel
8cd6e4d50f
Add printer 2022-08-06 21:49:08 +02:00
Rico Riedel
bac17f9bdb
Add term and error 2022-08-06 21:49:08 +02:00
Rico Riedel
a92cf27664
Reset repository 2022-08-06 21:49:07 +02:00
Rico Riedel
7500e5af1b
Allow skipping frames 2022-07-04 19:41:02 +02:00
Rico Riedel
94888dcc7d
Update dependencies 2022-07-04 19:41:02 +02:00
Rico Riedel
09e9fe12e9
Hide curser during animation 2022-07-03 13:13:12 +02:00
Nicolas
eb2d283770 Bump version number 2022-04-15 14:31:30 +02:00
Nicolas
0ef8a0d39b Update README.md 2022-04-15 14:30:00 +02:00
Nicolas
784464c96c Fix debian build script 2022-04-15 14:15:16 +02:00
Nicolas
202b01ebbf Replace shebang 2022-04-15 14:13:49 +02:00
Nicolas
1b49403057 Add debian build script 2022-04-15 14:13:36 +02:00
Nicolas
1a3f09f35d Rename package 2022-04-15 09:03:30 +02:00
Nicolas
ea466f63fd Add windows build script 2022-04-15 08:35:59 +02:00
Nicolas
ad95b2d167 Fix naming 2022-04-15 07:59:17 +02:00
Nicolas
f18d09b5c4 Rename rotation to sonar 2022-04-15 07:58:55 +02:00
Nicolas
96390b09b9 Fix angle function 2022-04-15 07:56:37 +02:00
Nicolas
35b555a04b Add more comments 2022-04-15 07:50:34 +02:00
Nicolas
b6a2ae85f4 Rename trait 2022-04-13 19:41:59 +02:00
Nicolas
3f8e27d313 Add unit tests 2022-04-13 19:38:28 +02:00
Nicolas
a7b29f9727 Small optimization 2022-04-13 19:25:33 +02:00
Nicolas
3678d26577 Add validation 2022-04-11 21:02:11 +02:00
Nicolas
042481609a Make PKGBUILD conform with arch packaging standards 2022-04-11 20:32:52 +02:00
Nicolas
06d4311a9c Add Cargo.lock 2022-04-11 20:28:40 +02:00
Nicolas
941ecfb757 Move mockall to dev-dependencies 2022-04-11 20:27:38 +02:00
55 changed files with 3472 additions and 1319 deletions

3
.gitignore vendored
View File

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

747
Cargo.lock generated Normal file
View File

@ -0,0 +1,747 @@
# 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,17 +1,20 @@
# Don't forget to update PKGBUILD
[package]
name = "wipe"
version = "1.0.0"
version = "2.1.1"
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"]
authors = ["Rico Riedel <rico.riedel@protonmail.ch>"]
[dependencies]
anyhow = "1.0"
clap = { version = "3.1", features = ["derive"]}
crossterm = "0.23"
clap = { version = "4.0", features = ["derive"] }
crossterm = "0.26"
ctrlc = "3.2"
cancellation = "0.1"
derive_more = "0.99"
rand = "0.8"
mockall = "0.11"
[dev-dependencies]
mockall = "0.11"
approx = "0.5"

View File

@ -1,18 +1,42 @@
# Wipe
Wipe your terminal with a smooth animation.
# wipe
Wipe the content of your terminal with a random animation.
This is the perfect program for you, if you like `clear` but want to add an unnecessary animation.
This is a fancy alternative to the `clear` command.
It plays randomly generated beautiful animations.
Download options can be found in the [release tab](https://github.com/ricoriedel/wipe/releases).
### Build & install
Building this project requires Rust and Cargo to be installed.
```shell
cargo build --release
```
```shell
cp ./target/release/wipe /usr/local/bin
```
## 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 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/`.
For a list of parameters, execute `wipe -h`.
Note that some parameters like `--color` can be specified multiple times with different values.
| 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.
## Showcase
[![Circle](doc/circle.gif)]()
[![Rhombus](doc/rhombus.gif)]()
[![Rotation](doc/rotation.gif)]()
[![Animation 1](misc/res/rec-1.gif)]()
[![Animation 2](misc/res/rec-2.gif)]()
[![Animation 3](misc/res/rec-3.gif)]()

28
dist/PKGBUILD vendored
View File

@ -1,28 +0,0 @@
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"
}

Binary file not shown.

Before

(image error) Size: 275 KiB

Binary file not shown.

Before

(image error) Size: 338 KiB

Binary file not shown.

Before

(image error) Size: 223 KiB

23
misc/res/placeholder.txt Normal file
View File

@ -0,0 +1,23 @@
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.

BIN
misc/res/rec-1.gif Normal file

Binary file not shown.

After

(image error) Size: 2.6 MiB

BIN
misc/res/rec-2.gif Normal file

Binary file not shown.

After

(image error) Size: 2.3 MiB

BIN
misc/res/rec-3.gif Normal file

Binary file not shown.

After

(image error) Size: 2.4 MiB

16
misc/res/script-1.sh Executable file
View File

@ -0,0 +1,16 @@
#!/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

16
misc/res/script-2.sh Executable file
View File

@ -0,0 +1,16 @@
#!/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

16
misc/res/script-3.sh Executable file
View File

@ -0,0 +1,16 @@
#!/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

7
misc/shell/wipe.fish Normal file
View File

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

8
misc/shell/wipe.zsh Normal file
View File

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

View File

@ -1,51 +0,0 @@
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);
}
}

View File

@ -1,12 +0,0 @@
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;
}

View File

@ -1,51 +0,0 @@
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);
}
}

View File

@ -1,46 +0,0 @@
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);
}
}

View File

@ -1,98 +0,0 @@
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)];
}
}

View File

@ -1,58 +0,0 @@
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);
}
}

View File

@ -1,61 +0,0 @@
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));
}
}

View File

@ -1,58 +0,0 @@
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);
}
}

74
src/convert/char.rs Normal file
View File

@ -0,0 +1,74 @@
/// 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));
}
}

69
src/convert/color.rs Normal file
View File

@ -0,0 +1,69 @@
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));
}
}

72
src/convert/mod.rs Normal file
View File

@ -0,0 +1,72 @@
//! 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));
}
}

49
src/error.rs Normal file
View File

@ -0,0 +1,49 @@
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));
}
}

196
src/exec.rs Normal file
View File

@ -0,0 +1,196 @@
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();
}
}

View File

@ -1,40 +0,0 @@
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);
}
}

View File

@ -1,29 +0,0 @@
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)));
}
}

View File

@ -1,14 +0,0 @@
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;
}

View File

@ -1,35 +0,0 @@
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,151 +1,453 @@
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;
mod color;
mod char;
mod fill;
mod vec;
mod array;
mod surface;
mod animation;
mod sampler;
mod render;
mod timer;
mod runner;
mod choose;
/// 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>,
}
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 {
Red,
Yellow,
Green,
Blue,
Magenta,
Cyan,
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 {
Circle,
Line,
Rhombus,
Wheel,
}
/// 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)
}
}
options!(AnimationType {
Circle,
Rhombus,
Rotation,
});
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()),
}
}
options!(ColorType {
Red,
Green,
Blue,
LightRed,
LightGreen,
LightBlue,
Grey,
Rainbow,
});
/// Creates a new composed [Pattern].
fn create(&self) -> Box<dyn PatternFactory> {
let mut pattern = self.create_base();
options!(FillModeType {
Circle,
Level,
Stripes,
});
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
}
}
#[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>,
/// 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(),
}
}
fn main() -> Result<(), Error> {
let args = Args::parse();
let mut chooser = Chooser::new(OsRng::default());
let rand = &mut thread_rng();
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 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 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 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 sampler = ComposedSampler::new(animation, fill, color, char);
let surface = WriteSurface::new(stdout(), width, height);
let clock = ClockImpl::new();
let executor = Executor::new(clock, duration, delay);
let renderer = SamplerRenderer::new(surface, sampler);
let timer = SimpleTimer::new(delay);
let runner = Runner::new(duration, timer, renderer);
let src = CancellationTokenSource::new();
let token = src.token().clone();
runner.run()
ctrlc::set_handler(move || {
src.cancel();
})?;
executor.run(renderer, &token)
}
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)),
#[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_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]))
}
}

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

@ -0,0 +1,51 @@
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);
}
}

50
src/pattern/line.rs Normal file
View File

@ -0,0 +1,50 @@
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);
}
}

157
src/pattern/mod.rs Normal file
View File

@ -0,0 +1,157 @@
//! 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()));
}
}

52
src/pattern/rhombus.rs Normal file
View File

@ -0,0 +1,52 @@
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);
}
}

51
src/pattern/wheel.rs Normal file
View File

@ -0,0 +1,51 @@
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);
}
}

335
src/printer.rs Normal file
View File

@ -0,0 +1,335 @@
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();
}
}

View File

@ -1,89 +0,0 @@
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();
}
}

345
src/renderer.rs Normal file
View File

@ -0,0 +1,345 @@
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));
}
}

View File

@ -1,66 +0,0 @@
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();
}
}

View File

@ -1,139 +0,0 @@
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 { .. }));
}
}

View File

@ -1,193 +0,0 @@
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);
}
}

47
src/term.rs Normal file
View File

@ -0,0 +1,47 @@
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()?)
}
}

View File

@ -1,39 +0,0 @@
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
}
}

90
src/transform/invert.rs Normal file
View File

@ -0,0 +1,90 @@
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));
}
}

13
src/transform/mod.rs Normal file
View File

@ -0,0 +1,13 @@
//! 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::*;

160
src/transform/segment.rs Normal file
View File

@ -0,0 +1,160 @@
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));
}
}

87
src/transform/shift.rs Normal file
View File

@ -0,0 +1,87 @@
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));
}
}

126
src/transform/shrink.rs Normal file
View File

@ -0,0 +1,126 @@
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));
}
}

90
src/transform/swap.rs Normal file
View File

@ -0,0 +1,90 @@
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,106 +1,91 @@
use std::ops::Sub;
/// A vector with a x and y axis.
#[derive(Copy, Clone)]
/// A vector with a x-axis and y-axis.
#[derive(Copy, Clone, PartialEq, Debug, Default, derive_more::Sub)]
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 }
}
pub fn center(self) -> Self {
Self::new(self.x / 2.0, self.y / 2.0)
/// 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)
}
pub fn length(self) -> f32 {
/// Returns the length.
pub fn len(&self) -> f32 {
(self.x * self.x + self.y * self.y).sqrt()
}
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 {
/// Returns a vector with absolute values.
pub fn abs(&self) -> Vector {
Self::new(self.x.abs(), self.y.abs())
}
pub fn sum(self) -> f32 {
/// Returns the sum of all axis.
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)
/// Returns the center.
pub fn center(&self) -> Vector {
Self::new(self.x / 2.0, self.y / 2.0)
}
}
impl Sub for Vector {
type Output = Vector;
/// Returns the angle.
pub fn angle(&self) -> f32 {
self.y.atan2(self.x)
}
fn sub(self, rhs: Self) -> Self::Output {
Vector::new(self.x - rhs.x, self.y - rhs.y)
/// Returns a vector with x and y swapped.
pub fn swap(&self) -> Vector {
Self::new(self.y, self.x)
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::*;
#[test]
fn new() {
let vec = Vector::new(3.0, 5.0);
let v = Vector::new(4.0, 7.0);
assert_abs_diff_eq!(4.0, v.x);
assert_abs_diff_eq!(7.0, v.y);
}
assert_eq!(3.0, vec.x);
assert_eq!(5.0, vec.y);
#[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());
}
#[test]
fn center() {
let vec = Vector::new(3.0, 8.0);
assert_eq!(1.5, vec.center().x);
assert_eq!(4.0, vec.center().y);
assert_eq!(Vector::new(4.0, 9.0), Vector::new(8.0, 18.0).center());
}
#[test]
fn length() {
let vec = Vector::new(3.0, 6.0);
assert!(6.7 < vec.length() && vec.length() < 6.8);
fn angle() {
assert_abs_diff_eq!(-1.5, Vector::new(2.0, -20.0).angle(), epsilon = 0.1);
}
#[test]
fn smaller() {
assert_eq!(4.0, Vector::new(7.0, 4.0).smaller());
assert_eq!(2.0, Vector::new(2.0, 9.0).smaller());
fn swap() {
assert_eq!(Vector::new(7.0, 2.0), Vector::new(2.0, 7.0).swap());
}
#[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);
}
}
}