├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── build-test-lint.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── src ├── main.rs ├── taskfile │ ├── command.rs │ ├── config.rs │ └── mod.rs ├── taskui │ ├── app.rs │ ├── config.rs │ ├── event.rs │ ├── mod.rs │ ├── terminal.rs │ ├── ui.rs │ └── update.rs └── trace.rs ├── taskui-example.png └── test ├── Taskfile.yml ├── docker ├── Taskfile.yml ├── helm.yml └── tiller.yml ├── k8s.yml ├── kubectl.yml └── podman.yml /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust", 3 | "image": "rust:latest", 4 | "extensions": [ 5 | "rust-lang.rust-analyzer" 6 | ], 7 | "settings": { 8 | "terminal.integrated.shell.linux": "/bin/bash" 9 | }, 10 | "forwardPorts": [ 11 | 3000 12 | ], 13 | "postCreateCommand": "rustup component add rustfmt && cargo build" 14 | } -------------------------------------------------------------------------------- /.github/workflows/build-test-lint.yml: -------------------------------------------------------------------------------- 1 | name: build-test-lint 2 | 3 | on: 4 | push: 5 | branches: [ "**" ] 6 | pull_request: 7 | branches: [ "**" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-test-lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: cachix/install-nix-action@v25 18 | - name: Build 19 | run: | 20 | nix build 21 | - name: Test 22 | run: | 23 | nix build .#test 24 | - name: Lint 25 | run: | 26 | nix build .#clippy 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: [ "**" ] 6 | tags: [ "v**" ] 7 | pull_request: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | release: 14 | name: Release - ${{ matrix.platform.release_for }} 15 | strategy: 16 | matrix: 17 | platform: 18 | - release_for: macOS-x86_64 19 | os: macos-latest 20 | target: x86_64-apple-darwin 21 | - release_for: macOS-aarch64 22 | os: macos-latest 23 | target: aarch64-apple-darwin 24 | - release_for: Linux-x86_64 25 | os: ubuntu-latest 26 | target: x86_64-unknown-linux-gnu 27 | - release_for: Linux-aarch64 28 | os: ubuntu-latest 29 | target: aarch64-unknown-linux-gnu 30 | runs-on: ${{ matrix.platform.os }} 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - uses: cachix/install-nix-action@v25 35 | - name: Build binary 36 | run: | 37 | nix build 38 | - name: Archive 39 | shell: bash 40 | run: | 41 | tar czvf taskui-${{ matrix.platform.target }}.tar.gz -h -C result/bin taskui 42 | if: startsWith(github.ref, 'refs/tags/v') 43 | - name: Release 44 | uses: softprops/action-gh-release@v2 45 | with: 46 | files: "taskui-*.tar.gz" 47 | if: startsWith(github.ref, 'refs/tags/v') 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | result 3 | .direnv/ 4 | .envrc 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "1.1.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "allocator-api2" 28 | version = "0.2.18" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 31 | 32 | [[package]] 33 | name = "anyhow" 34 | version = "1.0.82" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" 37 | 38 | [[package]] 39 | name = "autocfg" 40 | version = "1.3.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 43 | 44 | [[package]] 45 | name = "bitflags" 46 | version = "2.5.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 49 | 50 | [[package]] 51 | name = "cassowary" 52 | version = "0.3.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 55 | 56 | [[package]] 57 | name = "cfg-if" 58 | version = "1.0.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 61 | 62 | [[package]] 63 | name = "colored" 64 | version = "2.1.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 67 | dependencies = [ 68 | "lazy_static", 69 | "windows-sys", 70 | ] 71 | 72 | [[package]] 73 | name = "crossterm" 74 | version = "0.27.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 77 | dependencies = [ 78 | "bitflags", 79 | "crossterm_winapi", 80 | "libc", 81 | "mio", 82 | "parking_lot", 83 | "signal-hook", 84 | "signal-hook-mio", 85 | "winapi", 86 | ] 87 | 88 | [[package]] 89 | name = "crossterm_winapi" 90 | version = "0.9.1" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 93 | dependencies = [ 94 | "winapi", 95 | ] 96 | 97 | [[package]] 98 | name = "directories" 99 | version = "5.0.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 102 | dependencies = [ 103 | "dirs-sys", 104 | ] 105 | 106 | [[package]] 107 | name = "dirs-sys" 108 | version = "0.4.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 111 | dependencies = [ 112 | "libc", 113 | "option-ext", 114 | "redox_users", 115 | "windows-sys", 116 | ] 117 | 118 | [[package]] 119 | name = "either" 120 | version = "1.11.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" 123 | 124 | [[package]] 125 | name = "equivalent" 126 | version = "1.0.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 129 | 130 | [[package]] 131 | name = "getrandom" 132 | version = "0.2.15" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 135 | dependencies = [ 136 | "cfg-if", 137 | "libc", 138 | "wasi", 139 | ] 140 | 141 | [[package]] 142 | name = "hashbrown" 143 | version = "0.14.5" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 146 | dependencies = [ 147 | "ahash", 148 | "allocator-api2", 149 | ] 150 | 151 | [[package]] 152 | name = "heck" 153 | version = "0.4.1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 156 | 157 | [[package]] 158 | name = "indexmap" 159 | version = "2.2.6" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 162 | dependencies = [ 163 | "equivalent", 164 | "hashbrown", 165 | ] 166 | 167 | [[package]] 168 | name = "indoc" 169 | version = "2.0.5" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 172 | 173 | [[package]] 174 | name = "itertools" 175 | version = "0.12.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 178 | dependencies = [ 179 | "either", 180 | ] 181 | 182 | [[package]] 183 | name = "itoa" 184 | version = "1.0.11" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 187 | 188 | [[package]] 189 | name = "lazy_static" 190 | version = "1.5.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 193 | 194 | [[package]] 195 | name = "libc" 196 | version = "0.2.154" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 199 | 200 | [[package]] 201 | name = "libredox" 202 | version = "0.1.3" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 205 | dependencies = [ 206 | "bitflags", 207 | "libc", 208 | ] 209 | 210 | [[package]] 211 | name = "lock_api" 212 | version = "0.4.12" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 215 | dependencies = [ 216 | "autocfg", 217 | "scopeguard", 218 | ] 219 | 220 | [[package]] 221 | name = "log" 222 | version = "0.4.21" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 225 | 226 | [[package]] 227 | name = "lru" 228 | version = "0.12.3" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 231 | dependencies = [ 232 | "hashbrown", 233 | ] 234 | 235 | [[package]] 236 | name = "matchers" 237 | version = "0.1.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 240 | dependencies = [ 241 | "regex-automata 0.1.10", 242 | ] 243 | 244 | [[package]] 245 | name = "memchr" 246 | version = "2.7.4" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 249 | 250 | [[package]] 251 | name = "mio" 252 | version = "0.8.11" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 255 | dependencies = [ 256 | "libc", 257 | "log", 258 | "wasi", 259 | "windows-sys", 260 | ] 261 | 262 | [[package]] 263 | name = "nu-ansi-term" 264 | version = "0.46.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 267 | dependencies = [ 268 | "overload", 269 | "winapi", 270 | ] 271 | 272 | [[package]] 273 | name = "once_cell" 274 | version = "1.19.0" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 277 | 278 | [[package]] 279 | name = "option-ext" 280 | version = "0.2.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 283 | 284 | [[package]] 285 | name = "overload" 286 | version = "0.1.1" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 289 | 290 | [[package]] 291 | name = "parking_lot" 292 | version = "0.12.2" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" 295 | dependencies = [ 296 | "lock_api", 297 | "parking_lot_core", 298 | ] 299 | 300 | [[package]] 301 | name = "parking_lot_core" 302 | version = "0.9.10" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 305 | dependencies = [ 306 | "cfg-if", 307 | "libc", 308 | "redox_syscall", 309 | "smallvec", 310 | "windows-targets 0.52.5", 311 | ] 312 | 313 | [[package]] 314 | name = "paste" 315 | version = "1.0.14" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 318 | 319 | [[package]] 320 | name = "pin-project-lite" 321 | version = "0.2.14" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 324 | 325 | [[package]] 326 | name = "proc-macro2" 327 | version = "1.0.81" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 330 | dependencies = [ 331 | "unicode-ident", 332 | ] 333 | 334 | [[package]] 335 | name = "quote" 336 | version = "1.0.36" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 339 | dependencies = [ 340 | "proc-macro2", 341 | ] 342 | 343 | [[package]] 344 | name = "ratatui" 345 | version = "0.25.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" 348 | dependencies = [ 349 | "bitflags", 350 | "cassowary", 351 | "crossterm", 352 | "indoc", 353 | "itertools", 354 | "lru", 355 | "paste", 356 | "stability", 357 | "strum", 358 | "unicode-segmentation", 359 | "unicode-width", 360 | ] 361 | 362 | [[package]] 363 | name = "redox_syscall" 364 | version = "0.5.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 367 | dependencies = [ 368 | "bitflags", 369 | ] 370 | 371 | [[package]] 372 | name = "redox_users" 373 | version = "0.4.5" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" 376 | dependencies = [ 377 | "getrandom", 378 | "libredox", 379 | "thiserror", 380 | ] 381 | 382 | [[package]] 383 | name = "regex" 384 | version = "1.10.4" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 387 | dependencies = [ 388 | "aho-corasick", 389 | "memchr", 390 | "regex-automata 0.4.6", 391 | "regex-syntax 0.8.3", 392 | ] 393 | 394 | [[package]] 395 | name = "regex-automata" 396 | version = "0.1.10" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 399 | dependencies = [ 400 | "regex-syntax 0.6.29", 401 | ] 402 | 403 | [[package]] 404 | name = "regex-automata" 405 | version = "0.4.6" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 408 | dependencies = [ 409 | "aho-corasick", 410 | "memchr", 411 | "regex-syntax 0.8.3", 412 | ] 413 | 414 | [[package]] 415 | name = "regex-syntax" 416 | version = "0.6.29" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 419 | 420 | [[package]] 421 | name = "regex-syntax" 422 | version = "0.8.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 425 | 426 | [[package]] 427 | name = "rustversion" 428 | version = "1.0.15" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" 431 | 432 | [[package]] 433 | name = "ryu" 434 | version = "1.0.17" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 437 | 438 | [[package]] 439 | name = "scopeguard" 440 | version = "1.2.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 443 | 444 | [[package]] 445 | name = "serde" 446 | version = "1.0.200" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" 449 | dependencies = [ 450 | "serde_derive", 451 | ] 452 | 453 | [[package]] 454 | name = "serde_derive" 455 | version = "1.0.200" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" 458 | dependencies = [ 459 | "proc-macro2", 460 | "quote", 461 | "syn 2.0.60", 462 | ] 463 | 464 | [[package]] 465 | name = "serde_yaml" 466 | version = "0.9.34+deprecated" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 469 | dependencies = [ 470 | "indexmap", 471 | "itoa", 472 | "ryu", 473 | "serde", 474 | "unsafe-libyaml", 475 | ] 476 | 477 | [[package]] 478 | name = "sharded-slab" 479 | version = "0.1.7" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 482 | dependencies = [ 483 | "lazy_static", 484 | ] 485 | 486 | [[package]] 487 | name = "signal-hook" 488 | version = "0.3.17" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 491 | dependencies = [ 492 | "libc", 493 | "signal-hook-registry", 494 | ] 495 | 496 | [[package]] 497 | name = "signal-hook-mio" 498 | version = "0.2.3" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 501 | dependencies = [ 502 | "libc", 503 | "mio", 504 | "signal-hook", 505 | ] 506 | 507 | [[package]] 508 | name = "signal-hook-registry" 509 | version = "1.4.2" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 512 | dependencies = [ 513 | "libc", 514 | ] 515 | 516 | [[package]] 517 | name = "smallvec" 518 | version = "1.13.2" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 521 | 522 | [[package]] 523 | name = "stability" 524 | version = "0.1.1" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" 527 | dependencies = [ 528 | "quote", 529 | "syn 1.0.109", 530 | ] 531 | 532 | [[package]] 533 | name = "strum" 534 | version = "0.25.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" 537 | dependencies = [ 538 | "strum_macros", 539 | ] 540 | 541 | [[package]] 542 | name = "strum_macros" 543 | version = "0.25.3" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" 546 | dependencies = [ 547 | "heck", 548 | "proc-macro2", 549 | "quote", 550 | "rustversion", 551 | "syn 2.0.60", 552 | ] 553 | 554 | [[package]] 555 | name = "syn" 556 | version = "1.0.109" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 559 | dependencies = [ 560 | "proc-macro2", 561 | "quote", 562 | "unicode-ident", 563 | ] 564 | 565 | [[package]] 566 | name = "syn" 567 | version = "2.0.60" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 570 | dependencies = [ 571 | "proc-macro2", 572 | "quote", 573 | "unicode-ident", 574 | ] 575 | 576 | [[package]] 577 | name = "taskui" 578 | version = "0.1.0" 579 | dependencies = [ 580 | "anyhow", 581 | "colored", 582 | "crossterm", 583 | "directories", 584 | "lazy_static", 585 | "ratatui", 586 | "serde", 587 | "serde_yaml", 588 | "tracing", 589 | "tracing-error", 590 | "tracing-subscriber", 591 | ] 592 | 593 | [[package]] 594 | name = "thiserror" 595 | version = "1.0.63" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 598 | dependencies = [ 599 | "thiserror-impl", 600 | ] 601 | 602 | [[package]] 603 | name = "thiserror-impl" 604 | version = "1.0.63" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 607 | dependencies = [ 608 | "proc-macro2", 609 | "quote", 610 | "syn 2.0.60", 611 | ] 612 | 613 | [[package]] 614 | name = "thread_local" 615 | version = "1.1.8" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 618 | dependencies = [ 619 | "cfg-if", 620 | "once_cell", 621 | ] 622 | 623 | [[package]] 624 | name = "tracing" 625 | version = "0.1.40" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 628 | dependencies = [ 629 | "pin-project-lite", 630 | "tracing-attributes", 631 | "tracing-core", 632 | ] 633 | 634 | [[package]] 635 | name = "tracing-attributes" 636 | version = "0.1.27" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 639 | dependencies = [ 640 | "proc-macro2", 641 | "quote", 642 | "syn 2.0.60", 643 | ] 644 | 645 | [[package]] 646 | name = "tracing-core" 647 | version = "0.1.32" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 650 | dependencies = [ 651 | "once_cell", 652 | "valuable", 653 | ] 654 | 655 | [[package]] 656 | name = "tracing-error" 657 | version = "0.2.0" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 660 | dependencies = [ 661 | "tracing", 662 | "tracing-subscriber", 663 | ] 664 | 665 | [[package]] 666 | name = "tracing-log" 667 | version = "0.2.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 670 | dependencies = [ 671 | "log", 672 | "once_cell", 673 | "tracing-core", 674 | ] 675 | 676 | [[package]] 677 | name = "tracing-subscriber" 678 | version = "0.3.18" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 681 | dependencies = [ 682 | "matchers", 683 | "nu-ansi-term", 684 | "once_cell", 685 | "regex", 686 | "sharded-slab", 687 | "smallvec", 688 | "thread_local", 689 | "tracing", 690 | "tracing-core", 691 | "tracing-log", 692 | ] 693 | 694 | [[package]] 695 | name = "unicode-ident" 696 | version = "1.0.12" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 699 | 700 | [[package]] 701 | name = "unicode-segmentation" 702 | version = "1.11.0" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 705 | 706 | [[package]] 707 | name = "unicode-width" 708 | version = "0.1.12" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 711 | 712 | [[package]] 713 | name = "unsafe-libyaml" 714 | version = "0.2.11" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 717 | 718 | [[package]] 719 | name = "valuable" 720 | version = "0.1.0" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 723 | 724 | [[package]] 725 | name = "version_check" 726 | version = "0.9.4" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 729 | 730 | [[package]] 731 | name = "wasi" 732 | version = "0.11.0+wasi-snapshot-preview1" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 735 | 736 | [[package]] 737 | name = "winapi" 738 | version = "0.3.9" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 741 | dependencies = [ 742 | "winapi-i686-pc-windows-gnu", 743 | "winapi-x86_64-pc-windows-gnu", 744 | ] 745 | 746 | [[package]] 747 | name = "winapi-i686-pc-windows-gnu" 748 | version = "0.4.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 751 | 752 | [[package]] 753 | name = "winapi-x86_64-pc-windows-gnu" 754 | version = "0.4.0" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 757 | 758 | [[package]] 759 | name = "windows-sys" 760 | version = "0.48.0" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 763 | dependencies = [ 764 | "windows-targets 0.48.5", 765 | ] 766 | 767 | [[package]] 768 | name = "windows-targets" 769 | version = "0.48.5" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 772 | dependencies = [ 773 | "windows_aarch64_gnullvm 0.48.5", 774 | "windows_aarch64_msvc 0.48.5", 775 | "windows_i686_gnu 0.48.5", 776 | "windows_i686_msvc 0.48.5", 777 | "windows_x86_64_gnu 0.48.5", 778 | "windows_x86_64_gnullvm 0.48.5", 779 | "windows_x86_64_msvc 0.48.5", 780 | ] 781 | 782 | [[package]] 783 | name = "windows-targets" 784 | version = "0.52.5" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 787 | dependencies = [ 788 | "windows_aarch64_gnullvm 0.52.5", 789 | "windows_aarch64_msvc 0.52.5", 790 | "windows_i686_gnu 0.52.5", 791 | "windows_i686_gnullvm", 792 | "windows_i686_msvc 0.52.5", 793 | "windows_x86_64_gnu 0.52.5", 794 | "windows_x86_64_gnullvm 0.52.5", 795 | "windows_x86_64_msvc 0.52.5", 796 | ] 797 | 798 | [[package]] 799 | name = "windows_aarch64_gnullvm" 800 | version = "0.48.5" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 803 | 804 | [[package]] 805 | name = "windows_aarch64_gnullvm" 806 | version = "0.52.5" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 809 | 810 | [[package]] 811 | name = "windows_aarch64_msvc" 812 | version = "0.48.5" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 815 | 816 | [[package]] 817 | name = "windows_aarch64_msvc" 818 | version = "0.52.5" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 821 | 822 | [[package]] 823 | name = "windows_i686_gnu" 824 | version = "0.48.5" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 827 | 828 | [[package]] 829 | name = "windows_i686_gnu" 830 | version = "0.52.5" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 833 | 834 | [[package]] 835 | name = "windows_i686_gnullvm" 836 | version = "0.52.5" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 839 | 840 | [[package]] 841 | name = "windows_i686_msvc" 842 | version = "0.48.5" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 845 | 846 | [[package]] 847 | name = "windows_i686_msvc" 848 | version = "0.52.5" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 851 | 852 | [[package]] 853 | name = "windows_x86_64_gnu" 854 | version = "0.48.5" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 857 | 858 | [[package]] 859 | name = "windows_x86_64_gnu" 860 | version = "0.52.5" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 863 | 864 | [[package]] 865 | name = "windows_x86_64_gnullvm" 866 | version = "0.48.5" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 869 | 870 | [[package]] 871 | name = "windows_x86_64_gnullvm" 872 | version = "0.52.5" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 875 | 876 | [[package]] 877 | name = "windows_x86_64_msvc" 878 | version = "0.48.5" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 881 | 882 | [[package]] 883 | name = "windows_x86_64_msvc" 884 | version = "0.52.5" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 887 | 888 | [[package]] 889 | name = "zerocopy" 890 | version = "0.7.33" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" 893 | dependencies = [ 894 | "zerocopy-derive", 895 | ] 896 | 897 | [[package]] 898 | name = "zerocopy-derive" 899 | version = "0.7.33" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" 902 | dependencies = [ 903 | "proc-macro2", 904 | "quote", 905 | "syn 2.0.60", 906 | ] 907 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "taskui" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.79" 10 | colored = "2.1.0" 11 | crossterm = "0.27.0" 12 | directories = "5.0.1" 13 | lazy_static = "1.5.0" 14 | ratatui = "0.25.0" 15 | serde = "1.0.195" 16 | serde_yaml = "0.9.30" 17 | tracing = "0.1.40" 18 | tracing-error = "0.2.0" 19 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Thomas Hamm 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TaskUI - Simple Terminal UI for Task / taskfile.dev 2 | 3 | TaskUI is a lightweight terminal user interface for executing tasks defined using [taskfile.dev](https://taskfile.dev). It provides an easy way to navigate through tasks using arrow keys or Vim-like shortcuts. 4 | 5 | Current features are task `execution`, `search` and `preview`. 6 | 7 | ![taskui-example](./taskui-example.png) 8 | 9 | ## Usage 10 | 11 | - Navigate through tasks using arrow keys `up` and `down`, or use `j` and `k` to move. 12 | - Press `Enter` to execute the selected task. 13 | - Press `q` to exit the program without executing a task. 14 | - Press `/` to toggle the search bar. Use `Esc` to reset the search or `Enter` to get back to selection mode. 15 | - Press `p` to toggle the preview of a selected task. Use `p` again or `q` to close the preview. 16 | 17 | ## Configuration 18 | 19 | TaskUI can be configured using environment variables. 20 | 21 | Available configuration options are: 22 | 23 | | Environment Variable | Description | Default | 24 | |----------------------|-------------|---------| 25 | | `TASKUI_LIST_INTERNAL` | Show internal tasks in the task list | `false` | 26 | | `TASKUI_HIGHLIGHT_STYLE_BG` | Background color for highlighted task | `#ffffff` | 27 | | `TASKUI_HIGHLIGHT_STYLE_FG` | Foreground/text color for highlighted task | `#4c4f69` | 28 | 29 | ## Installation 30 | 31 | 1. Clone the repository: 32 | 33 | ```bash 34 | git clone https://github.com/thmshmm/taskui.git 35 | ``` 36 | 37 | 2. Build the binary 38 | 39 | using cargo: 40 | 41 | ```bash 42 | cd taskui 43 | cargo build --release 44 | ``` 45 | 46 | using Nix: 47 | 48 | ```bash 49 | nix build 50 | ``` 51 | 52 | 3. Create a shell alias for easy access: 53 | 54 | ```bash 55 | alias tui="/path/to/taskui" 56 | ``` 57 | 58 | ## Example Taskfile.yml 59 | 60 | ```yaml 61 | version: '3' 62 | 63 | includes: 64 | k8s: ./k8s.yml 65 | docker: ./docker # requires ./docker/Taskfile.yml to exits 66 | helm: 67 | taskfile: ./helm.yml 68 | optional: true 69 | 70 | tasks: 71 | uptime: 72 | cmds: 73 | - uptime 74 | date: 75 | cmds: 76 | - date 77 | ``` 78 | 79 | ## Contributing 80 | 81 | If you have any suggestions, improvements, or bug fixes, feel free to open an issue or submit a pull request. 82 | 83 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "naersk": { 22 | "inputs": { 23 | "nixpkgs": "nixpkgs" 24 | }, 25 | "locked": { 26 | "lastModified": 1713520724, 27 | "narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=", 28 | "owner": "nix-community", 29 | "repo": "naersk", 30 | "rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "nix-community", 35 | "repo": "naersk", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 0, 42 | "narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=", 43 | "path": "/nix/store/370qy3d3wg9zhbn5a3dcv6k1q1iigfh4-source", 44 | "type": "path" 45 | }, 46 | "original": { 47 | "id": "nixpkgs", 48 | "type": "indirect" 49 | } 50 | }, 51 | "nixpkgs_2": { 52 | "locked": { 53 | "lastModified": 1714902782, 54 | "narHash": "sha256-TdQNxaviQZlGU1VakHpDq3qqhP+0HhieieYRGZN46Ec=", 55 | "owner": "nixos", 56 | "repo": "nixpkgs", 57 | "rev": "1552982a8e5848fe2fec7d669d54ee86aa743101", 58 | "type": "github" 59 | }, 60 | "original": { 61 | "owner": "nixos", 62 | "ref": "release-23.11", 63 | "repo": "nixpkgs", 64 | "type": "github" 65 | } 66 | }, 67 | "root": { 68 | "inputs": { 69 | "flake-utils": "flake-utils", 70 | "naersk": "naersk", 71 | "nixpkgs": "nixpkgs_2", 72 | "rust-overlay": "rust-overlay" 73 | } 74 | }, 75 | "rust-overlay": { 76 | "inputs": { 77 | "flake-utils": [ 78 | "flake-utils" 79 | ], 80 | "nixpkgs": [ 81 | "nixpkgs" 82 | ] 83 | }, 84 | "locked": { 85 | "lastModified": 1714875369, 86 | "narHash": "sha256-dyyJEHKbnz2sZcz9yVxOCE/085covNljWKeTCIcBfL0=", 87 | "owner": "oxalica", 88 | "repo": "rust-overlay", 89 | "rev": "35cc508a9de1c971ce1dc610dec1ba75f66c6004", 90 | "type": "github" 91 | }, 92 | "original": { 93 | "owner": "oxalica", 94 | "repo": "rust-overlay", 95 | "type": "github" 96 | } 97 | }, 98 | "systems": { 99 | "locked": { 100 | "lastModified": 1681028828, 101 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 102 | "owner": "nix-systems", 103 | "repo": "default", 104 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 105 | "type": "github" 106 | }, 107 | "original": { 108 | "owner": "nix-systems", 109 | "repo": "default", 110 | "type": "github" 111 | } 112 | } 113 | }, 114 | "root": "root", 115 | "version": 7 116 | } 117 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "TaskUI flake"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs?ref=release-23.11"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | naersk.url = "github:nix-community/naersk"; 8 | rust-overlay = { 9 | url = "github:oxalica/rust-overlay"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | inputs.flake-utils.follows = "flake-utils"; 12 | }; 13 | }; 14 | 15 | outputs = { self, flake-utils, naersk, nixpkgs, rust-overlay }: 16 | flake-utils.lib.eachDefaultSystem (system: 17 | let 18 | overlays = [ (import rust-overlay) ]; 19 | 20 | pkgs = import nixpkgs { inherit system overlays; }; 21 | 22 | toolchain = pkgs.rust-bin.stable."1.78.0".default.override { 23 | targets = [ 24 | "x86_64-apple-darwin" 25 | "aarch64-apple-darwin" 26 | "x86_64-unknown-linux-gnu" 27 | "aarch64-unknown-linux-gnu" 28 | ]; 29 | }; 30 | 31 | naersk' = pkgs.callPackage naersk { 32 | cargo = toolchain; 33 | rustc = toolchain; 34 | }; 35 | 36 | in rec { 37 | packages = { 38 | default = naersk'.buildPackage { src = ./.; }; 39 | test = naersk'.buildPackage { 40 | src = ./.; 41 | mode = "test"; 42 | }; 43 | clippy = naersk'.buildPackage { 44 | src = ./.; 45 | mode = "clippy"; 46 | }; 47 | }; 48 | 49 | defaultPackage = packages.default; 50 | 51 | apps = rec { 52 | taskui = flake-utils.lib.mkApp { 53 | name = "taskui"; 54 | drv = self.packages.${system}.default; 55 | }; 56 | default = taskui; 57 | }; 58 | 59 | defaultApp = apps.default; 60 | 61 | devShell = pkgs.mkShell { 62 | nativeBuildInputs = [ toolchain pkgs.rust-analyzer ]; 63 | }; 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::taskui::{App, Config}; 2 | use anyhow::Result; 3 | use ratatui::{backend::CrosstermBackend, Terminal}; 4 | use taskui::{ 5 | event::{Event, EventHandler}, 6 | terminal::UserInterface, 7 | update, 8 | }; 9 | 10 | mod taskfile; 11 | mod taskui; 12 | mod trace; 13 | 14 | fn main() -> Result<()> { 15 | trace::initialize_logging()?; 16 | 17 | trace_dbg!("Starting taskui"); 18 | 19 | let taskfile = taskfile::config::load()?; 20 | 21 | let cfg = Config::load(); 22 | let mut app = App::new(cfg, taskfile); 23 | 24 | let backend = CrosstermBackend::new(std::io::stderr()); 25 | let terminal = Terminal::new(backend)?; 26 | let events = EventHandler::new(250); 27 | let mut tui = UserInterface::new(terminal, events); 28 | 29 | tui.enter()?; 30 | 31 | while !app.should_quit { 32 | tui.draw(&mut app)?; 33 | 34 | match tui.events.next()? { 35 | Event::Tick => {} 36 | Event::Key(key_event) => update(&mut app, key_event), 37 | Event::Mouse(_) => {} 38 | Event::Resize(_, _) => {} 39 | }; 40 | } 41 | 42 | tui.exit()?; 43 | 44 | if let Some(task) = app.task_to_exec { 45 | return taskfile::command::run_task(task.name); 46 | } 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /src/taskfile/command.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Ok, Result}; 2 | use colored::Colorize; 3 | use std::io::{BufRead, BufReader, Read}; 4 | use std::process::{Command, Stdio}; 5 | use std::thread; 6 | 7 | pub fn run_task(name: String) -> Result<()> { 8 | let proc = Command::new("task") 9 | .arg(name) 10 | .stdout(Stdio::piped()) 11 | .stderr(Stdio::piped()) 12 | .spawn() 13 | .unwrap(); 14 | 15 | let stdout = proc.stdout.unwrap(); 16 | let stderr = proc.stderr.unwrap(); 17 | 18 | let thread_print_out = thread::spawn(move || print_output(stdout)); 19 | let thread_print_err = thread::spawn(move || print_output(stderr)); 20 | 21 | let _ = thread_print_out.join(); 22 | let _ = thread_print_err.join(); 23 | 24 | Ok(()) 25 | } 26 | 27 | fn print_output(stream: T) { 28 | let reader = BufReader::new(stream); 29 | 30 | for line in reader.lines() { 31 | let l = line.unwrap(); 32 | 33 | if l.ends_with("is up to date") { 34 | println!("{}", l.purple()); 35 | } else if l.starts_with("task: Failed to run task") { 36 | println!("{}", l.red()); 37 | } else if l.starts_with("task: ") { 38 | println!("{}", l.green()); 39 | } else { 40 | println!("{}", l); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/taskfile/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | use serde_yaml::Value; 3 | use std::{ 4 | fs::{metadata, File}, 5 | path::PathBuf, 6 | }; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Task { 10 | pub name: String, 11 | pub body: String, 12 | pub internal: bool, 13 | } 14 | 15 | #[derive(Clone, Debug)] 16 | struct Include { 17 | name: String, 18 | path: String, 19 | optional: bool, 20 | internal: bool, 21 | } 22 | 23 | pub fn load() -> Result> { 24 | let taskfile = File::open(find_supported_file()?).unwrap(); 25 | let taskfile_yml: Value = serde_yaml::from_reader(taskfile)?; 26 | 27 | let mut tasks = get_tasks(&taskfile_yml)?; 28 | let includes = get_includes(&taskfile_yml)?; 29 | let current_path = std::env::current_dir().unwrap(); 30 | let included_tasks = handle_includes( 31 | includes.unwrap_or(Vec::new()), 32 | current_path.to_str().unwrap(), 33 | )?; 34 | 35 | tasks.extend(included_tasks); 36 | 37 | Ok(tasks) 38 | } 39 | 40 | fn find_supported_file() -> Result<&'static str> { 41 | // https://taskfile.dev/usage/#supported-file-names 42 | let file_names = [ 43 | "Taskfile.yml", 44 | "taskfile.yml", 45 | "Taskfile.yaml", 46 | "taskfile.yaml", 47 | "Taskfile.dist.yml", 48 | "taskfile.dist.yml", 49 | "Taskfile.dist.yaml", 50 | "taskfile.dist.yaml", 51 | ]; 52 | 53 | let found = file_names 54 | .iter() 55 | .find(|&file_name| metadata(file_name).is_ok()) 56 | .copied(); 57 | 58 | match found { 59 | Some(file_name) => Ok(file_name), 60 | None => Err(anyhow!("no supported file found")), 61 | } 62 | } 63 | 64 | fn handle_includes(includes: Vec, current_path: &str) -> Result> { 65 | let mut tasks: Vec = Vec::new(); 66 | 67 | for include in includes { 68 | let include_path = PathBuf::from(current_path).join(include.path.clone()); 69 | 70 | match File::open(&include_path) { 71 | Ok(taskfile) => { 72 | let taskfile_yml: Value = serde_yaml::from_reader(taskfile)?; 73 | let include_tasks = get_tasks(&taskfile_yml)?; 74 | let include_tasks: Vec = 75 | prefix_tasks(include_tasks.clone(), include.name.clone()); 76 | let include_tasks: Vec = flag_internal_tasks(&include, include_tasks); 77 | tasks.extend(include_tasks); 78 | 79 | if let Some(sub_includes) = get_includes(&taskfile_yml)? { 80 | // remove filename from path 81 | let include_path = include_path.parent().unwrap().to_str().unwrap(); 82 | let sub_include_tasks = handle_includes(sub_includes, include_path)?; 83 | let sub_include_tasks: Vec = 84 | prefix_tasks(sub_include_tasks.clone(), include.name.clone()); 85 | let sub_include_tasks = flag_internal_tasks(&include, sub_include_tasks); 86 | tasks.extend(sub_include_tasks); 87 | } 88 | } 89 | Err(_) => { 90 | if include.optional { 91 | continue; 92 | } 93 | bail!("include not found: {}", include.path); 94 | } 95 | } 96 | } 97 | 98 | tasks.sort_by(|a, b| a.name.cmp(&b.name)); 99 | 100 | Ok(tasks) 101 | } 102 | 103 | fn flag_internal_tasks(include: &Include, tasks: Vec) -> Vec { 104 | tasks 105 | .into_iter() 106 | .map(|task| Task { 107 | name: task.name, 108 | body: task.body, 109 | internal: task.internal || include.internal, 110 | }) 111 | .collect() 112 | } 113 | 114 | fn prefix_tasks(tasks: Vec, include_name: String) -> Vec { 115 | tasks 116 | .into_iter() 117 | .map(|task| Task { 118 | name: format!("{}:{}", include_name, task.name), 119 | body: task.body, 120 | internal: task.internal, 121 | }) 122 | .collect() 123 | } 124 | 125 | fn get_tasks(taskfile_yml: &Value) -> Result> { 126 | let mut tasks: Vec = Vec::new(); 127 | 128 | if let Some(task_mapping) = taskfile_yml.get("tasks").and_then(Value::as_mapping) { 129 | for (key, body) in task_mapping { 130 | let task_name = key.as_str().unwrap().to_string(); 131 | let internal = extract_bool(body, "internal", false)?; 132 | 133 | tasks.push(Task { 134 | name: task_name, 135 | body: serde_yaml::to_string(body).unwrap_or_else(|_| "no content".to_string()), 136 | internal, 137 | }); 138 | } 139 | } else { 140 | bail!("failed to extract tasks") 141 | } 142 | 143 | tasks.sort_by(|a, b| a.name.cmp(&b.name)); 144 | 145 | Ok(tasks) 146 | } 147 | 148 | fn get_includes(taskfile_yml: &Value) -> Result>> { 149 | let mut includes: Vec = Vec::new(); 150 | 151 | if let Some(include_mapping) = taskfile_yml.get("includes").and_then(Value::as_mapping) { 152 | for (key, value) in include_mapping { 153 | let name = extract_include_name(key); 154 | let path = extract_include_path(value)?; 155 | let optional = extract_bool(value, "optional", false)?; 156 | let internal = extract_bool(value, "internal", false)?; 157 | 158 | includes.push(Include { 159 | name, 160 | path, 161 | optional, 162 | internal, 163 | }); 164 | } 165 | } else { 166 | return Ok(None); 167 | } 168 | 169 | Ok(Some(includes)) 170 | } 171 | 172 | fn extract_include_name(include_key: &Value) -> String { 173 | include_key.as_str().unwrap().to_string() 174 | } 175 | 176 | fn extract_include_path(include_yml: &Value) -> Result { 177 | let path = match include_yml { 178 | Value::String(path) if path.ends_with(".yml") || path.ends_with(".yaml") => { 179 | path.to_string() 180 | } 181 | Value::String(path) if path.ends_with('/') => format!("{}Taskfile.yml", path), 182 | Value::String(path) => format!("{}/Taskfile.yml", path), 183 | Value::Mapping(v) => { 184 | if let Some(taskfile) = v.get(&Value::String("taskfile".to_string())) { 185 | if let Value::String(s) = taskfile { 186 | if s.ends_with(".yml") || s.ends_with(".yaml") { 187 | s.to_string() 188 | } else { 189 | bail!("value of taskfile key must end with .yml or .yaml") 190 | } 191 | } else { 192 | bail!("value of taskfile key must be of type string") 193 | } 194 | } else { 195 | bail!("taskfile key not found in include mapping") 196 | } 197 | } 198 | _ => bail!("invalid include found"), 199 | }; 200 | 201 | Ok(path) 202 | } 203 | 204 | fn extract_bool(yml: &Value, field: &str, default: bool) -> Result { 205 | match yml { 206 | Value::Mapping(v) => { 207 | if let Some(value) = v 208 | .get(&Value::String(field.to_string())) 209 | .and_then(Value::as_bool) 210 | { 211 | Ok(value) 212 | } else { 213 | Ok(default) 214 | } 215 | } 216 | _ => Ok(false), 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/taskfile/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | pub mod config; 3 | -------------------------------------------------------------------------------- /src/taskui/app.rs: -------------------------------------------------------------------------------- 1 | use crate::taskfile::config::Task; 2 | use ratatui::widgets::ListState; 3 | use std::usize; 4 | 5 | use super::Config; 6 | 7 | pub struct App { 8 | pub cfg: Config, 9 | pub tasks: StatefulList, 10 | pub search: String, 11 | pub input_mode: InputMode, 12 | pub should_quit: bool, 13 | pub task_to_exec: Option, 14 | } 15 | 16 | impl App { 17 | pub fn new(cfg: Config, tasks: Vec) -> App { 18 | let tasks = tasks 19 | .into_iter() 20 | .filter(|task| !task.internal || cfg.list_internal) 21 | .collect(); 22 | 23 | App { 24 | cfg, 25 | tasks: StatefulList::with_items(tasks), 26 | search: String::new(), 27 | input_mode: InputMode::Select, 28 | should_quit: false, 29 | task_to_exec: None, 30 | } 31 | } 32 | 33 | pub fn quit(&mut self) { 34 | self.should_quit = true; 35 | } 36 | } 37 | 38 | pub enum InputMode { 39 | Select, 40 | Search, 41 | Preview, 42 | } 43 | 44 | pub struct StatefulList { 45 | pub state: ListState, 46 | pub items: Vec, 47 | orig_items: Vec, 48 | last_selected: Option, 49 | } 50 | 51 | #[derive(Clone)] 52 | pub struct StatefulListItem { 53 | pub item: Task, 54 | } 55 | 56 | impl StatefulList { 57 | fn with_items(items: Vec) -> StatefulList { 58 | let list_items: Vec = items 59 | .into_iter() 60 | .map(|item| StatefulListItem { item }) 61 | .collect(); 62 | 63 | StatefulList { 64 | state: ListState::default(), 65 | orig_items: list_items.clone(), 66 | items: list_items, 67 | last_selected: None, 68 | } 69 | } 70 | 71 | pub fn next(&mut self) { 72 | if self.items.is_empty() { 73 | self.reset_selected(); 74 | return; 75 | } 76 | 77 | let i = match self.state.selected() { 78 | Some(i) => { 79 | if i >= self.items.len() - 1 { 80 | 0 81 | } else { 82 | i + 1 83 | } 84 | } 85 | None => self.last_selected.unwrap_or(0), 86 | }; 87 | 88 | self.state.select(Some(i)); 89 | } 90 | 91 | pub fn previous(&mut self) { 92 | if self.items.is_empty() { 93 | self.reset_selected(); 94 | return; 95 | } 96 | 97 | let i = match self.state.selected() { 98 | Some(i) => { 99 | if i == 0 { 100 | self.items.len() - 1 101 | } else { 102 | i - 1 103 | } 104 | } 105 | None => self.last_selected.unwrap_or(0), 106 | }; 107 | 108 | self.state.select(Some(i)); 109 | } 110 | 111 | fn reset_selected(&mut self) { 112 | self.state.select(None); 113 | self.last_selected = None; 114 | } 115 | 116 | pub fn get_selected(&mut self) -> Option { 117 | if let Some(idx) = self.state.selected() { 118 | Some(self.items[idx].item.clone()) 119 | } else { 120 | None 121 | } 122 | } 123 | 124 | pub fn filter(&mut self, search: &String) { 125 | self.items.clone_from(&self.orig_items); 126 | self.items.retain(|i| i.item.name.contains(search)); 127 | self.state = ListState::default(); 128 | 129 | if !self.items.is_empty() { 130 | self.state.select(Some(0)); 131 | } else { 132 | self.state.select(None); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/taskui/config.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use ratatui::style::Color; 4 | use std::str::FromStr; 5 | 6 | const ENV_PREFIX: &str = "TASKUI_"; 7 | 8 | pub struct Config { 9 | pub list_internal: bool, 10 | pub highlight_style_bg: Color, 11 | pub highlight_style_fg: Color, 12 | } 13 | 14 | impl Config { 15 | pub fn load() -> Config { 16 | Config { 17 | list_internal: env::var(ENV_PREFIX.to_string() + "LIST_INTERNAL") 18 | .unwrap_or("false".to_string()) 19 | .parse() 20 | .unwrap(), 21 | highlight_style_bg: env::var(ENV_PREFIX.to_string() + "HIGHLIGHT_STYLE_BG") 22 | .unwrap_or("".to_string()) 23 | .parse() 24 | .unwrap_or(Color::from_str("#ffffff").unwrap()), 25 | highlight_style_fg: env::var(ENV_PREFIX.to_string() + "HIGHLIGHT_STYLE_FG") 26 | .unwrap_or("".to_string()) 27 | .parse() 28 | .unwrap_or(Color::from_str("#4c4f69").unwrap()), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/taskui/event.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | use std::time::{Duration, Instant}; 6 | 7 | #[derive(Clone, Copy)] 8 | pub enum Event { 9 | Tick, 10 | Key(KeyEvent), 11 | Mouse(MouseEvent), 12 | Resize(u16, u16), 13 | } 14 | 15 | pub struct EventHandler { 16 | #[allow(dead_code)] 17 | sender: mpsc::Sender, 18 | receiver: mpsc::Receiver, 19 | #[allow(dead_code)] 20 | handler: thread::JoinHandle<()>, 21 | } 22 | 23 | impl EventHandler { 24 | pub fn new(tick_rate: u64) -> Self { 25 | let tick_rate = Duration::from_millis(tick_rate); 26 | let (sender, receiver) = mpsc::channel(); 27 | let handler = { 28 | let sender = sender.clone(); 29 | thread::spawn(move || { 30 | let mut last_tick = Instant::now(); 31 | loop { 32 | let timeout = tick_rate 33 | .checked_sub(last_tick.elapsed()) 34 | .unwrap_or(tick_rate); 35 | 36 | if event::poll(timeout).expect("unable to poll for event") { 37 | match event::read().expect("unable to read event") { 38 | CrosstermEvent::Key(e) => { 39 | if e.kind == event::KeyEventKind::Press { 40 | sender.send(Event::Key(e)) 41 | } else { 42 | Ok(()) // ignore KeyEventKind::Release on windows 43 | } 44 | } 45 | CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), 46 | CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 47 | _ => unimplemented!(), 48 | } 49 | .expect("failed to send terminal event") 50 | } 51 | 52 | if last_tick.elapsed() >= tick_rate { 53 | sender.send(Event::Tick).expect("failed to send tick event"); 54 | last_tick = Instant::now(); 55 | } 56 | } 57 | }) 58 | }; 59 | Self { 60 | sender, 61 | receiver, 62 | handler, 63 | } 64 | } 65 | 66 | pub fn next(&self) -> Result { 67 | Ok(self.receiver.recv()?) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/taskui/mod.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | pub use self::app::App; 3 | 4 | mod config; 5 | pub use self::config::Config; 6 | 7 | pub mod event; 8 | 9 | mod update; 10 | pub use self::update::update; 11 | 12 | pub mod terminal; 13 | 14 | mod ui; 15 | -------------------------------------------------------------------------------- /src/taskui/terminal.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::terminal; 3 | use crossterm::{ 4 | event::{DisableMouseCapture, EnableMouseCapture}, 5 | terminal::{EnterAlternateScreen, LeaveAlternateScreen}, 6 | }; 7 | use ratatui::backend::CrosstermBackend; 8 | use ratatui::Terminal; 9 | use std::io::Stderr; 10 | use std::{io, panic}; 11 | 12 | use super::event::EventHandler; 13 | use super::{app, ui}; 14 | 15 | pub type CrosstermTerminal = Terminal>; 16 | 17 | pub struct UserInterface { 18 | terminal: CrosstermTerminal, 19 | pub events: EventHandler, 20 | } 21 | 22 | impl UserInterface { 23 | pub fn new(terminal: CrosstermTerminal, events: EventHandler) -> Self { 24 | Self { terminal, events } 25 | } 26 | 27 | pub fn draw(&mut self, state: &mut app::App) -> Result<()> { 28 | self.terminal.draw(|frame| ui::render(frame, state))?; 29 | 30 | Ok(()) 31 | } 32 | 33 | pub fn enter(&mut self) -> Result<()> { 34 | terminal::enable_raw_mode()?; 35 | crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; 36 | 37 | let panic_hook = panic::take_hook(); 38 | panic::set_hook(Box::new(move |panic| { 39 | Self::reset().expect("failed to reset the terminal"); 40 | panic_hook(panic); 41 | })); 42 | 43 | self.terminal.hide_cursor()?; 44 | self.terminal.clear()?; 45 | 46 | Ok(()) 47 | } 48 | 49 | pub fn exit(&mut self) -> Result<()> { 50 | Self::reset()?; 51 | 52 | self.terminal.show_cursor()?; 53 | 54 | Ok(()) 55 | } 56 | 57 | fn reset() -> Result<()> { 58 | terminal::disable_raw_mode()?; 59 | 60 | crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; 61 | 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/taskui/ui.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | prelude::*, 3 | widgets::{ListItem, *}, 4 | }; 5 | 6 | use crate::taskfile::config::Task; 7 | 8 | use super::app::{App, InputMode}; 9 | 10 | pub fn render(f: &mut Frame, app: &mut App) { 11 | let mut search_chunk_size = 0; 12 | 13 | if matches!(app.input_mode, InputMode::Search) || !app.search.is_empty() { 14 | search_chunk_size = 3; 15 | } 16 | 17 | let chunks = Layout::default() 18 | .direction(Direction::Vertical) 19 | .constraints([Constraint::Length(search_chunk_size), Constraint::Min(1)]) 20 | .split(f.size()); 21 | 22 | let items: Vec = app 23 | .tasks 24 | .items 25 | .iter() 26 | .map(|i| formatted_list_item(&i.item)) 27 | .collect(); 28 | 29 | let items = List::new(items) 30 | .block(Block::default().borders(Borders::ALL).title("Tasks")) 31 | .highlight_style( 32 | Style::default() 33 | .bg(app.cfg.highlight_style_bg) 34 | .fg(app.cfg.highlight_style_fg) 35 | .add_modifier(Modifier::BOLD), 36 | ); 37 | 38 | let input = Paragraph::new(Text::from(app.search.clone())) 39 | .style(Style::default()) 40 | .block(Block::default().borders(Borders::ALL).title("Search")); 41 | 42 | f.render_widget(input, chunks[0]); 43 | 44 | f.render_stateful_widget(items, chunks[1], &mut app.tasks.state); 45 | 46 | match app.input_mode { 47 | InputMode::Search => f.set_cursor(1 + app.search.len() as u16, 1), 48 | InputMode::Preview => render_preview(f, app), 49 | _ => {} 50 | } 51 | } 52 | 53 | fn formatted_list_item(task: &Task) -> ListItem<'_> { 54 | if task.internal { 55 | ListItem::new(task.name.as_str().to_owned() + " (internal)") 56 | .style(Style::default().fg(Color::DarkGray)) 57 | } else { 58 | ListItem::new(task.name.as_str()).style(Style::default()) 59 | } 60 | } 61 | 62 | pub fn render_preview(f: &mut Frame, app: &mut App) { 63 | let selected_task = app.tasks.get_selected().unwrap(); 64 | let area = centered_rect(70, 90, f.size()); 65 | let paragraph = Paragraph::new(selected_task.body.as_str()) 66 | .alignment(Alignment::Left) 67 | .style(Style::default()) 68 | .block(Block::default().borders(Borders::ALL).title(format!( 69 | "Preview: {}", 70 | selected_task.name.split(':').last().unwrap() 71 | ))); 72 | 73 | f.render_widget(Clear, area); 74 | f.render_widget(paragraph, area); 75 | } 76 | 77 | fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 78 | let popup_layout = Layout::default() 79 | .direction(Direction::Vertical) 80 | .constraints([ 81 | Constraint::Percentage((100 - percent_y) / 2), 82 | Constraint::Percentage(percent_y), 83 | Constraint::Percentage((100 - percent_y) / 2), 84 | ]) 85 | .split(r); 86 | 87 | Layout::default() 88 | .direction(Direction::Horizontal) 89 | .constraints([ 90 | Constraint::Percentage((100 - percent_x) / 2), 91 | Constraint::Percentage(percent_x), 92 | Constraint::Percentage((100 - percent_x) / 2), 93 | ]) 94 | .split(popup_layout[1])[1] 95 | } 96 | -------------------------------------------------------------------------------- /src/taskui/update.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyEvent}; 2 | 3 | use super::app::{App, InputMode}; 4 | 5 | pub fn update(app: &mut App, key_event: KeyEvent) { 6 | match app.input_mode { 7 | InputMode::Select => match key_event.code { 8 | KeyCode::Char('q') => app.quit(), 9 | KeyCode::Char('p') => { 10 | if app.tasks.get_selected().is_some() { 11 | app.input_mode = InputMode::Preview; 12 | } 13 | } 14 | KeyCode::Enter => { 15 | if app.tasks.get_selected().is_some() { 16 | app.task_to_exec = app.tasks.get_selected(); 17 | app.quit() 18 | } 19 | } 20 | KeyCode::Down | KeyCode::Char('j') => app.tasks.next(), 21 | KeyCode::Up | KeyCode::Char('k') => app.tasks.previous(), 22 | KeyCode::Char('/') => app.input_mode = InputMode::Search, 23 | _ => {} 24 | }, 25 | InputMode::Search => match key_event.code { 26 | KeyCode::Char(c) => { 27 | app.search.push(c); 28 | app.tasks.filter(&app.search); 29 | } 30 | KeyCode::Backspace => { 31 | _ = app.search.pop(); 32 | app.tasks.filter(&app.search); 33 | } 34 | KeyCode::Esc => { 35 | app.search = String::new(); 36 | app.tasks.filter(&app.search); 37 | app.input_mode = InputMode::Select; 38 | } 39 | KeyCode::Enter => app.input_mode = InputMode::Select, 40 | _ => {} 41 | }, 42 | InputMode::Preview => match key_event.code { 43 | KeyCode::Char('q') | KeyCode::Char('p') => app.input_mode = InputMode::Select, 44 | _ => {} 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/trace.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use directories::ProjectDirs; 5 | use lazy_static::lazy_static; 6 | use tracing_error::ErrorLayer; 7 | use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer}; 8 | 9 | lazy_static! { 10 | pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); 11 | pub static ref DATA_FOLDER: Option = 12 | std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) 13 | .ok() 14 | .map(PathBuf::from); 15 | pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); 16 | pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); 17 | } 18 | 19 | fn project_directory() -> Option { 20 | ProjectDirs::from("de", "thmshmm", env!("CARGO_PKG_NAME")) 21 | } 22 | 23 | pub fn get_data_dir() -> PathBuf { 24 | let directory = if let Some(s) = DATA_FOLDER.clone() { 25 | s 26 | } else if let Some(proj_dirs) = project_directory() { 27 | proj_dirs.data_local_dir().to_path_buf() 28 | } else { 29 | PathBuf::from(".").join(".data") 30 | }; 31 | directory 32 | } 33 | 34 | pub fn initialize_logging() -> Result<()> { 35 | let directory = get_data_dir(); 36 | std::fs::create_dir_all(directory.clone())?; 37 | let log_path = directory.join(LOG_FILE.clone()); 38 | let log_file = std::fs::File::create(log_path)?; 39 | std::env::set_var( 40 | "RUST_LOG", 41 | std::env::var("RUST_LOG") 42 | .or_else(|_| std::env::var(LOG_ENV.clone())) 43 | .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))), 44 | ); 45 | let file_subscriber = tracing_subscriber::fmt::layer() 46 | .with_file(true) 47 | .with_line_number(true) 48 | .with_writer(log_file) 49 | .with_target(false) 50 | .with_ansi(false) 51 | .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); 52 | tracing_subscriber::registry() 53 | .with(file_subscriber) 54 | .with(ErrorLayer::default()) 55 | .init(); 56 | Ok(()) 57 | } 58 | 59 | #[macro_export] 60 | macro_rules! trace_dbg { 61 | (target: $target:expr, level: $level:expr, $ex:expr) => {{ 62 | match $ex { 63 | value => { 64 | tracing::event!(target: $target, $level, ?value, stringify!($ex)); 65 | value 66 | } 67 | } 68 | }}; 69 | (level: $level:expr, $ex:expr) => { 70 | trace_dbg!(target: module_path!(), level: $level, $ex) 71 | }; 72 | (target: $target:expr, $ex:expr) => { 73 | trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex) 74 | }; 75 | ($ex:expr) => { 76 | trace_dbg!(level: tracing::Level::DEBUG, $ex) 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /taskui-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thmshmm/taskui/e0912f28e32f2b8798befbec24a23d72fd698b6d/taskui-example.png -------------------------------------------------------------------------------- /test/Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | k8s: ./k8s.yml 5 | docker: ./docker 6 | helm: 7 | taskfile: ./docker/helm.yml 8 | kind: 9 | taskfile: kind.yml 10 | optional: true 11 | podman: 12 | taskfile: ./podman.yml 13 | internal: true 14 | 15 | tasks: 16 | uptime: 17 | - uptime 18 | date: 19 | - date 20 | -------------------------------------------------------------------------------- /test/docker/Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | task1: 5 | - echo "docker t1" 6 | task2: 7 | - echo "docker t2" 8 | -------------------------------------------------------------------------------- /test/docker/helm.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | tiller: ./tiller.yml 5 | 6 | tasks: 7 | task1: 8 | - echo "helm t1" 9 | task2: 10 | - echo "helm t2" 11 | -------------------------------------------------------------------------------- /test/docker/tiller.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | task1: 5 | - echo "tiller t1" 6 | -------------------------------------------------------------------------------- /test/k8s.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | kubectl: ./kubectl.yml 5 | 6 | tasks: 7 | task1: 8 | - echo "k8s t1" 9 | task2: 10 | - echo "k8s t2" 11 | task3: 12 | internal: true 13 | cmd: echo "k8s t3" 14 | -------------------------------------------------------------------------------- /test/kubectl.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | task1: 5 | - echo "kubectl t1" 6 | task2: 7 | - echo "kubectl t2" 8 | -------------------------------------------------------------------------------- /test/podman.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | task1: 5 | - echo "podman t1" 6 | task2: 7 | - echo "podman t2" 8 | --------------------------------------------------------------------------------