├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src ├── app.rs ├── consts.rs ├── event.rs ├── handler.rs ├── lib.rs ├── main.rs ├── projectile.rs ├── simulation_confetti.rs ├── simulation_fireworks.rs ├── simulation_shooting_star.rs ├── system.rs ├── tui.rs └── ui.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 = "ahash" 7 | version = "0.8.7" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "allocator-api2" 19 | version = "0.2.16" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 22 | 23 | [[package]] 24 | name = "anstream" 25 | version = "0.6.11" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 28 | dependencies = [ 29 | "anstyle", 30 | "anstyle-parse", 31 | "anstyle-query", 32 | "anstyle-wincon", 33 | "colorchoice", 34 | "utf8parse", 35 | ] 36 | 37 | [[package]] 38 | name = "anstyle" 39 | version = "1.0.6" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 42 | 43 | [[package]] 44 | name = "anstyle-parse" 45 | version = "0.2.3" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 48 | dependencies = [ 49 | "utf8parse", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-query" 54 | version = "1.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 57 | dependencies = [ 58 | "windows-sys 0.52.0", 59 | ] 60 | 61 | [[package]] 62 | name = "anstyle-wincon" 63 | version = "3.0.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 66 | dependencies = [ 67 | "anstyle", 68 | "windows-sys 0.52.0", 69 | ] 70 | 71 | [[package]] 72 | name = "approx" 73 | version = "0.5.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" 76 | dependencies = [ 77 | "num-traits", 78 | ] 79 | 80 | [[package]] 81 | name = "autocfg" 82 | version = "1.1.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 85 | 86 | [[package]] 87 | name = "bitflags" 88 | version = "1.3.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 91 | 92 | [[package]] 93 | name = "bitflags" 94 | version = "2.4.2" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 97 | 98 | [[package]] 99 | name = "bytemuck" 100 | version = "1.14.1" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9" 103 | 104 | [[package]] 105 | name = "cassowary" 106 | version = "0.3.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 109 | 110 | [[package]] 111 | name = "castaway" 112 | version = "0.2.2" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 115 | dependencies = [ 116 | "rustversion", 117 | ] 118 | 119 | [[package]] 120 | name = "cfg-if" 121 | version = "1.0.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 124 | 125 | [[package]] 126 | name = "clap" 127 | version = "4.4.18" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" 130 | dependencies = [ 131 | "clap_builder", 132 | "clap_derive", 133 | ] 134 | 135 | [[package]] 136 | name = "clap_builder" 137 | version = "4.4.18" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" 140 | dependencies = [ 141 | "anstream", 142 | "anstyle", 143 | "clap_lex", 144 | "strsim", 145 | ] 146 | 147 | [[package]] 148 | name = "clap_derive" 149 | version = "4.4.7" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 152 | dependencies = [ 153 | "heck", 154 | "proc-macro2", 155 | "quote", 156 | "syn 2.0.48", 157 | ] 158 | 159 | [[package]] 160 | name = "clap_lex" 161 | version = "0.6.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 164 | 165 | [[package]] 166 | name = "colorchoice" 167 | version = "1.0.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 170 | 171 | [[package]] 172 | name = "compact_str" 173 | version = "0.7.1" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 176 | dependencies = [ 177 | "castaway", 178 | "cfg-if", 179 | "itoa", 180 | "ryu", 181 | "static_assertions", 182 | ] 183 | 184 | [[package]] 185 | name = "confetty_rs" 186 | version = "0.1.0" 187 | dependencies = [ 188 | "clap", 189 | "crossterm", 190 | "nalgebra", 191 | "rand", 192 | "ratatui", 193 | ] 194 | 195 | [[package]] 196 | name = "crossterm" 197 | version = "0.27.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 200 | dependencies = [ 201 | "bitflags 2.4.2", 202 | "crossterm_winapi", 203 | "libc", 204 | "mio", 205 | "parking_lot", 206 | "signal-hook", 207 | "signal-hook-mio", 208 | "winapi", 209 | ] 210 | 211 | [[package]] 212 | name = "crossterm_winapi" 213 | version = "0.9.1" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 216 | dependencies = [ 217 | "winapi", 218 | ] 219 | 220 | [[package]] 221 | name = "either" 222 | version = "1.9.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 225 | 226 | [[package]] 227 | name = "getrandom" 228 | version = "0.2.12" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 231 | dependencies = [ 232 | "cfg-if", 233 | "libc", 234 | "wasi", 235 | ] 236 | 237 | [[package]] 238 | name = "hashbrown" 239 | version = "0.14.3" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 242 | dependencies = [ 243 | "ahash", 244 | "allocator-api2", 245 | ] 246 | 247 | [[package]] 248 | name = "heck" 249 | version = "0.4.1" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 252 | 253 | [[package]] 254 | name = "indoc" 255 | version = "2.0.4" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 258 | 259 | [[package]] 260 | name = "itertools" 261 | version = "0.12.1" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 264 | dependencies = [ 265 | "either", 266 | ] 267 | 268 | [[package]] 269 | name = "itoa" 270 | version = "1.0.10" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 273 | 274 | [[package]] 275 | name = "libc" 276 | version = "0.2.153" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 279 | 280 | [[package]] 281 | name = "lock_api" 282 | version = "0.4.11" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 285 | dependencies = [ 286 | "autocfg", 287 | "scopeguard", 288 | ] 289 | 290 | [[package]] 291 | name = "log" 292 | version = "0.4.20" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 295 | 296 | [[package]] 297 | name = "lru" 298 | version = "0.12.2" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" 301 | dependencies = [ 302 | "hashbrown", 303 | ] 304 | 305 | [[package]] 306 | name = "matrixmultiply" 307 | version = "0.3.8" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" 310 | dependencies = [ 311 | "autocfg", 312 | "rawpointer", 313 | ] 314 | 315 | [[package]] 316 | name = "mio" 317 | version = "0.8.10" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" 320 | dependencies = [ 321 | "libc", 322 | "log", 323 | "wasi", 324 | "windows-sys 0.48.0", 325 | ] 326 | 327 | [[package]] 328 | name = "nalgebra" 329 | version = "0.32.3" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa" 332 | dependencies = [ 333 | "approx", 334 | "matrixmultiply", 335 | "nalgebra-macros", 336 | "num-complex", 337 | "num-rational", 338 | "num-traits", 339 | "simba", 340 | "typenum", 341 | ] 342 | 343 | [[package]] 344 | name = "nalgebra-macros" 345 | version = "0.2.1" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998" 348 | dependencies = [ 349 | "proc-macro2", 350 | "quote", 351 | "syn 1.0.109", 352 | ] 353 | 354 | [[package]] 355 | name = "num-complex" 356 | version = "0.4.4" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" 359 | dependencies = [ 360 | "num-traits", 361 | ] 362 | 363 | [[package]] 364 | name = "num-integer" 365 | version = "0.1.45" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 368 | dependencies = [ 369 | "autocfg", 370 | "num-traits", 371 | ] 372 | 373 | [[package]] 374 | name = "num-rational" 375 | version = "0.4.1" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" 378 | dependencies = [ 379 | "autocfg", 380 | "num-integer", 381 | "num-traits", 382 | ] 383 | 384 | [[package]] 385 | name = "num-traits" 386 | version = "0.2.17" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 389 | dependencies = [ 390 | "autocfg", 391 | ] 392 | 393 | [[package]] 394 | name = "once_cell" 395 | version = "1.19.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 398 | 399 | [[package]] 400 | name = "parking_lot" 401 | version = "0.12.1" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 404 | dependencies = [ 405 | "lock_api", 406 | "parking_lot_core", 407 | ] 408 | 409 | [[package]] 410 | name = "parking_lot_core" 411 | version = "0.9.9" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 414 | dependencies = [ 415 | "cfg-if", 416 | "libc", 417 | "redox_syscall", 418 | "smallvec", 419 | "windows-targets 0.48.5", 420 | ] 421 | 422 | [[package]] 423 | name = "paste" 424 | version = "1.0.14" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 427 | 428 | [[package]] 429 | name = "ppv-lite86" 430 | version = "0.2.17" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 433 | 434 | [[package]] 435 | name = "proc-macro2" 436 | version = "1.0.78" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 439 | dependencies = [ 440 | "unicode-ident", 441 | ] 442 | 443 | [[package]] 444 | name = "quote" 445 | version = "1.0.35" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 448 | dependencies = [ 449 | "proc-macro2", 450 | ] 451 | 452 | [[package]] 453 | name = "rand" 454 | version = "0.8.5" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 457 | dependencies = [ 458 | "libc", 459 | "rand_chacha", 460 | "rand_core", 461 | ] 462 | 463 | [[package]] 464 | name = "rand_chacha" 465 | version = "0.3.1" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 468 | dependencies = [ 469 | "ppv-lite86", 470 | "rand_core", 471 | ] 472 | 473 | [[package]] 474 | name = "rand_core" 475 | version = "0.6.4" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 478 | dependencies = [ 479 | "getrandom", 480 | ] 481 | 482 | [[package]] 483 | name = "ratatui" 484 | version = "0.26.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373" 487 | dependencies = [ 488 | "bitflags 2.4.2", 489 | "cassowary", 490 | "compact_str", 491 | "crossterm", 492 | "indoc", 493 | "itertools", 494 | "lru", 495 | "paste", 496 | "stability", 497 | "strum", 498 | "unicode-segmentation", 499 | "unicode-width", 500 | ] 501 | 502 | [[package]] 503 | name = "rawpointer" 504 | version = "0.2.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" 507 | 508 | [[package]] 509 | name = "redox_syscall" 510 | version = "0.4.1" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 513 | dependencies = [ 514 | "bitflags 1.3.2", 515 | ] 516 | 517 | [[package]] 518 | name = "rustversion" 519 | version = "1.0.14" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 522 | 523 | [[package]] 524 | name = "ryu" 525 | version = "1.0.16" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 528 | 529 | [[package]] 530 | name = "safe_arch" 531 | version = "0.7.1" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" 534 | dependencies = [ 535 | "bytemuck", 536 | ] 537 | 538 | [[package]] 539 | name = "scopeguard" 540 | version = "1.2.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 543 | 544 | [[package]] 545 | name = "signal-hook" 546 | version = "0.3.17" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 549 | dependencies = [ 550 | "libc", 551 | "signal-hook-registry", 552 | ] 553 | 554 | [[package]] 555 | name = "signal-hook-mio" 556 | version = "0.2.3" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 559 | dependencies = [ 560 | "libc", 561 | "mio", 562 | "signal-hook", 563 | ] 564 | 565 | [[package]] 566 | name = "signal-hook-registry" 567 | version = "1.4.1" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 570 | dependencies = [ 571 | "libc", 572 | ] 573 | 574 | [[package]] 575 | name = "simba" 576 | version = "0.8.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" 579 | dependencies = [ 580 | "approx", 581 | "num-complex", 582 | "num-traits", 583 | "paste", 584 | "wide", 585 | ] 586 | 587 | [[package]] 588 | name = "smallvec" 589 | version = "1.13.1" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 592 | 593 | [[package]] 594 | name = "stability" 595 | version = "0.1.1" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" 598 | dependencies = [ 599 | "quote", 600 | "syn 1.0.109", 601 | ] 602 | 603 | [[package]] 604 | name = "static_assertions" 605 | version = "1.1.0" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 608 | 609 | [[package]] 610 | name = "strsim" 611 | version = "0.10.0" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 614 | 615 | [[package]] 616 | name = "strum" 617 | version = "0.26.1" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" 620 | dependencies = [ 621 | "strum_macros", 622 | ] 623 | 624 | [[package]] 625 | name = "strum_macros" 626 | version = "0.26.1" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" 629 | dependencies = [ 630 | "heck", 631 | "proc-macro2", 632 | "quote", 633 | "rustversion", 634 | "syn 2.0.48", 635 | ] 636 | 637 | [[package]] 638 | name = "syn" 639 | version = "1.0.109" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 642 | dependencies = [ 643 | "proc-macro2", 644 | "quote", 645 | "unicode-ident", 646 | ] 647 | 648 | [[package]] 649 | name = "syn" 650 | version = "2.0.48" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 653 | dependencies = [ 654 | "proc-macro2", 655 | "quote", 656 | "unicode-ident", 657 | ] 658 | 659 | [[package]] 660 | name = "typenum" 661 | version = "1.17.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 664 | 665 | [[package]] 666 | name = "unicode-ident" 667 | version = "1.0.12" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 670 | 671 | [[package]] 672 | name = "unicode-segmentation" 673 | version = "1.10.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 676 | 677 | [[package]] 678 | name = "unicode-width" 679 | version = "0.1.11" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 682 | 683 | [[package]] 684 | name = "utf8parse" 685 | version = "0.2.1" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 688 | 689 | [[package]] 690 | name = "version_check" 691 | version = "0.9.4" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 694 | 695 | [[package]] 696 | name = "wasi" 697 | version = "0.11.0+wasi-snapshot-preview1" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 700 | 701 | [[package]] 702 | name = "wide" 703 | version = "0.7.15" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "89beec544f246e679fc25490e3f8e08003bc4bf612068f325120dad4cea02c1c" 706 | dependencies = [ 707 | "bytemuck", 708 | "safe_arch", 709 | ] 710 | 711 | [[package]] 712 | name = "winapi" 713 | version = "0.3.9" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 716 | dependencies = [ 717 | "winapi-i686-pc-windows-gnu", 718 | "winapi-x86_64-pc-windows-gnu", 719 | ] 720 | 721 | [[package]] 722 | name = "winapi-i686-pc-windows-gnu" 723 | version = "0.4.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 726 | 727 | [[package]] 728 | name = "winapi-x86_64-pc-windows-gnu" 729 | version = "0.4.0" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 732 | 733 | [[package]] 734 | name = "windows-sys" 735 | version = "0.48.0" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 738 | dependencies = [ 739 | "windows-targets 0.48.5", 740 | ] 741 | 742 | [[package]] 743 | name = "windows-sys" 744 | version = "0.52.0" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 747 | dependencies = [ 748 | "windows-targets 0.52.0", 749 | ] 750 | 751 | [[package]] 752 | name = "windows-targets" 753 | version = "0.48.5" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 756 | dependencies = [ 757 | "windows_aarch64_gnullvm 0.48.5", 758 | "windows_aarch64_msvc 0.48.5", 759 | "windows_i686_gnu 0.48.5", 760 | "windows_i686_msvc 0.48.5", 761 | "windows_x86_64_gnu 0.48.5", 762 | "windows_x86_64_gnullvm 0.48.5", 763 | "windows_x86_64_msvc 0.48.5", 764 | ] 765 | 766 | [[package]] 767 | name = "windows-targets" 768 | version = "0.52.0" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 771 | dependencies = [ 772 | "windows_aarch64_gnullvm 0.52.0", 773 | "windows_aarch64_msvc 0.52.0", 774 | "windows_i686_gnu 0.52.0", 775 | "windows_i686_msvc 0.52.0", 776 | "windows_x86_64_gnu 0.52.0", 777 | "windows_x86_64_gnullvm 0.52.0", 778 | "windows_x86_64_msvc 0.52.0", 779 | ] 780 | 781 | [[package]] 782 | name = "windows_aarch64_gnullvm" 783 | version = "0.48.5" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 786 | 787 | [[package]] 788 | name = "windows_aarch64_gnullvm" 789 | version = "0.52.0" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 792 | 793 | [[package]] 794 | name = "windows_aarch64_msvc" 795 | version = "0.48.5" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 798 | 799 | [[package]] 800 | name = "windows_aarch64_msvc" 801 | version = "0.52.0" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 804 | 805 | [[package]] 806 | name = "windows_i686_gnu" 807 | version = "0.48.5" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 810 | 811 | [[package]] 812 | name = "windows_i686_gnu" 813 | version = "0.52.0" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 816 | 817 | [[package]] 818 | name = "windows_i686_msvc" 819 | version = "0.48.5" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 822 | 823 | [[package]] 824 | name = "windows_i686_msvc" 825 | version = "0.52.0" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 828 | 829 | [[package]] 830 | name = "windows_x86_64_gnu" 831 | version = "0.48.5" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 834 | 835 | [[package]] 836 | name = "windows_x86_64_gnu" 837 | version = "0.52.0" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 840 | 841 | [[package]] 842 | name = "windows_x86_64_gnullvm" 843 | version = "0.48.5" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 846 | 847 | [[package]] 848 | name = "windows_x86_64_gnullvm" 849 | version = "0.52.0" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 852 | 853 | [[package]] 854 | name = "windows_x86_64_msvc" 855 | version = "0.48.5" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 858 | 859 | [[package]] 860 | name = "windows_x86_64_msvc" 861 | version = "0.52.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 864 | 865 | [[package]] 866 | name = "zerocopy" 867 | version = "0.7.32" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 870 | dependencies = [ 871 | "zerocopy-derive", 872 | ] 873 | 874 | [[package]] 875 | name = "zerocopy-derive" 876 | version = "0.7.32" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 879 | dependencies = [ 880 | "proc-macro2", 881 | "quote", 882 | "syn 2.0.48", 883 | ] 884 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "confetty_rs" 3 | version = "0.1.0" 4 | authors = ["Ken Udovic - github.com/Handfish"] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | clap = { version = "4.4.18", features = ["derive"] } 10 | crossterm = "0.27.0" 11 | nalgebra = "0.32.3" 12 | rand = "0.8.5" 13 | ratatui = "0.26.0-alpha.3" 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## confetty_rs 2 | 3 | Particle System written in rust and rendered in the terminal via [ratatui](https://github.com/ratatui-org/ratatui). 4 | Mostly a rust port of [confetty](https://github.com/maaslalani/confetty). 5 | 6 | ![confetty demo in terminal](https://i.imgur.com/EjpdJXA.gif) 7 | ![fireworks demo in terminal](https://i.imgur.com/VPwOALP.gif) 8 | 9 | 10 | 11 | ### Homebrew Particle System 12 | 13 | Also made my own simulation: 14 | 15 | ![shooting stars demo in terminal](https://i.imgur.com/v6yRjxR.gif) 16 | 17 | ```bash 18 | # Confetti 19 | cargo run --release 20 | 21 | # Fireworks 22 | cargo run --release -- --name fireworks 23 | 24 | # Shooting Stars 25 | cargo run --release -- --name stars 26 | ``` 27 | Press any key for particles. `Cntrl-c` or `q` to quit. 28 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::simulation_confetti::SimulationStateConfetti; 2 | use crate::simulation_fireworks::SimulationStateFireworks; 3 | use crate::simulation_shooting_star::SimulationStateShootingStar; 4 | use crate::system::AppSimulation; 5 | use ratatui::layout::Rect; 6 | use std::error; 7 | 8 | /// Application result type. 9 | pub type AppResult = std::result::Result>; 10 | 11 | /// Application. 12 | #[derive(Debug)] 13 | pub struct App { 14 | /// Is the application running? 15 | pub running: bool, 16 | 17 | pub area: Rect, 18 | 19 | pub state: AppSimulation, 20 | 21 | pub num_particles: usize, 22 | } 23 | 24 | impl Default for App { 25 | fn default() -> Self { 26 | Self { 27 | running: true, 28 | area: Rect::new(0, 0, 0, 0), 29 | state: AppSimulation::Confetti(SimulationStateConfetti::new()), // Default to Fireworks 30 | num_particles: 0, 31 | } 32 | } 33 | } 34 | 35 | impl App { 36 | /// Constructs a new instance of [`App`]. 37 | pub fn new() -> Self { 38 | Self::default() 39 | } 40 | 41 | pub fn fireworks() -> Self { 42 | Self { 43 | running: true, 44 | area: Rect::new(0, 0, 0, 0), 45 | state: AppSimulation::Fireworks(SimulationStateFireworks::new()), // Default to Fireworks 46 | num_particles: 0, 47 | } 48 | } 49 | 50 | pub fn shooting_star() -> Self { 51 | Self { 52 | running: true, 53 | area: Rect::new(0, 0, 0, 0), 54 | state: AppSimulation::ShootingStar(SimulationStateShootingStar::new()), // Default to Fireworks 55 | num_particles: 0, 56 | } 57 | } 58 | /// Handles the tick event of the terminal. 59 | pub fn tick(&mut self) { 60 | match &mut self.state { 61 | AppSimulation::Confetti(state) => state.tick(), 62 | AppSimulation::Fireworks(state) => state.tick(), 63 | AppSimulation::ShootingStar(state) => state.tick(), 64 | } 65 | } 66 | 67 | /// Set running to false to quit the application. 68 | pub fn quit(&mut self) { 69 | self.running = false; 70 | } 71 | 72 | pub fn get_area(&self) -> Rect { 73 | self.area 74 | } 75 | 76 | pub fn set_area(&mut self, area: Rect) { 77 | self.area = area; 78 | } 79 | 80 | pub fn spawn_particles(&mut self) { 81 | match &mut self.state { 82 | AppSimulation::Confetti(state) => { 83 | // Handle Confetti state particles 84 | self.num_particles += state.spawn_particles(self.area.width as usize); 85 | } 86 | AppSimulation::Fireworks(state) => { 87 | // Handle Fireworks state particles 88 | self.num_particles += 89 | state.spawn_particles(self.area.width as usize, self.area.height as usize); 90 | } 91 | AppSimulation::ShootingStar(state) => { 92 | // Handle Fireworks state particles 93 | self.num_particles += 94 | state.spawn_particles(self.area.width as usize, self.area.height as usize); 95 | } 96 | } 97 | } 98 | 99 | pub fn get_simulation_state(&mut self) -> &mut AppSimulation { 100 | &mut self.state 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::Vector2; 2 | use ratatui::prelude::Color; 3 | 4 | pub const TICK_RATE_IN_MILI: u64 = 33; 5 | 6 | // Not sure why rustc is warning about these constants being unused 7 | #[allow(dead_code)] 8 | pub const FRAMES_PER_SECOND: f32 = 1000.0 / TICK_RATE_IN_MILI as f32; 9 | 10 | #[allow(dead_code)] 11 | pub const NUM_PARTICLES: usize = 75; 12 | 13 | #[allow(dead_code)] 14 | pub const CHARACTERS: [char; 6] = ['█', '▓', '▒', '░', '▄', '▀']; 15 | 16 | #[allow(dead_code)] 17 | pub const TERMINAL_GRAVITY: Vector2 = Vector2::new(0.0, 9.81); 18 | 19 | #[allow(dead_code)] 20 | pub const COLORS: [Color; 5] = [ 21 | Color::Rgb(168, 100, 253), // #a864fd 22 | Color::Rgb(41, 205, 255), // #29cdff 23 | Color::Rgb(120, 255, 68), // #78ff44 24 | Color::Rgb(255, 113, 141), // #ff718d 25 | Color::Rgb(253, 255, 106), // #fdff6a 26 | ]; 27 | 28 | // 29 | // pub const GRAVITY: Vector3 = Vector3::new(0.0, -9.81, 0.0); 30 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::app::AppResult; 2 | use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | use std::time::{Duration, Instant}; 6 | 7 | /// Terminal events. 8 | #[derive(Clone, Copy, Debug)] 9 | pub enum Event { 10 | /// Terminal tick. 11 | Tick, 12 | /// Key press. 13 | Key(KeyEvent), 14 | /// Mouse click/scroll. 15 | Mouse(MouseEvent), 16 | /// Terminal resize. 17 | Resize(u16, u16), 18 | } 19 | 20 | /// Terminal event handler. 21 | #[allow(dead_code)] 22 | #[derive(Debug)] 23 | pub struct EventHandler { 24 | /// Event sender channel. 25 | sender: mpsc::Sender, 26 | /// Event receiver channel. 27 | receiver: mpsc::Receiver, 28 | /// Event handler thread. 29 | handler: thread::JoinHandle<()>, 30 | } 31 | 32 | impl EventHandler { 33 | /// Constructs a new instance of [`EventHandler`]. 34 | pub fn new(tick_rate: u64) -> Self { 35 | let tick_rate = Duration::from_millis(tick_rate); 36 | let (sender, receiver) = mpsc::channel(); 37 | let handler = { 38 | let sender = sender.clone(); 39 | thread::spawn(move || { 40 | let mut last_tick = Instant::now(); 41 | loop { 42 | let timeout = tick_rate 43 | .checked_sub(last_tick.elapsed()) 44 | .unwrap_or(tick_rate); 45 | 46 | if event::poll(timeout).expect("failed to poll new events") { 47 | match event::read().expect("unable to read event") { 48 | CrosstermEvent::Key(e) => sender.send(Event::Key(e)), 49 | CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), 50 | CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 51 | CrosstermEvent::FocusGained => Ok(()), 52 | CrosstermEvent::FocusLost => Ok(()), 53 | CrosstermEvent::Paste(_) => unimplemented!(), 54 | } 55 | .expect("failed to send terminal event") 56 | } 57 | 58 | if last_tick.elapsed() >= tick_rate { 59 | sender.send(Event::Tick).expect("failed to send tick event"); 60 | last_tick = Instant::now(); 61 | } 62 | } 63 | }) 64 | }; 65 | Self { 66 | sender, 67 | receiver, 68 | handler, 69 | } 70 | } 71 | 72 | /// Receive the next event from the handler thread. 73 | /// 74 | /// This function will always block the current thread if 75 | /// there is no data available and it's possible for more data to be sent. 76 | pub fn next(&self) -> AppResult { 77 | Ok(self.receiver.recv()?) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{App, AppResult}; 2 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 3 | 4 | /// Handles the key events and updates the state of [`App`]. 5 | pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { 6 | match key_event.code { 7 | // Exit application on `ESC` or `q` 8 | KeyCode::Esc | KeyCode::Char('q') => { 9 | app.quit(); 10 | } 11 | // Exit application on `Ctrl-C` 12 | KeyCode::Char('c') | KeyCode::Char('C') => { 13 | if key_event.modifiers == KeyModifiers::CONTROL { 14 | app.quit(); 15 | } 16 | } 17 | _ => { 18 | app.spawn_particles(); 19 | } 20 | } 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Application. 2 | pub mod app; 3 | 4 | /// Terminal events handler. 5 | pub mod event; 6 | 7 | /// Widget renderer. 8 | pub mod ui; 9 | 10 | /// Terminal user interface. 11 | pub mod tui; 12 | 13 | /// Event handler. 14 | pub mod handler; 15 | 16 | /// Projectile Physics. 17 | pub mod projectile; 18 | 19 | // Stateful Widget 20 | pub mod system; 21 | 22 | /// Confetti Simulation. 23 | pub mod simulation_confetti; 24 | 25 | /// Fireworks Simulation. 26 | pub mod simulation_fireworks; 27 | 28 | /// Shooting Star Simulation. 29 | pub mod simulation_shooting_star; 30 | 31 | /// Constants. 32 | pub mod consts; 33 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod consts; 2 | use crate::consts::TICK_RATE_IN_MILI; 3 | use clap::Parser; 4 | use confetty_rs::app::{App, AppResult}; 5 | use confetty_rs::event::{Event, EventHandler}; 6 | use confetty_rs::handler::handle_key_events; 7 | use confetty_rs::tui::Tui; 8 | use ratatui::backend::CrosstermBackend; 9 | use ratatui::Terminal; 10 | use std::io; 11 | 12 | #[derive(Parser, Debug)] 13 | #[command(author, version, about, long_about = None)] 14 | struct Args { 15 | #[arg(short, long)] 16 | name: Option, 17 | } 18 | 19 | fn main() -> AppResult<()> { 20 | let args = Args::parse(); 21 | 22 | // Get the value of the state argument, if provided 23 | let mut name = args.name.unwrap_or_default(); 24 | 25 | if name.is_empty() { 26 | name = String::from("confetti"); 27 | } 28 | 29 | // Create an application. 30 | let mut app = match name.as_str() { 31 | "fireworks" => App::fireworks(), 32 | "stars" => App::shooting_star(), 33 | _ => App::new(), 34 | }; 35 | 36 | // Initialize the terminal user interface. 37 | let backend = CrosstermBackend::new(io::stderr()); 38 | let terminal = Terminal::new(backend)?; 39 | let events = EventHandler::new(TICK_RATE_IN_MILI); 40 | let mut tui = Tui::new(terminal, events); 41 | tui.init()?; 42 | 43 | // Start the main loop. 44 | while app.running { 45 | // Render the user interface. 46 | tui.draw(&mut app)?; 47 | // Handle events. 48 | match tui.events.next()? { 49 | Event::Tick => app.tick(), 50 | Event::Key(key_event) => handle_key_events(key_event, &mut app)?, 51 | Event::Mouse(_) => {} 52 | Event::Resize(_, _) => {} 53 | } 54 | } 55 | 56 | // Exit the user interface. 57 | tui.exit()?; 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/projectile.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::{Point2, Vector2}; 2 | 3 | // Projectile is the representation of a projectile that has a position on 4 | // a plane, an acceleration, and velocity. 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct Projectile { 7 | pos: Point2, 8 | vel: Vector2, 9 | acc: Vector2, 10 | delta_time: f32, 11 | } 12 | 13 | // NewProjectile creates a new projectile. It accepts a frame rate and initial 14 | // values for position, velocity, and acceleration. It returns a new 15 | // projectile. 16 | impl Projectile { 17 | pub fn new( 18 | delta_time: f32, 19 | initial_position: Point2, 20 | initial_velocity: Vector2, 21 | initial_acceleration: Vector2, 22 | ) -> Projectile { 23 | Projectile { 24 | pos: initial_position, 25 | vel: initial_velocity, 26 | acc: initial_acceleration, 27 | delta_time, 28 | } 29 | } 30 | 31 | // Update updates the position and velocity values for the given projectile. 32 | // Call this after calling NewProjectile to update values. 33 | pub fn update(&mut self) -> Point2 { 34 | self.pos.x += self.vel.x * self.delta_time; 35 | self.pos.y += self.vel.y * self.delta_time; 36 | 37 | self.vel.x += self.acc.x * self.delta_time; 38 | self.vel.y += self.acc.y * self.delta_time; 39 | 40 | self.pos 41 | } 42 | 43 | // Position returns the position of the projectile. 44 | pub fn position(&self) -> Point2 { 45 | self.pos 46 | } 47 | 48 | // Velocity returns the velocity of the projectile. 49 | pub fn velocity(&self) -> Vector2 { 50 | self.vel 51 | } 52 | 53 | // Acceleration returns the acceleration of the projectile. 54 | pub fn acceleration(&self) -> Vector2 { 55 | self.acc 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/simulation_confetti.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::{CHARACTERS, COLORS, FRAMES_PER_SECOND, NUM_PARTICLES, TERMINAL_GRAVITY}; 2 | use crate::projectile::Projectile; 3 | use nalgebra::{Point2, Vector2}; 4 | use rand::seq::SliceRandom; 5 | use ratatui::prelude::Color; 6 | 7 | #[derive(Debug)] 8 | pub struct Particle { 9 | pub char: char, 10 | pub color: Color, 11 | pub physics: Projectile, 12 | } 13 | 14 | // Sample a random element from the array 15 | fn sample_character() -> &'static char { 16 | let mut rng = rand::thread_rng(); 17 | CHARACTERS.choose(&mut rng).unwrap_or(&CHARACTERS[0]) 18 | } 19 | fn sample_color() -> Color { 20 | let mut rng = rand::thread_rng(); 21 | *COLORS.choose(&mut rng).unwrap_or(&COLORS[0]) 22 | } 23 | 24 | impl Particle { 25 | fn new(width: usize) -> Self { 26 | let x = width as f32 / 2.0; 27 | let y = 0.0; 28 | 29 | let physics = Projectile::new( 30 | 1.0 / FRAMES_PER_SECOND, 31 | Point2::new(x + (width as f32 / 4.0 * (rand::random::() - 0.5)), y), 32 | Vector2::new( 33 | (rand::random::() - 0.5) * 100.0, 34 | rand::random::() * 50.0, 35 | ), 36 | TERMINAL_GRAVITY, 37 | ); 38 | 39 | let char = *sample_character(); 40 | let color = sample_color(); 41 | 42 | Particle { 43 | char, 44 | color, 45 | physics, 46 | } 47 | } 48 | } 49 | 50 | #[derive(Debug)] 51 | pub struct SimulationStateConfetti { 52 | pub particles: Vec, 53 | } 54 | 55 | impl SimulationStateConfetti { 56 | pub fn new() -> Self { 57 | Self { particles: vec![] } 58 | } 59 | 60 | pub fn tick(&mut self) { 61 | for particle in &mut self.particles { 62 | particle.physics.update(); 63 | } 64 | } 65 | 66 | pub fn spawn_particles(&mut self, width: usize) -> usize { 67 | for _ in 0..NUM_PARTICLES { 68 | let particle = Particle::new(width); 69 | self.particles.push(particle); 70 | } 71 | NUM_PARTICLES 72 | } 73 | 74 | pub fn remove_indices_from_particles(&mut self, i: Vec) { 75 | for &index in i.iter().rev() { 76 | self.particles.swap_remove(index); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/simulation_fireworks.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::{COLORS, FRAMES_PER_SECOND, TERMINAL_GRAVITY}; 2 | use crate::projectile::Projectile; 3 | use nalgebra::{Point2, Vector2}; 4 | use rand::seq::SliceRandom; 5 | use ratatui::prelude::Color; 6 | 7 | const HEAD: char = '▄'; 8 | const TAIL: char = '│'; 9 | const EXPLOSION_CHARACTERS: [char; 3] = ['+', '*', '•']; 10 | const NUM_PARTICLES: usize = 50; 11 | 12 | #[derive(Debug)] 13 | pub struct Particle { 14 | pub char: char, 15 | pub color: Color, 16 | pub physics: Projectile, 17 | pub shooting: bool, 18 | pub tail_char: Option, 19 | } 20 | 21 | // Sample a random element from the array 22 | fn sample_character() -> &'static char { 23 | let mut rng = rand::thread_rng(); 24 | EXPLOSION_CHARACTERS 25 | .choose(&mut rng) 26 | .unwrap_or(&EXPLOSION_CHARACTERS[0]) 27 | } 28 | fn sample_color() -> Color { 29 | let mut rng = rand::thread_rng(); 30 | *COLORS.choose(&mut rng).unwrap_or(&COLORS[0]) 31 | } 32 | 33 | impl Particle { 34 | fn new(width: usize, height: usize) -> Self { 35 | let x = width as f32 * rand::random::(); 36 | let y = (height - 1) as f32; 37 | let v = rand::random::() * 15.0 + 15.0; 38 | 39 | let physics = Projectile::new( 40 | 1.0 / FRAMES_PER_SECOND, 41 | Point2::new(x, y), 42 | Vector2::new(0.0, -v), 43 | TERMINAL_GRAVITY, 44 | ); 45 | 46 | let color = sample_color(); 47 | 48 | Particle { 49 | char: HEAD, 50 | color, 51 | physics, 52 | shooting: true, 53 | tail_char: Some(TAIL), 54 | } 55 | } 56 | 57 | fn new_explosion(color: Color, x: f32, y: f32, v: f32, i: f32) -> Self { 58 | let physics = Projectile::new( 59 | 1.0 / FRAMES_PER_SECOND, 60 | Point2::new(x, y), 61 | Vector2::new(f32::cos(i) * v, f32::sin(i) * v / 2.0), 62 | TERMINAL_GRAVITY, 63 | ); 64 | 65 | let char = *sample_character(); 66 | 67 | Particle { 68 | char, 69 | color, 70 | physics, 71 | shooting: false, 72 | tail_char: None, 73 | } 74 | } 75 | } 76 | 77 | #[derive(Debug)] 78 | pub struct SimulationStateFireworks { 79 | pub particles: Vec, 80 | } 81 | 82 | impl SimulationStateFireworks { 83 | pub fn new() -> Self { 84 | Self { particles: vec![] } 85 | } 86 | 87 | pub fn tick(&mut self) { 88 | for particle in &mut self.particles { 89 | particle.physics.update(); 90 | } 91 | } 92 | 93 | pub fn spawn_particles(&mut self, width: usize, height: usize) -> usize { 94 | let particle = Particle::new(width, height); 95 | self.particles.push(particle); 96 | 1 97 | } 98 | 99 | pub fn spawn_explosion_particles(&mut self, color: Color, x: f32, y: f32) -> usize { 100 | let v = rand::random::() * 10.0 + 20.0; 101 | for i in 0..NUM_PARTICLES { 102 | let particle = Particle::new_explosion(color, x, y, v, i as f32); 103 | self.particles.push(particle); 104 | } 105 | NUM_PARTICLES 106 | } 107 | 108 | pub fn remove_indices_from_particles(&mut self, i: Vec) { 109 | for &index in i.iter().rev() { 110 | self.particles.swap_remove(index); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/simulation_shooting_star.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::{COLORS, FRAMES_PER_SECOND, TERMINAL_GRAVITY}; 2 | use crate::projectile::Projectile; 3 | use nalgebra::{Point2, Vector2}; 4 | use rand::seq::SliceRandom; 5 | use ratatui::prelude::Color; 6 | use std::f32::consts::PI; 7 | 8 | const HEAD: char = '●'; 9 | const TAIL: char = '·'; 10 | const EXPLOSION_CHARACTERS: [char; 3] = ['+', '*', '•']; 11 | const NUM_PARTICLES: usize = 40; 12 | 13 | #[derive(Debug)] 14 | pub struct Particle { 15 | pub char: char, 16 | pub color: Color, 17 | pub physics: Projectile, 18 | pub shooting: bool, 19 | pub tail_char: Option, 20 | } 21 | 22 | // Sample a random element from the array 23 | fn sample_character() -> &'static char { 24 | let mut rng = rand::thread_rng(); 25 | EXPLOSION_CHARACTERS 26 | .choose(&mut rng) 27 | .unwrap_or(&EXPLOSION_CHARACTERS[0]) 28 | } 29 | fn sample_color() -> Color { 30 | let mut rng = rand::thread_rng(); 31 | *COLORS.choose(&mut rng).unwrap_or(&COLORS[0]) 32 | } 33 | 34 | impl Particle { 35 | fn new(width: usize, height: usize) -> Self { 36 | // Generate a random angle in radians 37 | let angle = rand::random::() * 2.0 * PI; 38 | 39 | let v = rand::random::() * 25.0 + 20.0; 40 | 41 | // Calculate the x and y components of the velocity based on the angle 42 | let vx = angle.cos() * v * 1.2; 43 | let vy = angle.sin() * v; 44 | 45 | let x = (width as f32 / 2.0) + (0.40 * width as f32) * -angle.cos(); 46 | let y = (height as f32 / 2.0) + (0.40 * height as f32) * -angle.sin(); 47 | 48 | // let x = (width as f32 * 0.9) * -angle.cos(); 49 | // let y = (height as f32 * 0.9) * -angle.sin(); 50 | 51 | let physics = Projectile::new( 52 | 1.0 / FRAMES_PER_SECOND, 53 | Point2::new(x, y), 54 | //How do i make this any random direction? 55 | Vector2::new(vx, vy), 56 | TERMINAL_GRAVITY, 57 | // 0.0, 58 | ); 59 | 60 | let color = sample_color(); 61 | 62 | Particle { 63 | char: HEAD, 64 | color, 65 | physics, 66 | shooting: true, 67 | tail_char: Some(TAIL), 68 | } 69 | } 70 | 71 | fn new_explosion_y_bounds(color: Color, x: f32, y: f32, v: f32, i: f32) -> Self { 72 | let random_velocity = rand::random::() * v; 73 | 74 | let physics = Projectile::new( 75 | 1.0 / FRAMES_PER_SECOND, 76 | Point2::new(x, y), 77 | Vector2::new( 78 | f32::cos(i) * random_velocity, 79 | -f32::sin(i) * random_velocity, 80 | ), 81 | TERMINAL_GRAVITY, 82 | // 0.0, 83 | ); 84 | 85 | let char = *sample_character(); 86 | 87 | Particle { 88 | char, 89 | color, 90 | physics, 91 | shooting: false, 92 | tail_char: None, 93 | } 94 | } 95 | 96 | fn new_explosion_x_bounds(color: Color, x: f32, y: f32, v: f32, i: f32) -> Self { 97 | // Adjust the velocity between 100% and 20% of the original velocity 98 | let random_velocity = rand::random::() * v; 99 | 100 | let physics = Projectile::new( 101 | 1.0 / FRAMES_PER_SECOND, 102 | Point2::new(x, y), 103 | Vector2::new(f32::cos(i) * random_velocity, f32::sin(i) * random_velocity), 104 | TERMINAL_GRAVITY, 105 | // 0.0, 106 | ); 107 | 108 | let char = *sample_character(); 109 | 110 | Particle { 111 | char, 112 | color, 113 | physics, 114 | shooting: false, 115 | tail_char: None, 116 | } 117 | } 118 | } 119 | 120 | #[derive(Debug)] 121 | pub struct SimulationStateShootingStar { 122 | pub particles: Vec, 123 | } 124 | 125 | impl SimulationStateShootingStar { 126 | pub fn new() -> Self { 127 | Self { particles: vec![] } 128 | } 129 | 130 | pub fn tick(&mut self) { 131 | for particle in &mut self.particles { 132 | particle.physics.update(); 133 | } 134 | } 135 | 136 | pub fn spawn_particles(&mut self, width: usize, height: usize) -> usize { 137 | let particle = Particle::new(width, height); 138 | self.particles.push(particle); 139 | 1 140 | } 141 | 142 | pub fn spawn_explosion_particles_x_bounds( 143 | &mut self, 144 | color: Color, 145 | x: f32, 146 | y: f32, 147 | angle: f32, 148 | vel: Vector2, 149 | ) -> usize { 150 | let v = vel.norm() / 2.0; 151 | 152 | //TODO make reflected angle work 153 | // let reflected_angle = SimulationStateShootingStar::reflect_angle(angle); 154 | let reflected_angle = PI - angle; 155 | 156 | let spray_angle = std::f32::consts::PI / 6.0; // 30 degrees in radians 157 | 158 | // Define the range of angles to spray particles within 159 | let start_angle = reflected_angle - spray_angle; 160 | let end_angle = reflected_angle + spray_angle; 161 | 162 | // Define the number of particles to spawn 163 | let num_particles = NUM_PARTICLES; 164 | 165 | // Calculate the angular step size between particles 166 | let angle_step = (end_angle - start_angle) / (num_particles - 1) as f32; 167 | 168 | for i in 0..NUM_PARTICLES { 169 | let current_angle = start_angle + i as f32 * angle_step; 170 | let particle = Particle::new_explosion_x_bounds( 171 | color, 172 | x, 173 | y, 174 | v, 175 | current_angle, // + (i as f32 - NUM_PARTICLES as f32 / 2.0) * 10.0, 176 | ); 177 | self.particles.push(particle); 178 | } 179 | NUM_PARTICLES 180 | } 181 | 182 | pub fn spawn_explosion_particles_y_bounds( 183 | &mut self, 184 | color: Color, 185 | x: f32, 186 | y: f32, 187 | angle: f32, 188 | vel: Vector2, 189 | ) -> usize { 190 | let v = vel.norm() / 2.0; 191 | 192 | //TODO make reflected angle work 193 | // let reflected_angle = SimulationStateShootingStar::reflect_angle(angle); 194 | let reflected_angle = angle; 195 | 196 | let spray_angle = std::f32::consts::PI / 6.0; // 30 degrees in radians 197 | 198 | // Define the range of angles to spray particles within 199 | let start_angle = reflected_angle - spray_angle; 200 | let end_angle = reflected_angle + spray_angle; 201 | 202 | // Define the number of particles to spawn 203 | let num_particles = NUM_PARTICLES; 204 | 205 | // Calculate the angular step size between particles 206 | let angle_step = (end_angle - start_angle) / (num_particles - 1) as f32; 207 | 208 | for i in 0..NUM_PARTICLES { 209 | let current_angle = start_angle + i as f32 * angle_step; 210 | let particle = Particle::new_explosion_y_bounds( 211 | color, 212 | x, 213 | y, 214 | v, 215 | current_angle, // + (i as f32 - NUM_PARTICLES as f32 / 2.0) * 10.0, 216 | ); 217 | self.particles.push(particle); 218 | } 219 | NUM_PARTICLES 220 | } 221 | 222 | pub fn remove_indices_from_particles(&mut self, i: Vec) { 223 | for &index in i.iter().rev() { 224 | self.particles.swap_remove(index); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/system.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::TICK_RATE_IN_MILI; 2 | use crate::simulation_confetti::SimulationStateConfetti; 3 | use crate::simulation_fireworks::SimulationStateFireworks; 4 | use crate::simulation_shooting_star::SimulationStateShootingStar; 5 | use ratatui::prelude::*; 6 | 7 | #[derive(Debug)] 8 | pub enum AppSimulation { 9 | Fireworks(SimulationStateFireworks), 10 | Confetti(SimulationStateConfetti), 11 | ShootingStar(SimulationStateShootingStar), 12 | } 13 | 14 | impl StatefulWidget for AppSimulation { 15 | type State = AppSimulation; // Change the associated type to use the enum 16 | 17 | fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 18 | match state { 19 | AppSimulation::Fireworks(state) => { 20 | let mut indices_to_remove = vec![]; 21 | let mut indices_to_explode = vec![]; 22 | for (index, particle) in state.particles.iter().enumerate() { 23 | let pos = particle.physics.position(); 24 | 25 | if pos.x < 0.0 || pos.x >= area.width as f32 || pos.y >= area.height as f32 { 26 | indices_to_remove.push(index); 27 | continue; 28 | } else if particle.shooting && particle.physics.velocity().y > -3.0 { 29 | indices_to_explode.push(index); 30 | indices_to_remove.push(index); 31 | continue; 32 | } 33 | 34 | if pos.y.floor() > -1.0 { 35 | let cell = buf.get_mut(pos.x.floor() as u16, pos.y.floor() as u16); 36 | cell.set_char(particle.char); // Set the character 37 | cell.fg = particle.color; 38 | } 39 | 40 | if particle.shooting { 41 | let l = -particle.physics.velocity().y as isize; 42 | for i in 1..l { 43 | let y = pos.y as isize + i; 44 | if y > 0 && y < (area.height - 1) as isize { 45 | let cell = buf.get_mut(pos.x.floor() as u16, y as u16); 46 | cell.set_char(particle.tail_char.unwrap()); // Set the character 47 | cell.fg = particle.color; 48 | } 49 | } 50 | } 51 | } 52 | 53 | for &index in indices_to_explode.iter().rev() { 54 | let color = state.particles[index].color; 55 | let pos = &state.particles[index].physics.position(); 56 | let x = pos.x; 57 | let y = pos.y; 58 | state.spawn_explosion_particles(color, x, y); 59 | } 60 | 61 | state.remove_indices_from_particles(indices_to_remove); 62 | } 63 | AppSimulation::Confetti(state) => { 64 | let mut indices_to_remove = vec![]; 65 | for (index, particle) in state.particles.iter().enumerate() { 66 | let pos = particle.physics.position(); 67 | 68 | if pos.x < 0.0 69 | || pos.x >= area.width as f32 70 | || pos.y < 0.0 71 | || pos.y >= area.height as f32 72 | { 73 | indices_to_remove.push(index); 74 | continue; 75 | } 76 | 77 | let cell = buf.get_mut(pos.x.floor() as u16, pos.y.floor() as u16); 78 | cell.set_char(particle.char); // Set the character 79 | cell.fg = particle.color; 80 | } 81 | 82 | state.remove_indices_from_particles(indices_to_remove); 83 | } 84 | AppSimulation::ShootingStar(state) => { 85 | let mut indices_to_remove = vec![]; 86 | let mut indices_to_explode = vec![]; 87 | for (index, particle) in state.particles.iter().enumerate() { 88 | let pos = particle.physics.position(); 89 | 90 | if !particle.shooting 91 | && (pos.x <= 0.0 92 | || pos.x >= area.width as f32 93 | || pos.y >= area.height as f32) 94 | { 95 | indices_to_remove.push(index); 96 | continue; 97 | } else if particle.shooting 98 | && (pos.x <= 0.0 99 | || pos.y <= 0.0 100 | || pos.x >= area.width as f32 101 | || pos.y >= area.height as f32) 102 | { 103 | indices_to_explode.push(index); 104 | indices_to_remove.push(index); 105 | continue; 106 | } 107 | 108 | if particle.shooting { 109 | let vel = -particle.physics.velocity(); 110 | let opposite_vel = -vel; 111 | 112 | // Get the components of the opposite velocity 113 | let dx = opposite_vel.x; 114 | let dy = opposite_vel.y; 115 | 116 | // Iterate along the opposite velocity vector 117 | for i in 1..(opposite_vel.norm() / 2.0) as isize { 118 | let new_pos_x = 119 | (pos.x - dx / TICK_RATE_IN_MILI as f32 * i as f32).floor() as u16; 120 | let new_pos_y = 121 | (pos.y - dy / TICK_RATE_IN_MILI as f32 * i as f32).floor() as u16; 122 | 123 | // Check if the new position is within bounds 124 | if new_pos_y > 0 125 | && new_pos_y < (area.height - 1) as u16 126 | && new_pos_x > 0 127 | && new_pos_x < (area.width - 1) as u16 128 | { 129 | let cell = buf.get_mut(new_pos_x, new_pos_y); 130 | cell.set_char(particle.tail_char.unwrap()); // Set the character 131 | cell.fg = particle.color; 132 | } 133 | } 134 | } 135 | 136 | let cell = buf.get_mut(pos.x.floor() as u16, pos.y.floor() as u16); 137 | cell.set_char(particle.char); // Set the character 138 | cell.fg = particle.color; 139 | } 140 | 141 | for &index in indices_to_explode.iter().rev() { 142 | let color = state.particles[index].color; 143 | let pos = &state.particles[index].physics.position(); 144 | let vel = state.particles[index].physics.velocity(); 145 | 146 | let x = if pos.x <= 0.0 { 147 | 0.0 148 | } else if pos.x >= area.width as f32 { 149 | area.width as f32 150 | } else { 151 | pos.x 152 | }; 153 | 154 | let y = if pos.y <= 0.0 { 155 | 0.0 156 | } else if pos.y >= area.height as f32 { 157 | area.height as f32 158 | } else { 159 | pos.y 160 | }; 161 | let angle_rad = vel.y.atan2(vel.x); 162 | 163 | if y == 0.0 || y == area.height as f32 { 164 | state.spawn_explosion_particles_y_bounds(color, x, y, angle_rad, vel); 165 | } else if x == 0.0 || x == area.width as f32 { 166 | state.spawn_explosion_particles_x_bounds(color, x, y, angle_rad, vel); 167 | } 168 | } 169 | 170 | state.remove_indices_from_particles(indices_to_remove); 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{App, AppResult}; 2 | use crate::event::EventHandler; 3 | use crate::ui; 4 | use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; 5 | use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; 6 | use ratatui::backend::Backend; 7 | use ratatui::Terminal; 8 | use std::io; 9 | use std::panic; 10 | 11 | /// Representation of a terminal user interface. 12 | /// 13 | /// It is responsible for setting up the terminal, 14 | /// initializing the interface and handling the draw events. 15 | #[derive(Debug)] 16 | pub struct Tui { 17 | /// Interface to the Terminal. 18 | terminal: Terminal, 19 | /// Terminal event handler. 20 | pub events: EventHandler, 21 | } 22 | 23 | impl Tui { 24 | /// Constructs a new instance of [`Tui`]. 25 | pub fn new(terminal: Terminal, events: EventHandler) -> Self { 26 | Self { terminal, events } 27 | } 28 | 29 | /// Initializes the terminal interface. 30 | /// 31 | /// It enables the raw mode and sets terminal properties. 32 | pub fn init(&mut self) -> AppResult<()> { 33 | terminal::enable_raw_mode()?; 34 | crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; 35 | 36 | // Define a custom panic hook to reset the terminal properties. 37 | // This way, you won't have your terminal messed up if an unexpected error happens. 38 | let panic_hook = panic::take_hook(); 39 | panic::set_hook(Box::new(move |panic| { 40 | Self::reset().expect("failed to reset the terminal"); 41 | panic_hook(panic); 42 | })); 43 | 44 | self.terminal.hide_cursor()?; 45 | self.terminal.clear()?; 46 | Ok(()) 47 | } 48 | 49 | /// [`Draw`] the terminal interface by [`rendering`] the widgets. 50 | /// 51 | /// [`Draw`]: ratatui::Terminal::draw 52 | /// [`rendering`]: crate::ui:render 53 | pub fn draw(&mut self, app: &mut App) -> AppResult<()> { 54 | self.terminal.draw(|frame| ui::render(app, frame))?; 55 | Ok(()) 56 | } 57 | 58 | /// Resets the terminal interface. 59 | /// 60 | /// This function is also used for the panic hook to revert 61 | /// the terminal properties if unexpected errors occur. 62 | fn reset() -> AppResult<()> { 63 | terminal::disable_raw_mode()?; 64 | crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; 65 | Ok(()) 66 | } 67 | 68 | /// Exits the terminal interface. 69 | /// 70 | /// It disables the raw mode and reverts back the terminal properties. 71 | pub fn exit(&mut self) -> AppResult<()> { 72 | Self::reset()?; 73 | self.terminal.show_cursor()?; 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use ratatui::Frame; 2 | 3 | use crate::app::App; 4 | use crate::simulation_confetti::SimulationStateConfetti; 5 | use crate::simulation_fireworks::SimulationStateFireworks; 6 | use crate::simulation_shooting_star::SimulationStateShootingStar; 7 | use crate::system::AppSimulation; 8 | 9 | /// Renders the user interface widgets. 10 | pub fn render(app: &mut App, frame: &mut Frame) { 11 | // This is where you add new widgets. 12 | // See the following resources: 13 | // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html 14 | // - https://github.com/ratatui-org/ratatui/tree/master/examples 15 | app.set_area(frame.size()); 16 | 17 | match app.get_simulation_state() { 18 | AppSimulation::Fireworks(_) => { 19 | frame.render_stateful_widget( 20 | AppSimulation::Fireworks(SimulationStateFireworks::new()), 21 | frame.size(), 22 | app.get_simulation_state(), 23 | ); 24 | } 25 | AppSimulation::Confetti(_) => { 26 | frame.render_stateful_widget( 27 | AppSimulation::Confetti(SimulationStateConfetti::new()), 28 | frame.size(), 29 | app.get_simulation_state(), 30 | ); 31 | } 32 | AppSimulation::ShootingStar(_) => { 33 | frame.render_stateful_widget( 34 | AppSimulation::ShootingStar(SimulationStateShootingStar::new()), 35 | frame.size(), 36 | app.get_simulation_state(), 37 | ); 38 | } 39 | } 40 | } 41 | --------------------------------------------------------------------------------