+1
frontend/.gitignore
+1
frontend/.gitignore
···
1
+
target
+575
frontend/Cargo.lock
+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
+9
frontend/Cargo.toml
+783
frontend/src/main.rs
+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
+
}