├── .github └── workflows │ ├── main.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── custom_chunk.rs ├── custom_set.rs ├── custom_state.rs ├── message.rs └── minimal.rs ├── src ├── app.rs ├── chunks.rs ├── events.rs ├── layout.rs ├── lib.rs ├── set.rs ├── setup.rs ├── states.rs ├── widget │ ├── function_widget.rs │ ├── into_widget.rs │ ├── into_widget_set.rs │ ├── mod.rs │ └── param.rs └── widgets │ ├── message.rs │ └── mod.rs └── tui-helper-proc-macro ├── Cargo.lock ├── Cargo.toml └── src └── lib.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | - run : | 18 | VERSION="${GITHUB_REF#refs/*/}" make publish 19 | env: 20 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_KEY }} 21 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.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 = "allocator-api2" 7 | version = "0.2.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 10 | 11 | [[package]] 12 | name = "anyhow" 13 | version = "1.0.86" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 16 | 17 | [[package]] 18 | name = "autocfg" 19 | version = "1.3.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 22 | 23 | [[package]] 24 | name = "bitflags" 25 | version = "2.6.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 28 | 29 | [[package]] 30 | name = "cassowary" 31 | version = "0.3.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 34 | 35 | [[package]] 36 | name = "castaway" 37 | version = "0.2.3" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 40 | dependencies = [ 41 | "rustversion", 42 | ] 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "compact_str" 52 | version = "0.8.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" 55 | dependencies = [ 56 | "castaway", 57 | "cfg-if", 58 | "itoa", 59 | "rustversion", 60 | "ryu", 61 | "static_assertions", 62 | ] 63 | 64 | [[package]] 65 | name = "crossterm" 66 | version = "0.28.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 69 | dependencies = [ 70 | "bitflags", 71 | "crossterm_winapi", 72 | "mio", 73 | "parking_lot", 74 | "rustix", 75 | "signal-hook", 76 | "signal-hook-mio", 77 | "winapi", 78 | ] 79 | 80 | [[package]] 81 | name = "crossterm_winapi" 82 | version = "0.9.1" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 85 | dependencies = [ 86 | "winapi", 87 | ] 88 | 89 | [[package]] 90 | name = "either" 91 | version = "1.13.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 94 | 95 | [[package]] 96 | name = "equivalent" 97 | version = "1.0.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 100 | 101 | [[package]] 102 | name = "errno" 103 | version = "0.3.9" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 106 | dependencies = [ 107 | "libc", 108 | "windows-sys", 109 | ] 110 | 111 | [[package]] 112 | name = "foldhash" 113 | version = "0.1.3" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" 116 | 117 | [[package]] 118 | name = "hashbrown" 119 | version = "0.15.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 122 | dependencies = [ 123 | "allocator-api2", 124 | "equivalent", 125 | "foldhash", 126 | ] 127 | 128 | [[package]] 129 | name = "heck" 130 | version = "0.5.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 133 | 134 | [[package]] 135 | name = "hermit-abi" 136 | version = "0.3.9" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 139 | 140 | [[package]] 141 | name = "indoc" 142 | version = "2.0.5" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 145 | 146 | [[package]] 147 | name = "instability" 148 | version = "0.3.2" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" 151 | dependencies = [ 152 | "quote", 153 | "syn", 154 | ] 155 | 156 | [[package]] 157 | name = "itertools" 158 | version = "0.13.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 161 | dependencies = [ 162 | "either", 163 | ] 164 | 165 | [[package]] 166 | name = "itoa" 167 | version = "1.0.11" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 170 | 171 | [[package]] 172 | name = "libc" 173 | version = "0.2.158" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 176 | 177 | [[package]] 178 | name = "linux-raw-sys" 179 | version = "0.4.14" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 182 | 183 | [[package]] 184 | name = "lock_api" 185 | version = "0.4.12" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 188 | dependencies = [ 189 | "autocfg", 190 | "scopeguard", 191 | ] 192 | 193 | [[package]] 194 | name = "log" 195 | version = "0.4.22" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 198 | 199 | [[package]] 200 | name = "lru" 201 | version = "0.12.5" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 204 | dependencies = [ 205 | "hashbrown", 206 | ] 207 | 208 | [[package]] 209 | name = "mio" 210 | version = "1.0.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 213 | dependencies = [ 214 | "hermit-abi", 215 | "libc", 216 | "log", 217 | "wasi", 218 | "windows-sys", 219 | ] 220 | 221 | [[package]] 222 | name = "parking_lot" 223 | version = "0.12.3" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 226 | dependencies = [ 227 | "lock_api", 228 | "parking_lot_core", 229 | ] 230 | 231 | [[package]] 232 | name = "parking_lot_core" 233 | version = "0.9.10" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 236 | dependencies = [ 237 | "cfg-if", 238 | "libc", 239 | "redox_syscall", 240 | "smallvec", 241 | "windows-targets", 242 | ] 243 | 244 | [[package]] 245 | name = "paste" 246 | version = "1.0.15" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 249 | 250 | [[package]] 251 | name = "proc-macro2" 252 | version = "1.0.86" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 255 | dependencies = [ 256 | "unicode-ident", 257 | ] 258 | 259 | [[package]] 260 | name = "quote" 261 | version = "1.0.36" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 264 | dependencies = [ 265 | "proc-macro2", 266 | ] 267 | 268 | [[package]] 269 | name = "ratatui" 270 | version = "0.29.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 273 | dependencies = [ 274 | "bitflags", 275 | "cassowary", 276 | "compact_str", 277 | "crossterm", 278 | "indoc", 279 | "instability", 280 | "itertools", 281 | "lru", 282 | "paste", 283 | "strum", 284 | "unicode-segmentation", 285 | "unicode-truncate", 286 | "unicode-width 0.2.0", 287 | ] 288 | 289 | [[package]] 290 | name = "redox_syscall" 291 | version = "0.5.2" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" 294 | dependencies = [ 295 | "bitflags", 296 | ] 297 | 298 | [[package]] 299 | name = "rustix" 300 | version = "0.38.35" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" 303 | dependencies = [ 304 | "bitflags", 305 | "errno", 306 | "libc", 307 | "linux-raw-sys", 308 | "windows-sys", 309 | ] 310 | 311 | [[package]] 312 | name = "rustversion" 313 | version = "1.0.18" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 316 | 317 | [[package]] 318 | name = "ryu" 319 | version = "1.0.18" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 322 | 323 | [[package]] 324 | name = "scopeguard" 325 | version = "1.2.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 328 | 329 | [[package]] 330 | name = "signal-hook" 331 | version = "0.3.17" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 334 | dependencies = [ 335 | "libc", 336 | "signal-hook-registry", 337 | ] 338 | 339 | [[package]] 340 | name = "signal-hook-mio" 341 | version = "0.2.4" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 344 | dependencies = [ 345 | "libc", 346 | "mio", 347 | "signal-hook", 348 | ] 349 | 350 | [[package]] 351 | name = "signal-hook-registry" 352 | version = "1.4.2" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 355 | dependencies = [ 356 | "libc", 357 | ] 358 | 359 | [[package]] 360 | name = "smallvec" 361 | version = "1.13.2" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 364 | 365 | [[package]] 366 | name = "static_assertions" 367 | version = "1.1.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 370 | 371 | [[package]] 372 | name = "strum" 373 | version = "0.26.3" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 376 | dependencies = [ 377 | "strum_macros", 378 | ] 379 | 380 | [[package]] 381 | name = "strum_macros" 382 | version = "0.26.4" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 385 | dependencies = [ 386 | "heck", 387 | "proc-macro2", 388 | "quote", 389 | "rustversion", 390 | "syn", 391 | ] 392 | 393 | [[package]] 394 | name = "syn" 395 | version = "2.0.68" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" 398 | dependencies = [ 399 | "proc-macro2", 400 | "quote", 401 | "unicode-ident", 402 | ] 403 | 404 | [[package]] 405 | name = "thiserror" 406 | version = "1.0.61" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 409 | dependencies = [ 410 | "thiserror-impl", 411 | ] 412 | 413 | [[package]] 414 | name = "thiserror-impl" 415 | version = "1.0.61" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 418 | dependencies = [ 419 | "proc-macro2", 420 | "quote", 421 | "syn", 422 | ] 423 | 424 | [[package]] 425 | name = "tui-helper-proc-macro" 426 | version = "0.0.0" 427 | dependencies = [ 428 | "quote", 429 | "syn", 430 | ] 431 | 432 | [[package]] 433 | name = "unicode-ident" 434 | version = "1.0.12" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 437 | 438 | [[package]] 439 | name = "unicode-segmentation" 440 | version = "1.12.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 443 | 444 | [[package]] 445 | name = "unicode-truncate" 446 | version = "1.1.0" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 449 | dependencies = [ 450 | "itertools", 451 | "unicode-segmentation", 452 | "unicode-width 0.1.14", 453 | ] 454 | 455 | [[package]] 456 | name = "unicode-width" 457 | version = "0.1.14" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 460 | 461 | [[package]] 462 | name = "unicode-width" 463 | version = "0.2.0" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 466 | 467 | [[package]] 468 | name = "wasi" 469 | version = "0.11.0+wasi-snapshot-preview1" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 472 | 473 | [[package]] 474 | name = "widgetui" 475 | version = "0.0.0" 476 | dependencies = [ 477 | "anyhow", 478 | "crossterm", 479 | "ratatui", 480 | "thiserror", 481 | "tui-helper-proc-macro", 482 | ] 483 | 484 | [[package]] 485 | name = "winapi" 486 | version = "0.3.9" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 489 | dependencies = [ 490 | "winapi-i686-pc-windows-gnu", 491 | "winapi-x86_64-pc-windows-gnu", 492 | ] 493 | 494 | [[package]] 495 | name = "winapi-i686-pc-windows-gnu" 496 | version = "0.4.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 499 | 500 | [[package]] 501 | name = "winapi-x86_64-pc-windows-gnu" 502 | version = "0.4.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 505 | 506 | [[package]] 507 | name = "windows-sys" 508 | version = "0.52.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 511 | dependencies = [ 512 | "windows-targets", 513 | ] 514 | 515 | [[package]] 516 | name = "windows-targets" 517 | version = "0.52.5" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 520 | dependencies = [ 521 | "windows_aarch64_gnullvm", 522 | "windows_aarch64_msvc", 523 | "windows_i686_gnu", 524 | "windows_i686_gnullvm", 525 | "windows_i686_msvc", 526 | "windows_x86_64_gnu", 527 | "windows_x86_64_gnullvm", 528 | "windows_x86_64_msvc", 529 | ] 530 | 531 | [[package]] 532 | name = "windows_aarch64_gnullvm" 533 | version = "0.52.5" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 536 | 537 | [[package]] 538 | name = "windows_aarch64_msvc" 539 | version = "0.52.5" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 542 | 543 | [[package]] 544 | name = "windows_i686_gnu" 545 | version = "0.52.5" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 548 | 549 | [[package]] 550 | name = "windows_i686_gnullvm" 551 | version = "0.52.5" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 554 | 555 | [[package]] 556 | name = "windows_i686_msvc" 557 | version = "0.52.5" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 560 | 561 | [[package]] 562 | name = "windows_x86_64_gnu" 563 | version = "0.52.5" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 566 | 567 | [[package]] 568 | name = "windows_x86_64_gnullvm" 569 | version = "0.52.5" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 572 | 573 | [[package]] 574 | name = "windows_x86_64_msvc" 575 | version = "0.52.5" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 578 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "widgetui" 3 | edition = "2021" 4 | description = "A bevy like widget system for ratatui and crossterm" 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/TheEmeraldBee/widgetui" 7 | version.workspace = true 8 | 9 | [workspace] 10 | members = ["tui-helper-proc-macro"] 11 | package.version = "0.0.0" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | anyhow = "1.0.75" 17 | crossterm = "0.28.1" 18 | ratatui = "0.29.0" 19 | thiserror = "1.0.61" 20 | tui-helper-proc-macro = { path = "tui-helper-proc-macro", version = "0.0.0" } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TheEmeraldBee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION_FILE := Cargo.toml 2 | 3 | run: 4 | cargo run 5 | build: 6 | cargo build 7 | publish: 8 | sed -i -r "s/package.version=\"0\.0\.0\"/package.version=\"${VERSION}\"/g" "$(VERSION_FILE)" \ 9 | && sed -i -r "s/0\.0\.0/${VERSION}/g" "$(VERSION_FILE)" \ 10 | && cargo publish --package tui-helper-proc-macro --allow-dirty \ 11 | && cargo publish --allow-dirty \ 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Widgetui 2 | Turn 3 |
4 | Ratatui Minimal 5 | 6 | ```rust 7 | fn main() -> Result<(), Box> { 8 | let mut terminal = setup_terminal()?; 9 | run(&mut terminal)?; 10 | restore_terminal(&mut terminal)?; 11 | Ok(()) 12 | } 13 | 14 | fn setup_terminal() -> Result>, Box> { 15 | let mut stdout = io::stdout(); 16 | enable_raw_mode()?; 17 | execute!(stdout, EnterAlternateScreen)?; 18 | Ok(Terminal::new(CrosstermBackend::new(stdout))?) 19 | } 20 | 21 | fn restore_terminal( 22 | terminal: &mut Terminal>, 23 | ) -> Result<(), Box> { 24 | disable_raw_mode()?; 25 | execute!(terminal.backend_mut(), LeaveAlternateScreen,)?; 26 | Ok(terminal.show_cursor()?) 27 | } 28 | 29 | fn run(terminal: &mut Terminal>) -> Result<(), Box> { 30 | Ok(loop { 31 | terminal.draw(|frame| { 32 | let greeting = Paragraph::new("Hello World!"); 33 | frame.render_widget(greeting, frame.size()); 34 | })?; 35 | if event::poll(Duration::from_millis(250))? { 36 | if let Event::Key(key) = event::read()? { 37 | if KeyCode::Char('q') == key.code { 38 | break; 39 | } 40 | } 41 | } 42 | }) 43 | } 44 | ``` 45 |
46 | 47 | Into 48 | 49 |
50 | Much Better 51 | 52 | ```rust 53 | use crossterm::event::KeyCode; 54 | use ratatui::widgets::Paragraph; 55 | use widgetui::*; 56 | 57 | use std::error::Error; 58 | 59 | fn widget(mut frame: ResMut, mut events: ResMut) -> WidgetResult { 60 | let size = frame.size(); 61 | frame.render_widget(Paragraph::new("Hello, world!"), size); 62 | 63 | if events.key(KeyCode::Char('q')) { 64 | events.register_exit(); 65 | } 66 | 67 | Ok(()) 68 | } 69 | 70 | fn main() -> Result<(), Box> { 71 | Ok(App::new(100)?.widgets(widget).run()?) 72 | } 73 | ``` 74 |
75 | 76 | The goal of this project is to simplify the requirements to make a good project using tui. 77 | It removes boilerplate, and improves the developer experience by using the power of typemaps, and dependency injection 78 | 79 | # Installation 80 | Run the following within your project directory 81 | ```bash 82 | cargo add widgetui 83 | ``` 84 | # Introduction 85 | 86 | Widgetui is a wrapper over Ratatui's Crossterm backend which allows for powerful abstraction, and simplifies creating a good app within Ratatui. 87 | ## Why pick this over Ratatui? 88 | Widgetui isn't meant to replace or undermine Ratatui. It is simply a wrapper. Without Ratatui, this crate would not exist, as well, you will still require Ratatui and Crossterm crates just to work with the apps. 89 | 90 | **TLDR; Don't, use both together to improve developer experience, and build your apps faster!** 91 | 92 | # Quickstart 93 | ```rust 94 | use crossterm::event::KeyCode; 95 | use ratatui::widgets::Paragraph; 96 | use widgetui::*; 97 | 98 | use std::error::Error; 99 | 100 | fn widget(mut frame: ResMut, mut events: ResMut) -> WidgetResult { 101 | let size = frame.size(); 102 | frame.render_widget(Paragraph::new("Hello, world!"), size); 103 | 104 | if events.key(KeyCode::Char('q')) { 105 | events.register_exit(); 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | fn main() -> Result<(), impl Error> { 112 | App::new(100)?.widgets(widget).run() 113 | } 114 | ``` 115 | 116 | The above will create an application that will display an empty terminal window, then close once you press `q`. 117 | 118 | This application, with many less lines, will render the same thing that Ratatui's Quickstart renders. 119 | # Documentation 120 | Documentation can be found on [docs.rs](https://docs.rs/widgetui). 121 | Need help? Check the [wiki](https://github.com/TheEmeraldBee/widgetui/wiki)! 122 | 123 | # Fun Facts 124 | - I chose `WidgetFrame` because if I just used `Widget`, then you couldn't do the awesome 125 | ```rust 126 | use widgetui::*; 127 | use widgetui::ratatui::prelude::*; 128 | ``` 129 | 130 | - It took about 10 hours to get this project initially set up! 131 | - Much longer after I decided to add bevy system like widget methods. 132 | 133 | - You used to have to take in a States struct, but in order to fix it, there is a lot of behind the scenes things going on! 134 | 135 | - You can only use 11 states in the same widget! 136 | - If you need more, please add an issue with details on why, and we may consider adding access to more at once! 137 | -------------------------------------------------------------------------------- /examples/custom_chunk.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use ratatui::{ 4 | prelude::{Constraint, Direction, Layout}, 5 | widgets::Paragraph, 6 | }; 7 | use widgetui::*; 8 | 9 | struct TestChunk; 10 | 11 | pub fn chunk_generator(frame: Res, mut chunks: ResMut) -> WidgetResult { 12 | // A Custom macro to simplify creating your chunks! 13 | let chunk = layout! { 14 | frame.size(), 15 | (%50), 16 | (#1) => {#3, %100, #3}, 17 | (%50) 18 | }[1][1]; 19 | 20 | chunks.register_chunk::(chunk); 21 | 22 | Ok(()) 23 | } 24 | 25 | pub fn render( 26 | mut frame: ResMut, 27 | chunks: Res, 28 | mut events: ResMut, 29 | ) -> WidgetResult { 30 | let chunk = chunks.get_chunk::()?; 31 | 32 | frame.render_widget(Paragraph::new("Hello, world!"), chunk); 33 | 34 | if events.key(crossterm::event::KeyCode::Char('q')) { 35 | events.register_exit(); 36 | } 37 | 38 | Ok(()) 39 | } 40 | 41 | fn main() -> Result<(), Box> { 42 | Ok(App::new(100)? 43 | .handle_panics() 44 | .widgets((chunk_generator, render)) 45 | .run()?) 46 | } 47 | -------------------------------------------------------------------------------- /examples/custom_set.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use ratatui::widgets::Paragraph; 4 | use widgetui::*; 5 | 6 | #[derive(State)] 7 | pub struct CoolState { 8 | pub q_count: i32, 9 | } 10 | 11 | impl Default for CoolState { 12 | fn default() -> Self { 13 | Self { q_count: 8 } 14 | } 15 | } 16 | 17 | pub fn widget( 18 | mut frame: ResMut, 19 | mut events: ResMut, 20 | mut state: ResMut, 21 | ) -> WidgetResult { 22 | if events.key(crossterm::event::KeyCode::Char('q')) { 23 | state.q_count -= 1; 24 | if state.q_count <= 0 { 25 | events.register_exit(); 26 | return Ok(()); 27 | } 28 | } 29 | 30 | let size = frame.size(); 31 | 32 | frame.render_widget( 33 | Paragraph::new(format!("Press `q` {} more times", state.q_count)), 34 | size, 35 | ); 36 | 37 | Ok(()) 38 | } 39 | 40 | #[set] 41 | pub fn CoolSet(app: App) -> App { 42 | app.widgets(widget).states(CoolState::default()) 43 | } 44 | 45 | fn main() -> Result<(), Box> { 46 | Ok(App::new(100)?.sets(CoolSet).run()?) 47 | } 48 | -------------------------------------------------------------------------------- /examples/custom_state.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use ratatui::widgets::Paragraph; 4 | use widgetui::*; 5 | 6 | #[derive(State)] 7 | pub struct CustomState { 8 | state: i32, 9 | } 10 | 11 | pub struct CustomChunk; 12 | 13 | pub fn handle_state( 14 | mut frame: ResMut, 15 | mut custom_state: ResMut, 16 | mut events: ResMut, 17 | mut chunks: ResMut, 18 | ) -> WidgetResult { 19 | // Register A Test Chunk 20 | chunks.register_chunk::(frame.size()); 21 | let chunk = chunks.get_chunk::()?; 22 | 23 | custom_state.state += 1; 24 | 25 | if custom_state.state >= 50 { 26 | events.register_exit(); 27 | } 28 | 29 | frame.render_widget( 30 | Paragraph::new(format!("Custom State: {}", custom_state.state)), 31 | chunk, 32 | ); 33 | 34 | Ok(()) 35 | } 36 | 37 | fn main() -> Result<(), Box> { 38 | Ok(App::new(100)? 39 | .states(CustomState { state: 0 }) 40 | .widgets(handle_state) 41 | .handle_panics() 42 | .run()?) 43 | } 44 | -------------------------------------------------------------------------------- /examples/message.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, time::Duration}; 2 | 3 | use ratatui::prelude::{Constraint, Direction, Layout}; 4 | use widgetui::{ 5 | widgets::message::{Message, MessageChunk, MessageState}, 6 | *, 7 | }; 8 | 9 | fn chunk_builder(frame: Res<'_, WidgetFrame>, mut chunks: ResMut<'_, Chunks>) -> WidgetResult { 10 | let popup = layout![ 11 | frame.size(), 12 | (%50), 13 | (>3) => { 14 | %10, 15 | %80, 16 | %10 17 | }, 18 | (%50) 19 | ][1][1]; 20 | 21 | chunks.register_chunk::(popup); 22 | 23 | Ok(()) 24 | } 25 | 26 | fn my_widget(mut events: ResMut, mut message: ResMut) -> WidgetResult { 27 | if events.key(crossterm::event::KeyCode::Char('m')) { 28 | message.render_message("Custom Message", Duration::from_millis(500)); 29 | } 30 | 31 | if events.key(crossterm::event::KeyCode::Char('n')) { 32 | message.render_message("Cool", Duration::from_millis(500)); 33 | } 34 | 35 | if events.key(crossterm::event::KeyCode::Char('q')) { 36 | events.register_exit() 37 | } 38 | 39 | Ok(()) 40 | } 41 | 42 | fn main() -> Result<(), Box> { 43 | Ok(App::new(100)? 44 | .handle_panics() 45 | .widgets((chunk_builder, my_widget)) 46 | .sets(Message) 47 | .run()?) 48 | } 49 | -------------------------------------------------------------------------------- /examples/minimal.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::KeyCode; 2 | use ratatui::widgets::Paragraph; 3 | use widgetui::*; 4 | 5 | use std::error::Error; 6 | 7 | fn widget(mut frame: ResMut, mut events: ResMut) -> WidgetResult { 8 | let size = frame.size(); 9 | frame.render_widget(Paragraph::new("Hello, world!"), size); 10 | 11 | if events.key(KeyCode::Char('q')) { 12 | events.register_exit(); 13 | } 14 | 15 | Ok(()) 16 | } 17 | 18 | fn main() -> Result<(), Box> { 19 | Ok(App::new(100)?.widgets(widget).run()?) 20 | } 21 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | cell::RefCell, 4 | collections::HashMap, 5 | error::Error, 6 | io, 7 | ops::Deref, 8 | time::{Duration, SystemTime}, 9 | }; 10 | 11 | use ratatui::{buffer::Buffer, prelude::Backend}; 12 | 13 | use crate::{ 14 | chunks::Chunks, 15 | events::Events, 16 | set::{Set, Sets}, 17 | setup::{reset_terminal, restore_terminal, setup_terminal, WidgetFrame, WidgetTerminal}, 18 | states::{MultiFromStates, States, Time}, 19 | widget::{into_widget::IntoWidget, into_widget_set::IntoWidgetSet, Widget}, 20 | widgets::message::MessageState, 21 | Res, ResMut, WidgetParam, WidgetResult, 22 | }; 23 | 24 | /// The powerhouse of widgetui, runs all defined widgets for you 25 | pub struct App { 26 | terminal: WidgetTerminal, 27 | widgets: Vec>, 28 | pub(crate) states: States, 29 | clock: Duration, 30 | } 31 | 32 | impl App { 33 | /// Create a new app with the given clock time (in ms) 34 | pub fn new(clock: u64) -> Result { 35 | let terminal = setup_terminal()?; 36 | 37 | Ok(Self { 38 | terminal, 39 | widgets: vec![], 40 | states: HashMap::new(), 41 | clock: Duration::from_millis(clock), 42 | } 43 | .handle_panics() 44 | .states((Chunks::default(), Time::default(), Events::default()))) 45 | } 46 | 47 | /// Running this will ensure that any panic that happens, this will catch 48 | /// And prevent your terminal from messing up. 49 | pub fn handle_panics(self) -> Self { 50 | let original_hook = std::panic::take_hook(); 51 | 52 | std::panic::set_hook(Box::new(move |panic| { 53 | reset_terminal().unwrap(); 54 | original_hook(panic); 55 | })); 56 | 57 | self 58 | } 59 | 60 | /// Adds the following Widgets to the system. 61 | /// This will take in a tuple of widgets, or a single widget. 62 | pub fn widgets(mut self, widget: impl IntoWidgetSet) -> Self { 63 | for widget in widget.into_widget_set() { 64 | self.widgets.push(widget); 65 | } 66 | self 67 | } 68 | 69 | pub fn widget(mut self, widget: W) -> Self { 70 | self.widgets.push(Box::new(widget)); 71 | self 72 | } 73 | 74 | /// Add the following states to the system 75 | /// This will take in a state or a tuple of states. 76 | pub fn states(self, state: S) -> Self { 77 | state.insert_states(self) 78 | } 79 | 80 | /// Add a set to the system 81 | pub fn sets(self, set: impl Sets) -> Self { 82 | set.register_sets(self) 83 | } 84 | 85 | /// Run the app, returning an error if any of the functions error out. 86 | pub fn run(mut self) -> WidgetResult { 87 | let result = self.inner_run(); 88 | 89 | restore_terminal(self.terminal)?; 90 | 91 | result 92 | } 93 | 94 | fn inner_run(&mut self) -> WidgetResult { 95 | self.terminal.hide_cursor()?; 96 | 97 | loop { 98 | self.terminal.autoresize()?; 99 | let mut frame = self.terminal.get_frame(); 100 | 101 | let widget_frame = WidgetFrame { 102 | cursor_position: None, 103 | buffer: frame.buffer_mut().clone(), 104 | viewport_area: frame.area(), 105 | count: frame.count(), 106 | }; 107 | 108 | self.states.insert( 109 | TypeId::of::(), 110 | RefCell::new(Box::new(widget_frame)), 111 | ); 112 | 113 | { 114 | let mut chunks = ResMut::::retrieve(&self.states); 115 | 116 | chunks.clear(); 117 | 118 | let mut events = ResMut::::retrieve(&self.states); 119 | 120 | let mut time = ResMut::