Demonstrating core cloud concepts, starting with CaaS. Not for production use.

feat: tiny TUI frontend

Changed files
+1368
frontend
+1
frontend/.gitignore
··· 1 + target
+575
frontend/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "allocator-api2" 7 + version = "0.2.21" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 10 + 11 + [[package]] 12 + name = "bitflags" 13 + version = "2.10.0" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 16 + 17 + [[package]] 18 + name = "cassowary" 19 + version = "0.3.0" 20 + source = "registry+https://github.com/rust-lang/crates.io-index" 21 + checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 22 + 23 + [[package]] 24 + name = "castaway" 25 + version = "0.2.4" 26 + source = "registry+https://github.com/rust-lang/crates.io-index" 27 + checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" 28 + dependencies = [ 29 + "rustversion", 30 + ] 31 + 32 + [[package]] 33 + name = "cfg-if" 34 + version = "1.0.4" 35 + source = "registry+https://github.com/rust-lang/crates.io-index" 36 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 37 + 38 + [[package]] 39 + name = "compact_str" 40 + version = "0.7.1" 41 + source = "registry+https://github.com/rust-lang/crates.io-index" 42 + checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 43 + dependencies = [ 44 + "castaway", 45 + "cfg-if", 46 + "itoa", 47 + "ryu", 48 + "static_assertions", 49 + ] 50 + 51 + [[package]] 52 + name = "crossterm" 53 + version = "0.27.0" 54 + source = "registry+https://github.com/rust-lang/crates.io-index" 55 + checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 56 + dependencies = [ 57 + "bitflags", 58 + "crossterm_winapi", 59 + "libc", 60 + "mio", 61 + "parking_lot", 62 + "signal-hook", 63 + "signal-hook-mio", 64 + "winapi", 65 + ] 66 + 67 + [[package]] 68 + name = "crossterm_winapi" 69 + version = "0.9.1" 70 + source = "registry+https://github.com/rust-lang/crates.io-index" 71 + checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 72 + dependencies = [ 73 + "winapi", 74 + ] 75 + 76 + [[package]] 77 + name = "either" 78 + version = "1.15.0" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 81 + 82 + [[package]] 83 + name = "equivalent" 84 + version = "1.0.2" 85 + source = "registry+https://github.com/rust-lang/crates.io-index" 86 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 87 + 88 + [[package]] 89 + name = "foldhash" 90 + version = "0.1.5" 91 + source = "registry+https://github.com/rust-lang/crates.io-index" 92 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 93 + 94 + [[package]] 95 + name = "frontend" 96 + version = "0.1.0" 97 + dependencies = [ 98 + "crossterm", 99 + "rand", 100 + "ratatui", 101 + ] 102 + 103 + [[package]] 104 + name = "getrandom" 105 + version = "0.3.4" 106 + source = "registry+https://github.com/rust-lang/crates.io-index" 107 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 108 + dependencies = [ 109 + "cfg-if", 110 + "libc", 111 + "r-efi", 112 + "wasip2", 113 + ] 114 + 115 + [[package]] 116 + name = "hashbrown" 117 + version = "0.15.5" 118 + source = "registry+https://github.com/rust-lang/crates.io-index" 119 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 120 + dependencies = [ 121 + "allocator-api2", 122 + "equivalent", 123 + "foldhash", 124 + ] 125 + 126 + [[package]] 127 + name = "heck" 128 + version = "0.5.0" 129 + source = "registry+https://github.com/rust-lang/crates.io-index" 130 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 131 + 132 + [[package]] 133 + name = "itertools" 134 + version = "0.13.0" 135 + source = "registry+https://github.com/rust-lang/crates.io-index" 136 + checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 137 + dependencies = [ 138 + "either", 139 + ] 140 + 141 + [[package]] 142 + name = "itoa" 143 + version = "1.0.15" 144 + source = "registry+https://github.com/rust-lang/crates.io-index" 145 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 146 + 147 + [[package]] 148 + name = "libc" 149 + version = "0.2.177" 150 + source = "registry+https://github.com/rust-lang/crates.io-index" 151 + checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 152 + 153 + [[package]] 154 + name = "lock_api" 155 + version = "0.4.14" 156 + source = "registry+https://github.com/rust-lang/crates.io-index" 157 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 158 + dependencies = [ 159 + "scopeguard", 160 + ] 161 + 162 + [[package]] 163 + name = "log" 164 + version = "0.4.28" 165 + source = "registry+https://github.com/rust-lang/crates.io-index" 166 + checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 167 + 168 + [[package]] 169 + name = "lru" 170 + version = "0.12.5" 171 + source = "registry+https://github.com/rust-lang/crates.io-index" 172 + checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 173 + dependencies = [ 174 + "hashbrown", 175 + ] 176 + 177 + [[package]] 178 + name = "mio" 179 + version = "0.8.11" 180 + source = "registry+https://github.com/rust-lang/crates.io-index" 181 + checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 182 + dependencies = [ 183 + "libc", 184 + "log", 185 + "wasi", 186 + "windows-sys", 187 + ] 188 + 189 + [[package]] 190 + name = "parking_lot" 191 + version = "0.12.5" 192 + source = "registry+https://github.com/rust-lang/crates.io-index" 193 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 194 + dependencies = [ 195 + "lock_api", 196 + "parking_lot_core", 197 + ] 198 + 199 + [[package]] 200 + name = "parking_lot_core" 201 + version = "0.9.12" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 204 + dependencies = [ 205 + "cfg-if", 206 + "libc", 207 + "redox_syscall", 208 + "smallvec", 209 + "windows-link", 210 + ] 211 + 212 + [[package]] 213 + name = "paste" 214 + version = "1.0.15" 215 + source = "registry+https://github.com/rust-lang/crates.io-index" 216 + checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 217 + 218 + [[package]] 219 + name = "ppv-lite86" 220 + version = "0.2.21" 221 + source = "registry+https://github.com/rust-lang/crates.io-index" 222 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 223 + dependencies = [ 224 + "zerocopy", 225 + ] 226 + 227 + [[package]] 228 + name = "proc-macro2" 229 + version = "1.0.103" 230 + source = "registry+https://github.com/rust-lang/crates.io-index" 231 + checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 232 + dependencies = [ 233 + "unicode-ident", 234 + ] 235 + 236 + [[package]] 237 + name = "quote" 238 + version = "1.0.41" 239 + source = "registry+https://github.com/rust-lang/crates.io-index" 240 + checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 241 + dependencies = [ 242 + "proc-macro2", 243 + ] 244 + 245 + [[package]] 246 + name = "r-efi" 247 + version = "5.3.0" 248 + source = "registry+https://github.com/rust-lang/crates.io-index" 249 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 250 + 251 + [[package]] 252 + name = "rand" 253 + version = "0.9.2" 254 + source = "registry+https://github.com/rust-lang/crates.io-index" 255 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 256 + dependencies = [ 257 + "rand_chacha", 258 + "rand_core", 259 + ] 260 + 261 + [[package]] 262 + name = "rand_chacha" 263 + version = "0.9.0" 264 + source = "registry+https://github.com/rust-lang/crates.io-index" 265 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 266 + dependencies = [ 267 + "ppv-lite86", 268 + "rand_core", 269 + ] 270 + 271 + [[package]] 272 + name = "rand_core" 273 + version = "0.9.3" 274 + source = "registry+https://github.com/rust-lang/crates.io-index" 275 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 276 + dependencies = [ 277 + "getrandom", 278 + ] 279 + 280 + [[package]] 281 + name = "ratatui" 282 + version = "0.27.0" 283 + source = "registry+https://github.com/rust-lang/crates.io-index" 284 + checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" 285 + dependencies = [ 286 + "bitflags", 287 + "cassowary", 288 + "compact_str", 289 + "crossterm", 290 + "itertools", 291 + "lru", 292 + "paste", 293 + "stability", 294 + "strum", 295 + "strum_macros", 296 + "unicode-segmentation", 297 + "unicode-truncate", 298 + "unicode-width", 299 + ] 300 + 301 + [[package]] 302 + name = "redox_syscall" 303 + version = "0.5.18" 304 + source = "registry+https://github.com/rust-lang/crates.io-index" 305 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 306 + dependencies = [ 307 + "bitflags", 308 + ] 309 + 310 + [[package]] 311 + name = "rustversion" 312 + version = "1.0.22" 313 + source = "registry+https://github.com/rust-lang/crates.io-index" 314 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 315 + 316 + [[package]] 317 + name = "ryu" 318 + version = "1.0.20" 319 + source = "registry+https://github.com/rust-lang/crates.io-index" 320 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 321 + 322 + [[package]] 323 + name = "scopeguard" 324 + version = "1.2.0" 325 + source = "registry+https://github.com/rust-lang/crates.io-index" 326 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 327 + 328 + [[package]] 329 + name = "signal-hook" 330 + version = "0.3.18" 331 + source = "registry+https://github.com/rust-lang/crates.io-index" 332 + checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 333 + dependencies = [ 334 + "libc", 335 + "signal-hook-registry", 336 + ] 337 + 338 + [[package]] 339 + name = "signal-hook-mio" 340 + version = "0.2.5" 341 + source = "registry+https://github.com/rust-lang/crates.io-index" 342 + checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" 343 + dependencies = [ 344 + "libc", 345 + "mio", 346 + "signal-hook", 347 + ] 348 + 349 + [[package]] 350 + name = "signal-hook-registry" 351 + version = "1.4.6" 352 + source = "registry+https://github.com/rust-lang/crates.io-index" 353 + checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 354 + dependencies = [ 355 + "libc", 356 + ] 357 + 358 + [[package]] 359 + name = "smallvec" 360 + version = "1.15.1" 361 + source = "registry+https://github.com/rust-lang/crates.io-index" 362 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 363 + 364 + [[package]] 365 + name = "stability" 366 + version = "0.2.1" 367 + source = "registry+https://github.com/rust-lang/crates.io-index" 368 + checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" 369 + dependencies = [ 370 + "quote", 371 + "syn", 372 + ] 373 + 374 + [[package]] 375 + name = "static_assertions" 376 + version = "1.1.0" 377 + source = "registry+https://github.com/rust-lang/crates.io-index" 378 + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 379 + 380 + [[package]] 381 + name = "strum" 382 + version = "0.26.3" 383 + source = "registry+https://github.com/rust-lang/crates.io-index" 384 + checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 385 + dependencies = [ 386 + "strum_macros", 387 + ] 388 + 389 + [[package]] 390 + name = "strum_macros" 391 + version = "0.26.4" 392 + source = "registry+https://github.com/rust-lang/crates.io-index" 393 + checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 394 + dependencies = [ 395 + "heck", 396 + "proc-macro2", 397 + "quote", 398 + "rustversion", 399 + "syn", 400 + ] 401 + 402 + [[package]] 403 + name = "syn" 404 + version = "2.0.109" 405 + source = "registry+https://github.com/rust-lang/crates.io-index" 406 + checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" 407 + dependencies = [ 408 + "proc-macro2", 409 + "quote", 410 + "unicode-ident", 411 + ] 412 + 413 + [[package]] 414 + name = "unicode-ident" 415 + version = "1.0.22" 416 + source = "registry+https://github.com/rust-lang/crates.io-index" 417 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 418 + 419 + [[package]] 420 + name = "unicode-segmentation" 421 + version = "1.12.0" 422 + source = "registry+https://github.com/rust-lang/crates.io-index" 423 + checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 424 + 425 + [[package]] 426 + name = "unicode-truncate" 427 + version = "1.1.0" 428 + source = "registry+https://github.com/rust-lang/crates.io-index" 429 + checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 430 + dependencies = [ 431 + "itertools", 432 + "unicode-segmentation", 433 + "unicode-width", 434 + ] 435 + 436 + [[package]] 437 + name = "unicode-width" 438 + version = "0.1.14" 439 + source = "registry+https://github.com/rust-lang/crates.io-index" 440 + checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 441 + 442 + [[package]] 443 + name = "wasi" 444 + version = "0.11.1+wasi-snapshot-preview1" 445 + source = "registry+https://github.com/rust-lang/crates.io-index" 446 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 447 + 448 + [[package]] 449 + name = "wasip2" 450 + version = "1.0.1+wasi-0.2.4" 451 + source = "registry+https://github.com/rust-lang/crates.io-index" 452 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 453 + dependencies = [ 454 + "wit-bindgen", 455 + ] 456 + 457 + [[package]] 458 + name = "winapi" 459 + version = "0.3.9" 460 + source = "registry+https://github.com/rust-lang/crates.io-index" 461 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 462 + dependencies = [ 463 + "winapi-i686-pc-windows-gnu", 464 + "winapi-x86_64-pc-windows-gnu", 465 + ] 466 + 467 + [[package]] 468 + name = "winapi-i686-pc-windows-gnu" 469 + version = "0.4.0" 470 + source = "registry+https://github.com/rust-lang/crates.io-index" 471 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 472 + 473 + [[package]] 474 + name = "winapi-x86_64-pc-windows-gnu" 475 + version = "0.4.0" 476 + source = "registry+https://github.com/rust-lang/crates.io-index" 477 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 478 + 479 + [[package]] 480 + name = "windows-link" 481 + version = "0.2.1" 482 + source = "registry+https://github.com/rust-lang/crates.io-index" 483 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 484 + 485 + [[package]] 486 + name = "windows-sys" 487 + version = "0.48.0" 488 + source = "registry+https://github.com/rust-lang/crates.io-index" 489 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 490 + dependencies = [ 491 + "windows-targets", 492 + ] 493 + 494 + [[package]] 495 + name = "windows-targets" 496 + version = "0.48.5" 497 + source = "registry+https://github.com/rust-lang/crates.io-index" 498 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 499 + dependencies = [ 500 + "windows_aarch64_gnullvm", 501 + "windows_aarch64_msvc", 502 + "windows_i686_gnu", 503 + "windows_i686_msvc", 504 + "windows_x86_64_gnu", 505 + "windows_x86_64_gnullvm", 506 + "windows_x86_64_msvc", 507 + ] 508 + 509 + [[package]] 510 + name = "windows_aarch64_gnullvm" 511 + version = "0.48.5" 512 + source = "registry+https://github.com/rust-lang/crates.io-index" 513 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 514 + 515 + [[package]] 516 + name = "windows_aarch64_msvc" 517 + version = "0.48.5" 518 + source = "registry+https://github.com/rust-lang/crates.io-index" 519 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 520 + 521 + [[package]] 522 + name = "windows_i686_gnu" 523 + version = "0.48.5" 524 + source = "registry+https://github.com/rust-lang/crates.io-index" 525 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 526 + 527 + [[package]] 528 + name = "windows_i686_msvc" 529 + version = "0.48.5" 530 + source = "registry+https://github.com/rust-lang/crates.io-index" 531 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 532 + 533 + [[package]] 534 + name = "windows_x86_64_gnu" 535 + version = "0.48.5" 536 + source = "registry+https://github.com/rust-lang/crates.io-index" 537 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 538 + 539 + [[package]] 540 + name = "windows_x86_64_gnullvm" 541 + version = "0.48.5" 542 + source = "registry+https://github.com/rust-lang/crates.io-index" 543 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 544 + 545 + [[package]] 546 + name = "windows_x86_64_msvc" 547 + version = "0.48.5" 548 + source = "registry+https://github.com/rust-lang/crates.io-index" 549 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 550 + 551 + [[package]] 552 + name = "wit-bindgen" 553 + version = "0.46.0" 554 + source = "registry+https://github.com/rust-lang/crates.io-index" 555 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 556 + 557 + [[package]] 558 + name = "zerocopy" 559 + version = "0.8.27" 560 + source = "registry+https://github.com/rust-lang/crates.io-index" 561 + checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 562 + dependencies = [ 563 + "zerocopy-derive", 564 + ] 565 + 566 + [[package]] 567 + name = "zerocopy-derive" 568 + version = "0.8.27" 569 + source = "registry+https://github.com/rust-lang/crates.io-index" 570 + checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 571 + dependencies = [ 572 + "proc-macro2", 573 + "quote", 574 + "syn", 575 + ]
+9
frontend/Cargo.toml
··· 1 + [package] 2 + name = "frontend" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + crossterm = "0.27.0" 8 + rand = "0.9.2" 9 + ratatui = { version = "0.27.0", features = ["crossterm"] }
+783
frontend/src/main.rs
··· 1 + use crossterm::{ 2 + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, 3 + execute, 4 + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 5 + }; 6 + use rand::Rng; 7 + use ratatui::{ 8 + prelude::*, 9 + widgets::{ 10 + Axis, Block, Borders, Cell, Chart, Clear, Dataset, GraphType, List, ListItem, ListState, 11 + Paragraph, Row, Table, TableState, Tabs, 12 + }, 13 + }; 14 + use std::{ 15 + error::Error, 16 + io::{self, Stdout}, 17 + time::{Duration, Instant}, 18 + }; 19 + 20 + const MAX_DATA_POINTS: usize = 100; 21 + 22 + struct Pod { 23 + id: String, 24 + name: String, 25 + image: String, 26 + sidecars: Vec<String>, 27 + status: Status, 28 + cpu_history: Vec<(f64, f64)>, 29 + mem_history: Vec<(f64, f64)>, 30 + } 31 + 32 + #[derive(Clone, Copy, PartialEq, Eq)] 33 + enum Status { 34 + Running, 35 + Starting, 36 + Stopped, 37 + Error, 38 + } 39 + 40 + impl Status { 41 + fn as_str(&self) -> &'static str { 42 + match self { 43 + Status::Running => "Running", 44 + Status::Starting => "Starting", 45 + Status::Stopped => "Stopped", 46 + Status::Error => "Error", 47 + } 48 + } 49 + 50 + fn to_style(&self) -> Style { 51 + match self { 52 + Status::Running => Style::default().fg(Color::Green), 53 + Status::Starting => Style::default().fg(Color::Yellow), 54 + Status::Stopped => Style::default().fg(Color::Gray), 55 + Status::Error => Style::default().fg(Color::Red), 56 + } 57 + } 58 + } 59 + 60 + enum ActiveView { 61 + Pods, 62 + Account, 63 + Workspace, 64 + CreatePod, 65 + } 66 + 67 + enum ActiveModal { 68 + None, 69 + PodActions, 70 + } 71 + 72 + struct CreatePodForm { 73 + name: String, 74 + image_link: String, 75 + sidecars: String, 76 + focused_field: usize, 77 + } 78 + 79 + impl CreatePodForm { 80 + fn new() -> Self { 81 + Self { 82 + name: String::new(), 83 + image_link: String::new(), 84 + sidecars: String::new(), 85 + focused_field: 0, 86 + } 87 + } 88 + 89 + fn next_field(&mut self) { 90 + self.focused_field = (self.focused_field + 1) % 3; 91 + } 92 + 93 + fn prev_field(&mut self) { 94 + self.focused_field = (self.focused_field + 3 - 1) % 3; 95 + } 96 + 97 + fn get_active_field_mut(&mut self) -> &mut String { 98 + match self.focused_field { 99 + 0 => &mut self.name, 100 + 1 => &mut self.image_link, 101 + 2 => &mut self.sidecars, 102 + _ => unreachable!(), 103 + } 104 + } 105 + } 106 + 107 + struct App { 108 + should_quit: bool, 109 + active_view: ActiveView, 110 + active_modal: ActiveModal, 111 + tab_index: usize, 112 + pods: Vec<Pod>, 113 + pod_table_state: TableState, 114 + action_list_state: ListState, 115 + account_list_state: ListState, 116 + workspace_list_state: ListState, 117 + create_pod_form: CreatePodForm, 118 + data_tick_count: u64, 119 + } 120 + 121 + impl App { 122 + fn new() -> Self { 123 + let mut pod_table_state = TableState::default(); 124 + pod_table_state.select(Some(0)); 125 + let mut action_list_state = ListState::default(); 126 + action_list_state.select(Some(0)); 127 + let mut account_list_state = ListState::default(); 128 + account_list_state.select(Some(0)); 129 + let mut workspace_list_state = ListState::default(); 130 + workspace_list_state.select(Some(0)); 131 + 132 + App { 133 + should_quit: false, 134 + active_view: ActiveView::Pods, 135 + active_modal: ActiveModal::None, 136 + tab_index: 0, 137 + pods: vec![ 138 + Pod { 139 + id: "pod-uuid-1234".to_string(), 140 + name: "user-auth".to_string(), 141 + image: "ghcr.io/my-org/user-auth:latest".to_string(), 142 + sidecars: vec![], 143 + status: Status::Running, 144 + cpu_history: Vec::new(), 145 + mem_history: Vec::new(), 146 + }, 147 + Pod { 148 + id: "pod-uuid-5678".to_string(), 149 + name: "postgres-db".to_string(), 150 + image: "postgres:15-alpine".to_string(), 151 + sidecars: vec![], 152 + status: Status::Running, 153 + cpu_history: Vec::new(), 154 + mem_history: Vec::new(), 155 + }, 156 + Pod { 157 + id: "pod-uuid-9012".to_string(), 158 + name: "payment-processor".to_string(), 159 + image: "ghcr.io/my-org/payments:1.2.0".to_string(), 160 + sidecars: vec!["ghcr.io/my-org/cloud-sql-proxy".to_string()], 161 + status: Status::Error, 162 + cpu_history: Vec::new(), 163 + mem_history: Vec::new(), 164 + }, 165 + Pod { 166 + id: "pod-uuid-3456".to_string(), 167 + name: "redis-cache".to_string(), 168 + image: "redis:7-alpine".to_string(), 169 + sidecars: vec![], 170 + status: Status::Stopped, 171 + cpu_history: Vec::new(), 172 + mem_history: Vec::new(), 173 + }, 174 + ], 175 + pod_table_state, 176 + action_list_state, 177 + account_list_state, 178 + workspace_list_state, 179 + create_pod_form: CreatePodForm::new(), 180 + data_tick_count: 0, 181 + } 182 + } 183 + 184 + fn on_key(&mut self, key: KeyCode) { 185 + if let ActiveModal::PodActions = self.active_modal { 186 + self.handle_modal_key(key); 187 + return; 188 + } 189 + 190 + match key { 191 + KeyCode::Char('q') => self.should_quit = true, 192 + KeyCode::Char('1') => { 193 + self.tab_index = 0; 194 + self.active_view = ActiveView::Pods; 195 + } 196 + KeyCode::Char('2') => { 197 + self.tab_index = 1; 198 + self.active_view = ActiveView::Account; 199 + } 200 + KeyCode::Char('3') => { 201 + self.tab_index = 2; 202 + self.active_view = ActiveView::Workspace; 203 + } 204 + _ => {} 205 + } 206 + 207 + match self.active_view { 208 + ActiveView::Pods => { 209 + match key { 210 + KeyCode::Down | KeyCode::Char('j') => self.select_next_pod(), 211 + KeyCode::Up | KeyCode::Char('k') => self.select_previous_pod(), 212 + KeyCode::Char('m') => { 213 + self.action_list_state.select(Some(0)); 214 + self.active_modal = ActiveModal::PodActions; 215 + } 216 + KeyCode::Char('c') => { 217 + self.create_pod_form = CreatePodForm::new(); 218 + self.active_view = ActiveView::CreatePod; 219 + } 220 + _ => {} 221 + } 222 + } 223 + ActiveView::Account => { 224 + let item_count = 3; 225 + let i = match self.account_list_state.selected() { 226 + Some(i) => i, 227 + None => 0, 228 + }; 229 + match key { 230 + KeyCode::Down | KeyCode::Char('j') => { 231 + self.account_list_state.select(Some((i + 1) % item_count)); 232 + } 233 + KeyCode::Up | KeyCode::Char('k') => { 234 + self.account_list_state.select(Some((i + item_count - 1) % item_count)); 235 + } 236 + _ => {} 237 + } 238 + } 239 + ActiveView::Workspace => { 240 + let item_count = 3; 241 + let i = match self.workspace_list_state.selected() { 242 + Some(i) => i, 243 + None => 0, 244 + }; 245 + match key { 246 + KeyCode::Down | KeyCode::Char('j') => { 247 + self.workspace_list_state.select(Some((i + 1) % item_count)); 248 + } 249 + KeyCode::Up | KeyCode::Char('k') => { 250 + self.workspace_list_state.select(Some((i + item_count - 1) % item_count)); 251 + } 252 + _ => {} 253 + } 254 + } 255 + ActiveView::CreatePod => { 256 + match key { 257 + KeyCode::Esc => { 258 + self.active_view = ActiveView::Pods; 259 + } 260 + KeyCode::Down | KeyCode::Tab => { 261 + self.create_pod_form.next_field(); 262 + } 263 + KeyCode::Up => { 264 + self.create_pod_form.prev_field(); 265 + } 266 + KeyCode::Char(c) => { 267 + self.create_pod_form.get_active_field_mut().push(c); 268 + } 269 + KeyCode::Backspace => { 270 + self.create_pod_form.get_active_field_mut().pop(); 271 + } 272 + KeyCode::Enter => { 273 + if self.create_pod_form.focused_field == 2 { 274 + // Last field, create pod 275 + let form = &self.create_pod_form; 276 + let new_pod = Pod { 277 + id: format!("pod-uuid-{}", rand::rng().random_range(10000..99999)), 278 + name: form.name.clone(), 279 + image: form.image_link.clone(), 280 + sidecars: form 281 + .sidecars 282 + .split(',') 283 + .map(|s| s.trim().to_string()) 284 + .filter(|s| !s.is_empty()) 285 + .collect(), 286 + status: Status::Starting, 287 + cpu_history: Vec::new(), 288 + mem_history: Vec::new(), 289 + }; 290 + self.pods.push(new_pod); 291 + self.create_pod_form = CreatePodForm::new(); // Reset form 292 + self.active_view = ActiveView::Pods; 293 + } else { 294 + self.create_pod_form.next_field(); 295 + } 296 + } 297 + _ => {} 298 + } 299 + } 300 + } 301 + } 302 + 303 + fn handle_modal_key(&mut self, key: KeyCode) { 304 + match self.active_modal { 305 + ActiveModal::PodActions => { 306 + let item_count = 3; // "Resize", "Stop", "Delete" 307 + match key { 308 + KeyCode::Up | KeyCode::Char('k') => { 309 + self.select_previous_action(item_count); 310 + } 311 + KeyCode::Down | KeyCode::Char('j') => { 312 + self.select_next_action(item_count); 313 + } 314 + KeyCode::Esc => { 315 + self.active_modal = ActiveModal::None; 316 + } 317 + KeyCode::Enter => { 318 + // TODO: Implement action dispatch based on self.action_list_state.selected() 319 + self.active_modal = ActiveModal::None; 320 + } 321 + _ => {} 322 + } 323 + } 324 + ActiveModal::None => {} // Should be unreachable 325 + } 326 + } 327 + 328 + fn select_next_pod(&mut self) { 329 + let i = match self.pod_table_state.selected() { 330 + Some(i) => { 331 + if i >= self.pods.len() - 1 { 332 + 0 333 + } else { 334 + i + 1 335 + } 336 + } 337 + None => 0, 338 + }; 339 + self.pod_table_state.select(Some(i)); 340 + } 341 + 342 + fn select_previous_pod(&mut self) { 343 + let i = match self.pod_table_state.selected() { 344 + Some(i) => { 345 + if i == 0 { 346 + self.pods.len() - 1 347 + } else { 348 + i - 1 349 + } 350 + } 351 + None => 0, 352 + }; 353 + self.pod_table_state.select(Some(i)); 354 + } 355 + 356 + fn select_next_action(&mut self, count: usize) { 357 + let i = match self.action_list_state.selected() { 358 + Some(i) => (i + 1) % count, 359 + None => 0, 360 + }; 361 + self.action_list_state.select(Some(i)); 362 + } 363 + 364 + fn select_previous_action(&mut self, count: usize) { 365 + let i = match self.action_list_state.selected() { 366 + Some(i) => (i + count - 1) % count, 367 + None => 0, 368 + }; 369 + self.action_list_state.select(Some(i)); 370 + } 371 + 372 + fn on_tick(&mut self) { 373 + self.data_tick_count += 1; 374 + let mut rng = rand::rng(); 375 + for pod in &mut self.pods { 376 + let new_cpu = match pod.status { 377 + Status::Running => rng.random_range(5.0..15.0) + (self.data_tick_count % 10) as f64, 378 + Status::Starting => rng.random_range(30.0..50.0), 379 + _ => 0.0, 380 + }; 381 + pod.cpu_history.push((self.data_tick_count as f64, new_cpu)); 382 + if pod.cpu_history.len() > MAX_DATA_POINTS { 383 + pod.cpu_history.remove(0); 384 + } 385 + 386 + let new_mem = match pod.status { 387 + Status::Running => rng.random_range(20.0..30.0), 388 + Status::Starting => rng.random_range(10.0..20.0), 389 + _ => 0.0, 390 + }; 391 + pod.mem_history.push((self.data_tick_count as f64, new_mem)); 392 + if pod.mem_history.len() > MAX_DATA_POINTS { 393 + pod.mem_history.remove(0); 394 + } 395 + } 396 + } 397 + } 398 + 399 + fn main() -> Result<(), Box<dyn Error>> { 400 + let mut terminal = setup_terminal()?; 401 + run(&mut terminal)?; 402 + restore_terminal(&mut terminal)?; 403 + Ok(()) 404 + } 405 + 406 + fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> { 407 + let mut stdout = io::stdout(); 408 + enable_raw_mode()?; 409 + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 410 + let backend = CrosstermBackend::new(stdout); 411 + let terminal = Terminal::new(backend)?; 412 + Ok(terminal) 413 + } 414 + 415 + fn restore_terminal( 416 + terminal: &mut Terminal<CrosstermBackend<Stdout>>, 417 + ) -> Result<(), Box<dyn Error>> { 418 + disable_raw_mode()?; 419 + execute!( 420 + terminal.backend_mut(), 421 + LeaveAlternateScreen, 422 + DisableMouseCapture 423 + )?; 424 + terminal.show_cursor()?; 425 + Ok(()) 426 + } 427 + 428 + fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> { 429 + let mut app = App::new(); 430 + let tick_rate = Duration::from_millis(250); 431 + let mut last_tick = Instant::now(); 432 + 433 + loop { 434 + terminal.draw(|f| ui(f, &mut app))?; 435 + 436 + let timeout = tick_rate 437 + .checked_sub(last_tick.elapsed()) 438 + .unwrap_or_else(|| Duration::from_secs(0)); 439 + 440 + if crossterm::event::poll(timeout)? { 441 + if let Event::Key(key) = event::read()? { 442 + if key.kind == KeyEventKind::Press { 443 + app.on_key(key.code); 444 + } 445 + } 446 + } 447 + 448 + if last_tick.elapsed() >= tick_rate { 449 + app.on_tick(); 450 + last_tick = Instant::now(); 451 + } 452 + 453 + if app.should_quit { 454 + return Ok(()); 455 + } 456 + } 457 + } 458 + 459 + fn ui(frame: &mut Frame, app: &mut App) { 460 + let main_layout = Layout::vertical([ 461 + Constraint::Length(1), 462 + Constraint::Length(3), 463 + Constraint::Min(0), 464 + Constraint::Length(1), 465 + ]) 466 + .split(frame.size()); 467 + 468 + let title = Paragraph::new("Pod Management Dashboard") 469 + .style(Style::default().fg(Color::White).bg(Color::Blue)); 470 + frame.render_widget(title, main_layout[0]); 471 + 472 + let tabs = Tabs::new(vec!["[1] Pods", "[2] Account", "[3] Workspace"]) 473 + .block(Block::default().borders(Borders::BOTTOM)) 474 + .select(app.tab_index) 475 + .style(Style::default().fg(Color::Gray)) 476 + .highlight_style(Style::default().fg(Color::Yellow).bold()); 477 + frame.render_widget(tabs, main_layout[1]); 478 + 479 + match app.active_view { 480 + ActiveView::Pods => render_pods_view(frame, app, main_layout[2]), 481 + ActiveView::Account => render_account_view(frame, app, main_layout[2]), 482 + ActiveView::Workspace => render_workspace_view(frame, app, main_layout[2]), 483 + ActiveView::CreatePod => render_create_pod_view(frame, app, main_layout[2]), 484 + } 485 + 486 + let help_text = match app.active_view { 487 + ActiveView::CreatePod => "Use 'Esc' to cancel, 'Up/Down' to navigate, 'Enter' on last field to submit.", 488 + _ => "Use 'q' to quit, '1-3' to change tabs, 'm' for menu, 'c' to create pod.", 489 + }; 490 + 491 + let help = Paragraph::new(help_text) 492 + .style(Style::default().fg(Color::Gray)); 493 + frame.render_widget(help, main_layout[3]); 494 + 495 + if let ActiveModal::None = app.active_modal { 496 + } else { 497 + render_modal(frame, app); 498 + } 499 + } 500 + 501 + fn render_pods_view(frame: &mut Frame, app: &mut App, area: Rect) { 502 + let content_layout = Layout::horizontal([ 503 + Constraint::Percentage(50), 504 + Constraint::Percentage(50), 505 + ]) 506 + .split(area); 507 + 508 + render_pod_table(frame, app, content_layout[0]); 509 + render_pod_details(frame, app, content_layout[1]); 510 + } 511 + 512 + fn render_pod_table(frame: &mut Frame, app: &mut App, area: Rect) { 513 + let header_cells = ["Name", "Status"] 514 + .iter() 515 + .map(|h| Cell::from(*h).style(Style::default().bold().fg(Color::White))); 516 + let header = Row::new(header_cells) 517 + .style(Style::default().bg(Color::DarkGray)) 518 + .height(1); 519 + 520 + let rows = app.pods.iter().map(|s| { 521 + let name_cell = Cell::from(s.name.clone()); 522 + let status_cell = 523 + Cell::from(s.status.as_str()).style(s.status.to_style()); 524 + Row::new([name_cell, status_cell]).height(1) 525 + }); 526 + 527 + let table = Table::new(rows, [ 528 + Constraint::Min(20), 529 + Constraint::Length(10), 530 + ]) 531 + .header(header) 532 + .block( 533 + Block::default() 534 + .borders(Borders::ALL) 535 + .title("Pods"), 536 + ) 537 + .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) 538 + .highlight_symbol(">> "); 539 + 540 + frame.render_stateful_widget(table, area, &mut app.pod_table_state); 541 + } 542 + 543 + fn render_pod_details(frame: &mut Frame, app: &App, area: Rect) { 544 + let block = Block::default() 545 + .borders(Borders::ALL) 546 + .title("Pod Details"); 547 + 548 + let Some(selected_index) = app.pod_table_state.selected() else { 549 + frame.render_widget(block, area); 550 + return; 551 + }; 552 + 553 + let Some(pod) = app.pods.get(selected_index) else { 554 + frame.render_widget(block, area); 555 + return; 556 + }; 557 + 558 + let inner_layout = Layout::vertical([ 559 + Constraint::Length(3), 560 + Constraint::Percentage(50), 561 + Constraint::Percentage(50), 562 + ]) 563 + .margin(1) 564 + .split(area); 565 + 566 + frame.render_widget(block, area); 567 + 568 + let text = vec![ 569 + Line::from(vec![ 570 + "ID: ".bold(), 571 + Span::raw(pod.id.clone()), 572 + ]), 573 + Line::from(vec![ 574 + "Name: ".bold(), 575 + Span::raw(pod.name.clone()), 576 + ]), 577 + ]; 578 + let details_p = Paragraph::new(text); 579 + frame.render_widget(details_p, inner_layout[0]); 580 + 581 + let min_x = if pod.cpu_history.is_empty() { 0.0 } else { pod.cpu_history[0].0 }; 582 + let max_x = if pod.cpu_history.is_empty() { 100.0 } else { app.data_tick_count as f64 }; 583 + 584 + let cpu_dataset = Dataset::default() 585 + .name("CPU %") 586 + .marker(symbols::Marker::Dot) 587 + .graph_type(GraphType::Line) 588 + .style(Style::default().fg(Color::Cyan)) 589 + .data(&pod.cpu_history); 590 + 591 + let cpu_chart = Chart::new(vec![cpu_dataset]) 592 + .block(Block::default().title("CPU Usage")) 593 + .x_axis( 594 + Axis::default() 595 + .title("Time") 596 + .style(Style::default().fg(Color::Gray)) 597 + .bounds([min_x, max_x]), 598 + ) 599 + .y_axis( 600 + Axis::default() 601 + .title("Usage %") 602 + .style(Style::default().fg(Color::Gray)) 603 + .bounds([0.0, 100.0]) 604 + .labels(vec!["0".into(), "50".into(), "100".into()]), 605 + ); 606 + frame.render_widget(cpu_chart, inner_layout[1]); 607 + 608 + 609 + let mem_dataset = Dataset::default() 610 + .name("Mem %") 611 + .marker(symbols::Marker::Dot) 612 + .graph_type(GraphType::Line) 613 + .style(Style::default().fg(Color::Magenta)) 614 + .data(&pod.mem_history); 615 + 616 + let mem_chart = Chart::new(vec![mem_dataset]) 617 + .block(Block::default().title("Memory Usage")) 618 + .x_axis( 619 + Axis::default() 620 + .title("Time") 621 + .style(Style::default().fg(Color::Gray)) 622 + .bounds([min_x, max_x]), 623 + ) 624 + .y_axis( 625 + Axis::default() 626 + .title("Usage %") 627 + .style(Style::default().fg(Color::Gray)) 628 + .bounds([0.0, 100.0]) 629 + .labels(vec!["0".into(), "50".into(), "100".into()]), 630 + ); 631 + frame.render_widget(mem_chart, inner_layout[2]); 632 + } 633 + 634 + fn render_account_view(frame: &mut Frame, app: &mut App, area: Rect) { 635 + let block = Block::default() 636 + .borders(Borders::ALL) 637 + .title("Account Settings"); 638 + 639 + let items = [ 640 + ListItem::new("Manage SSH Keys..."), 641 + ListItem::new("Manage API Keys..."), 642 + ListItem::new("Profile..."), 643 + ]; 644 + 645 + let list = List::new(items) 646 + .block(block) 647 + .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) 648 + .highlight_symbol(">> "); 649 + 650 + frame.render_stateful_widget(list, area, &mut app.account_list_state); 651 + } 652 + 653 + fn render_workspace_view(frame: &mut Frame, app: &mut App, area: Rect) { 654 + let block = Block::default() 655 + .borders(Borders::ALL) 656 + .title("Workspace Settings"); 657 + 658 + let items = [ 659 + ListItem::new("Billing & Invoices..."), 660 + ListItem::new("User Management..."), 661 + ListItem::new("Workspace Name..."), 662 + ]; 663 + 664 + let list = List::new(items) 665 + .block(block) 666 + .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) 667 + .highlight_symbol(">> "); 668 + 669 + frame.render_stateful_widget(list, area, &mut app.workspace_list_state); 670 + } 671 + 672 + fn render_create_pod_view(frame: &mut Frame, app: &mut App, area: Rect) { 673 + let form = &app.create_pod_form; 674 + let block = Block::default() 675 + .borders(Borders::ALL) 676 + .title("Create New Pod"); 677 + 678 + let outer_area = centered_rect(60, 50, area); 679 + frame.render_widget(Clear, outer_area); // Clear background 680 + frame.render_widget(block, outer_area); 681 + 682 + let chunks = Layout::vertical([ 683 + Constraint::Length(3), // Name 684 + Constraint::Length(3), // Image 685 + Constraint::Length(3), // Sidecars 686 + Constraint::Min(0), // Spacer 687 + ]) 688 + .margin(2) 689 + .split(outer_area); 690 + 691 + let is_focused_0 = form.focused_field == 0; 692 + let name_input = Paragraph::new(form.name.as_str()) 693 + .block( 694 + Block::default() 695 + .borders(Borders::ALL) 696 + .title("Name") 697 + .border_style(if is_focused_0 { 698 + Style::default().fg(Color::Yellow) 699 + } else { 700 + Style::default() 701 + }), 702 + ); 703 + frame.render_widget(name_input, chunks[0]); 704 + 705 + let is_focused_1 = form.focused_field == 1; 706 + let image_input = Paragraph::new(form.image_link.as_str()) 707 + .block( 708 + Block::default() 709 + .borders(Borders::ALL) 710 + .title("Container Image URL") 711 + .border_style(if is_focused_1 { 712 + Style::default().fg(Color::Yellow) 713 + } else { 714 + Style::default() 715 + }), 716 + ); 717 + frame.render_widget(image_input, chunks[1]); 718 + 719 + let is_focused_2 = form.focused_field == 2; 720 + let sidecar_input = Paragraph::new(form.sidecars.as_str()) 721 + .block( 722 + Block::default() 723 + .borders(Borders::ALL) 724 + .title("Sidecar Images (comma-separated)") 725 + .border_style(if is_focused_2 { 726 + Style::default().fg(Color::Yellow) 727 + } else { 728 + Style::default() 729 + }), 730 + ); 731 + frame.render_widget(sidecar_input, chunks[2]); 732 + 733 + if let Some(active_field_text) = match form.focused_field { 734 + 0 => Some(&form.name), 735 + 1 => Some(&form.image_link), 736 + 2 => Some(&form.sidecars), 737 + _ => None, 738 + } { 739 + frame.set_cursor( 740 + chunks[form.focused_field].x + active_field_text.len() as u16 + 1, 741 + chunks[form.focused_field].y + 1, 742 + ); 743 + } 744 + } 745 + 746 + fn render_modal(frame: &mut Frame, app: &mut App) { 747 + if let ActiveModal::PodActions = app.active_modal { 748 + let block = Block::default() 749 + .title("Pod Actions") 750 + .borders(Borders::ALL); 751 + let area = centered_rect(30, 20, frame.size()); 752 + 753 + let items = [ 754 + ListItem::new("Resize"), 755 + ListItem::new("Stop"), 756 + ListItem::new("Delete"), 757 + ]; 758 + 759 + let list = List::new(items) 760 + .block(block) 761 + .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) 762 + .highlight_symbol(">> "); 763 + 764 + frame.render_widget(Clear, area); 765 + frame.render_stateful_widget(list, area, &mut app.action_list_state); 766 + } 767 + } 768 + 769 + fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 770 + let popup_layout = Layout::vertical([ 771 + Constraint::Percentage((100 - percent_y) / 2), 772 + Constraint::Percentage(percent_y), 773 + Constraint::Percentage((100 - percent_y) / 2), 774 + ]) 775 + .split(r); 776 + 777 + Layout::horizontal([ 778 + Constraint::Percentage((100 - percent_x) / 2), 779 + Constraint::Percentage(percent_x), 780 + Constraint::Percentage((100 - percent_x) / 2), 781 + ]) 782 + .split(popup_layout[1])[1] 783 + }