├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── debug.log ├── examples ├── async.rs ├── hello-world.rs ├── tabs.rs ├── text-input.rs └── todo-app.rs ├── rust-toolchain.toml └── src ├── context.rs ├── environment.rs ├── lib.rs ├── macros.rs ├── nodes.rs ├── prelude.rs ├── ratatui.rs ├── recievers.rs ├── runtime.rs ├── signal.rs └── tasks.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /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.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 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.18" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 37 | 38 | [[package]] 39 | name = "anyhow" 40 | version = "1.0.86" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 43 | 44 | [[package]] 45 | name = "autocfg" 46 | version = "1.3.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 49 | 50 | [[package]] 51 | name = "backtrace" 52 | version = "0.3.71" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 55 | dependencies = [ 56 | "addr2line", 57 | "cc", 58 | "cfg-if", 59 | "libc", 60 | "miniz_oxide", 61 | "object", 62 | "rustc-demangle", 63 | ] 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "2.5.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 70 | 71 | [[package]] 72 | name = "bytes" 73 | version = "1.6.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 76 | 77 | [[package]] 78 | name = "cassowary" 79 | version = "0.3.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 82 | 83 | [[package]] 84 | name = "castaway" 85 | version = "0.2.2" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 88 | dependencies = [ 89 | "rustversion", 90 | ] 91 | 92 | [[package]] 93 | name = "cc" 94 | version = "1.0.98" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" 97 | 98 | [[package]] 99 | name = "cfg-if" 100 | version = "1.0.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 103 | 104 | [[package]] 105 | name = "compact_str" 106 | version = "0.7.1" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 109 | dependencies = [ 110 | "castaway", 111 | "cfg-if", 112 | "itoa", 113 | "ryu", 114 | "static_assertions", 115 | ] 116 | 117 | [[package]] 118 | name = "crossterm" 119 | version = "0.27.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 122 | dependencies = [ 123 | "bitflags", 124 | "crossterm_winapi", 125 | "futures-core", 126 | "libc", 127 | "mio", 128 | "parking_lot", 129 | "signal-hook", 130 | "signal-hook-mio", 131 | "winapi", 132 | ] 133 | 134 | [[package]] 135 | name = "crossterm_winapi" 136 | version = "0.9.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 139 | dependencies = [ 140 | "winapi", 141 | ] 142 | 143 | [[package]] 144 | name = "either" 145 | version = "1.12.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 148 | 149 | [[package]] 150 | name = "futures" 151 | version = "0.3.30" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 154 | dependencies = [ 155 | "futures-channel", 156 | "futures-core", 157 | "futures-executor", 158 | "futures-io", 159 | "futures-sink", 160 | "futures-task", 161 | "futures-util", 162 | ] 163 | 164 | [[package]] 165 | name = "futures-channel" 166 | version = "0.3.30" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 169 | dependencies = [ 170 | "futures-core", 171 | "futures-sink", 172 | ] 173 | 174 | [[package]] 175 | name = "futures-core" 176 | version = "0.3.30" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 179 | 180 | [[package]] 181 | name = "futures-executor" 182 | version = "0.3.30" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 185 | dependencies = [ 186 | "futures-core", 187 | "futures-task", 188 | "futures-util", 189 | ] 190 | 191 | [[package]] 192 | name = "futures-io" 193 | version = "0.3.30" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 196 | 197 | [[package]] 198 | name = "futures-macro" 199 | version = "0.3.30" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 202 | dependencies = [ 203 | "proc-macro2", 204 | "quote", 205 | "syn", 206 | ] 207 | 208 | [[package]] 209 | name = "futures-sink" 210 | version = "0.3.30" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 213 | 214 | [[package]] 215 | name = "futures-task" 216 | version = "0.3.30" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 219 | 220 | [[package]] 221 | name = "futures-util" 222 | version = "0.3.30" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 225 | dependencies = [ 226 | "futures-channel", 227 | "futures-core", 228 | "futures-io", 229 | "futures-macro", 230 | "futures-sink", 231 | "futures-task", 232 | "memchr", 233 | "pin-project-lite", 234 | "pin-utils", 235 | "slab", 236 | ] 237 | 238 | [[package]] 239 | name = "gimli" 240 | version = "0.28.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 243 | 244 | [[package]] 245 | name = "hashbrown" 246 | version = "0.14.5" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 249 | dependencies = [ 250 | "ahash", 251 | "allocator-api2", 252 | ] 253 | 254 | [[package]] 255 | name = "heck" 256 | version = "0.4.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 259 | 260 | [[package]] 261 | name = "hermit-abi" 262 | version = "0.3.9" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 265 | 266 | [[package]] 267 | name = "indoc" 268 | version = "2.0.5" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 271 | 272 | [[package]] 273 | name = "itertools" 274 | version = "0.12.1" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 277 | dependencies = [ 278 | "either", 279 | ] 280 | 281 | [[package]] 282 | name = "itoa" 283 | version = "1.0.11" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 286 | 287 | [[package]] 288 | name = "libc" 289 | version = "0.2.154" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 292 | 293 | [[package]] 294 | name = "lock_api" 295 | version = "0.4.12" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 298 | dependencies = [ 299 | "autocfg", 300 | "scopeguard", 301 | ] 302 | 303 | [[package]] 304 | name = "log" 305 | version = "0.4.21" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 308 | 309 | [[package]] 310 | name = "lru" 311 | version = "0.12.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 314 | dependencies = [ 315 | "hashbrown", 316 | ] 317 | 318 | [[package]] 319 | name = "memchr" 320 | version = "2.7.2" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 323 | 324 | [[package]] 325 | name = "miniz_oxide" 326 | version = "0.7.3" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" 329 | dependencies = [ 330 | "adler", 331 | ] 332 | 333 | [[package]] 334 | name = "mio" 335 | version = "0.8.11" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 338 | dependencies = [ 339 | "libc", 340 | "log", 341 | "wasi", 342 | "windows-sys 0.48.0", 343 | ] 344 | 345 | [[package]] 346 | name = "num_cpus" 347 | version = "1.16.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 350 | dependencies = [ 351 | "hermit-abi", 352 | "libc", 353 | ] 354 | 355 | [[package]] 356 | name = "object" 357 | version = "0.32.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 360 | dependencies = [ 361 | "memchr", 362 | ] 363 | 364 | [[package]] 365 | name = "once_cell" 366 | version = "1.19.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 369 | 370 | [[package]] 371 | name = "parking_lot" 372 | version = "0.12.2" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" 375 | dependencies = [ 376 | "lock_api", 377 | "parking_lot_core", 378 | ] 379 | 380 | [[package]] 381 | name = "parking_lot_core" 382 | version = "0.9.10" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 385 | dependencies = [ 386 | "cfg-if", 387 | "libc", 388 | "redox_syscall", 389 | "smallvec", 390 | "windows-targets 0.52.5", 391 | ] 392 | 393 | [[package]] 394 | name = "paste" 395 | version = "1.0.15" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 398 | 399 | [[package]] 400 | name = "pin-project-lite" 401 | version = "0.2.14" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 404 | 405 | [[package]] 406 | name = "pin-utils" 407 | version = "0.1.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 410 | 411 | [[package]] 412 | name = "proc-macro2" 413 | version = "1.0.82" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" 416 | dependencies = [ 417 | "unicode-ident", 418 | ] 419 | 420 | [[package]] 421 | name = "quote" 422 | version = "1.0.36" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 425 | dependencies = [ 426 | "proc-macro2", 427 | ] 428 | 429 | [[package]] 430 | name = "ratatui" 431 | version = "0.26.2" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" 434 | dependencies = [ 435 | "bitflags", 436 | "cassowary", 437 | "compact_str", 438 | "crossterm", 439 | "indoc", 440 | "itertools", 441 | "lru", 442 | "paste", 443 | "stability", 444 | "strum", 445 | "unicode-segmentation", 446 | "unicode-width", 447 | ] 448 | 449 | [[package]] 450 | name = "redox_syscall" 451 | version = "0.5.1" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 454 | dependencies = [ 455 | "bitflags", 456 | ] 457 | 458 | [[package]] 459 | name = "rizzup" 460 | version = "0.0.1" 461 | dependencies = [ 462 | "anyhow", 463 | "crossterm", 464 | "futures", 465 | "ratatui", 466 | "slotmap", 467 | "tokio", 468 | "tracing", 469 | ] 470 | 471 | [[package]] 472 | name = "rustc-demangle" 473 | version = "0.1.24" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 476 | 477 | [[package]] 478 | name = "rustversion" 479 | version = "1.0.17" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 482 | 483 | [[package]] 484 | name = "ryu" 485 | version = "1.0.18" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 488 | 489 | [[package]] 490 | name = "scopeguard" 491 | version = "1.2.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 494 | 495 | [[package]] 496 | name = "signal-hook" 497 | version = "0.3.17" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 500 | dependencies = [ 501 | "libc", 502 | "signal-hook-registry", 503 | ] 504 | 505 | [[package]] 506 | name = "signal-hook-mio" 507 | version = "0.2.3" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 510 | dependencies = [ 511 | "libc", 512 | "mio", 513 | "signal-hook", 514 | ] 515 | 516 | [[package]] 517 | name = "signal-hook-registry" 518 | version = "1.4.2" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 521 | dependencies = [ 522 | "libc", 523 | ] 524 | 525 | [[package]] 526 | name = "slab" 527 | version = "0.4.9" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 530 | dependencies = [ 531 | "autocfg", 532 | ] 533 | 534 | [[package]] 535 | name = "slotmap" 536 | version = "1.0.7" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 539 | dependencies = [ 540 | "version_check", 541 | ] 542 | 543 | [[package]] 544 | name = "smallvec" 545 | version = "1.13.2" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 548 | 549 | [[package]] 550 | name = "socket2" 551 | version = "0.5.7" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 554 | dependencies = [ 555 | "libc", 556 | "windows-sys 0.52.0", 557 | ] 558 | 559 | [[package]] 560 | name = "stability" 561 | version = "0.2.0" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" 564 | dependencies = [ 565 | "quote", 566 | "syn", 567 | ] 568 | 569 | [[package]] 570 | name = "static_assertions" 571 | version = "1.1.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 574 | 575 | [[package]] 576 | name = "strum" 577 | version = "0.26.2" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 580 | dependencies = [ 581 | "strum_macros", 582 | ] 583 | 584 | [[package]] 585 | name = "strum_macros" 586 | version = "0.26.2" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" 589 | dependencies = [ 590 | "heck", 591 | "proc-macro2", 592 | "quote", 593 | "rustversion", 594 | "syn", 595 | ] 596 | 597 | [[package]] 598 | name = "syn" 599 | version = "2.0.63" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" 602 | dependencies = [ 603 | "proc-macro2", 604 | "quote", 605 | "unicode-ident", 606 | ] 607 | 608 | [[package]] 609 | name = "tokio" 610 | version = "1.37.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" 613 | dependencies = [ 614 | "backtrace", 615 | "bytes", 616 | "libc", 617 | "mio", 618 | "num_cpus", 619 | "parking_lot", 620 | "pin-project-lite", 621 | "signal-hook-registry", 622 | "socket2", 623 | "tokio-macros", 624 | "windows-sys 0.48.0", 625 | ] 626 | 627 | [[package]] 628 | name = "tokio-macros" 629 | version = "2.2.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 632 | dependencies = [ 633 | "proc-macro2", 634 | "quote", 635 | "syn", 636 | ] 637 | 638 | [[package]] 639 | name = "tracing" 640 | version = "0.1.40" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 643 | dependencies = [ 644 | "pin-project-lite", 645 | "tracing-attributes", 646 | "tracing-core", 647 | ] 648 | 649 | [[package]] 650 | name = "tracing-attributes" 651 | version = "0.1.27" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 654 | dependencies = [ 655 | "proc-macro2", 656 | "quote", 657 | "syn", 658 | ] 659 | 660 | [[package]] 661 | name = "tracing-core" 662 | version = "0.1.32" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 665 | dependencies = [ 666 | "once_cell", 667 | ] 668 | 669 | [[package]] 670 | name = "unicode-ident" 671 | version = "1.0.12" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 674 | 675 | [[package]] 676 | name = "unicode-segmentation" 677 | version = "1.11.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 680 | 681 | [[package]] 682 | name = "unicode-width" 683 | version = "0.1.12" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 686 | 687 | [[package]] 688 | name = "version_check" 689 | version = "0.9.4" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 692 | 693 | [[package]] 694 | name = "wasi" 695 | version = "0.11.0+wasi-snapshot-preview1" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 698 | 699 | [[package]] 700 | name = "winapi" 701 | version = "0.3.9" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 704 | dependencies = [ 705 | "winapi-i686-pc-windows-gnu", 706 | "winapi-x86_64-pc-windows-gnu", 707 | ] 708 | 709 | [[package]] 710 | name = "winapi-i686-pc-windows-gnu" 711 | version = "0.4.0" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 714 | 715 | [[package]] 716 | name = "winapi-x86_64-pc-windows-gnu" 717 | version = "0.4.0" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 720 | 721 | [[package]] 722 | name = "windows-sys" 723 | version = "0.48.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 726 | dependencies = [ 727 | "windows-targets 0.48.5", 728 | ] 729 | 730 | [[package]] 731 | name = "windows-sys" 732 | version = "0.52.0" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 735 | dependencies = [ 736 | "windows-targets 0.52.5", 737 | ] 738 | 739 | [[package]] 740 | name = "windows-targets" 741 | version = "0.48.5" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 744 | dependencies = [ 745 | "windows_aarch64_gnullvm 0.48.5", 746 | "windows_aarch64_msvc 0.48.5", 747 | "windows_i686_gnu 0.48.5", 748 | "windows_i686_msvc 0.48.5", 749 | "windows_x86_64_gnu 0.48.5", 750 | "windows_x86_64_gnullvm 0.48.5", 751 | "windows_x86_64_msvc 0.48.5", 752 | ] 753 | 754 | [[package]] 755 | name = "windows-targets" 756 | version = "0.52.5" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 759 | dependencies = [ 760 | "windows_aarch64_gnullvm 0.52.5", 761 | "windows_aarch64_msvc 0.52.5", 762 | "windows_i686_gnu 0.52.5", 763 | "windows_i686_gnullvm", 764 | "windows_i686_msvc 0.52.5", 765 | "windows_x86_64_gnu 0.52.5", 766 | "windows_x86_64_gnullvm 0.52.5", 767 | "windows_x86_64_msvc 0.52.5", 768 | ] 769 | 770 | [[package]] 771 | name = "windows_aarch64_gnullvm" 772 | version = "0.48.5" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 775 | 776 | [[package]] 777 | name = "windows_aarch64_gnullvm" 778 | version = "0.52.5" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 781 | 782 | [[package]] 783 | name = "windows_aarch64_msvc" 784 | version = "0.48.5" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 787 | 788 | [[package]] 789 | name = "windows_aarch64_msvc" 790 | version = "0.52.5" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 793 | 794 | [[package]] 795 | name = "windows_i686_gnu" 796 | version = "0.48.5" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 799 | 800 | [[package]] 801 | name = "windows_i686_gnu" 802 | version = "0.52.5" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 805 | 806 | [[package]] 807 | name = "windows_i686_gnullvm" 808 | version = "0.52.5" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 811 | 812 | [[package]] 813 | name = "windows_i686_msvc" 814 | version = "0.48.5" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 817 | 818 | [[package]] 819 | name = "windows_i686_msvc" 820 | version = "0.52.5" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 823 | 824 | [[package]] 825 | name = "windows_x86_64_gnu" 826 | version = "0.48.5" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 829 | 830 | [[package]] 831 | name = "windows_x86_64_gnu" 832 | version = "0.52.5" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 835 | 836 | [[package]] 837 | name = "windows_x86_64_gnullvm" 838 | version = "0.48.5" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 841 | 842 | [[package]] 843 | name = "windows_x86_64_gnullvm" 844 | version = "0.52.5" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 847 | 848 | [[package]] 849 | name = "windows_x86_64_msvc" 850 | version = "0.48.5" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 853 | 854 | [[package]] 855 | name = "windows_x86_64_msvc" 856 | version = "0.52.5" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 859 | 860 | [[package]] 861 | name = "zerocopy" 862 | version = "0.7.34" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" 865 | dependencies = [ 866 | "zerocopy-derive", 867 | ] 868 | 869 | [[package]] 870 | name = "zerocopy-derive" 871 | version = "0.7.34" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" 874 | dependencies = [ 875 | "proc-macro2", 876 | "quote", 877 | "syn", 878 | ] 879 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rizzup" 3 | version = "0.0.1" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ratatui = { version = "0.26.2", features = ["unstable-widget-ref"] } 8 | anyhow = "1.0.86" 9 | crossterm = { version = "0.27.0", features = ["event-stream"] } 10 | futures = "0.3.30" 11 | slotmap = "1.0.7" 12 | tokio = { version = "1.37.0", features = ["full"] } 13 | tracing = "0.1.40" 14 | 15 | # [features] 16 | # ratatui-widget-ref = ["ratatui/unstable-widget-ref"] 17 | 18 | [[example]] 19 | name = "hello-world" 20 | path = "examples/hello-world.rs" 21 | 22 | [[example]] 23 | name = "text-input" 24 | path = "examples/text-input.rs" 25 | 26 | [[example]] 27 | name = "async" 28 | path = "examples/async.rs" 29 | 30 | [[example]] 31 | name = "todo-app" 32 | path = "examples/todo-app.rs" 33 | 34 | [[example]] 35 | name = "tabs" 36 | path = "examples/tabs.rs" 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive TUI component library 2 | This library heavily employs reactive signals akin to those found in libraries like [leptos](https://github.com/leptos-rs/leptos) or [dioxus](https://github.com/dioxuslabs/dioxus). For rendering, it currently interfaces with ratatui widgets. Check out the examples, such as the text input, to understand its usage better. 3 | 4 | ```rust 5 | fn input(_: ReadSignal<()>) -> Child { 6 | let (input_r, input_w) = create_signal("".to_string()); 7 | 8 | on(move |key: &event::KeyCode| match key { 9 | event::KeyCode::Char(ch) => input_w.update(|x| x.push(*ch)), 10 | event::KeyCode::Backspace => input_w.update(|x| { 11 | x.pop(); 12 | }), 13 | _ => {} 14 | }); 15 | 16 | view_widget(move || { 17 | let block = Block::default() 18 | .padding(Padding::horizontal(1)) 19 | .borders(Borders::all()) 20 | .title("Start typeing") 21 | 22 | Paragraph::new(input_r.get()).block(block) 23 | }) 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | 2 | CLEANUP NodeId(1v1) 1 -------------------------------------------------------------------------------- /examples/async.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event; 3 | use futures::StreamExt; 4 | use ratatui::{style::Stylize, text::*, widgets::*, Terminal}; 5 | use rizzup::prelude::*; 6 | 7 | #[derive(Debug, Clone)] 8 | enum Message { 9 | Tick, 10 | } 11 | 12 | fn input() -> RatView { 13 | let input = create_signal("".to_string()); 14 | let blink = create_signal(true); 15 | let active = create_signal(true); 16 | 17 | let ticker = create_async_task(active.clone(), move |active, send| async move { 18 | if !active { 19 | return; 20 | } 21 | loop { 22 | send.send(Message::Tick); 23 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 24 | } 25 | }); 26 | 27 | let input_c = input.clone(); 28 | create_memo(move || { 29 | let _ = input_c.get(); 30 | active.set(true); 31 | blink.set(true); 32 | }); 33 | 34 | let input_c = input.clone(); 35 | on(move |ev: &event::KeyCode| match ev { 36 | event::KeyCode::Char(ch) => input_c.update(|x| x.push(*ch)), 37 | event::KeyCode::Backspace => input_c.update(|x| { 38 | x.pop(); 39 | }), 40 | event::KeyCode::Up => active.set(false), 41 | event::KeyCode::Down => active.set(true), 42 | _ => {} 43 | }); 44 | 45 | on(move |ev: &Message| match ev { 46 | Message::Tick => blink.update(|v| *v = !*v), 47 | }); 48 | 49 | widget_ref(move || { 50 | let block = Block::default() 51 | .padding(Padding::horizontal(1)) 52 | .borders(Borders::all()) 53 | .title("Start typeing") 54 | .title("(Press esc to exit)") 55 | .title(format!("{:?}", ticker.get_state())); 56 | 57 | let lines = Line::from(vec![ 58 | Span::from(format!("{}", input.get())), 59 | Span::from(format!("│")).fg(match blink.get() { 60 | true => ratatui::style::Color::Black, 61 | false => ratatui::style::Color::Blue, 62 | }), 63 | ]); 64 | 65 | Paragraph::new(lines).block(block) 66 | }) 67 | } 68 | 69 | #[tokio::main] 70 | async fn main() -> Result<()> { 71 | let mut term = init_tui()?; 72 | init_panic_hook(); 73 | 74 | create_async_scope(move |handle| async move { 75 | let mut reader = crossterm::event::EventStream::new(); 76 | let app = input(); 77 | loop { 78 | term.draw(|f| f.render_widget_ref(app, f.size()))?; 79 | 80 | tokio::select! { 81 | _ = handle.listen() => {}, 82 | ev = reader.next() => match ev { 83 | Some(Ok(event)) => { 84 | if let event::Event::Key(key) = event { 85 | if key.kind == event::KeyEventKind::Press { 86 | send(key.code) 87 | } 88 | if key.kind == event::KeyEventKind::Press && key.code == event::KeyCode::Esc { 89 | handle.shutdown().await; 90 | break; 91 | } 92 | } 93 | } 94 | _ => {} 95 | } 96 | } 97 | } 98 | 99 | restore_tui()?; 100 | 101 | Ok::<(), anyhow::Error>(()) 102 | }).await; 103 | 104 | Ok(()) 105 | } 106 | 107 | pub fn init_panic_hook() { 108 | let original_hook = std::panic::take_hook(); 109 | std::panic::set_hook(Box::new(move |panic_info| { 110 | let _ = restore_tui(); 111 | original_hook(panic_info); 112 | })); 113 | } 114 | 115 | pub fn init_tui() -> std::io::Result> { 116 | crossterm::terminal::enable_raw_mode()?; 117 | crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; 118 | Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stderr())) 119 | } 120 | 121 | pub fn restore_tui() -> Result<()> { 122 | crossterm::terminal::disable_raw_mode()?; 123 | crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?; 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /examples/hello-world.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event; 3 | use ratatui::Terminal; 4 | use rizzup::prelude::*; 5 | 6 | fn hello_world() -> RatView { 7 | widget_ref(|| "Hello World! (press 'q' to quit)") 8 | } 9 | 10 | fn main() -> Result<()> { 11 | let mut term = init_tui()?; 12 | init_panic_hook(); 13 | 14 | with_tracking_scope(|| { 15 | let app = hello_world(); 16 | 17 | loop { 18 | term.draw(|f| f.render_widget_ref(app, f.size()))?; 19 | 20 | let event = event::read()?; 21 | if let event::Event::Key(key) = event { 22 | if key.kind == event::KeyEventKind::Press && key.code == event::KeyCode::Char('q') { 23 | break; 24 | } 25 | } 26 | } 27 | 28 | restore_tui()?; 29 | Ok::<(), anyhow::Error>(()) 30 | })?; 31 | Ok(()) 32 | } 33 | 34 | pub fn init_panic_hook() { 35 | let original_hook = std::panic::take_hook(); 36 | std::panic::set_hook(Box::new(move |panic_info| { 37 | let _ = restore_tui(); 38 | original_hook(panic_info); 39 | })); 40 | } 41 | 42 | pub fn init_tui() -> std::io::Result> { 43 | crossterm::terminal::enable_raw_mode()?; 44 | crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; 45 | Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stderr())) 46 | } 47 | 48 | pub fn restore_tui() -> Result<()> { 49 | crossterm::terminal::disable_raw_mode()?; 50 | crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/tabs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event; 3 | use ratatui::Terminal; 4 | use rizzup::prelude::*; 5 | 6 | #[derive(Debug, Clone, Default)] 7 | struct State { 8 | input: String, 9 | tab: usize, 10 | } 11 | 12 | fn tab1() -> RatView { 13 | let text = use_context::>(); 14 | 15 | reactive!(receiver: clone(text): { 16 | event::KeyCode::Char(ch) => text.update(|x| x.input.push(*ch)), 17 | event::KeyCode::Backspace => text.update(|x| { 18 | x.input.pop(); 19 | }), 20 | }); 21 | 22 | widget_ref(move || format!("Tab 1 {}", text.get().input)) 23 | } 24 | 25 | fn tab2() -> RatView { 26 | let value = create_signal("".to_string()); 27 | let text = use_context::>(); 28 | 29 | reactive!(receiver: clone(value): { 30 | event::KeyCode::Char(ch) => value.update(|x| x.push(*ch)), 31 | event::KeyCode::Backspace => value.update(|x| { x.pop(); }), 32 | }); 33 | 34 | widget_ref(move || format!("Tab 1 {} Tab 2 {}", text.get().input, value.get())) 35 | } 36 | 37 | fn input() -> RatView { 38 | provide_context(create_signal(State::default())); 39 | let state = use_context::>(); 40 | 41 | let tab = reactive!(selector: clone(state): state.get().tab); 42 | 43 | reactive!(receiver: clone(state): { 44 | event::KeyCode::Left => state.update(|v| v.tab = 0), 45 | event::KeyCode::Right => state.update(|v| v.tab = 1), 46 | }); 47 | 48 | widget_ref(move || match tab.get() == 0 { 49 | true => tab1(), 50 | false => tab2(), 51 | }) 52 | } 53 | 54 | fn main() -> Result<()> { 55 | let mut term = init_tui()?; 56 | init_panic_hook(); 57 | 58 | with_tracking_scope(|| { 59 | let app = create_memo(|| input()); 60 | 61 | loop { 62 | term.draw(|f| f.render_widget_ref(app.get(), f.size()))?; 63 | 64 | let event = event::read()?; 65 | if let event::Event::Key(key) = event { 66 | if key.kind == event::KeyEventKind::Press { 67 | send(key.code); 68 | } 69 | if key.kind == event::KeyEventKind::Press && key.code == event::KeyCode::Esc { 70 | break; 71 | } 72 | } 73 | } 74 | 75 | restore_tui()?; 76 | 77 | Ok::<(), anyhow::Error>(()) 78 | })?; 79 | Ok(()) 80 | } 81 | 82 | pub fn init_panic_hook() { 83 | let original_hook = std::panic::take_hook(); 84 | std::panic::set_hook(Box::new(move |panic_info| { 85 | let _ = restore_tui(); 86 | original_hook(panic_info); 87 | })); 88 | } 89 | 90 | pub fn init_tui() -> std::io::Result> { 91 | crossterm::terminal::enable_raw_mode()?; 92 | crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; 93 | Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stderr())) 94 | } 95 | 96 | pub fn restore_tui() -> Result<()> { 97 | crossterm::terminal::disable_raw_mode()?; 98 | crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?; 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /examples/text-input.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event; 3 | use ratatui::{widgets::*, Terminal}; 4 | use rizzup::prelude::*; 5 | 6 | fn input() -> RatView { 7 | let value = create_signal("".to_string()); 8 | 9 | let value_c = value.clone(); 10 | on(move |key: &event::KeyCode| match key { 11 | event::KeyCode::Char(ch) => value_c.update(|x| x.push(*ch)), 12 | event::KeyCode::Backspace => value_c.update(|x| { 13 | x.pop(); 14 | }), 15 | _ => {} 16 | }); 17 | 18 | widget_ref(move || { 19 | let block = Block::default() 20 | .padding(Padding::horizontal(1)) 21 | .borders(Borders::all()) 22 | .title("Start typeing") 23 | .title("(Press esc to exit)"); 24 | 25 | Paragraph::new(value.get()).block(block) 26 | }) 27 | } 28 | 29 | fn main() -> Result<()> { 30 | let mut term = init_tui()?; 31 | init_panic_hook(); 32 | 33 | with_tracking_scope(|| { 34 | let app = input(); 35 | 36 | loop { 37 | term.draw(|f| f.render_widget_ref(app, f.size()))?; 38 | 39 | let event = event::read()?; 40 | if let event::Event::Key(key) = event { 41 | if key.kind == event::KeyEventKind::Press { 42 | send(key.code) 43 | } 44 | if key.kind == event::KeyEventKind::Press && key.code == event::KeyCode::Esc { 45 | break; 46 | } 47 | } 48 | } 49 | 50 | restore_tui()?; 51 | 52 | Ok::<(), anyhow::Error>(()) 53 | })?; 54 | Ok(()) 55 | } 56 | 57 | pub fn init_panic_hook() { 58 | let original_hook = std::panic::take_hook(); 59 | std::panic::set_hook(Box::new(move |panic_info| { 60 | let _ = restore_tui(); 61 | original_hook(panic_info); 62 | })); 63 | } 64 | 65 | pub fn init_tui() -> std::io::Result> { 66 | crossterm::terminal::enable_raw_mode()?; 67 | crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; 68 | Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stderr())) 69 | } 70 | 71 | pub fn restore_tui() -> Result<()> { 72 | crossterm::terminal::disable_raw_mode()?; 73 | crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?; 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /examples/todo-app.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event; 3 | use ratatui::{ 4 | layout::{Constraint, Layout, Rect}, 5 | style::{Color, Modifier, Style, Stylize}, 6 | widgets::*, 7 | Terminal, 8 | }; 9 | use rizzup::{prelude::*, ratatui::widget_ref}; 10 | 11 | #[derive(Default, Clone)] 12 | struct Todo { 13 | text: String, 14 | complete: bool, 15 | } 16 | 17 | impl Todo { 18 | pub fn new(text: String) -> Self { 19 | Self { 20 | text, 21 | complete: false, 22 | } 23 | } 24 | } 25 | 26 | fn todo_text_input(focused: ReadSignal) -> RatView { 27 | let value = create_signal("".to_string()); 28 | let todos = use_context::>>(); 29 | 30 | let value_c = value.clone(); 31 | on(move |key: &event::KeyCode| { 32 | if focused.get() == false { 33 | return; 34 | } 35 | match key { 36 | event::KeyCode::Char(ch) => value_c.update(|x| x.push(*ch)), 37 | event::KeyCode::Backspace => value_c.update(|x| { 38 | x.pop(); 39 | }), 40 | event::KeyCode::Enter => { 41 | todos.update(|t| t.push(Todo::new(value_c.get()))); 42 | value_c.set("".into()); 43 | } 44 | _ => {} 45 | } 46 | }); 47 | 48 | widget_ref(move || { 49 | let block = Block::default() 50 | .padding(Padding::horizontal(1)) 51 | .borders(Borders::all()) 52 | .border_style(match focused.get() { 53 | true => Color::Cyan, 54 | false => Color::default(), 55 | }) 56 | .title("Add a todo"); 57 | Paragraph::new(value.get()).block(block) 58 | }) 59 | } 60 | 61 | fn todo_list(focused: ReadSignal) -> RatView { 62 | let todos = use_context::>>(); 63 | let state = create_signal({ 64 | let mut s = ListState::default(); 65 | s.select(Some(0)); 66 | s 67 | }); 68 | 69 | let todos_c = todos.clone(); 70 | let state_c = state.clone(); 71 | on(move |key: &event::KeyCode| { 72 | if focused.get() == false { 73 | return; 74 | } 75 | let size = todos_c.get().len(); 76 | match key { 77 | event::KeyCode::Enter => todos_c.update(|t| { 78 | if let Some(s) = state_c.get().selected() { 79 | if let Some(todo) = t.get_mut(s) { 80 | todo.complete = !todo.complete 81 | } 82 | } 83 | }), 84 | event::KeyCode::Backspace => todos_c.update(|t| { 85 | if let Some(selected) = state_c.get().selected() { 86 | if selected < t.len() && t.len() > 0 { 87 | t.remove(selected); 88 | state_c.update(|s| s.select(Some(selected.saturating_sub(1)))); 89 | } 90 | } 91 | }), 92 | event::KeyCode::Up => state_c.update(|s| { 93 | s.select(match s.selected() { 94 | Some(0) => Some(size.max(1) - 1), 95 | Some(n) => Some(n - 1), 96 | n => n, 97 | }) 98 | }), 99 | event::KeyCode::Down => state_c.update(|s| { 100 | let selected = s.selected().unwrap_or(0); 101 | match selected >= size.max(1) - 1 { 102 | true => s.select(Some(0)), 103 | false => s.select(Some(selected + 1)), 104 | }; 105 | }), 106 | _ => {} 107 | } 108 | }); 109 | 110 | statefull_widget_ref(state, move || { 111 | let block = Block::default() 112 | .padding(Padding::horizontal(1)) 113 | .borders(Borders::all()) 114 | .border_style(match focused.get() { 115 | true => Color::Cyan, 116 | false => Color::default(), 117 | }) 118 | .title("Todo"); 119 | 120 | let list = todos.get().into_iter().map(|x| { 121 | ListItem::new(x.text.clone()).add_modifier(match x.complete { 122 | true => Modifier::CROSSED_OUT, 123 | false => Modifier::empty(), 124 | }) 125 | }); 126 | 127 | let list = List::new(list) 128 | .block(block) 129 | .highlight_style(Style::new().add_modifier(Modifier::REVERSED)); 130 | list 131 | }) 132 | } 133 | 134 | #[derive(Clone, Copy, PartialEq)] 135 | enum Focus { 136 | Input, 137 | List, 138 | } 139 | 140 | fn todo_list_app() -> RatView { 141 | let focus = create_signal(Focus::Input); 142 | provide_context(create_signal::>(vec![])); 143 | 144 | let todo_list = todo_list(create_memo(move || focus.get() == Focus::List)); 145 | let todo_list_input = todo_text_input(create_memo(move || focus.get() == Focus::Input)); 146 | 147 | on(move |ev: &event::KeyCode| match ev { 148 | event::KeyCode::Char(_) => focus.update(|v| *v = Focus::Input), 149 | event::KeyCode::Down => focus.update(|v| match v { 150 | Focus::Input => *v = Focus::List, 151 | Focus::List => {} 152 | }), 153 | event::KeyCode::Tab => focus.update(|v| match v { 154 | Focus::Input => *v = Focus::List, 155 | Focus::List => *v = Focus::Input, 156 | }), 157 | _ => {} 158 | }); 159 | 160 | render(move |area, buf| { 161 | let areas: [Rect; 2] = Layout::new( 162 | ratatui::layout::Direction::Vertical, 163 | [Constraint::Max(3), Constraint::Fill(1)], 164 | ) 165 | .areas(area); 166 | 167 | todo_list_input.render_ref(areas[0], buf); 168 | todo_list.render_ref(areas[1], buf); 169 | }) 170 | } 171 | 172 | fn main() -> Result<()> { 173 | let mut term = init_tui()?; 174 | init_panic_hook(); 175 | 176 | with_tracking_scope(|| { 177 | let app = todo_list_app(); 178 | 179 | loop { 180 | term.draw(|f| f.render_widget_ref(app, f.size()))?; 181 | 182 | let event = event::read()?; 183 | if let event::Event::Key(key) = event { 184 | if key.kind == event::KeyEventKind::Press { 185 | send(key.code) 186 | } 187 | if key.kind == event::KeyEventKind::Press && key.code == event::KeyCode::Esc { 188 | break; 189 | } 190 | } 191 | } 192 | 193 | restore_tui()?; 194 | 195 | Ok::<(), anyhow::Error>(()) 196 | })?; 197 | 198 | Ok(()) 199 | } 200 | 201 | pub fn init_panic_hook() { 202 | let original_hook = std::panic::take_hook(); 203 | std::panic::set_hook(Box::new(move |panic_info| { 204 | let _ = restore_tui(); 205 | original_hook(panic_info); 206 | })); 207 | } 208 | 209 | pub fn init_tui() -> std::io::Result> { 210 | crossterm::terminal::enable_raw_mode()?; 211 | crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; 212 | Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stderr())) 213 | } 214 | 215 | pub fn restore_tui() -> Result<()> { 216 | crossterm::terminal::disable_raw_mode()?; 217 | crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?; 218 | Ok(()) 219 | } 220 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | cell::RefCell, 4 | collections::HashMap, 5 | rc::Rc, 6 | }; 7 | 8 | use crate::nodes::Scope; 9 | 10 | #[derive(Default, Debug, Clone)] 11 | pub struct Contexts(pub(crate) Rc>>>); 12 | 13 | impl Contexts { 14 | pub(crate) fn provide_context(&self, scope: Scope, x: T) { 15 | let mut layers = self.0.borrow_mut(); 16 | layers.insert((scope, TypeId::of::()), Box::new(x)); 17 | } 18 | 19 | pub(crate) fn use_context_from_scope(&self, id: Scope) -> Option { 20 | let layers = self.0.borrow(); 21 | let ctx = layers.get(&(id, TypeId::of::())); 22 | if let Some(value) = ctx { 23 | let s = value.downcast_ref::().expect("Failed to downcast"); 24 | return Some(s.clone()); 25 | } 26 | None 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/environment.rs: -------------------------------------------------------------------------------- 1 | use std::any::{type_name, Any}; 2 | 3 | use crate::{nodes::ReactiveNode, runtime::Runtime, signal::*}; 4 | 5 | thread_local! { 6 | static RUNTIME: Runtime = Runtime::default(); 7 | } 8 | 9 | pub fn with_runtime(f: impl FnOnce(&Runtime) -> R) -> R { 10 | RUNTIME.with(f) 11 | } 12 | 13 | pub fn with_tracking_scope(f: impl FnOnce() -> R) -> R { 14 | with_runtime(|r| { 15 | let scope = r.nodes.insert(ReactiveNode::default()); 16 | let previous = r.tracker.replace(Some(scope)); 17 | let value = f(); 18 | r.tracker.replace(previous); 19 | r.cleanup_child_scope(scope); 20 | r.nodes.dispose(scope); 21 | value 22 | }) 23 | } 24 | 25 | /// Cleanup 26 | pub fn on_cleanup(f: impl FnOnce() + 'static) { 27 | with_runtime(|r| r.add_cleanup(r.get_current_scope(), f)) 28 | } 29 | 30 | /// Context 31 | pub fn provide_context(x: T) { 32 | with_runtime(|r| r.context.provide_context(r.get_current_scope(), x)) 33 | } 34 | 35 | pub fn use_context_option() -> Option { 36 | with_runtime(|r| r.get_context(r.get_current_scope())) 37 | } 38 | 39 | pub fn use_context() -> T { 40 | use_context_option().expect( 41 | format!( 42 | "Missing {} in parent scope {:?}", 43 | type_name::(), 44 | with_runtime(|r| r.get_current_scope()) 45 | ) 46 | .as_str(), 47 | ) 48 | } 49 | 50 | /// Messages 51 | pub fn on(f: impl Fn(&T) + 'static) { 52 | with_runtime(|r| r.recievers.create_reciever(r.get_current_scope(), f)); 53 | } 54 | 55 | pub fn send_boxed(message: &Box) { 56 | with_runtime(|r| r.send(r.get_current_scope(), message, true)); 57 | } 58 | 59 | pub fn send(message: T) { 60 | let message = Box::new(message) as Box; 61 | send_boxed(&message); 62 | } 63 | 64 | /// Signals 65 | pub fn create_signal(value: T) -> Signal { 66 | let scope = with_runtime(|r| r.create_value_node(Box::new(value))); 67 | Signal(scope, std::marker::PhantomData) 68 | } 69 | 70 | pub fn create_memo(f: impl Fn() -> T + 'static) -> ReadSignal { 71 | let scope = with_runtime(|r| r.create_cb_node(move |_| Some(Box::new(f())))); 72 | ReadSignal(scope, std::marker::PhantomData) 73 | } 74 | 75 | pub fn create_selector( 76 | f: impl Fn() -> T + 'static, 77 | ) -> ReadSignal { 78 | let scope = with_runtime(|r| { 79 | r.create_cb_node(move |previous| { 80 | let previous = previous.map(|v| v.downcast_ref::()).flatten(); 81 | let next = f(); 82 | match Some(&next) != previous { 83 | true => Some(Box::new(next)), 84 | false => None, 85 | } 86 | }) 87 | }); 88 | ReadSignal(scope, std::marker::PhantomData) 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use std::{cell::Cell, rc::Rc}; 94 | 95 | use crate::environment::{with_runtime, with_tracking_scope}; 96 | 97 | use super::*; 98 | 99 | fn test_signal_memo_dependancy() { 100 | let sig = create_signal("Foo"); 101 | let m1 = create_memo(move || sig.get().to_uppercase()); 102 | let m1_c = m1.clone(); 103 | let m2 = create_memo(move || m1_c.get().to_lowercase()); 104 | assert_eq!(sig.get(), "Foo"); 105 | assert_eq!(m1.get(), "FOO"); 106 | assert_eq!(m2.get(), "foo"); 107 | sig.set("Bar"); 108 | assert_eq!(sig.get(), "Bar"); 109 | assert_eq!(m1.get(), "BAR"); 110 | assert_eq!(m2.get(), "bar"); 111 | } 112 | 113 | fn test_runtime_cleanup_up() { 114 | with_runtime(|r| { 115 | assert_eq!(r.nodes.0.borrow().len(), 0); 116 | assert_eq!(r.cleanup.borrow().len(), 0); 117 | }); 118 | } 119 | 120 | #[test] 121 | fn test_signal_dependancy_tracking() { 122 | with_tracking_scope(|| test_signal_memo_dependancy()); 123 | test_runtime_cleanup_up() 124 | } 125 | 126 | #[test] 127 | fn test_signal_dependancy_nested() { 128 | with_tracking_scope(|| { 129 | create_memo(|| test_signal_memo_dependancy()); 130 | }); 131 | test_runtime_cleanup_up() 132 | } 133 | 134 | #[test] 135 | fn test_signal_dependancy_tracks_latest() { 136 | with_tracking_scope(|| { 137 | let count = Rc::new(Cell::new(0)); 138 | let trigger = create_signal(0); 139 | let s1 = create_signal("foo"); 140 | let s2 = create_signal("bar"); 141 | 142 | let count_c = count.clone(); 143 | let m = create_memo(move || { 144 | count_c.set(count_c.get() + 1); 145 | return match trigger.get() % 2 == 0 { 146 | true => s1.get(), 147 | false => s2.get(), 148 | }; 149 | }); 150 | 151 | assert_eq!(m.get(), "foo"); 152 | trigger.set(1); 153 | assert_eq!(m.get(), "bar"); 154 | assert_eq!(count.get(), 2); 155 | s1.set("FOO"); 156 | assert_eq!( 157 | count.get(), 158 | 2, 159 | "Doesn't rerender due to no longer depending on s1" 160 | ); 161 | }); 162 | test_runtime_cleanup_up() 163 | } 164 | 165 | #[test] 166 | fn test_signal_selector() { 167 | with_tracking_scope(|| { 168 | let count = Rc::new(Cell::new(0)); 169 | let source = create_signal((0, 0)); 170 | let s1 = create_selector(move || Signal::<(i32, i32)>::get(&source).0); 171 | 172 | let count_c = count.clone(); 173 | let m = create_memo(move || { 174 | count_c.set(count_c.get() + 1); 175 | s1.get() 176 | }); 177 | 178 | assert_eq!(count.get(), 1); 179 | source.set((0, 10)); 180 | 181 | // Doesn't update because selector equals previous 182 | assert_eq!(m.get(), 0); 183 | assert_eq!(count.get(), 1,); 184 | 185 | source.set((10, 10)); 186 | assert_eq!(m.get(), 10); 187 | assert_eq!(count.get(), 2); 188 | }) 189 | } 190 | 191 | #[test] 192 | fn test_cleanup() { 193 | with_tracking_scope(|| { 194 | let trig = create_signal(0); 195 | let count = create_signal(0); 196 | 197 | create_memo(move || match trig.get() { 198 | 0 => create_memo(move || on_cleanup(move || count.update(|v| *v += 1))), 199 | _ => create_memo(move || {}), 200 | }); 201 | trig.set(1); 202 | assert_eq!(count.get(), 1, "executed cleanup"); 203 | }) 204 | } 205 | 206 | #[test] 207 | fn test_recievers_cleaned_up() { 208 | with_tracking_scope(|| { 209 | let recieved = create_signal(0); 210 | let trig = create_signal(0); 211 | 212 | create_memo(move || match trig.get() { 213 | 0 => { 214 | on(move |ev: &usize| recieved.set(*ev)); 215 | create_memo(move || on(move |ev: &usize| recieved.set(*ev))); 216 | } 217 | _ => {} 218 | }); 219 | 220 | send(10 as usize); 221 | assert_eq!(recieved.get(), 10); 222 | trig.set(1); 223 | send(20 as usize); 224 | assert_eq!(recieved.get(), 10, ""); 225 | trig.set(0); 226 | send(20 as usize); 227 | assert_eq!(recieved.get(), 20, ""); 228 | }) 229 | } 230 | 231 | #[test] 232 | fn test_recievers_send_deep() { 233 | with_tracking_scope(|| { 234 | let recieved = create_signal(0); 235 | 236 | create_memo(move || { 237 | create_memo(move || { 238 | create_memo(move || on(move |ev: &usize| recieved.update(|v| *v += ev))) 239 | }) 240 | }); 241 | 242 | send(10 as usize); 243 | assert_eq!(recieved.get(), 10); 244 | send(10 as usize); 245 | assert_eq!(recieved.get(), 20); 246 | }) 247 | } 248 | 249 | #[test] 250 | fn test_recievers_cleanup_on_send() { 251 | fn even(f: impl Fn((String, usize)) + 'static) { 252 | on(move |ev: &usize| f(("even".to_string(), *ev))) 253 | } 254 | fn odd(f: impl Fn((String, usize)) + 'static) { 255 | on(move |ev: &usize| f(("odd".to_string(), *ev))) 256 | } 257 | with_tracking_scope(|| { 258 | let sig = create_signal(0); 259 | let result = create_signal(("init".to_string(), 0)); 260 | 261 | let result_c = result.clone(); 262 | on(move |ev: &usize| sig.set(*ev)); 263 | 264 | create_memo(move || { 265 | let (result_a, result_b) = (result_c.clone(), result_c.clone()); 266 | match sig.get() % 2 == 0 { 267 | true => even(move |a| result_a.set(a)), 268 | false => odd(move |b| result_b.set(b)), 269 | } 270 | }); 271 | 272 | send(10 as usize); 273 | assert_eq!(result.get(), ("even".to_string(), 10)); 274 | send(11 as usize); 275 | assert_eq!(result.get(), ("odd".to_string(), 11)); 276 | send(10 as usize); 277 | assert_eq!(result.get(), ("even".to_string(), 10)); 278 | }) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(min_specialization)] 2 | 3 | pub mod context; 4 | pub mod environment; 5 | pub mod macros; 6 | pub mod nodes; 7 | pub mod prelude; 8 | pub mod ratatui; 9 | pub mod recievers; 10 | pub mod runtime; 11 | pub mod signal; 12 | pub mod tasks; 13 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! reactive { 3 | (signal: $body:expr) => {{ 4 | create_signal($body) 5 | }}; 6 | (memo: $body:expr) => {{ 7 | create_memo(move || $body) 8 | }}; 9 | (memo: $($m:ident($s:ident)),*: $body:expr) => {{ 10 | let ($($s),*) = ($($s.$m()),*); 11 | create_memo(move || $body) 12 | }}; 13 | (selector: $body:expr) => {{ 14 | create_selector(move || $body) 15 | }}; 16 | (selector: $($m:ident($s:ident)),*: $body:expr) => {{ 17 | let ($($s),*) = ($($s.$m()),*); 18 | create_selector(move || $body) 19 | }}; 20 | (receiver: { $($pt:pat => $exp:expr,)* }) => {{ 21 | on(move |ev| { 22 | match ev { 23 | $($pt => { $exp; },)* 24 | _ => {}, 25 | }; 26 | }); 27 | }}; 28 | (receiver: $($m:ident($s:ident)),*: { $($pt:pat => $exp:expr,)* }) => {{ 29 | let ($($s),*) = ($($s.$m()),*); 30 | on(move |ev| { 31 | match ev { 32 | $($pt => { $exp; },)* 33 | _ => {}, 34 | }; 35 | }); 36 | }}; 37 | } 38 | pub use reactive; 39 | -------------------------------------------------------------------------------- /src/nodes.rs: -------------------------------------------------------------------------------- 1 | use slotmap::{new_key_type, SlotMap}; 2 | use std::{any::Any, cell::RefCell, collections::HashSet, rc::Rc}; 3 | 4 | new_key_type! { 5 | pub struct Scope; 6 | } 7 | 8 | pub trait IntoScope { 9 | fn into_scope(&self) -> Scope; 10 | } 11 | 12 | impl IntoScope for Scope { 13 | fn into_scope(&self) -> Scope { 14 | *self 15 | } 16 | } 17 | 18 | #[derive(Default, Debug)] 19 | pub struct ReactiveNode { 20 | /// Any value 21 | pub(crate) value: Option>, 22 | /// Any Fn that accepts a Box and returns a Box 23 | pub(crate) callback: Option, 24 | /// Node of the parent scope 25 | pub(crate) parent: Option, 26 | /// Nodes who depend on the value from this node 27 | pub(crate) dependants: HashSet, 28 | } 29 | 30 | #[derive(Default, Debug, Clone)] 31 | pub struct ReactiveNodes(pub Rc>>); 32 | 33 | impl ReactiveNodes { 34 | pub(crate) fn insert(&self, node: ReactiveNode) -> Scope { 35 | self.0.borrow_mut().insert(node) 36 | } 37 | 38 | pub(crate) fn add_node( 39 | &self, 40 | scope: Scope, 41 | cb: Option, 42 | value: Option>, 43 | ) -> Scope { 44 | let mut node = ReactiveNode::default(); 45 | node.parent = Some(scope); 46 | node.callback = cb; 47 | node.value = value; 48 | self.insert(node) 49 | } 50 | 51 | pub(crate) fn with_node( 52 | &self, 53 | id: Scope, 54 | f: impl FnOnce(&mut ReactiveNode) -> R, 55 | ) -> Option { 56 | self.0.borrow_mut().get_mut(id).map(|n| f(n)) 57 | } 58 | 59 | pub(crate) fn get_node_children(&self, scope: Scope) -> Vec { 60 | let nodes = self.0.borrow(); 61 | let children = nodes.iter().filter(|x| x.1.parent == Some(scope)); 62 | let children = children.map(|x| x.0); 63 | children.collect() 64 | } 65 | 66 | pub(crate) fn get_node_children_recursive(&self, scope: Scope) -> Vec { 67 | let mut all = vec![]; 68 | for child in self.get_node_children(scope) { 69 | all.extend(self.get_node_children_recursive(child)); 70 | all.push(child) 71 | } 72 | all 73 | } 74 | 75 | pub(crate) fn get_parent(&self, id: Scope) -> Option { 76 | self.0.borrow().get(id).map(|n| n.parent).flatten() 77 | } 78 | 79 | pub(crate) fn take_node_callback_and_value( 80 | &self, 81 | scope: Scope, 82 | ) -> (Option, Option>) { 83 | self.with_node(scope, |n| match n.callback.is_some() { 84 | true => (n.callback.take(), n.value.take()), 85 | false => (None, None), 86 | }) 87 | .unwrap_or((None, None)) 88 | } 89 | 90 | /// Update value if there is a new value and return dependants otherwise set value to previous 91 | pub(crate) fn update( 92 | &self, 93 | scope: Scope, 94 | cb: Callback, 95 | value: Option>, 96 | previous: Option>, 97 | ) -> Vec { 98 | self.with_node(scope, |n| { 99 | n.callback = Some(cb); 100 | if let Some(val) = value { 101 | n.value = Some(val); 102 | return n.dependants.drain().collect::>(); 103 | } 104 | n.value = previous; 105 | vec![] 106 | }) 107 | .expect("Disposed") 108 | } 109 | 110 | pub(crate) fn take_dependants(&self, scope: Scope) -> Vec { 111 | self.with_node(scope, |n| n.dependants.drain().collect()) 112 | .unwrap_or_default() 113 | } 114 | 115 | pub(crate) fn remove_scope_from_dependants(&self, scope: Scope) { 116 | let mut nodes = self.0.borrow_mut(); 117 | for (_, node) in nodes.iter_mut() { 118 | node.dependants.remove(&scope); 119 | } 120 | } 121 | 122 | pub(crate) fn dispose(&self, id: Scope) { 123 | self.remove_scope_from_dependants(id); 124 | self.0.borrow_mut().remove(id); 125 | } 126 | 127 | pub(crate) fn with_value( 128 | &self, 129 | scope: Scope, 130 | f: impl FnOnce(&mut T) -> R, 131 | ) -> Option { 132 | let value = self.with_node(scope, |n| n.value.take()).flatten(); 133 | let mut result = None; 134 | if let Some(Ok(mut value)) = value.map(|x| x.downcast::()) { 135 | result = Some(f(&mut value)); 136 | self.with_node(scope, |n| n.value.replace(value)); 137 | } 138 | return result; 139 | } 140 | } 141 | 142 | pub(crate) struct Callback(pub(crate) Box>) -> Option>>); 143 | impl std::fmt::Debug for Callback { 144 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 145 | write!(f, "Fn") 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::{environment::*, macros::*, ratatui::*, signal::*, tasks::*}; 2 | -------------------------------------------------------------------------------- /src/ratatui.rs: -------------------------------------------------------------------------------- 1 | use ratatui::widgets::{StatefulWidgetRef, Widget, WidgetRef}; 2 | use std::{any::Any, marker::PhantomData}; 3 | 4 | use crate::{ 5 | environment::*, 6 | nodes::{IntoScope, Scope}, 7 | prelude::{ReadSignal, Signal, SignalRead, SignalUpdate}, 8 | }; 9 | 10 | // Child 11 | #[derive(Clone, Copy)] 12 | pub struct RatView(Scope); 13 | impl IntoScope for RatView { 14 | fn into_scope(&self) -> Scope { 15 | self.0 16 | } 17 | } 18 | 19 | // WidgetRef Wrapper type 20 | pub struct WidgetNode(Box); 21 | 22 | impl WidgetRef for RatView { 23 | fn render_ref(&self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { 24 | let signal = ReadSignal::(self.into_scope(), PhantomData); 25 | signal.with_untracked(|v| v.0(area, buf)); 26 | } 27 | } 28 | 29 | pub fn render( 30 | f: impl Fn(ratatui::prelude::Rect, &mut ratatui::prelude::Buffer) + 'static, 31 | ) -> RatView { 32 | let id = with_runtime(|s| s.create_value_node(Box::new(WidgetNode(Box::new(f))))); 33 | RatView(id) 34 | } 35 | 36 | pub fn widget(f: impl Fn() -> V + 'static) -> RatView { 37 | let node = Box::new(WidgetNode(Box::new(move |area, buf| f().render(area, buf)))); 38 | let id = with_runtime(|r| r.create_value_node(node)); 39 | RatView(id) 40 | } 41 | 42 | pub fn widget_ref(f: impl Fn() -> V + 'static) -> RatView { 43 | let memo = create_memo(move || { 44 | let w = f(); 45 | WidgetNode(Box::new(move |area, buf| w.render_ref(area, buf))) 46 | }); 47 | RatView(memo.0) 48 | } 49 | 50 | impl From> for RatView 51 | where 52 | T: WidgetRef + 'static, 53 | { 54 | fn from(value: ReadSignal) -> Self { 55 | render(move |area, buf| { 56 | value.with_untracked(|x| x.render_ref(area, buf)); 57 | }) 58 | } 59 | } 60 | 61 | impl From> for RatView 62 | where 63 | T: WidgetRef + 'static, 64 | { 65 | fn from(value: Signal) -> Self { 66 | render(move |area, buf| { 67 | value.with_untracked(|x| x.render_ref(area, buf)); 68 | }) 69 | } 70 | } 71 | 72 | pub fn statefull_widget_ref(state: S, f: impl Fn() -> V + 'static) -> RatView 73 | where 74 | S: SignalUpdate + Clone + 'static, 75 | State: Clone + Any + 'static, 76 | V: StatefulWidgetRef + 'static, 77 | { 78 | let memo = create_memo(move || { 79 | let widget = f(); 80 | let state = state.clone(); 81 | WidgetNode(Box::new(move |area, buf| { 82 | state 83 | .clone() 84 | .update_silent(|state| StatefulWidgetRef::render_ref(&widget, area, buf, state)) 85 | })) 86 | }); 87 | RatView(memo.0) 88 | } 89 | 90 | #[macro_export] 91 | macro_rules! widget { 92 | ($expr:expr) => {{ 93 | render(move |a, b| $expr(a, b)) 94 | }}; 95 | {$($stmt:tt)*} => {{ 96 | widget(move || {$($stmt)*}) 97 | }}; 98 | } 99 | -------------------------------------------------------------------------------- /src/recievers.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{type_name, Any}, 3 | cell::{Cell, RefCell}, 4 | rc::Rc, 5 | }; 6 | 7 | use slotmap::SecondaryMap; 8 | 9 | use crate::nodes::Scope; 10 | 11 | pub struct Reciever(Box)>); 12 | impl std::fmt::Debug for Reciever { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | write!(f, "{}", type_name::()) 15 | } 16 | } 17 | 18 | #[derive(Default, Debug, Clone)] 19 | pub struct Recievers { 20 | handlers: Rc>>>, 21 | borrowed: Rc>>, 22 | } 23 | 24 | impl Recievers { 25 | pub fn create_reciever(&self, scope: Scope, f: impl Fn(&T) + 'static) { 26 | let borrowed = self.borrowed.clone(); 27 | let reciever = Reciever(Box::new(move |v| { 28 | if borrowed.get() != Some(scope) { 29 | return; 30 | } 31 | match v.downcast_ref::() { 32 | Some(v) => f(v), 33 | None => {} 34 | } 35 | })); 36 | let mut map = self.handlers.borrow_mut(); 37 | match map.get_mut(scope) { 38 | Some(v) => v.push(reciever), 39 | None => { 40 | map.insert(scope, vec![reciever]); 41 | } 42 | }; 43 | } 44 | 45 | pub fn send(&self, scope: Scope, value: &Box) { 46 | let recievers = self.handlers.borrow_mut().remove(scope); 47 | if let Some(recievers) = recievers { 48 | let previous = self.borrowed.replace(Some(scope)); 49 | for r in &recievers { 50 | r.0(value) 51 | } 52 | if self.borrowed.get() == Some(scope) { 53 | self.handlers.borrow_mut().insert(scope, recievers); 54 | } 55 | self.borrowed.replace(previous); 56 | } 57 | } 58 | 59 | pub fn dispose(&self, scope: Scope) { 60 | match Some(scope) == self.borrowed.get() { 61 | true => self.borrowed.set(None), 62 | false => { 63 | self.handlers.borrow_mut().remove(scope); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/runtime.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::Any, 3 | cell::{Cell, RefCell}, 4 | rc::Rc, 5 | }; 6 | 7 | use slotmap::SecondaryMap; 8 | 9 | use crate::{ 10 | context::Contexts, 11 | nodes::{Callback, ReactiveNodes, Scope}, 12 | recievers::Recievers, 13 | }; 14 | 15 | #[derive(Default, Clone)] 16 | pub struct Runtime { 17 | pub tracker: Rc>>, 18 | pub nodes: ReactiveNodes, 19 | pub cleanup: Rc>>>>, 20 | pub context: Contexts, 21 | pub recievers: Recievers, 22 | } 23 | 24 | impl Runtime { 25 | pub fn create_cb_node( 26 | &self, 27 | cb: impl Fn(Option<&Box>) -> Option> + 'static, 28 | ) -> Scope { 29 | let scope = self.get_current_scope(); 30 | let cb = Callback(Box::new(cb)); 31 | let id = self.nodes.add_node(scope, None, None); 32 | let value = self.with_tracking_scope(id, || cb.0(None)); 33 | self.nodes.with_node(id, |n| { 34 | n.callback = Some(cb); 35 | n.value = value 36 | }); 37 | id 38 | } 39 | 40 | pub fn create_value_node(&self, value: Box) -> Scope { 41 | let scope = self.get_current_scope(); 42 | self.nodes.add_node(scope, None, Some(value)) 43 | } 44 | 45 | pub fn update_dependants(&self, node: Scope) { 46 | self.recompute(node); 47 | } 48 | 49 | pub fn track_dependant(&self, scope: Scope) { 50 | let parent = self.get_current_scope(); 51 | self.nodes.with_node(scope, |n| n.dependants.insert(parent)); 52 | } 53 | 54 | pub fn get_current_scope(&self) -> Scope { 55 | self.tracker.get().expect("Missing scope") 56 | } 57 | 58 | pub fn with_tracking_scope(&self, id: Scope, f: impl FnOnce() -> R) -> R { 59 | let previous = self.tracker.replace(Some(id)); 60 | let result = f(); 61 | self.tracker.replace(previous); 62 | result 63 | } 64 | 65 | pub fn recompute(&self, id: Scope) { 66 | let deps = self.recompute_node(id); 67 | for dep in deps { 68 | self.recompute(dep); 69 | } 70 | } 71 | 72 | fn recompute_node(&self, id: Scope) -> Vec { 73 | if let (Some(callback), previous_value) = self.nodes.take_node_callback_and_value(id) { 74 | self.run_cleanups(id); 75 | self.dispose_of_children(id); 76 | self.nodes.remove_scope_from_dependants(id); 77 | self.recievers.dispose(id); 78 | 79 | let new_value = match &previous_value { 80 | Some(val) => self.with_tracking_scope(id, || callback.0(Some(val))), 81 | None => self.with_tracking_scope(id, || callback.0(None)), 82 | }; 83 | return self.nodes.update(id, callback, new_value, previous_value); 84 | } 85 | self.nodes.take_dependants(id) 86 | } 87 | 88 | pub fn add_cleanup(&self, id: Scope, f: impl FnOnce() + 'static) { 89 | let mut cleanups = self.cleanup.borrow_mut(); 90 | match cleanups.get_mut(id) { 91 | Some(v) => v.push(Box::new(f)), 92 | None => { 93 | cleanups.insert(id, vec![Box::new(f)]); 94 | } 95 | } 96 | } 97 | 98 | pub fn run_cleanups(&self, id: Scope) { 99 | let mut children = self.nodes.get_node_children_recursive(id); 100 | children.push(id); 101 | for child in children { 102 | for cleanup in self.cleanup.borrow_mut().remove(child).unwrap_or_default() { 103 | cleanup() 104 | } 105 | } 106 | } 107 | 108 | pub fn dispose_of_children(&self, scope: Scope) { 109 | for child in self.nodes.get_node_children_recursive(scope) { 110 | self.recievers.dispose(child); 111 | self.nodes.dispose(child); 112 | } 113 | } 114 | 115 | pub fn cleanup_child_scope(&self, scope: Scope) { 116 | let children = self.nodes.get_node_children_recursive(scope); 117 | for child in &children { 118 | for cleanup in self.cleanup.borrow_mut().remove(*child).unwrap_or_default() { 119 | cleanup() 120 | } 121 | } 122 | for child in children { 123 | self.recievers.dispose(child); 124 | self.nodes.dispose(child); 125 | } 126 | } 127 | 128 | pub fn get_context(&self, id: Scope) -> Option { 129 | match self.context.use_context_from_scope(id) { 130 | Some(v) => Some(v), 131 | None => match self.nodes.get_parent(id) { 132 | Some(id) => self.get_context(id), 133 | None => None, 134 | }, 135 | } 136 | } 137 | 138 | pub fn send(&self, scope: Scope, value: &Box, deep: bool) { 139 | self.recievers.send(scope, value); 140 | if deep { 141 | for c in self.nodes.get_node_children_recursive(scope) { 142 | self.recievers.send(c, value); 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/signal.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::environment::with_runtime; 4 | use crate::nodes::{IntoScope, Scope}; 5 | 6 | pub trait SignalRead: IntoScope { 7 | fn with_untracked(&self, f: impl FnOnce(&T) -> R) -> Option { 8 | with_runtime(|r| r.nodes.with_value::(self.into_scope(), |n| f(n))) 9 | } 10 | fn with(&self, f: impl FnOnce(&T) -> R) -> Option { 11 | with_runtime(|r| r.track_dependant(self.into_scope())); 12 | self.with_untracked(f) 13 | } 14 | } 15 | 16 | pub trait SignalGet: SignalRead { 17 | fn get_untracked(&self) -> T; 18 | fn get(&self) -> T; 19 | } 20 | 21 | pub trait SignalUpdate: IntoScope { 22 | fn update_silent(&self, f: impl FnOnce(&mut T)) { 23 | with_runtime(|r| r.nodes.with_value(self.into_scope(), f)); 24 | } 25 | fn update(&self, f: impl FnOnce(&mut T)) { 26 | self.update_silent(f); 27 | with_runtime(|r| r.update_dependants(self.into_scope())); 28 | } 29 | } 30 | 31 | pub trait SignalSet: SignalUpdate { 32 | fn set_silent(&self, new: T) { 33 | self.update_silent(|v| *v = new); 34 | } 35 | fn set(&self, new: T) { 36 | self.update(|v| *v = new) 37 | } 38 | } 39 | 40 | macro_rules! impl_signal_get { 41 | ($iden:ident) => { 42 | impl std::fmt::Debug for $iden { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | let value = self.get_untracked(); 45 | f.debug_struct(stringify!($type)) 46 | .field("value", &value) 47 | .finish() 48 | } 49 | } 50 | 51 | impl SignalGet for $iden { 52 | default fn get_untracked(&self) -> T { 53 | self.with_untracked(|v| v.clone()) 54 | .expect(format!("Node {:?} has been disposed", self.into_scope()).as_str()) 55 | } 56 | default fn get(&self) -> T { 57 | self.with(|v| v.clone()) 58 | .expect(format!("Node {:?} has been disposed", self.into_scope()).as_str()) 59 | } 60 | } 61 | 62 | impl SignalGet for $iden { 63 | fn get_untracked(&self) -> T { 64 | self.with_untracked(|v| *v) 65 | .expect(format!("Node {:?} has been disposed", self.into_scope()).as_str()) 66 | } 67 | fn get(&self) -> T { 68 | self.with(|v| *v) 69 | .expect(format!("Node {:?} has been disposed", self.into_scope()).as_str()) 70 | } 71 | } 72 | }; 73 | } 74 | 75 | #[derive(Clone, Copy)] 76 | pub struct ReadSignal(pub Scope, pub PhantomData); 77 | 78 | impl IntoScope for ReadSignal { 79 | fn into_scope(&self) -> Scope { 80 | self.0 81 | } 82 | } 83 | impl SignalRead for ReadSignal {} 84 | impl_signal_get!(ReadSignal); 85 | 86 | #[derive(Clone, Copy)] 87 | pub struct WriteSignal(pub Scope, pub PhantomData); 88 | 89 | impl IntoScope for WriteSignal { 90 | fn into_scope(&self) -> Scope { 91 | self.0 92 | } 93 | } 94 | impl SignalUpdate for WriteSignal {} 95 | impl SignalSet for WriteSignal {} 96 | 97 | #[derive(Clone, Copy)] 98 | pub struct Signal(pub Scope, pub PhantomData); 99 | impl IntoScope for Signal { 100 | fn into_scope(&self) -> Scope { 101 | self.0 102 | } 103 | } 104 | impl SignalRead for Signal {} 105 | impl_signal_get!(Signal); 106 | impl SignalUpdate for Signal {} 107 | impl SignalSet for Signal {} 108 | -------------------------------------------------------------------------------- /src/tasks.rs: -------------------------------------------------------------------------------- 1 | use futures::Future; 2 | use std::{any::Any, pin::Pin, sync::Arc}; 3 | use tokio::{ 4 | sync::{broadcast, mpsc, Mutex}, 5 | task::JoinHandle, 6 | }; 7 | 8 | use crate::{ 9 | environment::*, 10 | nodes::{IntoScope, ReactiveNode, Scope}, 11 | signal::*, 12 | }; 13 | 14 | pub type Message = (Scope, Box); 15 | pub type Task = (Scope, Pin + Send>>); 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct TaskRunner { 19 | message_rx: Arc>>, 20 | message_tx: mpsc::UnboundedSender, 21 | cancel_tx: broadcast::Sender, 22 | shutdown_tx: broadcast::Sender<()>, 23 | task_tx: mpsc::UnboundedSender, 24 | task_handle: Arc>>>, 25 | } 26 | 27 | impl TaskRunner { 28 | pub async fn new() -> Self { 29 | let (message_tx, message_rx) = mpsc::unbounded_channel::(); 30 | let (task_tx, mut task_rx) = mpsc::unbounded_channel::(); 31 | let (cancel_tx, _) = broadcast::channel::(100); 32 | let (shutdown_tx, _) = broadcast::channel::<()>(100); 33 | 34 | let shutdown_tx_c = shutdown_tx.clone(); 35 | let task_handle = tokio::spawn(async move { 36 | loop { 37 | let shutdown_tx = shutdown_tx_c.clone(); 38 | let mut shutdown_rx = shutdown_tx.subscribe(); 39 | tokio::select! { 40 | task = task_rx.recv() => { 41 | if let Some(task) = task { 42 | Self::spawn_task(task, shutdown_tx).await 43 | } 44 | }, 45 | _ = shutdown_rx.recv() => { 46 | break 47 | } 48 | } 49 | } 50 | }); 51 | 52 | Self { 53 | message_rx: Arc::new(Mutex::new(message_rx)), 54 | message_tx, 55 | cancel_tx, 56 | shutdown_tx, 57 | task_tx, 58 | task_handle: Arc::new(Mutex::new(Some(task_handle))), 59 | } 60 | } 61 | 62 | async fn spawn_task(task: Task, shutdown: broadcast::Sender<()>) { 63 | tokio::spawn(async move { 64 | let mut shutdown_rx = shutdown.subscribe(); 65 | tokio::select! { 66 | _ = task.1 => {}, 67 | _ = shutdown_rx.recv() => {} 68 | } 69 | }); 70 | } 71 | 72 | async fn await_cancel(cancel_tx: broadcast::Sender, task_id: Scope) { 73 | let mut cancel = cancel_tx.subscribe(); 74 | loop { 75 | if let Ok(id) = cancel.recv().await { 76 | if task_id == id { 77 | break; 78 | } 79 | } 80 | } 81 | } 82 | 83 | pub async fn listen(&self) { 84 | if let Some((_, message)) = self.message_rx.lock().await.recv().await { 85 | let message = message as Box; 86 | send_boxed(&message); 87 | } 88 | } 89 | 90 | pub async fn shutdown(&self) { 91 | let _ = self.shutdown_tx.send(()); 92 | if let Some(handle) = self.task_handle.lock().await.take() { 93 | let _ = handle.await; 94 | }; 95 | } 96 | } 97 | 98 | #[derive(Clone)] 99 | pub struct TaskMessageTransmitter(Scope, mpsc::UnboundedSender); 100 | impl TaskMessageTransmitter { 101 | pub fn send(&self, x: T) { 102 | let _ = self.1.send((self.0, Box::new(x))); 103 | } 104 | } 105 | 106 | pub async fn create_async_scope(f: impl FnOnce(TaskRunner) -> F) 107 | where 108 | F: Future, 109 | { 110 | let scope = with_runtime(|r| r.nodes.insert(ReactiveNode::default())); 111 | let previous = with_runtime(|r| r.tracker.replace(Some(scope))); 112 | 113 | let runtime = TaskRunner::new().await; 114 | provide_context(runtime.clone()); 115 | 116 | f(runtime).await; 117 | 118 | with_runtime(|s| s.tracker.replace(previous)); 119 | with_runtime(|r| { 120 | r.tracker.replace(previous); 121 | r.cleanup_child_scope(scope); 122 | r.nodes.dispose(scope); 123 | }); 124 | } 125 | 126 | pub fn create_async_task( 127 | arg: impl SignalGet + 'static, 128 | fut: impl Fn(Value, TaskMessageTransmitter) -> Fu + 'static, 129 | ) -> TaskControl 130 | where 131 | Value: Clone + 'static, 132 | Fu: Future + Send + 'static, 133 | { 134 | let status = create_signal(TaskState::Initial); 135 | let tasks = use_context::(); 136 | 137 | let canceller = tasks.cancel_tx.clone(); 138 | let message_tx = tasks.message_tx.clone(); 139 | 140 | let id = create_memo(move || { 141 | let data = arg.get(); 142 | 143 | let id = with_runtime(|s| s.tracker.get()).unwrap(); 144 | 145 | let canceller_c = canceller.clone(); 146 | let message_tx = message_tx.clone(); 147 | let inner = fut(data, TaskMessageTransmitter(id, message_tx.clone())); 148 | let future = Box::pin(async move { 149 | let _ = message_tx.send((id, Box::new((id, TaskState::Pending)))); 150 | tokio::select! { 151 | _ = TaskRunner::await_cancel(canceller_c, id) => { 152 | let _ = message_tx.send((id, Box::new((id, TaskState::Cancelled)))); 153 | }, 154 | _ = inner => { 155 | let _ = message_tx.send((id, Box::new((id, TaskState::Finnished)))); 156 | }, 157 | } 158 | }); 159 | 160 | let _ = tasks.task_tx.send((id, future)); 161 | let canceller = canceller.clone(); 162 | on_cleanup(move || { 163 | let _ = canceller.send(id); 164 | }) 165 | }); 166 | 167 | let status_c = status.clone(); 168 | on(move |(task_id, ev): &(Scope, TaskState)| { 169 | if *task_id != id.into_scope() { 170 | return; 171 | } 172 | status_c.set(*ev); 173 | }); 174 | 175 | TaskControl { 176 | id: id.into_scope(), 177 | cancel_tx: tasks.cancel_tx.clone(), 178 | status, 179 | } 180 | } 181 | 182 | #[derive(Debug, Clone)] 183 | pub struct TaskControl { 184 | id: Scope, 185 | cancel_tx: broadcast::Sender, 186 | status: Signal, 187 | } 188 | 189 | impl TaskControl { 190 | pub fn get_state(&self) -> TaskState { 191 | self.status.get() 192 | } 193 | pub fn stop(&self) { 194 | let _ = self.cancel_tx.send(self.id); 195 | } 196 | } 197 | 198 | #[derive(Debug, Clone, Copy)] 199 | pub enum TaskState { 200 | Initial, 201 | Pending, 202 | Cancelled, 203 | Finnished, 204 | } 205 | --------------------------------------------------------------------------------