├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── doc ├── demo.gif └── demo.tape ├── scripts └── setup.sh └── src ├── app.rs ├── cli.rs ├── main.rs ├── state ├── buffer.rs ├── dropping.rs ├── mod.rs ├── tail.rs ├── timer.rs └── wind.rs ├── tui.rs ├── ui.rs ├── util.rs ├── weather ├── dropping.rs ├── empty.rs └── mod.rs └── widget ├── fps.rs ├── mod.rs ├── timer.rs └── weather.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.7" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "allocator-api2" 34 | version = "0.2.16" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 37 | 38 | [[package]] 39 | name = "android-tzdata" 40 | version = "0.1.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 43 | 44 | [[package]] 45 | name = "android_system_properties" 46 | version = "0.1.5" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 49 | dependencies = [ 50 | "libc", 51 | ] 52 | 53 | [[package]] 54 | name = "anstream" 55 | version = "0.6.11" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 58 | dependencies = [ 59 | "anstyle", 60 | "anstyle-parse", 61 | "anstyle-query", 62 | "anstyle-wincon", 63 | "colorchoice", 64 | "utf8parse", 65 | ] 66 | 67 | [[package]] 68 | name = "anstyle" 69 | version = "1.0.4" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 72 | 73 | [[package]] 74 | name = "anstyle-parse" 75 | version = "0.2.3" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 78 | dependencies = [ 79 | "utf8parse", 80 | ] 81 | 82 | [[package]] 83 | name = "anstyle-query" 84 | version = "1.0.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 87 | dependencies = [ 88 | "windows-sys 0.52.0", 89 | ] 90 | 91 | [[package]] 92 | name = "anstyle-wincon" 93 | version = "3.0.2" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 96 | dependencies = [ 97 | "anstyle", 98 | "windows-sys 0.52.0", 99 | ] 100 | 101 | [[package]] 102 | name = "anyhow" 103 | version = "1.0.79" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" 106 | 107 | [[package]] 108 | name = "autocfg" 109 | version = "1.1.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 112 | 113 | [[package]] 114 | name = "backtrace" 115 | version = "0.3.69" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 118 | dependencies = [ 119 | "addr2line", 120 | "cc", 121 | "cfg-if", 122 | "libc", 123 | "miniz_oxide", 124 | "object", 125 | "rustc-demangle", 126 | ] 127 | 128 | [[package]] 129 | name = "bitflags" 130 | version = "1.3.2" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 133 | 134 | [[package]] 135 | name = "bitflags" 136 | version = "2.4.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 139 | dependencies = [ 140 | "serde", 141 | ] 142 | 143 | [[package]] 144 | name = "bumpalo" 145 | version = "3.14.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 148 | 149 | [[package]] 150 | name = "bytes" 151 | version = "1.5.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 154 | 155 | [[package]] 156 | name = "cassowary" 157 | version = "0.3.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 160 | 161 | [[package]] 162 | name = "castaway" 163 | version = "0.2.2" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 166 | dependencies = [ 167 | "rustversion", 168 | ] 169 | 170 | [[package]] 171 | name = "cc" 172 | version = "1.0.83" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 175 | dependencies = [ 176 | "libc", 177 | ] 178 | 179 | [[package]] 180 | name = "cfg-if" 181 | version = "1.0.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 184 | 185 | [[package]] 186 | name = "chrono" 187 | version = "0.4.31" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 190 | dependencies = [ 191 | "android-tzdata", 192 | "iana-time-zone", 193 | "num-traits", 194 | "windows-targets 0.48.5", 195 | ] 196 | 197 | [[package]] 198 | name = "clap" 199 | version = "4.4.18" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" 202 | dependencies = [ 203 | "clap_builder", 204 | "clap_derive", 205 | ] 206 | 207 | [[package]] 208 | name = "clap-num" 209 | version = "1.1.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "0e063d263364859dc54fb064cedb7c122740cd4733644b14b176c097f51e8ab7" 212 | dependencies = [ 213 | "num-traits", 214 | ] 215 | 216 | [[package]] 217 | name = "clap_builder" 218 | version = "4.4.18" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" 221 | dependencies = [ 222 | "anstream", 223 | "anstyle", 224 | "clap_lex", 225 | "strsim", 226 | ] 227 | 228 | [[package]] 229 | name = "clap_derive" 230 | version = "4.4.7" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 233 | dependencies = [ 234 | "heck 0.4.1", 235 | "proc-macro2", 236 | "quote", 237 | "syn", 238 | ] 239 | 240 | [[package]] 241 | name = "clap_lex" 242 | version = "0.6.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 245 | 246 | [[package]] 247 | name = "colorchoice" 248 | version = "1.0.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 251 | 252 | [[package]] 253 | name = "compact_str" 254 | version = "0.7.1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 257 | dependencies = [ 258 | "castaway", 259 | "cfg-if", 260 | "itoa", 261 | "ryu", 262 | "static_assertions", 263 | ] 264 | 265 | [[package]] 266 | name = "core-foundation-sys" 267 | version = "0.8.6" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 270 | 271 | [[package]] 272 | name = "crossterm" 273 | version = "0.27.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 276 | dependencies = [ 277 | "bitflags 2.4.1", 278 | "crossterm_winapi", 279 | "futures-core", 280 | "libc", 281 | "mio", 282 | "parking_lot", 283 | "serde", 284 | "signal-hook", 285 | "signal-hook-mio", 286 | "winapi", 287 | ] 288 | 289 | [[package]] 290 | name = "crossterm_winapi" 291 | version = "0.9.1" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 294 | dependencies = [ 295 | "winapi", 296 | ] 297 | 298 | [[package]] 299 | name = "either" 300 | version = "1.9.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 303 | 304 | [[package]] 305 | name = "futures" 306 | version = "0.3.30" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 309 | dependencies = [ 310 | "futures-channel", 311 | "futures-core", 312 | "futures-executor", 313 | "futures-io", 314 | "futures-sink", 315 | "futures-task", 316 | "futures-util", 317 | ] 318 | 319 | [[package]] 320 | name = "futures-channel" 321 | version = "0.3.30" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 324 | dependencies = [ 325 | "futures-core", 326 | "futures-sink", 327 | ] 328 | 329 | [[package]] 330 | name = "futures-core" 331 | version = "0.3.30" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 334 | 335 | [[package]] 336 | name = "futures-executor" 337 | version = "0.3.30" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 340 | dependencies = [ 341 | "futures-core", 342 | "futures-task", 343 | "futures-util", 344 | ] 345 | 346 | [[package]] 347 | name = "futures-io" 348 | version = "0.3.30" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 351 | 352 | [[package]] 353 | name = "futures-macro" 354 | version = "0.3.30" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 357 | dependencies = [ 358 | "proc-macro2", 359 | "quote", 360 | "syn", 361 | ] 362 | 363 | [[package]] 364 | name = "futures-sink" 365 | version = "0.3.30" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 368 | 369 | [[package]] 370 | name = "futures-task" 371 | version = "0.3.30" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 374 | 375 | [[package]] 376 | name = "futures-util" 377 | version = "0.3.30" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 380 | dependencies = [ 381 | "futures-channel", 382 | "futures-core", 383 | "futures-io", 384 | "futures-macro", 385 | "futures-sink", 386 | "futures-task", 387 | "memchr", 388 | "pin-project-lite", 389 | "pin-utils", 390 | "slab", 391 | ] 392 | 393 | [[package]] 394 | name = "getrandom" 395 | version = "0.2.12" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 398 | dependencies = [ 399 | "cfg-if", 400 | "libc", 401 | "wasi", 402 | ] 403 | 404 | [[package]] 405 | name = "gimli" 406 | version = "0.28.1" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 409 | 410 | [[package]] 411 | name = "hashbrown" 412 | version = "0.14.3" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 415 | dependencies = [ 416 | "ahash", 417 | "allocator-api2", 418 | ] 419 | 420 | [[package]] 421 | name = "heck" 422 | version = "0.4.1" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 425 | 426 | [[package]] 427 | name = "heck" 428 | version = "0.5.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 431 | 432 | [[package]] 433 | name = "hermit-abi" 434 | version = "0.3.3" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 437 | 438 | [[package]] 439 | name = "iana-time-zone" 440 | version = "0.1.59" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" 443 | dependencies = [ 444 | "android_system_properties", 445 | "core-foundation-sys", 446 | "iana-time-zone-haiku", 447 | "js-sys", 448 | "wasm-bindgen", 449 | "windows-core", 450 | ] 451 | 452 | [[package]] 453 | name = "iana-time-zone-haiku" 454 | version = "0.1.2" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 457 | dependencies = [ 458 | "cc", 459 | ] 460 | 461 | [[package]] 462 | name = "itertools" 463 | version = "0.12.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 466 | dependencies = [ 467 | "either", 468 | ] 469 | 470 | [[package]] 471 | name = "itertools" 472 | version = "0.13.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 475 | dependencies = [ 476 | "either", 477 | ] 478 | 479 | [[package]] 480 | name = "itoa" 481 | version = "1.0.10" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 484 | 485 | [[package]] 486 | name = "js-sys" 487 | version = "0.3.67" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" 490 | dependencies = [ 491 | "wasm-bindgen", 492 | ] 493 | 494 | [[package]] 495 | name = "libc" 496 | version = "0.2.152" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" 499 | 500 | [[package]] 501 | name = "lock_api" 502 | version = "0.4.11" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 505 | dependencies = [ 506 | "autocfg", 507 | "scopeguard", 508 | ] 509 | 510 | [[package]] 511 | name = "log" 512 | version = "0.4.20" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 515 | 516 | [[package]] 517 | name = "lru" 518 | version = "0.12.1" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "2994eeba8ed550fd9b47a0b38f0242bc3344e496483c6180b69139cc2fa5d1d7" 521 | dependencies = [ 522 | "hashbrown", 523 | ] 524 | 525 | [[package]] 526 | name = "memchr" 527 | version = "2.7.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 530 | 531 | [[package]] 532 | name = "miniz_oxide" 533 | version = "0.7.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 536 | dependencies = [ 537 | "adler", 538 | ] 539 | 540 | [[package]] 541 | name = "mio" 542 | version = "0.8.10" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" 545 | dependencies = [ 546 | "libc", 547 | "log", 548 | "wasi", 549 | "windows-sys 0.48.0", 550 | ] 551 | 552 | [[package]] 553 | name = "num-traits" 554 | version = "0.2.17" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 557 | dependencies = [ 558 | "autocfg", 559 | ] 560 | 561 | [[package]] 562 | name = "num_cpus" 563 | version = "1.16.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 566 | dependencies = [ 567 | "hermit-abi", 568 | "libc", 569 | ] 570 | 571 | [[package]] 572 | name = "object" 573 | version = "0.32.2" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 576 | dependencies = [ 577 | "memchr", 578 | ] 579 | 580 | [[package]] 581 | name = "once_cell" 582 | version = "1.19.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 585 | 586 | [[package]] 587 | name = "parking_lot" 588 | version = "0.12.1" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 591 | dependencies = [ 592 | "lock_api", 593 | "parking_lot_core", 594 | ] 595 | 596 | [[package]] 597 | name = "parking_lot_core" 598 | version = "0.9.9" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 601 | dependencies = [ 602 | "cfg-if", 603 | "libc", 604 | "redox_syscall", 605 | "smallvec", 606 | "windows-targets 0.48.5", 607 | ] 608 | 609 | [[package]] 610 | name = "paste" 611 | version = "1.0.14" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 614 | 615 | [[package]] 616 | name = "pin-project-lite" 617 | version = "0.2.13" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 620 | 621 | [[package]] 622 | name = "pin-utils" 623 | version = "0.1.0" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 626 | 627 | [[package]] 628 | name = "ppv-lite86" 629 | version = "0.2.17" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 632 | 633 | [[package]] 634 | name = "proc-macro2" 635 | version = "1.0.76" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" 638 | dependencies = [ 639 | "unicode-ident", 640 | ] 641 | 642 | [[package]] 643 | name = "quote" 644 | version = "1.0.35" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 647 | dependencies = [ 648 | "proc-macro2", 649 | ] 650 | 651 | [[package]] 652 | name = "rand" 653 | version = "0.8.5" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 656 | dependencies = [ 657 | "libc", 658 | "rand_chacha", 659 | "rand_core", 660 | ] 661 | 662 | [[package]] 663 | name = "rand_chacha" 664 | version = "0.3.1" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 667 | dependencies = [ 668 | "ppv-lite86", 669 | "rand_core", 670 | ] 671 | 672 | [[package]] 673 | name = "rand_core" 674 | version = "0.6.4" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 677 | dependencies = [ 678 | "getrandom", 679 | ] 680 | 681 | [[package]] 682 | name = "ratatui" 683 | version = "0.27.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" 686 | dependencies = [ 687 | "bitflags 2.4.1", 688 | "cassowary", 689 | "compact_str", 690 | "crossterm", 691 | "itertools 0.13.0", 692 | "lru", 693 | "paste", 694 | "stability", 695 | "strum", 696 | "strum_macros", 697 | "unicode-segmentation", 698 | "unicode-truncate", 699 | "unicode-width", 700 | ] 701 | 702 | [[package]] 703 | name = "redox_syscall" 704 | version = "0.4.1" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 707 | dependencies = [ 708 | "bitflags 1.3.2", 709 | ] 710 | 711 | [[package]] 712 | name = "rustc-demangle" 713 | version = "0.1.23" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 716 | 717 | [[package]] 718 | name = "rustversion" 719 | version = "1.0.14" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 722 | 723 | [[package]] 724 | name = "ryu" 725 | version = "1.0.16" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 728 | 729 | [[package]] 730 | name = "scopeguard" 731 | version = "1.2.0" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 734 | 735 | [[package]] 736 | name = "serde" 737 | version = "1.0.195" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" 740 | dependencies = [ 741 | "serde_derive", 742 | ] 743 | 744 | [[package]] 745 | name = "serde_derive" 746 | version = "1.0.195" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" 749 | dependencies = [ 750 | "proc-macro2", 751 | "quote", 752 | "syn", 753 | ] 754 | 755 | [[package]] 756 | name = "signal-hook" 757 | version = "0.3.17" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 760 | dependencies = [ 761 | "libc", 762 | "signal-hook-registry", 763 | ] 764 | 765 | [[package]] 766 | name = "signal-hook-mio" 767 | version = "0.2.3" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 770 | dependencies = [ 771 | "libc", 772 | "mio", 773 | "signal-hook", 774 | ] 775 | 776 | [[package]] 777 | name = "signal-hook-registry" 778 | version = "1.4.1" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 781 | dependencies = [ 782 | "libc", 783 | ] 784 | 785 | [[package]] 786 | name = "slab" 787 | version = "0.4.9" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 790 | dependencies = [ 791 | "autocfg", 792 | ] 793 | 794 | [[package]] 795 | name = "smallvec" 796 | version = "1.11.2" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 799 | 800 | [[package]] 801 | name = "stability" 802 | version = "0.2.0" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" 805 | dependencies = [ 806 | "quote", 807 | "syn", 808 | ] 809 | 810 | [[package]] 811 | name = "static_assertions" 812 | version = "1.1.0" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 815 | 816 | [[package]] 817 | name = "strsim" 818 | version = "0.10.0" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 821 | 822 | [[package]] 823 | name = "strum" 824 | version = "0.26.1" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" 827 | dependencies = [ 828 | "strum_macros", 829 | ] 830 | 831 | [[package]] 832 | name = "strum_macros" 833 | version = "0.26.4" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 836 | dependencies = [ 837 | "heck 0.5.0", 838 | "proc-macro2", 839 | "quote", 840 | "rustversion", 841 | "syn", 842 | ] 843 | 844 | [[package]] 845 | name = "syn" 846 | version = "2.0.48" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 849 | dependencies = [ 850 | "proc-macro2", 851 | "quote", 852 | "unicode-ident", 853 | ] 854 | 855 | [[package]] 856 | name = "tenki" 857 | version = "1.11.0" 858 | dependencies = [ 859 | "anyhow", 860 | "chrono", 861 | "clap", 862 | "clap-num", 863 | "crossterm", 864 | "futures", 865 | "itertools 0.13.0", 866 | "rand", 867 | "ratatui", 868 | "serde", 869 | "tinyvec", 870 | "tokio", 871 | ] 872 | 873 | [[package]] 874 | name = "tinyvec" 875 | version = "1.6.0" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 878 | 879 | [[package]] 880 | name = "tokio" 881 | version = "1.35.1" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" 884 | dependencies = [ 885 | "backtrace", 886 | "bytes", 887 | "libc", 888 | "mio", 889 | "num_cpus", 890 | "pin-project-lite", 891 | "signal-hook-registry", 892 | "tokio-macros", 893 | "windows-sys 0.48.0", 894 | ] 895 | 896 | [[package]] 897 | name = "tokio-macros" 898 | version = "2.2.0" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 901 | dependencies = [ 902 | "proc-macro2", 903 | "quote", 904 | "syn", 905 | ] 906 | 907 | [[package]] 908 | name = "unicode-ident" 909 | version = "1.0.12" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 912 | 913 | [[package]] 914 | name = "unicode-segmentation" 915 | version = "1.10.1" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 918 | 919 | [[package]] 920 | name = "unicode-truncate" 921 | version = "1.0.0" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" 924 | dependencies = [ 925 | "itertools 0.12.1", 926 | "unicode-width", 927 | ] 928 | 929 | [[package]] 930 | name = "unicode-width" 931 | version = "0.1.13" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 934 | 935 | [[package]] 936 | name = "utf8parse" 937 | version = "0.2.1" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 940 | 941 | [[package]] 942 | name = "version_check" 943 | version = "0.9.4" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 946 | 947 | [[package]] 948 | name = "wasi" 949 | version = "0.11.0+wasi-snapshot-preview1" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 952 | 953 | [[package]] 954 | name = "wasm-bindgen" 955 | version = "0.2.90" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" 958 | dependencies = [ 959 | "cfg-if", 960 | "wasm-bindgen-macro", 961 | ] 962 | 963 | [[package]] 964 | name = "wasm-bindgen-backend" 965 | version = "0.2.90" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" 968 | dependencies = [ 969 | "bumpalo", 970 | "log", 971 | "once_cell", 972 | "proc-macro2", 973 | "quote", 974 | "syn", 975 | "wasm-bindgen-shared", 976 | ] 977 | 978 | [[package]] 979 | name = "wasm-bindgen-macro" 980 | version = "0.2.90" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" 983 | dependencies = [ 984 | "quote", 985 | "wasm-bindgen-macro-support", 986 | ] 987 | 988 | [[package]] 989 | name = "wasm-bindgen-macro-support" 990 | version = "0.2.90" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" 993 | dependencies = [ 994 | "proc-macro2", 995 | "quote", 996 | "syn", 997 | "wasm-bindgen-backend", 998 | "wasm-bindgen-shared", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "wasm-bindgen-shared" 1003 | version = "0.2.90" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" 1006 | 1007 | [[package]] 1008 | name = "winapi" 1009 | version = "0.3.9" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1012 | dependencies = [ 1013 | "winapi-i686-pc-windows-gnu", 1014 | "winapi-x86_64-pc-windows-gnu", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "winapi-i686-pc-windows-gnu" 1019 | version = "0.4.0" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1022 | 1023 | [[package]] 1024 | name = "winapi-x86_64-pc-windows-gnu" 1025 | version = "0.4.0" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1028 | 1029 | [[package]] 1030 | name = "windows-core" 1031 | version = "0.52.0" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1034 | dependencies = [ 1035 | "windows-targets 0.52.0", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "windows-sys" 1040 | version = "0.48.0" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1043 | dependencies = [ 1044 | "windows-targets 0.48.5", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "windows-sys" 1049 | version = "0.52.0" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1052 | dependencies = [ 1053 | "windows-targets 0.52.0", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "windows-targets" 1058 | version = "0.48.5" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1061 | dependencies = [ 1062 | "windows_aarch64_gnullvm 0.48.5", 1063 | "windows_aarch64_msvc 0.48.5", 1064 | "windows_i686_gnu 0.48.5", 1065 | "windows_i686_msvc 0.48.5", 1066 | "windows_x86_64_gnu 0.48.5", 1067 | "windows_x86_64_gnullvm 0.48.5", 1068 | "windows_x86_64_msvc 0.48.5", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "windows-targets" 1073 | version = "0.52.0" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 1076 | dependencies = [ 1077 | "windows_aarch64_gnullvm 0.52.0", 1078 | "windows_aarch64_msvc 0.52.0", 1079 | "windows_i686_gnu 0.52.0", 1080 | "windows_i686_msvc 0.52.0", 1081 | "windows_x86_64_gnu 0.52.0", 1082 | "windows_x86_64_gnullvm 0.52.0", 1083 | "windows_x86_64_msvc 0.52.0", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "windows_aarch64_gnullvm" 1088 | version = "0.48.5" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1091 | 1092 | [[package]] 1093 | name = "windows_aarch64_gnullvm" 1094 | version = "0.52.0" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 1097 | 1098 | [[package]] 1099 | name = "windows_aarch64_msvc" 1100 | version = "0.48.5" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1103 | 1104 | [[package]] 1105 | name = "windows_aarch64_msvc" 1106 | version = "0.52.0" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 1109 | 1110 | [[package]] 1111 | name = "windows_i686_gnu" 1112 | version = "0.48.5" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1115 | 1116 | [[package]] 1117 | name = "windows_i686_gnu" 1118 | version = "0.52.0" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 1121 | 1122 | [[package]] 1123 | name = "windows_i686_msvc" 1124 | version = "0.48.5" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1127 | 1128 | [[package]] 1129 | name = "windows_i686_msvc" 1130 | version = "0.52.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 1133 | 1134 | [[package]] 1135 | name = "windows_x86_64_gnu" 1136 | version = "0.48.5" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1139 | 1140 | [[package]] 1141 | name = "windows_x86_64_gnu" 1142 | version = "0.52.0" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 1145 | 1146 | [[package]] 1147 | name = "windows_x86_64_gnullvm" 1148 | version = "0.48.5" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1151 | 1152 | [[package]] 1153 | name = "windows_x86_64_gnullvm" 1154 | version = "0.52.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 1157 | 1158 | [[package]] 1159 | name = "windows_x86_64_msvc" 1160 | version = "0.48.5" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1163 | 1164 | [[package]] 1165 | name = "windows_x86_64_msvc" 1166 | version = "0.52.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 1169 | 1170 | [[package]] 1171 | name = "zerocopy" 1172 | version = "0.7.32" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 1175 | dependencies = [ 1176 | "zerocopy-derive", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "zerocopy-derive" 1181 | version = "0.7.32" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 1184 | dependencies = [ 1185 | "proc-macro2", 1186 | "quote", 1187 | "syn", 1188 | ] 1189 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tenki" 3 | version = "1.11.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.79" 8 | crossterm = { version = "0.27.0", features = ["serde", "event-stream"] } 9 | ratatui = "0.27.0" 10 | tokio = { version = "1.35.1", features = ["macros", "process", "rt", "rt-multi-thread", "signal", "time", "sync"] } 11 | serde = { version = "1.0", features = ["derive"] } 12 | futures = "0.3.30" 13 | rand = { version = "0.8.5", features = ["small_rng"] } 14 | tinyvec = "1.6.0" 15 | itertools = "0.13.0" 16 | chrono = { version = "0.4.31", features = ["std", "clock"], default-features = false } 17 | clap = { version = "4.4.18", features = ["derive"] } 18 | clap-num = "1.1.1" 19 | 20 | [profile.release] 21 | opt-level = "z" 22 | lto = true 23 | codegen-units = 1 24 | panic = 'abort' 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ckaznable 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | cargo build --release 3 | 4 | install: 5 | ./scripts/setup.sh 6 | 7 | uninstall: 8 | ./scripts/setup.sh uninstall 9 | 10 | clean: 11 | cargo clean -q 12 | 13 | all: build install clean 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tenki(天気) 2 | 3 | tty-clock with weather effect written in Rust and powerd by [ratatui](https://github.com/ratatui-org/ratatui) and tenki means weather in japanese 4 | 5 | ![demo](./doc/demo.gif) 6 | 7 | ## Installation 8 | 9 | 10 | [![Packaging status](https://repology.org/badge/vertical-allrepos/tenki.svg)](https://repology.org/project/tenki/versions) 11 | 12 | ### Install from Cargo 13 | 14 | ``` 15 | cargo install --git https://github.com/ckaznable/tenki.git 16 | ``` 17 | 18 | ### Install from Source Code 19 | 20 | tenki is written in Rust, so you'll need to grab a [Rust installation](https://www.rust-lang.org/) in order to compile it. 21 | 22 | ```shell 23 | git clone https://github.com/ckaznable/tenki 24 | cd tenki 25 | make build 26 | make install 27 | ``` 28 | 29 | If you want to uninstall 30 | 31 | ```shell 32 | make uninsall 33 | ``` 34 | 35 | ### Install from the AUR 36 | 37 | If you are using Arch Linux, you can install tenki using an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers). For example: 38 | 39 | ```shell 40 | paru -S tenki 41 | ``` 42 | 43 | ## Usage 44 | 45 | ``` 46 | Usage: tenki [OPTIONS] 47 | 48 | Options: 49 | --mode [default: rain] [possible values: rain, snow, meteor, disable] 50 | --timer-mode [possible values: dvd] 51 | --timer-color color of the effect. [red, green, blue] [default: white] 52 | -f, --fps frame per second [default: 60] 53 | -t, --tps tick per second [default: 60] 54 | -l, --level effect level, The lower, the stronger [4-1000] 55 | --wind wind mode. [random, disable, only-right, only-left, right, left] [default: random] 56 | --show-fps show fps at right-top in screen 57 | --blink-colon blinking colon of timer 58 | -h, --help Print help 59 | -V, --version Print version 60 | ``` 61 | 62 | ## Roadmap 63 | 64 | - [x] CLI options 65 | - [ ] customizable 66 | 67 | ## LICENSE 68 | 69 | [MIT](./LICENSE) 70 | 71 | -------------------------------------------------------------------------------- /doc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckaznable/tenki/a70c1dc5bcf01d7650692fe0a7b6c38ff767bc76/doc/demo.gif -------------------------------------------------------------------------------- /doc/demo.tape: -------------------------------------------------------------------------------- 1 | Output demo.gif 2 | 3 | Set FontSize 16 4 | Set Width 1280 5 | Set Height 600 6 | 7 | Hide 8 | Type "tenki --fps 60" 9 | Enter 10 | Sleep 1s 11 | 12 | Show 13 | Sleep 8s 14 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$(id -u)" -eq 0 ]; then 4 | TARGET="/usr/local/bin/tenki" 5 | else 6 | if [[ ":$PATH:" == *":$HOME/.local/bin:"* ]]; then 7 | TARGET="$HOME/.local/bin/tenki" 8 | elif [[ ":$PATH:" == *":$HOME/bin:"* ]]; then 9 | TARGET="$HOME/bin/tenki" 10 | else 11 | echo "This operation requires root privileges. Please run this script as root or use sudo to obtain the necessary permissions." 12 | exit 1 13 | fi 14 | fi 15 | 16 | if [ -n "$1" ] && [ "$1" = "uninstall" ] && [ -f "$TARGET" ]; then 17 | rm $TARGET 18 | else 19 | cp -r target/release/tenki $TARGET 20 | chmod +x $TARGET 21 | echo "The installation was successful. The application is now installed in $TARGET" 22 | fi 23 | 24 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::{ 3 | cursor, 4 | event::{DisableMouseCapture, KeyCode, KeyEvent}, 5 | execute, 6 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 7 | }; 8 | use ratatui::{backend::CrosstermBackend, Terminal}; 9 | 10 | use crate::{ 11 | cli::Args, state::{EachFrameImpl, ShouldRender, State}, tui::{Event, Tui}, ui::ui, widget::AsWeatherWidget 12 | }; 13 | 14 | #[derive(Copy, Clone)] 15 | pub struct AppRuntimeInfo { 16 | pub fps: usize, 17 | } 18 | 19 | pub struct App { 20 | terminal: Terminal>, 21 | tui: Tui, 22 | state: State, 23 | should_quit: bool, 24 | should_render: ShouldRender, 25 | args: Args, 26 | frame_in_second: usize, 27 | runtime_info: AppRuntimeInfo, 28 | } 29 | 30 | impl App 31 | where 32 | T: EachFrameImpl + AsWeatherWidget, 33 | { 34 | pub fn new(args: Args, weather: T) -> Result { 35 | // setup terminal 36 | enable_raw_mode()?; 37 | let mut stdout = std::io::stdout(); 38 | execute!(stdout, EnterAlternateScreen)?; 39 | 40 | let backend = CrosstermBackend::new(stdout); 41 | let terminal = Terminal::new(backend)?; 42 | let state = State::new(terminal.size()?, weather, args); 43 | 44 | Ok(Self { 45 | terminal, 46 | state, 47 | args, 48 | tui: Tui::new(args.fps as f64, args.tps as f64)?, 49 | should_quit: false, 50 | should_render: ShouldRender::Render, 51 | frame_in_second: 0, 52 | runtime_info: AppRuntimeInfo { fps: 0 }, 53 | }) 54 | } 55 | 56 | pub async fn run(&mut self) -> Result<()> { 57 | use Event::*; 58 | self.tui.run(); 59 | 60 | loop { 61 | if let Some(event) = self.tui.next().await { 62 | match event { 63 | Init => (), 64 | Quit | Error => self.should_quit = true, 65 | Render => self.on_render()?, 66 | Key(key) => self.handle_keyboard(key), 67 | Tick => self.on_tick(), 68 | Timer => self.on_timer(), 69 | Resize(columns, rows) => self.on_resize(columns, rows), 70 | }; 71 | }; 72 | 73 | if self.should_quit { 74 | break; 75 | } 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | fn handle_keyboard(&mut self, key: KeyEvent) { 82 | match key.code { 83 | KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => self.should_quit = true, 84 | _ => {} 85 | } 86 | } 87 | 88 | fn on_resize(&mut self, columns: u16, rows: u16) { 89 | self.state.on_resize(columns, rows); 90 | self.should_render = ShouldRender::Render; 91 | } 92 | 93 | fn on_tick(&mut self) { 94 | self.should_render = self.should_render.or(self.state.tick()); 95 | self.frame_in_second = self.frame_in_second.saturating_add(1); 96 | } 97 | 98 | fn on_render(&mut self) -> anyhow::Result<()> { 99 | if self.args.fps == self.args.tps { 100 | self.on_tick() 101 | } 102 | 103 | if self.should_render.is_render() { 104 | self.should_render = ShouldRender::Skip; 105 | self.terminal.draw(|f| ui(f, &mut self.state, self.args, self.runtime_info))?; 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | fn on_timer(&mut self) { 112 | self.state.tick_timer(); 113 | self.runtime_info.fps = self.frame_in_second; 114 | self.frame_in_second = 0; 115 | self.should_render = ShouldRender::Render; 116 | } 117 | } 118 | 119 | impl Drop for App { 120 | fn drop(&mut self) { 121 | // restore terminal 122 | if crossterm::terminal::is_raw_mode_enabled().unwrap() { 123 | let _ = disable_raw_mode(); 124 | let _ = execute!( 125 | self.terminal.backend_mut(), 126 | LeaveAlternateScreen, 127 | DisableMouseCapture, 128 | cursor::Show 129 | ); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use clap::Parser; 4 | use clap_num::number_range; 5 | use ratatui::style::Color; 6 | 7 | use crate::state::{timer::TimerMode, wind::WindMode, Mode}; 8 | 9 | #[derive(Parser, Clone, Copy)] 10 | #[command(author, version, about, long_about = None)] 11 | pub struct Args { 12 | #[arg(long, default_value_t = Mode::Rain)] 13 | pub mode: Mode, 14 | 15 | #[arg(long)] 16 | pub timer_mode: Option, 17 | 18 | /// color of the effect. [red, green, blue, yellow, cyan, magenta, white, black] 19 | #[arg(long, value_parser = Color::from_str, default_value = "white")] 20 | pub timer_color: Color, 21 | 22 | /// frame per second 23 | #[arg(short, long, value_parser = process_rate_range, default_value_t = 60)] 24 | pub fps: u8, 25 | 26 | /// tick per second 27 | #[arg(short, long, value_parser = process_rate_range, default_value_t = 60)] 28 | pub tps: u8, 29 | 30 | /// effect level, The lower, the stronger [4-1000] 31 | #[arg(short, long, value_parser = level_range)] 32 | pub level: Option, 33 | 34 | /// wind mode. [random, disable, only-right, only-left, right, left] 35 | #[arg(long, value_parser = WindMode::from_str, default_value = "random")] 36 | pub wind: WindMode, 37 | 38 | /// show fps at right-top in screen 39 | #[arg(long)] 40 | pub show_fps: bool, 41 | 42 | /// blinking colon of timer 43 | #[arg(long)] 44 | pub blink_colon: bool, 45 | } 46 | 47 | fn process_rate_range(s: &str) -> Result { 48 | number_range(s, 1, 240) 49 | } 50 | 51 | fn level_range(s: &str) -> Result { 52 | number_range(s, 0, 1000) 53 | } 54 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod cli; 3 | mod state; 4 | mod tui; 5 | mod ui; 6 | mod util; 7 | mod weather; 8 | mod widget; 9 | 10 | use anyhow::Result; 11 | use app::App; 12 | use clap::Parser; 13 | use cli::Args; 14 | use weather::Weather; 15 | 16 | #[tokio::main] 17 | async fn main() -> Result<()> { 18 | let args = Args::parse(); 19 | let mut app = App::new(args, Weather::from(args))?; 20 | app.run().await?; 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /src/state/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use ratatui::layout::Rect; 5 | use tinyvec::ArrayVec; 6 | 7 | use super::Column; 8 | use super::CellType; 9 | 10 | pub struct RenderBuffer { 11 | pub buf: Vec, 12 | pub line: Vec, 13 | } 14 | 15 | impl RenderBuffer { 16 | pub fn new(size: Rect) -> Self { 17 | let mut buf = Vec::with_capacity(size.width as usize); 18 | for _ in 0..size.width { 19 | let mut column = Vec::with_capacity(size.height as usize); 20 | for _ in 0..size.height { 21 | column.push(ArrayVec::<[CellType; 3]>::default()); 22 | } 23 | 24 | buf.push(Rc::new(RefCell::new(column))); 25 | } 26 | 27 | Self { 28 | line: Vec::with_capacity(buf.len()), 29 | buf, 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/state/dropping.rs: -------------------------------------------------------------------------------- 1 | use super::{buffer::RenderBuffer, Cell, CellType, Column, EachFrameImpl, Mode, ShouldRender}; 2 | 3 | pub struct DroppingState { 4 | pub threshold: u16, 5 | pub mode: Mode, 6 | } 7 | 8 | impl DroppingState { 9 | fn gen_drop(&self, rb: &mut RenderBuffer, seed: u64) { 10 | rb.line.clear(); 11 | 12 | const GROUP_SIZE: u64 = 64; 13 | let len = rb.buf.len() as u64; 14 | let last_group = len % GROUP_SIZE; 15 | let groups = len / GROUP_SIZE + if last_group > 0 { 1 } else { 0 }; 16 | 17 | for g in 0..groups { 18 | let range = if groups.saturating_sub(1) == g { last_group } else { GROUP_SIZE }; 19 | for i in 0..range { 20 | rb.line.push(if seed & (1 << i) != 0 { 21 | Self::get_drop_speed(seed.saturating_sub(i), self.threshold) 22 | } else { 23 | CellType::None 24 | }); 25 | } 26 | } 27 | } 28 | 29 | /// generate new drop line 30 | fn new_drop(&self, rb: &mut RenderBuffer, seed: u64) { 31 | self.gen_drop(rb, seed); 32 | rb.line 33 | .iter() 34 | .enumerate() 35 | .for_each(|(i, d)| { 36 | if let Some(cell) = rb.buf 37 | .get_mut(i) 38 | .unwrap() 39 | .try_borrow_mut() 40 | .unwrap() 41 | .get_mut(0) { 42 | 43 | *cell = Self::merge_drop_state(*cell, *d) 44 | }; 45 | }); 46 | } 47 | 48 | fn drop(col: &mut Column, frame: u64, mode: Mode) { 49 | let len = col.borrow().len(); 50 | 51 | for col_index in 0..len { 52 | let next_index = len.saturating_sub(col_index.saturating_add(1)); 53 | let current_index = len.saturating_sub(col_index.saturating_add(2)); 54 | let current = { col.borrow().get(current_index).cloned() }; 55 | let Some(current) = current else { continue; }; 56 | let mut column = col.try_borrow_mut().unwrap(); 57 | 58 | 'state: for i in 0..current.len() { 59 | let state = match current.get(i) { 60 | Some(s) if frame % mode.get_frame_by_speed(*s) == 0 => s, 61 | _ => continue 'state 62 | }; 63 | 64 | column[current_index] = Self::remove_drop_state(column[current_index], *state); 65 | column[next_index] = Self::merge_drop_state(column[next_index], *state); 66 | } 67 | } 68 | } 69 | 70 | #[inline] 71 | fn clean_latest_drop(col: &mut Column) { 72 | let len = col.borrow().len(); 73 | if len > 0 { 74 | let mut col = col.try_borrow_mut().unwrap(); 75 | if let Some(c) = col.get_mut(len - 1) { 76 | c.clear() 77 | }; 78 | } 79 | } 80 | 81 | #[inline] 82 | fn merge_drop_state(mut cell: Cell, state: CellType) -> Cell { 83 | if !cell.contains(&state) && state != CellType::None { 84 | let _ = cell.try_push(state); 85 | }; 86 | 87 | cell 88 | } 89 | 90 | #[inline] 91 | fn remove_drop_state(cell: Cell, state: CellType) -> Cell { 92 | cell.into_iter().filter(|c| *c != state).collect() 93 | } 94 | 95 | #[inline] 96 | fn get_drop_speed(num: u64, threshold: u16) -> CellType { 97 | match num % threshold as u64 { 98 | 0 => CellType::Normal, 99 | 1 => CellType::Fast, 100 | 2 => CellType::Slow, 101 | _ => CellType::None, 102 | } 103 | } 104 | } 105 | 106 | impl EachFrameImpl for DroppingState { 107 | fn on_frame(&mut self, rb: &mut super::buffer::RenderBuffer, seed: u64, frame: u64) -> ShouldRender { 108 | // each column 109 | for i in 0..rb.buf.len() { 110 | Self::clean_latest_drop(&mut rb.buf[i]); 111 | Self::drop(&mut rb.buf[i], frame, self.mode); 112 | } 113 | 114 | self.new_drop(rb, seed); 115 | ShouldRender::Render 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/state/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, fmt::Display, rc::Rc}; 2 | 3 | use clap::ValueEnum; 4 | use rand::{rngs::SmallRng, RngCore, SeedableRng}; 5 | use ratatui::layout::Rect; 6 | use tinyvec::ArrayVec; 7 | 8 | use self::{ 9 | buffer::RenderBuffer, 10 | timer::{Timer, TimerState}, 11 | }; 12 | 13 | pub mod buffer; 14 | pub mod dropping; 15 | pub mod tail; 16 | pub mod timer; 17 | pub mod wind; 18 | 19 | pub type Cell = ArrayVec<[CellType; 3]>; 20 | pub type Column = Rc>>; 21 | 22 | pub trait EachFrameImpl { 23 | fn on_frame(&mut self, _: &mut RenderBuffer, _: u64, _: u64) -> ShouldRender { 24 | ShouldRender::Skip 25 | } 26 | } 27 | 28 | #[derive(PartialEq, Eq, Copy, Clone)] 29 | pub enum ShouldRender { 30 | Render, 31 | Skip, 32 | } 33 | 34 | impl ShouldRender { 35 | pub fn or(self, sr: Self) -> Self { 36 | match sr { 37 | Self::Skip => self, 38 | Self::Render => sr, 39 | } 40 | } 41 | 42 | pub fn is_render(&self) -> bool { 43 | *self == Self::Render 44 | } 45 | } 46 | 47 | #[derive(Copy, Clone, Default)] 48 | pub enum Direction { 49 | #[default] 50 | LeftTop, 51 | LeftBottom, 52 | RightTop, 53 | RightBottom, 54 | Up, 55 | Down, 56 | Left, 57 | Right, 58 | } 59 | 60 | impl Direction { 61 | pub fn reflection_h(&self) -> Self { 62 | use Direction::*; 63 | match *self { 64 | LeftTop => LeftBottom, 65 | LeftBottom => LeftTop, 66 | RightTop => RightBottom, 67 | RightBottom => RightTop, 68 | Up => Down, 69 | Down => Up, 70 | Left => Right, 71 | Right => Left, 72 | } 73 | } 74 | 75 | pub fn reflection_v(&self) -> Self { 76 | use Direction::*; 77 | match *self { 78 | LeftTop => RightTop, 79 | LeftBottom => RightBottom, 80 | RightTop => LeftTop, 81 | RightBottom => LeftBottom, 82 | Up => Down, 83 | Down => Up, 84 | Left => Right, 85 | Right => Left, 86 | } 87 | } 88 | 89 | pub fn reflection_reverse(&self) -> Self { 90 | use Direction::*; 91 | match *self { 92 | LeftTop => RightBottom, 93 | LeftBottom => RightTop, 94 | RightTop => LeftBottom, 95 | RightBottom => LeftTop, 96 | Up => Down, 97 | Down => Up, 98 | Left => Right, 99 | Right => Left, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Copy, Clone)] 105 | pub struct Position(u16, u16); 106 | impl From for Position { 107 | fn from(value: Rect) -> Self { 108 | Self(value.x, value.y) 109 | } 110 | } 111 | 112 | impl Position { 113 | pub fn into_rect(self, width: u16, height: u16) -> Rect { 114 | Rect { 115 | height, 116 | width, 117 | x: self.0, 118 | y: self.1, 119 | } 120 | } 121 | 122 | pub fn mv(self, dir: Direction) -> Self { 123 | use Direction::*; 124 | let Position(x, y) = self; 125 | match dir { 126 | LeftTop => Self(x.saturating_sub(1), y.saturating_sub(1)), 127 | LeftBottom => Self(x.saturating_sub(1), y.saturating_add(1)), 128 | RightTop => Self(x.saturating_add(1), y.saturating_sub(1)), 129 | RightBottom => Self(x.saturating_add(1), y.saturating_add(1)), 130 | Up => Self(x, y.saturating_sub(1)), 131 | Down => Self(x, y.saturating_add(1)), 132 | Left => Self(x.saturating_sub(1), y), 133 | Right => Self(x.saturating_add(1), y), 134 | } 135 | } 136 | } 137 | 138 | #[derive(Copy, Clone, PartialEq, Eq, Default)] 139 | pub enum CellType { 140 | Fast, 141 | Normal, 142 | Slow, 143 | Tail, 144 | #[default] 145 | None, 146 | } 147 | 148 | impl CellType { 149 | pub fn is_dropping_cell(&self) -> bool { 150 | use CellType::*; 151 | matches!(*self, Fast | Normal | Slow) 152 | } 153 | } 154 | 155 | #[derive(Copy, Clone, Default, ValueEnum, PartialEq, Eq)] 156 | pub enum Mode { 157 | #[default] 158 | Rain, 159 | Snow, 160 | Meteor, 161 | // Star, 162 | // PingPong, 163 | Disable, 164 | } 165 | 166 | impl Display for Mode { 167 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 168 | let s = match *self { 169 | Mode::Rain => "rain", 170 | Mode::Snow => "snow", 171 | Mode::Meteor => "meteor", 172 | // Mode::Star => "star", 173 | // Mode::PingPong => "pingpong", 174 | Mode::Disable => "disable", 175 | }; 176 | 177 | s.fmt(f) 178 | } 179 | } 180 | 181 | impl Mode { 182 | pub fn get_frame_by_speed(&self, s: CellType) -> u64 { 183 | use CellType::*; 184 | use Mode::*; 185 | 186 | match self { 187 | Rain => match s { 188 | Fast => 1, 189 | Normal => 2, 190 | Slow => 3, 191 | _ => 0, 192 | } 193 | Snow => match s { 194 | Fast => 2, 195 | Normal => 4, 196 | Slow => 6, 197 | _ => 0, 198 | } 199 | Meteor => 4, 200 | _ => 0 201 | } 202 | } 203 | } 204 | 205 | pub struct State { 206 | pub rb: RenderBuffer, 207 | pub timer: Timer, 208 | pub timer_state: TimerState, 209 | pub weather: T, 210 | frame: u64, 211 | rng: SmallRng, 212 | seed: u64, 213 | } 214 | 215 | impl State { 216 | pub fn new(size: Rect, weather: T, args: crate::cli::Args) -> Self { 217 | let mut timer_state = TimerState::new(size, args.timer_mode.map(|mode| mode.into())); 218 | if args.blink_colon { 219 | timer_state.colon.enable_blink(); 220 | } 221 | 222 | State { 223 | rb: RenderBuffer::new(size), 224 | rng: SmallRng::from_entropy(), 225 | frame: 0, 226 | timer: Timer::default(), 227 | timer_state, 228 | seed: 0, 229 | weather, 230 | } 231 | } 232 | 233 | pub fn on_resize(&mut self, columns: u16, rows: u16) { 234 | let rect = Rect { 235 | x: 0, 236 | y: 0, 237 | height: rows, 238 | width: columns, 239 | }; 240 | 241 | self.rb = RenderBuffer::new(rect); 242 | self.timer_state = TimerState::new(rect, self.timer_state.mode); 243 | } 244 | 245 | pub fn tick_timer(&mut self) { 246 | self.timer = Timer::new(); 247 | } 248 | 249 | pub fn tick(&mut self) -> ShouldRender { 250 | self.frame = if self.frame == u64::MAX { 0 } else { self.frame.saturating_add(1) }; 251 | self.seed = self.rng.next_u64(); 252 | 253 | self.weather.on_frame(&mut self.rb, self.seed, self.frame) 254 | .or(self.timer_state.on_frame(&mut self.rb, self.seed, self.frame)) 255 | } 256 | } 257 | 258 | -------------------------------------------------------------------------------- /src/state/tail.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use super::{buffer::RenderBuffer, wind::WindMode, Cell, CellType, Column, EachFrameImpl, ShouldRender}; 4 | 5 | #[derive(Default, Copy, Clone, Eq, PartialEq)] 6 | pub enum TailMode { 7 | Left, 8 | Right, 9 | #[default] 10 | Default, 11 | } 12 | 13 | const TAIL_LEN: usize = 3; 14 | 15 | impl From for TailMode { 16 | fn from(value: WindMode) -> Self { 17 | match value { 18 | WindMode::Random | WindMode::Disable => Self::default(), 19 | WindMode::OnlyRight => Self::Right, 20 | WindMode::OnlyLeft => Self::Left, 21 | } 22 | } 23 | } 24 | 25 | pub struct TailState { 26 | pub mode: TailMode, 27 | } 28 | 29 | impl TailState { 30 | pub fn new(mode: TailMode) -> Self { 31 | Self { mode } 32 | } 33 | 34 | fn remove_tail_from_cell(cell: Cell) -> Cell { 35 | cell.into_iter().filter(|t| *t != CellType::Tail).collect() 36 | } 37 | 38 | fn append_tail(mut cell: Cell) -> Cell { 39 | if !cell.contains(&CellType::Tail) { 40 | cell.push(CellType::Tail); 41 | } 42 | 43 | cell 44 | } 45 | 46 | fn render_default_tail(cols: &mut Vec) { 47 | for col in cols { 48 | let mut col = RefCell::borrow_mut(col); 49 | for i in 0..col.len() { 50 | if let Some(cell) = col.get_mut(i) { 51 | *cell = Self::remove_tail_from_cell(*cell); 52 | 53 | if i == 0 { 54 | continue; 55 | } 56 | 57 | if !cell.iter().any(|t| t.is_dropping_cell()) { 58 | continue; 59 | } 60 | 61 | for j in i.saturating_sub(TAIL_LEN + 1)..(i - 1) { 62 | let cell = col.get_mut(j).unwrap(); 63 | *cell = Self::append_tail(*cell) 64 | } 65 | }; 66 | } 67 | } 68 | } 69 | 70 | fn render_left_tail(cols: &mut [Column]) { 71 | cols.reverse(); 72 | Self::render_right_tail(cols); 73 | cols.reverse() 74 | } 75 | 76 | fn render_right_tail(cols: &mut [Column]) { 77 | let mut tail_pos: Vec<(usize, usize)> = vec![]; 78 | 79 | for x in 0..cols.len() { 80 | let mut col = RefCell::borrow_mut(cols.get(x).unwrap()); 81 | for y in 0..col.len() { 82 | if let Some(cell) = col.get_mut(y) { 83 | *cell = Self::remove_tail_from_cell(*cell); 84 | 85 | if y == 0 { 86 | continue; 87 | } 88 | 89 | if !cell.iter().any(|t| t.is_dropping_cell()) { 90 | continue; 91 | } 92 | 93 | for (i, xi) in (x.saturating_sub(TAIL_LEN)..x).enumerate() { 94 | let yi = y.saturating_sub(TAIL_LEN.saturating_sub(i)); 95 | tail_pos.push((xi, yi)); 96 | } 97 | }; 98 | } 99 | } 100 | 101 | tail_pos.into_iter().for_each(|(x, y)| { 102 | if let Some(col) = cols.get(x) { 103 | let mut col = RefCell::borrow_mut(col); 104 | if let Some(cell) = col.get_mut(y) { 105 | *cell = Self::append_tail(*cell) 106 | } 107 | } 108 | }); 109 | } 110 | } 111 | 112 | impl EachFrameImpl for TailState { 113 | fn on_frame(&mut self, rb: &mut RenderBuffer, _seed: u64, _frame: u64) -> ShouldRender { 114 | match self.mode { 115 | TailMode::Left => Self::render_left_tail(&mut rb.buf), 116 | TailMode::Right => Self::render_right_tail(&mut rb.buf), 117 | TailMode::Default => Self::render_default_tail(&mut rb.buf), 118 | }; 119 | 120 | ShouldRender::Render 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/state/timer.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, time::SystemTime}; 2 | 3 | use chrono::{DateTime, Local, Timelike}; 4 | use clap::ValueEnum; 5 | use ratatui::layout::Rect; 6 | 7 | use crate::widget::timer::{TIMER_LAYOUT_HEIGHT, TIMER_LAYOUT_WIDTH}; 8 | 9 | use super::{buffer::RenderBuffer, Direction, EachFrameImpl, Position, ShouldRender}; 10 | 11 | #[derive(Copy, Clone)] 12 | pub struct Timer { 13 | pub hours: u8, 14 | pub minutes: u8, 15 | pub seconds: u8, 16 | } 17 | 18 | impl Timer { 19 | pub fn new() -> Self { 20 | Self::default() 21 | } 22 | } 23 | 24 | impl Default for Timer { 25 | fn default() -> Self { 26 | let system_time = SystemTime::now(); 27 | let datetime: DateTime = system_time.into(); 28 | Self { 29 | hours: datetime.hour() as u8, 30 | minutes: datetime.minute() as u8, 31 | seconds: datetime.second() as u8, 32 | } 33 | } 34 | } 35 | 36 | /// enum alias for parsed from cli 37 | #[derive(Copy, Clone, ValueEnum)] 38 | pub enum TimerMode { 39 | Dvd, 40 | } 41 | 42 | impl Display for TimerMode { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | let s = match *self { 45 | TimerMode::Dvd => "dvd", 46 | }; 47 | 48 | s.fmt(f) 49 | } 50 | } 51 | 52 | #[derive(Copy, Clone)] 53 | pub enum TimerRenderMode { 54 | Dvd(Direction), 55 | } 56 | 57 | impl Default for TimerRenderMode { 58 | fn default() -> Self { 59 | Self::Dvd(Direction::default()) 60 | } 61 | } 62 | 63 | impl From for TimerRenderMode { 64 | fn from(value: TimerMode) -> Self { 65 | match value { 66 | TimerMode::Dvd => Self::Dvd(Direction::default()) 67 | } 68 | } 69 | } 70 | 71 | pub struct ColonState { 72 | pub show: bool, 73 | blink: bool, 74 | } 75 | 76 | impl Default for ColonState { 77 | fn default() -> Self { 78 | Self { show: true, blink: false } 79 | } 80 | } 81 | 82 | impl ColonState { 83 | pub fn enable_blink(&mut self) { 84 | self.blink = true; 85 | } 86 | 87 | fn toggle(&mut self) { 88 | self.show = !self.show; 89 | } 90 | } 91 | 92 | impl EachFrameImpl for ColonState { 93 | fn on_frame(&mut self, _: &mut RenderBuffer, _: u64, frame: u64) -> ShouldRender { 94 | if self.blink && frame % 24 == 0 { 95 | self.toggle(); 96 | ShouldRender::Render 97 | } else { 98 | ShouldRender::Skip 99 | } 100 | } 101 | } 102 | 103 | pub struct TimerState { 104 | pub mode: Option, 105 | pub area: Rect, 106 | pub pos: Position, 107 | pub boundary: Rect, 108 | pub colon: ColonState, 109 | } 110 | 111 | impl TimerState { 112 | pub fn new(area: Rect, mode: Option) -> Self { 113 | let boundary = area; 114 | let area = Self::get_center_area(area); 115 | 116 | Self { 117 | mode, 118 | area, 119 | boundary, 120 | pos: area.into(), 121 | colon: ColonState::default(), 122 | } 123 | } 124 | 125 | fn on_dvd_frame(&mut self) { 126 | let Some(TimerRenderMode::Dvd(dir)) = self.mode else { 127 | return; 128 | }; 129 | 130 | let is_collision_h = self.is_collision_h(); 131 | let is_collision_v = self.is_collision_v(); 132 | 133 | let dir = 134 | if is_collision_h && is_collision_v { 135 | dir.reflection_reverse() 136 | } else if is_collision_h { 137 | dir.reflection_h() 138 | } else if is_collision_v { 139 | dir.reflection_v() 140 | } else { 141 | dir 142 | }; 143 | 144 | self.pos = self.pos.mv(dir); 145 | self.mode = Some(TimerRenderMode::Dvd(dir)); 146 | } 147 | 148 | fn get_center_area(area: Rect) -> Rect { 149 | let padding_h = (area.width.saturating_sub(TIMER_LAYOUT_WIDTH)) / 2; 150 | let padding_v = (area.height.saturating_sub(TIMER_LAYOUT_HEIGHT)) / 2; 151 | 152 | Rect { 153 | x: padding_h, 154 | y: padding_v, 155 | height: TIMER_LAYOUT_HEIGHT, 156 | width: TIMER_LAYOUT_WIDTH, 157 | } 158 | } 159 | 160 | fn get_area_with_pos(pos: Position) -> Rect { 161 | pos.into_rect(TIMER_LAYOUT_WIDTH, TIMER_LAYOUT_HEIGHT) 162 | } 163 | 164 | fn is_collision_v(&self) -> bool { 165 | self.pos.0 == 0 || (self.pos.0 + TIMER_LAYOUT_WIDTH) >= self.boundary.width 166 | } 167 | 168 | fn is_collision_h(&self) -> bool { 169 | self.pos.1 == 0 || (self.pos.1 + TIMER_LAYOUT_HEIGHT) >= self.boundary.height 170 | } 171 | 172 | fn handle_mode(&mut self, frame: u64) -> ShouldRender { 173 | if self.mode.is_none() { 174 | return ShouldRender::Skip; 175 | } 176 | 177 | if frame % 8 > 0 { 178 | return ShouldRender::Skip; 179 | } 180 | 181 | match self.mode.unwrap() { 182 | TimerRenderMode::Dvd(_) => self.on_dvd_frame(), 183 | } 184 | 185 | self.area = Self::get_area_with_pos(self.pos); 186 | ShouldRender::Render 187 | } 188 | } 189 | 190 | impl EachFrameImpl for TimerState { 191 | fn on_frame(&mut self, rb: &mut RenderBuffer, seed: u64, frame: u64) -> ShouldRender { 192 | self.handle_mode(frame) 193 | .or(self.colon.on_frame(rb, seed, frame)) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/state/wind.rs: -------------------------------------------------------------------------------- 1 | use super::{buffer::RenderBuffer, EachFrameImpl, ShouldRender}; 2 | 3 | pub trait WindImpl { 4 | fn direction(&self) -> WindDirection; 5 | } 6 | 7 | #[derive(Clone, Copy, Default, Eq, PartialEq)] 8 | pub enum WindMode { 9 | #[default] 10 | Random, 11 | Disable, 12 | OnlyRight, 13 | OnlyLeft, 14 | } 15 | 16 | impl WindMode { 17 | pub fn from_str(s: &str) -> Result { 18 | match s { 19 | "random" => Ok(WindMode::Random), 20 | "disable" => Ok(WindMode::Disable), 21 | "right" | "only-right" => Ok(WindMode::OnlyRight), 22 | "left" | "only-left" => Ok(WindMode::OnlyLeft), 23 | _ => Err("Invalid parameter, only accept random, disable, only-right, only-left, right or left."), 24 | } 25 | } 26 | 27 | pub fn without_random(self) -> Self { 28 | match self { 29 | WindMode::Random => Self::Disable, 30 | other => other, 31 | } 32 | } 33 | } 34 | 35 | #[derive(Clone, Copy, Default, Eq, PartialEq)] 36 | pub enum WindDirection { 37 | Left, 38 | Right, 39 | #[default] 40 | None, 41 | } 42 | 43 | type FrameCount = usize; 44 | 45 | #[derive(Clone, Copy, Default)] 46 | pub struct WindState { 47 | pub mode: WindMode, 48 | pub direction: WindDirection, 49 | frame: FrameCount, 50 | } 51 | 52 | impl WindImpl for WindState { 53 | fn direction(&self) -> WindDirection { 54 | self.direction 55 | } 56 | } 57 | 58 | impl WindState { 59 | pub fn new(mode: WindMode) -> Self { 60 | Self { 61 | mode, 62 | direction: Self::direction_from_mode(mode), 63 | ..Default::default() 64 | } 65 | } 66 | 67 | pub fn direction_from_mode(mode: WindMode) -> WindDirection { 68 | match mode { 69 | WindMode::OnlyRight => WindDirection::Right, 70 | WindMode::OnlyLeft => WindDirection::Left, 71 | _ => WindDirection::None 72 | } 73 | } 74 | } 75 | 76 | impl EachFrameImpl for WindState { 77 | fn on_frame(&mut self, rb: &mut RenderBuffer, seed: u64, _: u64) -> ShouldRender { 78 | self.direction = match self.mode { 79 | WindMode::Disable => WindDirection::None, 80 | WindMode::OnlyLeft => WindDirection::Left, 81 | WindMode::OnlyRight => WindDirection::Right, 82 | _ => self.direction, 83 | }; 84 | 85 | if self.mode == WindMode::Disable { 86 | return ShouldRender::Skip; 87 | } 88 | 89 | if self.frame == 0 || self.direction == WindDirection::None { 90 | self.frame = 255; 91 | self.direction = if seed % 2024 == 0 { 92 | WindDirection::Left 93 | } else if seed % 123 == 0 { 94 | WindDirection::Right 95 | } else { 96 | WindDirection::None 97 | } 98 | } 99 | 100 | if self.direction == WindDirection::None { 101 | return ShouldRender::Skip; 102 | } 103 | 104 | self.frame = self.frame.saturating_sub(1); 105 | 106 | if self.direction == WindDirection::Left { 107 | rb.buf.reverse(); 108 | } 109 | 110 | for i in 1..rb.buf.len() { 111 | rb.buf.swap(0, i); 112 | } 113 | 114 | if self.direction == WindDirection::Left { 115 | rb.buf.reverse(); 116 | } 117 | 118 | ShouldRender::Render 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event::{Event as CrosstermEvent, KeyEvent, KeyEventKind}; 3 | use futures::{FutureExt, StreamExt}; 4 | use serde::{Deserialize, Serialize}; 5 | use tokio::{ 6 | sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, 7 | task::JoinHandle, 8 | }; 9 | 10 | use crate::util::waiting_time_to_sync; 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | pub enum Event { 14 | Init, 15 | Quit, 16 | Error, 17 | Render, 18 | Tick, 19 | Timer, 20 | Key(KeyEvent), 21 | Resize(u16, u16), 22 | } 23 | 24 | pub struct Tui { 25 | frame_rate: f64, 26 | tick_rate: f64, 27 | event_rx: UnboundedReceiver, 28 | event_tx: UnboundedSender, 29 | task: Option>, 30 | } 31 | 32 | impl Tui { 33 | pub fn new(frame_rate: f64, tick_rate: f64) -> Result { 34 | let (event_tx, event_rx) = mpsc::unbounded_channel(); 35 | let task = None; 36 | Ok(Self { 37 | task, 38 | event_rx, 39 | event_tx, 40 | frame_rate, 41 | tick_rate, 42 | }) 43 | } 44 | 45 | pub fn run(&mut self) { 46 | let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate); 47 | let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate); 48 | let timer_delay = std::time::Duration::from_secs_f64(1.0); 49 | let _event_tx = self.event_tx.clone(); 50 | let do_tick = self.frame_rate != self.tick_rate; 51 | 52 | let task = tokio::spawn(async move { 53 | _event_tx.send(Event::Init).unwrap(); 54 | waiting_time_to_sync(); 55 | 56 | let mut reader = crossterm::event::EventStream::new(); 57 | let mut render_interval = tokio::time::interval(render_delay); 58 | let mut tick_interval = tokio::time::interval(tick_delay); 59 | let mut timer_interval = tokio::time::interval(timer_delay); 60 | 61 | loop { 62 | let tick_delay = tick_interval.tick(); 63 | let render_delay = render_interval.tick(); 64 | let timer_delay = timer_interval.tick(); 65 | let crossterm_event = reader.next().fuse(); 66 | tokio::select! { 67 | maybe_event = crossterm_event => { 68 | match maybe_event { 69 | Some(Ok(evt)) => { 70 | match evt { 71 | CrosstermEvent::Key(key) => { 72 | if key.kind == KeyEventKind::Press { 73 | _event_tx.send(Event::Key(key)).unwrap(); 74 | } 75 | }, 76 | CrosstermEvent::Resize(x, y) => { 77 | _event_tx.send(Event::Resize(x, y)).unwrap(); 78 | }, 79 | _ => () 80 | } 81 | } 82 | Some(Err(_)) => { 83 | _event_tx.send(Event::Error).unwrap(); 84 | } 85 | None => (), 86 | } 87 | }, 88 | _ = tick_delay => { 89 | if do_tick { 90 | _event_tx.send(Event::Tick).unwrap(); 91 | } 92 | }, 93 | _ = render_delay => { 94 | _event_tx.send(Event::Render).unwrap(); 95 | }, 96 | _ = timer_delay => { 97 | _event_tx.send(Event::Timer).unwrap(); 98 | }, 99 | } 100 | } 101 | }); 102 | 103 | self.task = Some(task); 104 | } 105 | 106 | pub async fn next(&mut self) -> Option { 107 | self.event_rx.recv().await 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use ratatui::Frame; 2 | use crate::app::AppRuntimeInfo; 3 | use crate::state::{EachFrameImpl, State}; 4 | use crate::cli::Args; 5 | 6 | use crate::widget::fps::FpsWidget; 7 | use crate::widget::{AsWeatherWidget, WeatherWidget}; 8 | use crate::widget::timer::Timer; 9 | 10 | pub fn ui(f: &mut Frame, state: &mut State, args: Args, runtime_info: AppRuntimeInfo) { 11 | let area = f.size(); 12 | 13 | f.render_stateful_widget(WeatherWidget::new(state.weather.as_weather_widget()), area, &mut state.rb); 14 | f.render_widget(Timer { 15 | timer: state.timer, 16 | color: args.timer_color, 17 | state: &state.timer_state, 18 | }, area); 19 | 20 | if args.show_fps { 21 | f.render_widget(FpsWidget(runtime_info.fps), area) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | thread::sleep, 3 | time::{Duration, SystemTime, UNIX_EPOCH}, 4 | }; 5 | 6 | pub fn waiting_time_to_sync() { 7 | let now = SystemTime::now(); 8 | let since_the_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards"); 9 | 10 | let millis = since_the_epoch.as_millis(); 11 | let millis_until_next_second = 1000 - (millis % 1000); 12 | sleep(Duration::from_millis(millis_until_next_second as u64)); 13 | } 14 | -------------------------------------------------------------------------------- /src/weather/dropping.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::Args, 3 | state::{buffer::RenderBuffer, dropping::DroppingState, tail::TailState, wind::WindState, EachFrameImpl, Mode, ShouldRender}, 4 | widget::{weather::GeneralWeatherWidget, AsWeatherWidget}, 5 | }; 6 | 7 | use super::WeatherImpl; 8 | 9 | const DEF_LEVEL: u16 = 50; 10 | const DEF_TAIL_LEVEL: u16 = 500; 11 | 12 | pub struct GeneralDropping { 13 | wind: WindState, 14 | dropping: DroppingState, 15 | } 16 | 17 | impl GeneralDropping { 18 | pub fn new(args: Args) -> Self { 19 | Self { 20 | wind: WindState::new(args.wind), 21 | dropping: DroppingState { 22 | threshold: args.level.unwrap_or(DEF_LEVEL), 23 | mode: args.mode, 24 | }, 25 | } 26 | } 27 | } 28 | 29 | impl WeatherImpl for GeneralDropping {} 30 | 31 | impl EachFrameImpl for GeneralDropping { 32 | fn on_frame(&mut self, rb: &mut RenderBuffer, seed: u64, frame: u64) -> ShouldRender { 33 | self.wind.on_frame(rb, seed, frame) 34 | .or(self.dropping.on_frame(rb, seed, frame)) 35 | } 36 | } 37 | 38 | impl AsWeatherWidget for GeneralDropping { 39 | type Weather = GeneralWeatherWidget; 40 | 41 | fn as_weather_widget(&self) -> Self::Weather { 42 | use Mode::*; 43 | match self.dropping.mode { 44 | Rain => GeneralWeatherWidget::Rain(self.wind.direction), 45 | Snow => GeneralWeatherWidget::Snow, 46 | _ => panic!("has not been implemented yet"), 47 | } 48 | } 49 | } 50 | 51 | pub struct TailDropping { 52 | wind: WindState, 53 | dropping: DroppingState, 54 | tail: TailState, 55 | } 56 | 57 | impl TailDropping { 58 | pub fn new(args: Args) -> Self { 59 | Self { 60 | wind: WindState::new(args.wind.without_random()), 61 | tail: TailState::new(args.wind.into()), 62 | dropping: DroppingState { 63 | threshold: args.level.unwrap_or(DEF_TAIL_LEVEL), 64 | mode: args.mode, 65 | }, 66 | } 67 | } 68 | } 69 | 70 | impl WeatherImpl for TailDropping {} 71 | 72 | impl EachFrameImpl for TailDropping { 73 | fn on_frame(&mut self, rb: &mut RenderBuffer, seed: u64, frame: u64) -> ShouldRender { 74 | self.wind.on_frame(rb, seed, frame) 75 | .or(self.dropping.on_frame(rb, seed, frame)) 76 | .or(self.tail.on_frame(rb, seed, frame)) 77 | } 78 | } 79 | 80 | impl AsWeatherWidget for TailDropping { 81 | type Weather = GeneralWeatherWidget; 82 | 83 | fn as_weather_widget(&self) -> Self::Weather { 84 | use Mode::*; 85 | match self.dropping.mode { 86 | Meteor => GeneralWeatherWidget::Meteor(self.tail.mode), 87 | _ => panic!("has not been implemented yet"), 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/weather/empty.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | state::EachFrameImpl, 3 | widget::{weather::GeneralWeatherWidget, AsWeatherWidget}, 4 | }; 5 | 6 | use super::WeatherImpl; 7 | 8 | pub struct EmptyWeather; 9 | 10 | impl AsWeatherWidget for EmptyWeather { 11 | type Weather = GeneralWeatherWidget; 12 | 13 | fn as_weather_widget(&self) -> Self::Weather { 14 | GeneralWeatherWidget::Disable 15 | } 16 | } 17 | 18 | impl EachFrameImpl for EmptyWeather {} 19 | impl WeatherImpl for EmptyWeather {} 20 | -------------------------------------------------------------------------------- /src/weather/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::Args, 3 | state::{EachFrameImpl, Mode, ShouldRender}, 4 | weather::{dropping::{GeneralDropping, TailDropping}, empty::EmptyWeather}, widget::{weather::GeneralWeatherWidget, AsWeatherWidget}, 5 | }; 6 | 7 | pub mod dropping; 8 | pub mod empty; 9 | 10 | pub trait WeatherImpl: EachFrameImpl + AsWeatherWidget {} 11 | 12 | pub struct Weather(Box); 13 | 14 | impl Weather { 15 | pub fn from(args: Args) -> impl EachFrameImpl + AsWeatherWidget { 16 | use Mode::*; 17 | match args.mode { 18 | Rain | Snow => Self(Box::new(GeneralDropping::new(args))), 19 | Meteor => Self(Box::new(TailDropping::new(args))), 20 | Disable => Self(Box::new(EmptyWeather)), 21 | } 22 | } 23 | } 24 | 25 | impl EachFrameImpl for Weather { 26 | fn on_frame(&mut self, rb: &mut crate::state::buffer::RenderBuffer, seed: u64, frame: u64) -> ShouldRender { 27 | self.0.on_frame(rb, seed, frame) 28 | } 29 | } 30 | 31 | impl AsWeatherWidget for Weather { 32 | type Weather = GeneralWeatherWidget; 33 | 34 | fn as_weather_widget(&self) -> Self::Weather { 35 | self.0.as_weather_widget() 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/widget/fps.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Constraint, Direction, Layout}, 3 | style::{Style, Stylize}, 4 | text::Line, 5 | widgets::Widget, 6 | }; 7 | 8 | pub struct FpsWidget(pub usize); 9 | 10 | impl Widget for FpsWidget { 11 | fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { 12 | let [_, fps_area, _] = Layout::new( 13 | Direction::Horizontal, 14 | [ 15 | Constraint::Min(0), 16 | Constraint::Length(3), 17 | Constraint::Length(2), 18 | ], 19 | ) 20 | .areas(area); 21 | 22 | Line::styled(self.0.to_string(), Style::new().green()).render(fps_area, buf); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/widget/mod.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | buffer::Buffer, 3 | layout::Rect, 4 | style::Color, 5 | widgets::StatefulWidget, 6 | }; 7 | 8 | use crate::state::{buffer::RenderBuffer, Cell, CellType}; 9 | 10 | pub mod fps; 11 | pub mod timer; 12 | pub mod weather; 13 | 14 | pub trait WeatherWidgetImpl { 15 | fn get_char(&self, _: CellType) -> char; 16 | fn get_render_cell_type(&self, cell: &Cell) -> CellType; 17 | fn get_color(&self, cell: CellType) -> Color; 18 | 19 | fn render_background(&self, area: Rect, buf: &mut Buffer, rb: &RenderBuffer) { 20 | for x in area.left()..area.right() { 21 | let Some(column) = rb.buf.get(x as usize) else { 22 | continue; 23 | }; 24 | 25 | let column = column.borrow(); 26 | for y in area.top()..area.bottom() { 27 | if let Some(cell) = column.get(y as usize) { 28 | let cell_type = self.get_render_cell_type(cell); 29 | buf.get_mut(x, y) 30 | .set_char(self.get_char(cell_type)) 31 | .set_fg(self.get_color(cell_type)); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | pub trait AsWeatherWidget { 39 | type Weather: WeatherWidgetImpl; 40 | fn as_weather_widget(&self) -> Self::Weather; 41 | } 42 | 43 | pub struct WeatherWidget { 44 | implement: T, 45 | } 46 | 47 | impl WeatherWidget { 48 | pub fn new(implement: T) -> Self { 49 | Self { implement } 50 | } 51 | } 52 | 53 | impl StatefulWidget for WeatherWidget { 54 | type State = RenderBuffer; 55 | fn render(self, area: Rect, buf: &mut Buffer, rb: &mut Self::State) { 56 | self.implement.render_background(area, buf, rb) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/widget/timer.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use ratatui::{widgets::Widget, layout::{Rect, Layout, Direction, Constraint}, buffer::Buffer, style::Color}; 3 | 4 | use crate::state::timer::TimerState; 5 | 6 | pub const TIMER_CHAR: char = '█'; 7 | pub const COLON_CHAR: char = '▀'; 8 | 9 | const TIMER_LAYOUT: [u16; 5] = [11, 3, 11, 3, 11]; 10 | pub static TIMER_LAYOUT_WIDTH: u16 = 39; 11 | pub static TIMER_LAYOUT_HEIGHT: u16 = 5; 12 | 13 | const ASCII_0: [u8; 25] = [ 14 | 1, 1, 1, 1, 1, 15 | 1, 1, 0, 1, 1, 16 | 1, 1, 0, 1, 1, 17 | 1, 1, 0, 1, 1, 18 | 1, 1, 1, 1, 1, 19 | ]; 20 | 21 | const ASCII_1: [u8; 25] = [ 22 | 0, 0, 1, 1, 0, 23 | 0, 0, 1, 1, 0, 24 | 0, 0, 1, 1, 0, 25 | 0, 0, 1, 1, 0, 26 | 0, 0, 1, 1, 0, 27 | ]; 28 | 29 | const ASCII_2: [u8; 25] = [ 30 | 1, 1, 1, 1, 1, 31 | 0, 0, 0, 1, 1, 32 | 1, 1, 1, 1, 1, 33 | 1, 1, 0, 0, 0, 34 | 1, 1, 1, 1, 1, 35 | ]; 36 | 37 | const ASCII_3: [u8; 25] = [ 38 | 1, 1, 1, 1, 1, 39 | 0, 0, 0, 1, 1, 40 | 1, 1, 1, 1, 1, 41 | 0, 0, 0, 1, 1, 42 | 1, 1, 1, 1, 1, 43 | ]; 44 | 45 | const ASCII_4: [u8; 25] = [ 46 | 1, 1, 0, 1, 1, 47 | 1, 1, 0, 1, 1, 48 | 1, 1, 1, 1, 1, 49 | 0, 0, 0, 1, 1, 50 | 0, 0, 0, 1, 1, 51 | ]; 52 | 53 | const ASCII_5: [u8; 25] = [ 54 | 1, 1, 1, 1, 1, 55 | 1, 1, 0, 0, 0, 56 | 1, 1, 1, 1, 1, 57 | 0, 0, 0, 1, 1, 58 | 1, 1, 1, 1, 1, 59 | ]; 60 | 61 | const ASCII_6: [u8; 25] = [ 62 | 1, 1, 1, 1, 1, 63 | 1, 1, 0, 0, 0, 64 | 1, 1, 1, 1, 1, 65 | 1, 1, 0, 1, 1, 66 | 1, 1, 1, 1, 1, 67 | ]; 68 | 69 | const ASCII_7: [u8; 25] = [ 70 | 1, 1, 1, 1, 1, 71 | 1, 1, 0, 1, 1, 72 | 0, 0, 0, 1, 1, 73 | 0, 0, 0, 1, 1, 74 | 0, 0, 0, 1, 1, 75 | ]; 76 | 77 | const ASCII_8: [u8; 25] = [ 78 | 1, 1, 1, 1, 1, 79 | 1, 1, 0, 1, 1, 80 | 1, 1, 1, 1, 1, 81 | 1, 1, 0, 1, 1, 82 | 1, 1, 1, 1, 1, 83 | ]; 84 | 85 | const ASCII_9: [u8; 25] = [ 86 | 1, 1, 1, 1, 1, 87 | 1, 1, 0, 1, 1, 88 | 1, 1, 1, 1, 1, 89 | 0, 0, 0, 1, 1, 90 | 1, 1, 1, 1, 1, 91 | ]; 92 | 93 | pub struct Timer<'a> { 94 | pub timer: crate::state::timer::Timer, 95 | pub color: Color, 96 | pub state: &'a TimerState, 97 | } 98 | 99 | impl<'a> Timer<'a> { 100 | fn render_colon(area: Rect, color: Color, buf: &mut Buffer) { 101 | let left = area.left(); 102 | let top = area.top(); 103 | 104 | buf.get_mut(left + 1, top + 1).set_char(COLON_CHAR).set_fg(color); 105 | buf.get_mut(left + 1, top + 3).set_char(COLON_CHAR).set_fg(color); 106 | } 107 | 108 | fn render_decimal(d: u8, area: Rect, color: Color, buf: &mut Buffer) { 109 | let layout = Layout::new(Direction::Horizontal, Constraint::from_lengths([5, 1, 5])).split(area); 110 | Self::render_number(d / 10, layout[0], buf, color); 111 | Self::render_number(d % 10, layout[2], buf, color); 112 | } 113 | 114 | fn render_number(number: u8, area: Rect, buf: &mut Buffer, color: Color) { 115 | let left = area.left(); 116 | let top = area.top(); 117 | 118 | let ascii = match number { 119 | 0 => ASCII_0, 120 | 1 => ASCII_1, 121 | 2 => ASCII_2, 122 | 3 => ASCII_3, 123 | 4 => ASCII_4, 124 | 5 => ASCII_5, 125 | 6 => ASCII_6, 126 | 7 => ASCII_7, 127 | 8 => ASCII_8, 128 | 9 => ASCII_9, 129 | _ => ASCII_0, 130 | }; 131 | 132 | ascii.iter() 133 | .chunks(5) 134 | .into_iter() 135 | .enumerate() 136 | .for_each(|(y, chunk)| { 137 | chunk.into_iter() 138 | .enumerate() 139 | .for_each(|(x, c)| { 140 | if *c > 0 { 141 | buf.get_mut(left + x as u16, top + y as u16) 142 | .set_char(TIMER_CHAR) 143 | .set_fg(color); 144 | } 145 | }) 146 | }); 147 | } 148 | } 149 | 150 | impl<'a> Widget for Timer<'a> { 151 | fn render(self, _: Rect, buf: &mut Buffer) { 152 | let [hours, colon_left, minutes, colon_right, seconds] = Layout::new( 153 | Direction::Horizontal, 154 | Constraint::from_lengths(TIMER_LAYOUT), 155 | ) 156 | .areas(self.state.area); 157 | 158 | Self::render_decimal(self.timer.hours, hours, self.color, buf); 159 | Self::render_decimal(self.timer.minutes, minutes, self.color, buf); 160 | Self::render_decimal(self.timer.seconds, seconds, self.color , buf); 161 | 162 | if self.state.colon.show { 163 | Self::render_colon(colon_left, self.color, buf); 164 | Self::render_colon(colon_right, self.color, buf); 165 | } 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /src/widget/weather.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::Color; 2 | 3 | use crate::state::{tail::TailMode, wind::WindDirection, Cell, CellType}; 4 | 5 | use super::WeatherWidgetImpl; 6 | 7 | #[allow(dead_code)] 8 | #[derive(Copy, Clone, PartialEq, Eq)] 9 | pub enum GeneralWeatherWidget { 10 | Rain(WindDirection), 11 | Snow, 12 | Meteor(TailMode), 13 | Star, 14 | Disable, 15 | } 16 | 17 | impl WeatherWidgetImpl for GeneralWeatherWidget { 18 | fn get_color(&self, cell: CellType) -> Color { 19 | use CellType::*; 20 | match self { 21 | Self::Rain(_) => Color::Rgb(150, 150, 150), 22 | Self::Meteor(_) => match cell { 23 | Fast | Normal | Slow => Color::Yellow, 24 | _ => Color::Reset, 25 | } 26 | _ => Color::Reset, 27 | } 28 | } 29 | 30 | fn get_char(&self, d: CellType) -> char { 31 | use CellType::*; 32 | match self { 33 | Self::Rain(wind) => match d { 34 | Fast => '.', 35 | Normal => ':', 36 | Slow => match wind { 37 | WindDirection::Left => '/', 38 | WindDirection::Right => '\\', 39 | WindDirection::None => '|', 40 | } 41 | _ => ' ', 42 | } 43 | Self::Snow => match d { 44 | CellType::Normal => '●', 45 | _ => ' ', 46 | } 47 | Self::Meteor(tail) => match d { 48 | Fast | Normal | Slow => '★', 49 | Tail => match tail { 50 | TailMode::Left => '/', 51 | TailMode::Right => '\\', 52 | TailMode::Default => '|', 53 | }, 54 | _ => ' ', 55 | } 56 | _ => ' ', 57 | } 58 | } 59 | 60 | fn get_render_cell_type(&self, cell: &Cell) -> CellType { 61 | if *self == Self::Disable { 62 | return CellType::None; 63 | } 64 | 65 | match self { 66 | Self::Snow => if !cell.is_empty() && cell.contains(&CellType::Normal) { 67 | CellType::Normal 68 | } else { 69 | CellType::None 70 | }, 71 | 72 | _ => if cell.contains(&CellType::Slow) { 73 | CellType::Slow 74 | } else if !cell.is_empty() { 75 | *cell.first().unwrap() 76 | } else { 77 | CellType::None 78 | }, 79 | } 80 | } 81 | } 82 | 83 | --------------------------------------------------------------------------------