From 09304342e75a50c68572c16864b0bacbfccee7f7 Mon Sep 17 00:00:00 2001 From: JasterV <49537445+JasterV@users.noreply.github.com> Date: Thu, 29 Jan 2026 01:28:31 +0100 Subject: [PATCH 1/2] playing around with a TUI --- Cargo.lock | 975 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 +- granc-tui/Cargo.toml | 28 ++ granc-tui/src/config.rs | 68 +++ granc-tui/src/effects.rs | 243 ++++++++++ granc-tui/src/globals.rs | 15 + granc-tui/src/main.rs | 28 ++ granc-tui/src/model.rs | 100 ++++ granc-tui/src/msg.rs | 46 ++ granc-tui/src/update.rs | 365 +++++++++++++++ granc-tui/src/view.rs | 217 +++++++++ 11 files changed, 2065 insertions(+), 22 deletions(-) create mode 100644 granc-tui/Cargo.toml create mode 100644 granc-tui/src/config.rs create mode 100644 granc-tui/src/effects.rs create mode 100644 granc-tui/src/globals.rs create mode 100644 granc-tui/src/main.rs create mode 100644 granc-tui/src/model.rs create mode 100644 granc-tui/src/msg.rs create mode 100644 granc-tui/src/update.rs create mode 100644 granc-tui/src/view.rs diff --git a/Cargo.lock b/Cargo.lock index 5bccec5..2504263 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.21" @@ -133,6 +154,21 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.22.1" @@ -145,12 +181,33 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -197,6 +254,33 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -212,6 +296,158 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.3", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "echo-service" version = "0.0.0" @@ -246,6 +482,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -322,6 +568,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -334,6 +591,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "granc" version = "0.7.0" @@ -345,6 +608,25 @@ dependencies = [ "tokio", ] +[[package]] +name = "granc-tui" +version = "0.1.0" +dependencies = [ + "anyhow", + "color-eyre", + "crossterm 0.29.0", + "directories", + "granc_core 0.6.0", + "once_cell", + "ratatui", + "serde", + "serde_json", + "teatui", + "tokio", + "tui-textarea", + "uuid", +] + [[package]] name = "granc_core" version = "0.6.0" @@ -357,7 +639,7 @@ dependencies = [ "prost-reflect", "prost-types", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-stream", "tonic", @@ -377,7 +659,7 @@ dependencies = [ "prost-reflect", "prost-types", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-stream", "tonic", @@ -409,6 +691,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -526,6 +810,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "2.13.0" @@ -536,12 +832,43 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -557,24 +884,80 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "matchit" version = "0.8.4" @@ -593,6 +976,15 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.1.1" @@ -600,6 +992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -619,6 +1012,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -631,6 +1033,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "2.10.1" @@ -640,6 +1048,41 @@ dependencies = [ "num-traits", ] +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -725,7 +1168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "log", "multimap", "petgraph", @@ -746,7 +1189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn", @@ -810,12 +1253,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "regex" -version = "1.12.2" +name = "ratatui" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "aho-corasick", + "bitflags", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", "memchr", "regex-automata", "regex-syntax", @@ -838,6 +1322,34 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.3" @@ -847,10 +1359,34 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -858,6 +1394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -903,6 +1440,46 @@ dependencies = [ "zmij", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "slab" version = "0.4.11" @@ -925,12 +1502,40 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.114" @@ -948,6 +1553,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "teatui" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a13b47a097b60254a881d30d701665c8fd83556e3c682e039e0650585790ba" +dependencies = [ + "color-eyre", + "crossterm 0.29.0", + "ratatui", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -955,19 +1571,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.3", "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -981,6 +1617,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tokio" version = "1.49.0" @@ -990,7 +1635,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -1173,6 +1820,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -1181,6 +1850,17 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-textarea" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" +dependencies = [ + "crossterm 0.28.1", + "ratatui", + "unicode-width 0.2.0", +] + [[package]] name = "unicase" version = "2.9.0" @@ -1193,12 +1873,59 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "want" version = "0.3.1" @@ -1223,19 +1950,104 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1247,6 +2059,37 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1254,58 +2097,148 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/Cargo.toml b/Cargo.toml index 17944f4..2358050 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["granc", "granc-core", "echo-service"] +members = ["granc", "granc-core", "granc-tui", "echo-service"] resolver = "2" [workspace.package] diff --git a/granc-tui/Cargo.toml b/granc-tui/Cargo.toml new file mode 100644 index 0000000..ea7b537 --- /dev/null +++ b/granc-tui/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "granc-tui" +version = "0.1.0" +edition = "2024" + +[dependencies] +# Core Logic +granc_core = { path = "../granc-core" } + +# Architecture +teatui = "0.1" +ratatui = "0.29.0" +crossterm = "0.29.0" +tui-textarea = "0.7.0" + +# Async Runtime +tokio = { version = "1.43", features = ["full"] } + +# Data & Config +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +directories = "5.0" +uuid = { version = "1.10", features = ["v4", "serde"] } +once_cell = "1.19" + +# Error Handling +color-eyre = "0.6" +anyhow = "1.0" diff --git a/granc-tui/src/config.rs b/granc-tui/src/config.rs new file mode 100644 index 0000000..5d8c1f0 --- /dev/null +++ b/granc-tui/src/config.rs @@ -0,0 +1,68 @@ +use anyhow::{Context, Result}; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] +pub struct AppConfig { + pub projects: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Project { + pub id: Uuid, + pub name: String, + pub connection: ConnectionConfig, + pub saved_requests: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(tag = "type", content = "value")] +pub enum ConnectionConfig { + Reflection { url: String }, + File { url: String, path: PathBuf }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct SavedRequest { + pub id: Uuid, + pub name: String, + pub service: String, + pub method: String, + pub body: String, + pub headers: Vec<(String, String)>, +} + +pub struct ConfigManager { + config_path: PathBuf, +} + +impl ConfigManager { + pub fn new() -> Result { + let proj_dirs = ProjectDirs::from("com", "granc", "granc-tui") + .context("Could not determine config directory")?; + let config_dir = proj_dirs.config_dir(); + fs::create_dir_all(config_dir)?; + + Ok(Self { + config_path: config_dir.join("config.json"), + }) + } + + pub fn load(&self) -> Result { + if !self.config_path.exists() { + return Ok(AppConfig::default()); + } + let content = fs::read_to_string(&self.config_path)?; + let config = serde_json::from_str(&content).unwrap_or_default(); + Ok(config) + } + + pub fn save(&self, config: &AppConfig) -> Result<()> { + let content = serde_json::to_string_pretty(config)?; + fs::write(&self.config_path, content)?; + Ok(()) + } +} diff --git a/granc-tui/src/effects.rs b/granc-tui/src/effects.rs new file mode 100644 index 0000000..099e04b --- /dev/null +++ b/granc-tui/src/effects.rs @@ -0,0 +1,243 @@ +use crate::config::{ConfigManager, ConnectionConfig, Project}; +use crate::globals; +use crate::model::{MethodData, Model}; +use crate::msg::Msg; +use color_eyre::Result; +use granc_core::client::{Descriptor, DynamicRequest, DynamicResponse, GrancClient}; + +#[derive(Debug, Clone)] +pub enum Effect { + LoadConfigFromDisk, + SaveConfigToDisk(crate::config::AppConfig), + FetchServices(Project), + FetchMethods { + project: Project, + service: String, + }, + ExecuteCall { + project: Project, + service: String, + method: String, + body: String, + headers: Vec<(String, String)>, + }, +} + +pub fn handle_effect(_model: &Model, effect: Effect) -> Result> { + match effect { + Effect::LoadConfigFromDisk => { + let manager = ConfigManager::new().map_err(|e| color_eyre::eyre::eyre!(e))?; + match manager.load() { + Ok(cfg) => Ok(Some(Msg::ConfigLoaded(Ok(cfg)))), + Err(e) => Ok(Some(Msg::ConfigLoaded(Err(e.to_string())))), + } + } + + Effect::SaveConfigToDisk(cfg) => { + if let Ok(manager) = ConfigManager::new() { + let _ = manager.save(&cfg); + } + Ok(None) + } + + Effect::FetchServices(proj) => { + let handle = globals::get_handle(); + let project_id = proj.id; + let result = handle.block_on(async move { fetch_services_async(&proj).await }); + + match result { + Ok(services) => Ok(Some(Msg::ServicesFetched { + project_id, + services, + })), + Err(e) => Ok(Some(Msg::CallResponse(Err(format!("Fetch failed: {}", e))))), + } + } + + Effect::FetchMethods { project, service } => { + let handle = globals::get_handle(); + let service_name = service.clone(); + let result = + handle.block_on(async move { fetch_methods_async(&project, &service_name).await }); + + match result { + Ok(methods) => Ok(Some(Msg::MethodsFetched { service, methods })), + Err(e) => Ok(Some(Msg::CallResponse(Err(format!( + "Fetch methods failed: {}", + e + ))))), + } + } + + Effect::ExecuteCall { + project, + service, + method, + body, + headers, + } => { + let handle = globals::get_handle(); + let result = handle.block_on(async move { + execute_call_async(&project, service, method, body, headers).await + }); + + Ok(Some(Msg::CallResponse(result))) + } + } +} + +// --- Async Handlers --- + +async fn fetch_services_async(proj: &Project) -> std::result::Result, String> { + match &proj.connection { + ConnectionConfig::Reflection { url } => { + let mut client = GrancClient::connect(url).await.map_err(|e| e.to_string())?; + client.list_services().await.map_err(|e| e.to_string()) + } + ConnectionConfig::File { url, path } => { + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + if let Ok(c) = GrancClient::connect(url).await { + if let Ok(fc) = c.with_file_descriptor(bytes.clone()) { + return Ok(fc.list_services()); + } + } + let client = GrancClient::offline(bytes).map_err(|e| e.to_string())?; + Ok(client.list_services()) + } + } +} + +async fn fetch_methods_async( + proj: &Project, + service: &str, +) -> std::result::Result, String> { + fn extract(descriptor: Descriptor) -> std::result::Result, String> { + match descriptor { + Descriptor::ServiceDescriptor(sd) => { + let methods = sd + .methods() + .map(|m| { + let input_desc = m.input(); + let input = input_desc.name(); + let output_desc = m.output(); + let output = output_desc.name(); + let client_stream = if m.is_client_streaming() { + "stream " + } else { + "" + }; + let server_stream = if m.is_server_streaming() { + "stream " + } else { + "" + }; + + MethodData { + name: m.name().to_string(), + signature: format!( + "rpc {}({}{}) returns ({}{})", + m.name(), + client_stream, + input, + server_stream, + output + ), + } + }) + .collect(); + Ok(methods) + } + _ => Err("Symbol is not a service".to_string()), + } + } + + match &proj.connection { + ConnectionConfig::Reflection { url } => { + let mut client = GrancClient::connect(url).await.map_err(|e| e.to_string())?; + let descriptor = client + .get_descriptor_by_symbol(service) + .await + .map_err(|e| e.to_string())?; + extract(descriptor) + } + ConnectionConfig::File { url, path } => { + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + if let Ok(c) = GrancClient::connect(url).await { + // Fixed: Removed 'mut' from fc + if let Ok(fc) = c.with_file_descriptor(bytes.clone()) { + if let Some(d) = fc.get_descriptor_by_symbol(service) { + return extract(d); + } + } + } + let client = GrancClient::offline(bytes).map_err(|e| e.to_string())?; + if let Some(d) = client.get_descriptor_by_symbol(service) { + return extract(d); + } + Err("Service not found".to_string()) + } + } +} + +async fn execute_call_async( + proj: &Project, + service: String, + method: String, + body: String, + headers: Vec<(String, String)>, +) -> std::result::Result { + let json_body: serde_json::Value = + serde_json::from_str(&body).map_err(|e| format!("Invalid JSON: {}", e))?; + + let req = DynamicRequest { + service, + method, + body: json_body, + headers, + }; + + match &proj.connection { + ConnectionConfig::Reflection { url } => { + let mut client = GrancClient::connect(url).await.map_err(|e| e.to_string())?; + let resp = client.dynamic(req).await.map_err(|e| e.to_string())?; + format_response(resp) + } + ConnectionConfig::File { url, path } => { + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + let client = GrancClient::connect(url).await.map_err(|e| e.to_string())?; + let mut client = client + .with_file_descriptor(bytes) + .map_err(|e| e.to_string())?; + let resp = client.dynamic(req).await.map_err(|e| e.to_string())?; + format_response(resp) + } + } +} + +fn format_response(resp: DynamicResponse) -> std::result::Result { + match resp { + DynamicResponse::Unary(Ok(v)) => Ok(serde_json::to_string_pretty(&v).unwrap_or_default()), + DynamicResponse::Unary(Err(s)) => { + Err(format!("gRPC Error: {} (Code: {})", s.message(), s.code())) + } + DynamicResponse::Streaming(r) => { + let mut out = String::new(); + match r { + Ok(msgs) => { + for (i, msg) in msgs.into_iter().enumerate() { + match msg { + Ok(v) => out.push_str(&format!( + "Msg {}:\n{}\n", + i, + serde_json::to_string_pretty(&v).unwrap_or_default() + )), + Err(s) => out.push_str(&format!("Msg {} Error: {}\n", i, s.message())), + } + } + Ok(out) + } + Err(s) => Err(format!("Stream Error: {}", s.message())), + } + } + } +} diff --git a/granc-tui/src/globals.rs b/granc-tui/src/globals.rs new file mode 100644 index 0000000..5dc7c4c --- /dev/null +++ b/granc-tui/src/globals.rs @@ -0,0 +1,15 @@ +use once_cell::sync::OnceCell; +use tokio::runtime::Handle; + +pub static TOKIO_HANDLE: OnceCell = OnceCell::new(); + +pub fn init_handle() { + let handle = Handle::current(); + TOKIO_HANDLE + .set(handle) + .expect("Failed to set global Tokio handle"); +} + +pub fn get_handle() -> &'static Handle { + TOKIO_HANDLE.get().expect("Tokio handle not initialized") +} diff --git a/granc-tui/src/main.rs b/granc-tui/src/main.rs new file mode 100644 index 0000000..7a71bdd --- /dev/null +++ b/granc-tui/src/main.rs @@ -0,0 +1,28 @@ +mod config; +mod effects; +mod globals; +mod model; +mod msg; +mod update; +mod view; + +use model::Model; + +#[tokio::main] +async fn main() -> color_eyre::Result<()> { + color_eyre::install()?; + + // Initialize global Tokio handle for effects thread + globals::init_handle(); + + let initial_model = Model::default(); + + teatui::start( + initial_model, + update::update, + view::view, + effects::handle_effect, + )?; + + Ok(()) +} diff --git a/granc-tui/src/model.rs b/granc-tui/src/model.rs new file mode 100644 index 0000000..3f0ba7c --- /dev/null +++ b/granc-tui/src/model.rs @@ -0,0 +1,100 @@ +use crate::config::{AppConfig, Project}; +use tui_textarea::TextArea; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq)] +pub enum Screen { + Dashboard, + NewProject, + ServiceBrowser, + MethodBrowser, + MethodView, + ResponseView, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Focus { + Body, + HeaderKey(usize), + HeaderValue(usize), +} + +#[derive(Debug, Clone)] +pub struct MethodData { + pub name: String, + pub signature: String, +} + +#[derive(Debug, Clone)] +pub struct HeaderPair { + pub key: String, + pub value: String, +} + +#[derive(Debug, Clone)] +pub struct Model { + pub screen: Screen, + pub config: AppConfig, + + // Global Input Buffer (New Project / URL) + pub input_buffer: String, + + // Navigation Indices + pub project_list_idx: usize, + pub service_list_idx: usize, + pub method_list_idx: usize, + + // Data + pub selected_project_id: Option, + pub services: Vec, + pub methods: Vec, // Updated to hold signature + pub selected_service: Option, + pub selected_method: Option, + + // Request Editor State + pub body_editor: TextArea<'static>, + pub headers: Vec, + pub focus: Focus, + + // Results + pub response_output: String, + pub status_message: Option, +} + +impl Default for Model { + fn default() -> Self { + let mut editor = TextArea::default(); + editor.set_block( + ratatui::widgets::Block::default() + .borders(ratatui::widgets::Borders::ALL) + .title("Body (JSON)"), + ); + editor.insert_str("{}"); + + Self { + screen: Screen::Dashboard, + config: AppConfig::default(), + input_buffer: String::new(), + project_list_idx: 0, + service_list_idx: 0, + method_list_idx: 0, + selected_project_id: None, + services: vec![], + methods: vec![], + selected_service: None, + selected_method: None, + body_editor: editor, + headers: vec![], + focus: Focus::Body, + response_output: String::new(), + status_message: None, + } + } +} + +impl Model { + pub fn current_project(&self) -> Option<&Project> { + self.selected_project_id + .and_then(|id| self.config.projects.iter().find(|p| p.id == id)) + } +} diff --git a/granc-tui/src/msg.rs b/granc-tui/src/msg.rs new file mode 100644 index 0000000..73906dd --- /dev/null +++ b/granc-tui/src/msg.rs @@ -0,0 +1,46 @@ +use crate::config::AppConfig; +use crate::model::MethodData; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub enum Msg { + // --- User Input --- + Key(KeyEvent), + + // --- Config Lifecycle --- + ConfigLoaded(Result), + + // --- Async Results --- + ServicesFetched { + project_id: Uuid, + services: Vec, + }, + MethodsFetched { + service: String, + methods: Vec, + }, + CallResponse(Result), + + // --- System --- + NoOp, + Exit, +} + +impl From for Msg { + fn from(event: Event) -> Self { + match event { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Char('c') + if key + .modifiers + .contains(crossterm::event::KeyModifiers::CONTROL) => + { + Msg::Exit + } + _ => Msg::Key(key), + }, + _ => Msg::NoOp, + } + } +} diff --git a/granc-tui/src/update.rs b/granc-tui/src/update.rs new file mode 100644 index 0000000..b448cb4 --- /dev/null +++ b/granc-tui/src/update.rs @@ -0,0 +1,365 @@ +use color_eyre::Result; +use crossterm::event::{KeyCode, KeyModifiers}; +use teatui::Update; +use uuid::Uuid; + +use crate::config::{ConnectionConfig, Project}; +use crate::effects::Effect; +use crate::model::{Focus, HeaderPair, Model, Screen}; +use crate::msg::Msg; + +pub fn update(mut model: Model, msg: Msg) -> Result> { + match msg { + Msg::Exit => Ok(Update::Exit), + Msg::NoOp => Ok(Update::Next(model)), + + Msg::ConfigLoaded(res) => { + match res { + Ok(cfg) => { + model.config = cfg; + model.status_message = Some("Config loaded".into()); + } + Err(e) => model.status_message = Some(format!("Config Error: {}", e)), + } + Ok(Update::Next(model)) + } + + // Handle Editor Specific Messages (Method View) + Msg::Key(key) if model.screen == Screen::MethodView => match key.code { + KeyCode::Esc => { + model.screen = Screen::MethodBrowser; + Ok(Update::Next(model)) + } + KeyCode::Tab => { + match model.focus { + Focus::Body => { + if !model.headers.is_empty() { + model.focus = Focus::HeaderKey(0); + } + } + Focus::HeaderKey(i) => model.focus = Focus::HeaderValue(i), + Focus::HeaderValue(i) => { + if i + 1 < model.headers.len() { + model.focus = Focus::HeaderKey(i + 1); + } else { + model.focus = Focus::Body; + } + } + } + Ok(Update::Next(model)) + } + KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) => { + model.headers.push(HeaderPair { + key: "".into(), + value: "".into(), + }); + model.focus = Focus::HeaderKey(model.headers.len() - 1); + Ok(Update::Next(model)) + } + KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => { + match model.focus { + Focus::HeaderKey(i) | Focus::HeaderValue(i) => { + model.headers.remove(i); + model.focus = Focus::Body; + } + _ => {} + } + Ok(Update::Next(model)) + } + KeyCode::Enter | KeyCode::Char('s') + if key.modifiers.contains(KeyModifiers::CONTROL) => + { + execute_request(model) + } + _ => { + match model.focus { + Focus::Body => { + model.body_editor.input(to_ratatui_key(key)); + } + Focus::HeaderKey(i) => { + handle_text_input(&mut model.headers[i].key, key); + } + Focus::HeaderValue(i) => { + handle_text_input(&mut model.headers[i].value, key); + } + } + Ok(Update::Next(model)) + } + }, + + Msg::Key(key) => match (model.screen.clone(), key.code) { + // Global + (_, KeyCode::Char('q')) => Ok(Update::Exit), + + // --- Dashboard --- + (Screen::Dashboard, KeyCode::Char('l')) => { + Ok(Update::NextWithEffect(model, Effect::LoadConfigFromDisk)) + } + (Screen::Dashboard, KeyCode::Char('n')) => { + model.screen = Screen::NewProject; + model.input_buffer.clear(); + Ok(Update::Next(model)) + } + (Screen::Dashboard, KeyCode::Down) => { + if !model.config.projects.is_empty() { + model.project_list_idx = + (model.project_list_idx + 1) % model.config.projects.len(); + } + Ok(Update::Next(model)) + } + (Screen::Dashboard, KeyCode::Up) => { + if !model.config.projects.is_empty() { + model.project_list_idx = if model.project_list_idx == 0 { + model.config.projects.len() - 1 + } else { + model.project_list_idx - 1 + }; + } + Ok(Update::Next(model)) + } + (Screen::Dashboard, KeyCode::Enter) => { + let project_opt = model.config.projects.get(model.project_list_idx).cloned(); + if let Some(proj) = project_opt { + model.selected_project_id = Some(proj.id); + model.screen = Screen::ServiceBrowser; + model.service_list_idx = 0; + model.status_message = Some("Fetching services...".into()); + Ok(Update::NextWithEffect(model, Effect::FetchServices(proj))) + } else { + Ok(Update::Next(model)) + } + } + + // --- New Project --- + (Screen::NewProject, KeyCode::Enter) => { + let new_proj = Project { + id: Uuid::new_v4(), + name: if model.input_buffer.is_empty() { + "Untitled".to_string() + } else { + model.input_buffer.clone() + }, + connection: ConnectionConfig::Reflection { + url: model.input_buffer.clone(), + }, + saved_requests: vec![], + }; + model.config.projects.push(new_proj); + model.screen = Screen::Dashboard; + model.project_list_idx = model.config.projects.len() - 1; + let effect = Effect::SaveConfigToDisk(model.config.clone()); + Ok(Update::NextWithEffect(model, effect)) + } + (Screen::NewProject, KeyCode::Char(c)) => { + model.input_buffer.push(c); + Ok(Update::Next(model)) + } + (Screen::NewProject, KeyCode::Backspace) => { + model.input_buffer.pop(); + Ok(Update::Next(model)) + } + (Screen::NewProject, KeyCode::Esc) => { + model.screen = Screen::Dashboard; + Ok(Update::Next(model)) + } + + // --- Service Browser --- + (Screen::ServiceBrowser, KeyCode::Down) => { + if !model.services.is_empty() { + model.service_list_idx = (model.service_list_idx + 1) % model.services.len(); + } + Ok(Update::Next(model)) + } + (Screen::ServiceBrowser, KeyCode::Up) => { + if !model.services.is_empty() { + model.service_list_idx = if model.service_list_idx == 0 { + model.services.len() - 1 + } else { + model.service_list_idx - 1 + }; + } + Ok(Update::Next(model)) + } + (Screen::ServiceBrowser, KeyCode::Enter) => { + if let (Some(svc), Some(proj)) = ( + model.services.get(model.service_list_idx).cloned(), + model.current_project().cloned(), + ) { + model.selected_service = Some(svc.clone()); + model.status_message = Some("Fetching methods...".into()); + Ok(Update::NextWithEffect( + model, + Effect::FetchMethods { + project: proj, + service: svc, + }, + )) + } else { + Ok(Update::Next(model)) + } + } + (Screen::ServiceBrowser, KeyCode::Esc) => { + model.screen = Screen::Dashboard; + Ok(Update::Next(model)) + } + + // --- Method Browser --- + (Screen::MethodBrowser, KeyCode::Down) => { + if !model.methods.is_empty() { + model.method_list_idx = (model.method_list_idx + 1) % model.methods.len(); + } + Ok(Update::Next(model)) + } + (Screen::MethodBrowser, KeyCode::Up) => { + if !model.methods.is_empty() { + model.method_list_idx = if model.method_list_idx == 0 { + model.methods.len() - 1 + } else { + model.method_list_idx - 1 + }; + } + Ok(Update::Next(model)) + } + (Screen::MethodBrowser, KeyCode::Enter) => { + if let Some(m) = model.methods.get(model.method_list_idx).cloned() { + model.selected_method = Some(m.name); + model.headers.clear(); + model.focus = Focus::Body; + model.screen = Screen::MethodView; + } + Ok(Update::Next(model)) + } + (Screen::MethodBrowser, KeyCode::Esc) => { + model.screen = Screen::ServiceBrowser; + Ok(Update::Next(model)) + } + + // --- Response View --- + (Screen::ResponseView, KeyCode::Esc) => { + model.screen = Screen::MethodView; + Ok(Update::Next(model)) + } + + _ => Ok(Update::Next(model)), + }, + + Msg::ServicesFetched { + project_id, + services, + } => { + if model.selected_project_id == Some(project_id) { + model.services = services; + model.service_list_idx = 0; + model.status_message = Some("Services loaded.".into()); + } + Ok(Update::Next(model)) + } + + Msg::MethodsFetched { service, methods } => { + if model.selected_service.as_deref() == Some(&service) { + model.methods = methods; + model.method_list_idx = 0; + model.screen = Screen::MethodBrowser; + model.status_message = Some("Methods loaded.".into()); + } + Ok(Update::Next(model)) + } + + Msg::CallResponse(res) => { + model.screen = Screen::ResponseView; + match res { + Ok(s) => { + model.response_output = s; + model.status_message = Some("Call success".into()); + } + Err(e) => { + model.response_output = format!("Error: {}", e); + model.status_message = Some("Call failed".into()); + } + } + Ok(Update::Next(model)) + } + } +} + +fn execute_request(mut model: Model) -> Result> { + let execution_data = if let (Some(p), Some(s), Some(m)) = ( + model.current_project(), + &model.selected_service, + &model.selected_method, + ) { + Some((p.clone(), s.clone(), m.clone())) + } else { + None + }; + + if let Some((p, s, m)) = execution_data { + let headers: Vec<(String, String)> = model + .headers + .iter() + .filter(|h| !h.key.is_empty()) + .map(|h| (h.key.clone(), h.value.clone())) + .collect(); + + let body_lines = model.body_editor.lines().to_vec(); + let body = body_lines.join("\n"); + + let effect = Effect::ExecuteCall { + project: p, + service: s, + method: m, + body, + headers, + }; + model.status_message = Some("Executing request...".into()); + Ok(Update::NextWithEffect(model, effect)) + } else { + model.status_message = + Some("Error: Missing execution context (project/service/method)".into()); + Ok(Update::Next(model)) + } +} + +fn handle_text_input(target: &mut String, key: crossterm::event::KeyEvent) { + match key.code { + KeyCode::Char(c) => target.push(c), + KeyCode::Backspace => { + target.pop(); + } + _ => {} + } +} + +fn to_ratatui_key(key: crossterm::event::KeyEvent) -> ratatui::crossterm::event::KeyEvent { + let code = match key.code { + crossterm::event::KeyCode::Backspace => ratatui::crossterm::event::KeyCode::Backspace, + crossterm::event::KeyCode::Enter => ratatui::crossterm::event::KeyCode::Enter, + crossterm::event::KeyCode::Left => ratatui::crossterm::event::KeyCode::Left, + crossterm::event::KeyCode::Right => ratatui::crossterm::event::KeyCode::Right, + crossterm::event::KeyCode::Up => ratatui::crossterm::event::KeyCode::Up, + crossterm::event::KeyCode::Down => ratatui::crossterm::event::KeyCode::Down, + crossterm::event::KeyCode::Home => ratatui::crossterm::event::KeyCode::Home, + crossterm::event::KeyCode::End => ratatui::crossterm::event::KeyCode::End, + crossterm::event::KeyCode::PageUp => ratatui::crossterm::event::KeyCode::PageUp, + crossterm::event::KeyCode::PageDown => ratatui::crossterm::event::KeyCode::PageDown, + crossterm::event::KeyCode::Tab => ratatui::crossterm::event::KeyCode::Tab, + crossterm::event::KeyCode::BackTab => ratatui::crossterm::event::KeyCode::BackTab, + crossterm::event::KeyCode::Delete => ratatui::crossterm::event::KeyCode::Delete, + crossterm::event::KeyCode::Insert => ratatui::crossterm::event::KeyCode::Insert, + crossterm::event::KeyCode::F(n) => ratatui::crossterm::event::KeyCode::F(n), + crossterm::event::KeyCode::Char(c) => ratatui::crossterm::event::KeyCode::Char(c), + crossterm::event::KeyCode::Null => ratatui::crossterm::event::KeyCode::Null, + crossterm::event::KeyCode::Esc => ratatui::crossterm::event::KeyCode::Esc, + _ => ratatui::crossterm::event::KeyCode::Null, + }; + + let modifiers = + ratatui::crossterm::event::KeyModifiers::from_bits_truncate(key.modifiers.bits()); + + ratatui::crossterm::event::KeyEvent { + code, + modifiers, + kind: ratatui::crossterm::event::KeyEventKind::Press, + state: ratatui::crossterm::event::KeyEventState::empty(), + } +} diff --git a/granc-tui/src/view.rs b/granc-tui/src/view.rs new file mode 100644 index 0000000..fa7c43c --- /dev/null +++ b/granc-tui/src/view.rs @@ -0,0 +1,217 @@ +use crate::model::{Focus, Model, Screen}; +use color_eyre::Result; +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style, Stylize}, + widgets::ListState, + widgets::{Block, Borders, List, ListItem, Paragraph, StatefulWidget, Widget, WidgetRef}, +}; +use teatui::View; + +struct RootWidget { + model: Model, +} + +impl WidgetRef for RootWidget { + fn render_ref(&self, area: Rect, buf: &mut ratatui::prelude::Buffer) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(1)]) + .split(area); + + let main_area = chunks[0]; + let status_bar = chunks[1]; + + match self.model.screen { + Screen::Dashboard => draw_dashboard(&self.model, main_area, buf), + Screen::NewProject => draw_new_project(&self.model, main_area, buf), + Screen::ServiceBrowser => draw_services(&self.model, main_area, buf), + Screen::MethodBrowser => draw_method_browser(&self.model, main_area, buf), + Screen::MethodView => draw_method_execution(&self.model, main_area, buf), + Screen::ResponseView => draw_response(&self.model, main_area, buf), + } + + let msg = self.model.status_message.as_deref().unwrap_or("Ready"); + let status_text = format!( + " {} | Screen: {:?} | [Q] Quit | [L] Load", + msg, self.model.screen + ); + Paragraph::new(status_text) + .style(Style::default().bg(Color::Blue).fg(Color::White)) + .render_ref(status_bar, buf); + } +} + +pub fn view(model: &Model) -> Result { + Ok(View::new(RootWidget { + model: model.clone(), + })) +} + +// --- Helpers --- + +fn draw_dashboard(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { + let items: Vec = model + .config + .projects + .iter() + .map(|p| ListItem::new(p.name.as_str())) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .title("Projects (Press 'n' for new)") + .borders(Borders::ALL), + ) + .highlight_style(Style::default().bg(Color::DarkGray).bold()); + + let mut state = ListState::default().with_selected(Some(model.project_list_idx)); + StatefulWidget::render(list, area, buf, &mut state); +} + +fn draw_new_project(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { + let text = Paragraph::new(format!("Server URL: {}", model.input_buffer)).block( + Block::default() + .title("New Project (Enter URL)") + .borders(Borders::ALL), + ); + text.render_ref(area, buf); +} + +fn draw_services(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { + let items: Vec = model + .services + .iter() + .map(|s| ListItem::new(s.as_str())) + .collect(); + + let list = List::new(items) + .block(Block::default().title("Services").borders(Borders::ALL)) + .highlight_style(Style::default().bg(Color::DarkGray).bold()); + + let mut state = ListState::default().with_selected(Some(model.service_list_idx)); + StatefulWidget::render(list, area, buf, &mut state); +} + +fn draw_method_browser(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { + let items: Vec = model + .methods + .iter() + .map(|m| ListItem::new(m.signature.as_str())) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .title(format!( + "Methods of {}", + model.selected_service.as_deref().unwrap_or("?") + )) + .borders(Borders::ALL), + ) + .highlight_style(Style::default().bg(Color::DarkGray).bold()); + + let mut state = ListState::default().with_selected(Some(model.method_list_idx)); + StatefulWidget::render(list, area, buf, &mut state); +} + +fn draw_method_execution(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), // Title + Constraint::Percentage(60), // Body + Constraint::Percentage(30), // Headers + Constraint::Length(1), // Hint + ]) + .split(area); + + let title = format!( + "Executing: {}", + model.selected_method.as_deref().unwrap_or("?") + ); + Paragraph::new(title).bold().render_ref(chunks[0], buf); + + // Body Editor + let mut editor = model.body_editor.clone(); + let body_block = Block::default() + .borders(Borders::ALL) + .title("Request Body (JSON)"); + + if model.focus == Focus::Body { + editor.set_style(Style::default()); + editor.set_block(body_block.border_style(Style::default().fg(Color::Yellow))); + } else { + editor.set_style(Style::default().fg(Color::DarkGray)); + editor.set_block(body_block); + } + + editor.render(chunks[1], buf); + + // Headers + let header_block = Block::default() + .borders(Borders::ALL) + .title("Headers (Ctrl+H to add)"); + header_block.render_ref(chunks[2], buf); + + let header_area = chunks[2].inner(ratatui::layout::Margin { + horizontal: 1, + vertical: 1, + }); + + let header_rows = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(1); model.headers.len()]) + .split(header_area); + + for (i, header) in model.headers.iter().enumerate() { + if i >= header_rows.len() { + break; + } + + let row_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(45), + Constraint::Length(1), + Constraint::Percentage(45), + ]) + .split(header_rows[i]); + + let k_style = if model.focus == Focus::HeaderKey(i) { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + Paragraph::new(header.key.as_str()) + .style(k_style) + .render_ref(row_chunks[0], buf); + + Paragraph::new(":").render_ref(row_chunks[1], buf); + + let v_style = if model.focus == Focus::HeaderValue(i) { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + Paragraph::new(header.value.as_str()) + .style(v_style) + .render_ref(row_chunks[2], buf); + } + + Paragraph::new( + "[Tab] Cycle Focus | [Ctrl+Enter/S] Send | [Ctrl+H] Add Header | [Ctrl+D] Remove Header", + ) + .style(Style::default().fg(Color::Gray)) + .render_ref(chunks[3], buf); +} + +fn draw_response(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { + let p = Paragraph::new(model.response_output.as_str()).block( + Block::default() + .title("Response (Esc to back)") + .borders(Borders::ALL), + ); + p.render_ref(area, buf); +} From 2598bd40937cc171e74edcac58846fd9f32b4433 Mon Sep 17 00:00:00 2001 From: JasterV <49537445+JasterV@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:19:51 +0100 Subject: [PATCH 2/2] feat: it almost works! --- Cargo.lock | 1180 +++++++++++++++++++++++--------------- granc-tui/Cargo.toml | 25 +- granc-tui/src/config.rs | 68 --- granc-tui/src/effects.rs | 243 -------- granc-tui/src/globals.rs | 15 - granc-tui/src/main.rs | 473 ++++++++++++++- granc-tui/src/model.rs | 100 ---- granc-tui/src/msg.rs | 46 -- granc-tui/src/update.rs | 365 ------------ granc-tui/src/view.rs | 217 ------- 10 files changed, 1175 insertions(+), 1557 deletions(-) delete mode 100644 granc-tui/src/config.rs delete mode 100644 granc-tui/src/effects.rs delete mode 100644 granc-tui/src/globals.rs delete mode 100644 granc-tui/src/model.rs delete mode 100644 granc-tui/src/msg.rs delete mode 100644 granc-tui/src/update.rs delete mode 100644 granc-tui/src/view.rs diff --git a/Cargo.lock b/Cargo.lock index 2504263..eb99d9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aho-corasick" version = "1.1.4" @@ -96,7 +81,16 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", ] [[package]] @@ -155,25 +149,31 @@ dependencies = [ ] [[package]] -name = "backtrace" -version = "0.3.76" +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", + "bit-vec", ] [[package]] -name = "base64" -version = "0.22.1" +name = "bit-vec" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" @@ -181,6 +181,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -188,16 +197,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] -name = "bytes" -version = "1.11.0" +name = "bytemuck" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] -name = "cassowary" -version = "0.3.0" +name = "bytes" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "castaway" @@ -214,6 +223,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.55" @@ -245,7 +260,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -254,33 +269,6 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - [[package]] name = "colorchoice" version = "1.0.4" @@ -298,9 +286,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ "castaway", "cfg-if", @@ -320,19 +308,12 @@ dependencies = [ ] [[package]] -name = "crossterm" -version = "0.28.1" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "bitflags", - "crossterm_winapi", - "mio", - "parking_lot", - "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", + "libc", ] [[package]] @@ -341,13 +322,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "derive_more", "document-features", "mio", "parking_lot", - "rustix 1.1.3", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -362,6 +343,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "darling" version = "0.23.0" @@ -382,7 +383,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.114", ] [[package]] @@ -393,7 +394,22 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", ] [[package]] @@ -415,28 +431,17 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", -] - -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys", + "syn 2.0.114", ] [[package]] -name = "dirs-sys" -version = "0.4.1" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", + "block-buffer", + "crypto-common", ] [[package]] @@ -483,13 +488,22 @@ dependencies = [ ] [[package]] -name = "eyre" -version = "0.6.12" +name = "euclid" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" dependencies = [ - "indenter", - "once_cell", + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", ] [[package]] @@ -498,6 +512,29 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -516,6 +553,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures-channel" version = "0.3.31" @@ -539,7 +582,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -569,14 +612,13 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "cfg-if", - "libc", - "wasi", + "typenum", + "version_check", ] [[package]] @@ -591,12 +633,6 @@ dependencies = [ "wasip2", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "granc" version = "0.7.0" @@ -613,18 +649,14 @@ name = "granc-tui" version = "0.1.0" dependencies = [ "anyhow", - "color-eyre", - "crossterm 0.29.0", - "directories", + "colored", + "crossterm", "granc_core 0.6.0", - "once_cell", "ratatui", - "serde", "serde_json", "teatui", + "thiserror 2.0.18", "tokio", - "tui-textarea", - "uuid", ] [[package]] @@ -691,9 +723,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -701,6 +731,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -708,6 +743,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.0" @@ -816,12 +857,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - [[package]] name = "indexmap" version = "2.13.0" @@ -851,7 +886,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -860,15 +895,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -894,6 +920,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.5.0" @@ -907,21 +950,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] -name = "libredox" -version = "0.1.12" +name = "line-clipping" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" dependencies = [ - "bitflags", - "libc", + "bitflags 2.10.0", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -951,11 +987,21 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.5" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", ] [[package]] @@ -970,6 +1016,21 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -977,13 +1038,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" @@ -1003,6 +1061,46 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1013,12 +1111,12 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.37.3" +name = "num_threads" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ - "memchr", + "libc", ] [[package]] @@ -1033,12 +1131,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "ordered-float" version = "2.10.1" @@ -1049,10 +1141,13 @@ dependencies = [ ] [[package]] -name = "owo-colors" -version = "4.2.3" +name = "ordered-float" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] [[package]] name = "parking_lot" @@ -1077,12 +1172,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1090,38 +1179,133 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "petgraph" -version = "0.8.3" +name = "pest" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", + "memchr", + "ucd-trie", ] [[package]] -name = "pin-project" -version = "1.1.10" +name = "pest_derive" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" dependencies = [ - "pin-project-internal", + "pest", + "pest_generator", ] [[package]] -name = "pin-project-internal" -version = "1.1.10" +name = "pest_generator" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" dependencies = [ + "pest", + "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] -name = "pin-project-lite" +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset 0.5.7", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" @@ -1132,6 +1316,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prettyplease" version = "0.2.37" @@ -1139,7 +1335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.114", ] [[package]] @@ -1168,7 +1364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ "heck", - "itertools 0.14.0", + "itertools", "log", "multimap", "petgraph", @@ -1178,7 +1374,7 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn", + "syn 2.0.114", "tempfile", ] @@ -1189,10 +1385,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1223,7 +1419,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags", + "bitflags 2.10.0", "memchr", "unicase", ] @@ -1252,45 +1448,113 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "ratatui" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" dependencies = [ - "bitflags", - "cassowary", + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.10.0", "compact_str", - "crossterm 0.28.1", + "hashbrown 0.16.1", "indoc", - "instability", - "itertools 0.13.0", + "itertools", + "kasuari", "lru", - "paste", "strum", + "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "ratatui-crossterm" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" dependencies = [ - "bitflags", + "cfg-if", + "crossterm", + "instability", + "ratatui-core", ] [[package]] -name = "redox_users" -version = "0.4.6" +name = "ratatui-macros" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", ] [[package]] @@ -1322,12 +1586,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -1337,29 +1595,16 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -1403,7 +1648,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ - "ordered-float", + "ordered-float 2.10.1", "serde", ] @@ -1424,7 +1669,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1441,12 +1686,14 @@ dependencies = [ ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "lazy_static", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -1480,6 +1727,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.11" @@ -1516,24 +1769,34 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -1555,13 +1818,14 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "teatui" -version = "0.1.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a13b47a097b60254a881d30d701665c8fd83556e3c682e039e0650585790ba" +checksum = "3bf729327e4d649bd0ec48bdee903ef4edf149c3c7b198eb6353a471a3254aab" dependencies = [ - "color-eyre", - "crossterm 0.29.0", + "crossterm", "ratatui", + "thiserror 2.0.18", + "tokio", ] [[package]] @@ -1571,12 +1835,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom", "once_cell", - "rustix 1.1.3", + "rustix", "windows-sys 0.61.2", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.10.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset 0.4.2", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float 4.6.0", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1603,7 +1930,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1614,18 +1941,30 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] -name = "thread_local" -version = "1.1.9" +name = "time" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ - "cfg-if", + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", ] +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tokio" version = "1.49.0" @@ -1651,7 +1990,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1716,7 +2055,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1741,7 +2080,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn", + "syn 2.0.114", "tempfile", "tonic-build", ] @@ -1810,7 +2149,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1820,28 +2159,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", ] [[package]] @@ -1851,15 +2168,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "tui-textarea" -version = "0.7.0" +name = "typenum" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" -dependencies = [ - "crossterm 0.28.1", - "ratatui", - "unicode-width 0.2.0", -] +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" @@ -1881,21 +2199,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" -version = "1.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ - "itertools 0.13.0", + "itertools", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.0" @@ -1914,17 +2226,26 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom 0.3.4", + "atomic", + "getrandom", "js-sys", - "serde_core", "wasm-bindgen", ] [[package]] -name = "valuable" -version = "0.1.1" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] [[package]] name = "want" @@ -1982,7 +2303,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -1995,6 +2316,78 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float 4.6.0", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2023,31 +2416,13 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2059,37 +2434,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - [[package]] name = "windows-targets" version = "0.53.5" @@ -2097,148 +2441,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/granc-tui/Cargo.toml b/granc-tui/Cargo.toml index ea7b537..ba4f498 100644 --- a/granc-tui/Cargo.toml +++ b/granc-tui/Cargo.toml @@ -4,25 +4,12 @@ version = "0.1.0" edition = "2024" [dependencies] -# Core Logic granc_core = { path = "../granc-core" } - -# Architecture -teatui = "0.1" -ratatui = "0.29.0" -crossterm = "0.29.0" -tui-textarea = "0.7.0" - -# Async Runtime -tokio = { version = "1.43", features = ["full"] } - -# Data & Config -serde = { version = "1.0", features = ["derive"] } +teatui = { version = "0.4.0", features = ["tokio"] } +ratatui = "0.30" +crossterm = "0.29" serde_json = "1.0" -directories = "5.0" -uuid = { version = "1.10", features = ["v4", "serde"] } -once_cell = "1.19" - -# Error Handling -color-eyre = "0.6" +thiserror = "2.0" anyhow = "1.0" +colored = "3.1" +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/granc-tui/src/config.rs b/granc-tui/src/config.rs deleted file mode 100644 index 5d8c1f0..0000000 --- a/granc-tui/src/config.rs +++ /dev/null @@ -1,68 +0,0 @@ -use anyhow::{Context, Result}; -use directories::ProjectDirs; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::path::PathBuf; -use uuid::Uuid; - -#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] -pub struct AppConfig { - pub projects: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct Project { - pub id: Uuid, - pub name: String, - pub connection: ConnectionConfig, - pub saved_requests: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -#[serde(tag = "type", content = "value")] -pub enum ConnectionConfig { - Reflection { url: String }, - File { url: String, path: PathBuf }, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct SavedRequest { - pub id: Uuid, - pub name: String, - pub service: String, - pub method: String, - pub body: String, - pub headers: Vec<(String, String)>, -} - -pub struct ConfigManager { - config_path: PathBuf, -} - -impl ConfigManager { - pub fn new() -> Result { - let proj_dirs = ProjectDirs::from("com", "granc", "granc-tui") - .context("Could not determine config directory")?; - let config_dir = proj_dirs.config_dir(); - fs::create_dir_all(config_dir)?; - - Ok(Self { - config_path: config_dir.join("config.json"), - }) - } - - pub fn load(&self) -> Result { - if !self.config_path.exists() { - return Ok(AppConfig::default()); - } - let content = fs::read_to_string(&self.config_path)?; - let config = serde_json::from_str(&content).unwrap_or_default(); - Ok(config) - } - - pub fn save(&self, config: &AppConfig) -> Result<()> { - let content = serde_json::to_string_pretty(config)?; - fs::write(&self.config_path, content)?; - Ok(()) - } -} diff --git a/granc-tui/src/effects.rs b/granc-tui/src/effects.rs deleted file mode 100644 index 099e04b..0000000 --- a/granc-tui/src/effects.rs +++ /dev/null @@ -1,243 +0,0 @@ -use crate::config::{ConfigManager, ConnectionConfig, Project}; -use crate::globals; -use crate::model::{MethodData, Model}; -use crate::msg::Msg; -use color_eyre::Result; -use granc_core::client::{Descriptor, DynamicRequest, DynamicResponse, GrancClient}; - -#[derive(Debug, Clone)] -pub enum Effect { - LoadConfigFromDisk, - SaveConfigToDisk(crate::config::AppConfig), - FetchServices(Project), - FetchMethods { - project: Project, - service: String, - }, - ExecuteCall { - project: Project, - service: String, - method: String, - body: String, - headers: Vec<(String, String)>, - }, -} - -pub fn handle_effect(_model: &Model, effect: Effect) -> Result> { - match effect { - Effect::LoadConfigFromDisk => { - let manager = ConfigManager::new().map_err(|e| color_eyre::eyre::eyre!(e))?; - match manager.load() { - Ok(cfg) => Ok(Some(Msg::ConfigLoaded(Ok(cfg)))), - Err(e) => Ok(Some(Msg::ConfigLoaded(Err(e.to_string())))), - } - } - - Effect::SaveConfigToDisk(cfg) => { - if let Ok(manager) = ConfigManager::new() { - let _ = manager.save(&cfg); - } - Ok(None) - } - - Effect::FetchServices(proj) => { - let handle = globals::get_handle(); - let project_id = proj.id; - let result = handle.block_on(async move { fetch_services_async(&proj).await }); - - match result { - Ok(services) => Ok(Some(Msg::ServicesFetched { - project_id, - services, - })), - Err(e) => Ok(Some(Msg::CallResponse(Err(format!("Fetch failed: {}", e))))), - } - } - - Effect::FetchMethods { project, service } => { - let handle = globals::get_handle(); - let service_name = service.clone(); - let result = - handle.block_on(async move { fetch_methods_async(&project, &service_name).await }); - - match result { - Ok(methods) => Ok(Some(Msg::MethodsFetched { service, methods })), - Err(e) => Ok(Some(Msg::CallResponse(Err(format!( - "Fetch methods failed: {}", - e - ))))), - } - } - - Effect::ExecuteCall { - project, - service, - method, - body, - headers, - } => { - let handle = globals::get_handle(); - let result = handle.block_on(async move { - execute_call_async(&project, service, method, body, headers).await - }); - - Ok(Some(Msg::CallResponse(result))) - } - } -} - -// --- Async Handlers --- - -async fn fetch_services_async(proj: &Project) -> std::result::Result, String> { - match &proj.connection { - ConnectionConfig::Reflection { url } => { - let mut client = GrancClient::connect(url).await.map_err(|e| e.to_string())?; - client.list_services().await.map_err(|e| e.to_string()) - } - ConnectionConfig::File { url, path } => { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - if let Ok(c) = GrancClient::connect(url).await { - if let Ok(fc) = c.with_file_descriptor(bytes.clone()) { - return Ok(fc.list_services()); - } - } - let client = GrancClient::offline(bytes).map_err(|e| e.to_string())?; - Ok(client.list_services()) - } - } -} - -async fn fetch_methods_async( - proj: &Project, - service: &str, -) -> std::result::Result, String> { - fn extract(descriptor: Descriptor) -> std::result::Result, String> { - match descriptor { - Descriptor::ServiceDescriptor(sd) => { - let methods = sd - .methods() - .map(|m| { - let input_desc = m.input(); - let input = input_desc.name(); - let output_desc = m.output(); - let output = output_desc.name(); - let client_stream = if m.is_client_streaming() { - "stream " - } else { - "" - }; - let server_stream = if m.is_server_streaming() { - "stream " - } else { - "" - }; - - MethodData { - name: m.name().to_string(), - signature: format!( - "rpc {}({}{}) returns ({}{})", - m.name(), - client_stream, - input, - server_stream, - output - ), - } - }) - .collect(); - Ok(methods) - } - _ => Err("Symbol is not a service".to_string()), - } - } - - match &proj.connection { - ConnectionConfig::Reflection { url } => { - let mut client = GrancClient::connect(url).await.map_err(|e| e.to_string())?; - let descriptor = client - .get_descriptor_by_symbol(service) - .await - .map_err(|e| e.to_string())?; - extract(descriptor) - } - ConnectionConfig::File { url, path } => { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - if let Ok(c) = GrancClient::connect(url).await { - // Fixed: Removed 'mut' from fc - if let Ok(fc) = c.with_file_descriptor(bytes.clone()) { - if let Some(d) = fc.get_descriptor_by_symbol(service) { - return extract(d); - } - } - } - let client = GrancClient::offline(bytes).map_err(|e| e.to_string())?; - if let Some(d) = client.get_descriptor_by_symbol(service) { - return extract(d); - } - Err("Service not found".to_string()) - } - } -} - -async fn execute_call_async( - proj: &Project, - service: String, - method: String, - body: String, - headers: Vec<(String, String)>, -) -> std::result::Result { - let json_body: serde_json::Value = - serde_json::from_str(&body).map_err(|e| format!("Invalid JSON: {}", e))?; - - let req = DynamicRequest { - service, - method, - body: json_body, - headers, - }; - - match &proj.connection { - ConnectionConfig::Reflection { url } => { - let mut client = GrancClient::connect(url).await.map_err(|e| e.to_string())?; - let resp = client.dynamic(req).await.map_err(|e| e.to_string())?; - format_response(resp) - } - ConnectionConfig::File { url, path } => { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - let client = GrancClient::connect(url).await.map_err(|e| e.to_string())?; - let mut client = client - .with_file_descriptor(bytes) - .map_err(|e| e.to_string())?; - let resp = client.dynamic(req).await.map_err(|e| e.to_string())?; - format_response(resp) - } - } -} - -fn format_response(resp: DynamicResponse) -> std::result::Result { - match resp { - DynamicResponse::Unary(Ok(v)) => Ok(serde_json::to_string_pretty(&v).unwrap_or_default()), - DynamicResponse::Unary(Err(s)) => { - Err(format!("gRPC Error: {} (Code: {})", s.message(), s.code())) - } - DynamicResponse::Streaming(r) => { - let mut out = String::new(); - match r { - Ok(msgs) => { - for (i, msg) in msgs.into_iter().enumerate() { - match msg { - Ok(v) => out.push_str(&format!( - "Msg {}:\n{}\n", - i, - serde_json::to_string_pretty(&v).unwrap_or_default() - )), - Err(s) => out.push_str(&format!("Msg {} Error: {}\n", i, s.message())), - } - } - Ok(out) - } - Err(s) => Err(format!("Stream Error: {}", s.message())), - } - } - } -} diff --git a/granc-tui/src/globals.rs b/granc-tui/src/globals.rs deleted file mode 100644 index 5dc7c4c..0000000 --- a/granc-tui/src/globals.rs +++ /dev/null @@ -1,15 +0,0 @@ -use once_cell::sync::OnceCell; -use tokio::runtime::Handle; - -pub static TOKIO_HANDLE: OnceCell = OnceCell::new(); - -pub fn init_handle() { - let handle = Handle::current(); - TOKIO_HANDLE - .set(handle) - .expect("Failed to set global Tokio handle"); -} - -pub fn get_handle() -> &'static Handle { - TOKIO_HANDLE.get().expect("Tokio handle not initialized") -} diff --git a/granc-tui/src/main.rs b/granc-tui/src/main.rs index 7a71bdd..6048cf4 100644 --- a/granc-tui/src/main.rs +++ b/granc-tui/src/main.rs @@ -1,28 +1,459 @@ -mod config; -mod effects; -mod globals; -mod model; -mod msg; -mod update; -mod view; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; +use granc_core::client::{Descriptor, DynamicRequest, DynamicResponse, GrancClient}; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, Paragraph, Widget, Wrap}, +}; +use serde_json::json; +use teatui::{ProgramError, update::Update}; -use model::Model; +fn main() -> Result<(), ProgramError> { + teatui::start( + || { + ( + Model::default(), + Some(Effect::Connect("http://localhost:50051".to_string())), + ) + }, + update, + view, + run_effects, + ) +} -#[tokio::main] -async fn main() -> color_eyre::Result<()> { - color_eyre::install()?; +// --- Model: Functional Application State --- - // Initialize global Tokio handle for effects thread - globals::init_handle(); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Pane { + Services, + Methods, + Payload, +} - let initial_model = Model::default(); +#[derive(Clone, Debug)] +pub struct Model { + pub uri: String, + pub services: Vec, + pub selected_service_idx: usize, + pub methods: Vec, + pub selected_method_idx: usize, + pub method_definition: String, + pub json_payload: String, + pub response_log: String, + pub active_pane: Pane, + pub error: Option, +} - teatui::start( - initial_model, - update::update, - view::view, - effects::handle_effect, - )?; +impl Default for Model { + fn default() -> Self { + Self { + uri: "http://localhost:50051".into(), + services: vec![], + selected_service_idx: 0, + methods: vec![], + selected_method_idx: 0, + method_definition: "Select a method to see schema...".into(), + json_payload: json!({ "name": "Granc" }).to_string(), + response_log: "Ready to inspect server.".into(), + active_pane: Pane::Services, + error: None, + } + } +} + +// --- Messages & Effects --- + +#[derive(Debug)] +pub enum Message { + SetServices(Vec), + SetMethods(Vec), + SetMethodDefinition(String), + SetResponse(String), + SetError(String), + MoveDown, + MoveUp, + SwitchPane, + ExecuteCall, + Tick, + Exit, +} + +#[derive(Debug, Clone)] +pub enum Effect { + Connect(String), + FetchMethods(String), + DescribeSymbol(String), + Call(String, String, String), +} + +impl From for Message { + fn from(value: Event) -> Self { + match value { + Event::Key(KeyEvent { + code: KeyCode::Char('q') | KeyCode::Esc, + kind: KeyEventKind::Press, + .. + }) => Self::Exit, + Event::Key(KeyEvent { + code: KeyCode::Tab, + kind: KeyEventKind::Press, + .. + }) => Self::SwitchPane, + Event::Key(KeyEvent { + code: KeyCode::Down | KeyCode::Char('j'), + kind: KeyEventKind::Press, + .. + }) => Self::MoveDown, + Event::Key(KeyEvent { + code: KeyCode::Up | KeyCode::Char('k'), + kind: KeyEventKind::Press, + .. + }) => Self::MoveUp, + Event::Key(KeyEvent { + code: KeyCode::Enter, + kind: KeyEventKind::Press, + .. + }) => Self::ExecuteCall, + _ => Self::Tick, + } + } +} + +// --- Update: Pure Logic --- + +pub fn update(mut model: Model, msg: Message) -> Update { + match msg { + Message::Exit => Update::Exit, + Message::SwitchPane => { + model.active_pane = match model.active_pane { + Pane::Services => Pane::Methods, + Pane::Methods => Pane::Payload, + Pane::Payload => Pane::Services, + }; + Update::Next(model, None) + } + Message::MoveDown => match model.active_pane { + Pane::Services if !model.services.is_empty() => { + model.selected_service_idx = + (model.selected_service_idx + 1) % model.services.len(); + Update::Next( + model.clone(), + Some(Effect::FetchMethods( + model.services[model.selected_service_idx].clone(), + )), + ) + } + Pane::Methods if !model.methods.is_empty() => { + model.selected_method_idx = (model.selected_method_idx + 1) % model.methods.len(); + let symbol = format!( + "{}.{}", + model.services[model.selected_service_idx], + model.methods[model.selected_method_idx] + ); + Update::Next(model, Some(Effect::DescribeSymbol(symbol))) + } + _ => Update::Next(model, None), + }, + Message::MoveUp => match model.active_pane { + Pane::Services if !model.services.is_empty() => { + model.selected_service_idx = if model.selected_service_idx == 0 { + model.services.len() - 1 + } else { + model.selected_service_idx - 1 + }; + Update::Next( + model.clone(), + Some(Effect::FetchMethods( + model.services[model.selected_service_idx].clone(), + )), + ) + } + Pane::Methods if !model.methods.is_empty() => { + model.selected_method_idx = if model.selected_method_idx == 0 { + model.methods.len() - 1 + } else { + model.selected_method_idx - 1 + }; + let symbol = format!( + "{}.{}", + model.services[model.selected_service_idx], + model.methods[model.selected_method_idx] + ); + Update::Next(model, Some(Effect::DescribeSymbol(symbol))) + } + _ => Update::Next(model, None), + }, + Message::SetServices(svcs) => { + model.services = svcs; + Update::Next(model, None) + } + Message::SetMethods(meths) => { + model.methods = meths; + Update::Next(model, None) + } + Message::SetMethodDefinition(def) => { + model.method_definition = def; + Update::Next(model, None) + } + Message::ExecuteCall => { + if model.services.is_empty() || model.methods.is_empty() { + return Update::Next(model, None); + } + let svc = model.services[model.selected_service_idx].clone(); + let meth = model.methods[model.selected_method_idx].clone(); + Update::Next( + model.clone(), + Some(Effect::Call(svc, meth, model.json_payload.clone())), + ) + } + Message::SetResponse(res) => { + model.response_log = res; + Update::Next(model, None) + } + Message::SetError(err) => { + model.error = Some(err); + Update::Next(model, None) + } + _ => Update::Next(model, None), + } +} + +// --- Effects: Async Isolation with local Tokio Reactor --- + +pub async fn run_effects(model: Model, effect: Effect) -> Option { + let uri = model.uri.clone(); + + match effect { + Effect::Connect(url) => match GrancClient::connect(&url).await { + Ok(mut client) => Some(Message::SetServices( + client.list_services().await.unwrap_or_default(), + )), + Err(e) => Some(Message::SetError(e.to_string())), + }, + Effect::FetchMethods(svc_name) => { + let mut client = GrancClient::connect(&uri).await.ok()?; + if let Ok(Descriptor::ServiceDescriptor(sd)) = + client.get_descriptor_by_symbol(&svc_name).await + { + return Some(Message::SetMethods( + sd.methods().map(|m| m.name().to_string()).collect(), + )); + } + None + } + Effect::DescribeSymbol(symbol) => { + let mut client = GrancClient::connect(&uri).await.ok()?; + if let Ok(descriptor) = client.get_descriptor_by_symbol(&symbol).await { + let def = match descriptor { + Descriptor::MessageDescriptor(m) => { + format!("message {} {{ // ... }}", m.name()) + } + Descriptor::ServiceDescriptor(s) => { + format!("service {} {{ // ... }}", s.name()) + } + Descriptor::EnumDescriptor(e) => format!("enum {} {{ // ... }}", e.name()), + }; + return Some(Message::SetMethodDefinition(def)); + } + None + } + Effect::Call(svc, meth, payload) => { + let mut client = GrancClient::connect(&uri).await.ok()?; + let body = serde_json::from_str(&payload).unwrap_or(json!({})); + let req = DynamicRequest { + service: svc, + method: meth, + body, + headers: vec![], + }; + match client.dynamic(req).await { + Ok(DynamicResponse::Unary(Ok(v))) => Some(Message::SetResponse(v.to_string())), + Ok(DynamicResponse::Unary(Err(s))) => { + Some(Message::SetError(s.message().to_string())) + } + _ => Some(Message::SetError("Call failed".into())), + } + } + } +} + +// --- View: Fully Featured Multi-Pane Widget --- + +pub fn view(model: Model) -> AppWidget { + AppWidget { model } +} + +pub struct AppWidget { + model: Model, +} + +impl Widget for AppWidget { + fn render(self, area: Rect, buf: &mut Buffer) { + let active_style = Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD); + let normal_style = Style::default().fg(Color::DarkGray); + + // Layout: [Header] (3) -> [Content] (rest) -> [Footer] (1) + let root = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(10), + Constraint::Length(1), + ]) + .split(area); + + // 1. Header + let header_content = vec![ + Line::from(vec![ + Span::styled( + " 🦀 GRANC WORKSPACE ", + Style::default().bg(Color::Blue).fg(Color::White).bold(), + ), + Span::raw(" Server: "), + Span::styled(self.model.uri.clone(), Color::Green).underlined(), + ]), + Line::from( + " (TAB: Switch Pane | Arrows/JK: Select | ENTER: Call) " + .italic() + .dark_gray(), + ), + ]; + Paragraph::new(header_content).render(root[0], buf); + + // 2. Content Split: Sidebar (30%) and Main (70%) + let body = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) + .split(root[1]); + + // 2a. Sidebar Vertical: Services (50%) and Methods (50%) + let sidebar = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(body[0]); + + // Services List + let svc_items: Vec = self + .model + .services + .iter() + .enumerate() + .map(|(i, s)| { + let style = if i == self.model.selected_service_idx { + Style::default().fg(Color::Yellow).bold() + } else { + Style::default() + }; + ListItem::new(format!(" • {}", s)).style(style) + }) + .collect(); + List::new(svc_items) + .block( + Block::default() + .borders(Borders::ALL) + .title(" 1. SERVICES ") + .border_style(if self.model.active_pane == Pane::Services { + active_style + } else { + normal_style + }), + ) + .render(sidebar[0], buf); + + // Methods List + let meth_items: Vec = self + .model + .methods + .iter() + .enumerate() + .map(|(i, m)| { + let style = if i == self.model.selected_method_idx { + Style::default().fg(Color::Cyan).bold() + } else { + Style::default() + }; + ListItem::new(format!(" ƒ {}", m)).style(style) + }) + .collect(); + List::new(meth_items) + .block( + Block::default() + .borders(Borders::ALL) + .title(" 2. METHODS ") + .border_style(if self.model.active_pane == Pane::Methods { + active_style + } else { + normal_style + }), + ) + .render(sidebar[1], buf); + + // 2b. Main Vertical: Definition (40%) and Payload/Response (60%) + let main = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) + .split(body[1]); + + Paragraph::new(self.model.method_definition.clone()) + .block( + Block::default() + .borders(Borders::ALL) + .title(" PROTO DEFINITION ") + .italic() + .cyan(), + ) + .render(main[0], buf); + + // Payload & Response Horizontal Split + let execution = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(main[1]); + + Paragraph::new(self.model.json_payload.clone()) + .block( + Block::default() + .borders(Borders::ALL) + .title(" 3. PAYLOAD ") + .border_style(if self.model.active_pane == Pane::Payload { + active_style + } else { + normal_style + }), + ) + .render(execution[0], buf); + + let resp_style = if self.model.error.is_some() { + Color::Red + } else { + Color::LightGreen + }; + Paragraph::new( + self.model + .error + .clone() + .unwrap_or(self.model.response_log.clone()), + ) + .block( + Block::default() + .borders(Borders::ALL) + .title(" RESPONSE LOG "), + ) + .style(Style::default().fg(resp_style)) + .wrap(Wrap { trim: true }) + .render(execution[1], buf); - Ok(()) + // 3. Footer + Paragraph::new( + " Press 'q' to Quit | Granc Workspace v0.1.0 " + .on_dark_gray() + .white(), + ) + .render(root[2], buf); + } } diff --git a/granc-tui/src/model.rs b/granc-tui/src/model.rs deleted file mode 100644 index 3f0ba7c..0000000 --- a/granc-tui/src/model.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::config::{AppConfig, Project}; -use tui_textarea::TextArea; -use uuid::Uuid; - -#[derive(Debug, Clone, PartialEq)] -pub enum Screen { - Dashboard, - NewProject, - ServiceBrowser, - MethodBrowser, - MethodView, - ResponseView, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Focus { - Body, - HeaderKey(usize), - HeaderValue(usize), -} - -#[derive(Debug, Clone)] -pub struct MethodData { - pub name: String, - pub signature: String, -} - -#[derive(Debug, Clone)] -pub struct HeaderPair { - pub key: String, - pub value: String, -} - -#[derive(Debug, Clone)] -pub struct Model { - pub screen: Screen, - pub config: AppConfig, - - // Global Input Buffer (New Project / URL) - pub input_buffer: String, - - // Navigation Indices - pub project_list_idx: usize, - pub service_list_idx: usize, - pub method_list_idx: usize, - - // Data - pub selected_project_id: Option, - pub services: Vec, - pub methods: Vec, // Updated to hold signature - pub selected_service: Option, - pub selected_method: Option, - - // Request Editor State - pub body_editor: TextArea<'static>, - pub headers: Vec, - pub focus: Focus, - - // Results - pub response_output: String, - pub status_message: Option, -} - -impl Default for Model { - fn default() -> Self { - let mut editor = TextArea::default(); - editor.set_block( - ratatui::widgets::Block::default() - .borders(ratatui::widgets::Borders::ALL) - .title("Body (JSON)"), - ); - editor.insert_str("{}"); - - Self { - screen: Screen::Dashboard, - config: AppConfig::default(), - input_buffer: String::new(), - project_list_idx: 0, - service_list_idx: 0, - method_list_idx: 0, - selected_project_id: None, - services: vec![], - methods: vec![], - selected_service: None, - selected_method: None, - body_editor: editor, - headers: vec![], - focus: Focus::Body, - response_output: String::new(), - status_message: None, - } - } -} - -impl Model { - pub fn current_project(&self) -> Option<&Project> { - self.selected_project_id - .and_then(|id| self.config.projects.iter().find(|p| p.id == id)) - } -} diff --git a/granc-tui/src/msg.rs b/granc-tui/src/msg.rs deleted file mode 100644 index 73906dd..0000000 --- a/granc-tui/src/msg.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::config::AppConfig; -use crate::model::MethodData; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; -use uuid::Uuid; - -#[derive(Debug, Clone)] -pub enum Msg { - // --- User Input --- - Key(KeyEvent), - - // --- Config Lifecycle --- - ConfigLoaded(Result), - - // --- Async Results --- - ServicesFetched { - project_id: Uuid, - services: Vec, - }, - MethodsFetched { - service: String, - methods: Vec, - }, - CallResponse(Result), - - // --- System --- - NoOp, - Exit, -} - -impl From for Msg { - fn from(event: Event) -> Self { - match event { - Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { - KeyCode::Char('c') - if key - .modifiers - .contains(crossterm::event::KeyModifiers::CONTROL) => - { - Msg::Exit - } - _ => Msg::Key(key), - }, - _ => Msg::NoOp, - } - } -} diff --git a/granc-tui/src/update.rs b/granc-tui/src/update.rs deleted file mode 100644 index b448cb4..0000000 --- a/granc-tui/src/update.rs +++ /dev/null @@ -1,365 +0,0 @@ -use color_eyre::Result; -use crossterm::event::{KeyCode, KeyModifiers}; -use teatui::Update; -use uuid::Uuid; - -use crate::config::{ConnectionConfig, Project}; -use crate::effects::Effect; -use crate::model::{Focus, HeaderPair, Model, Screen}; -use crate::msg::Msg; - -pub fn update(mut model: Model, msg: Msg) -> Result> { - match msg { - Msg::Exit => Ok(Update::Exit), - Msg::NoOp => Ok(Update::Next(model)), - - Msg::ConfigLoaded(res) => { - match res { - Ok(cfg) => { - model.config = cfg; - model.status_message = Some("Config loaded".into()); - } - Err(e) => model.status_message = Some(format!("Config Error: {}", e)), - } - Ok(Update::Next(model)) - } - - // Handle Editor Specific Messages (Method View) - Msg::Key(key) if model.screen == Screen::MethodView => match key.code { - KeyCode::Esc => { - model.screen = Screen::MethodBrowser; - Ok(Update::Next(model)) - } - KeyCode::Tab => { - match model.focus { - Focus::Body => { - if !model.headers.is_empty() { - model.focus = Focus::HeaderKey(0); - } - } - Focus::HeaderKey(i) => model.focus = Focus::HeaderValue(i), - Focus::HeaderValue(i) => { - if i + 1 < model.headers.len() { - model.focus = Focus::HeaderKey(i + 1); - } else { - model.focus = Focus::Body; - } - } - } - Ok(Update::Next(model)) - } - KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) => { - model.headers.push(HeaderPair { - key: "".into(), - value: "".into(), - }); - model.focus = Focus::HeaderKey(model.headers.len() - 1); - Ok(Update::Next(model)) - } - KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => { - match model.focus { - Focus::HeaderKey(i) | Focus::HeaderValue(i) => { - model.headers.remove(i); - model.focus = Focus::Body; - } - _ => {} - } - Ok(Update::Next(model)) - } - KeyCode::Enter | KeyCode::Char('s') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - execute_request(model) - } - _ => { - match model.focus { - Focus::Body => { - model.body_editor.input(to_ratatui_key(key)); - } - Focus::HeaderKey(i) => { - handle_text_input(&mut model.headers[i].key, key); - } - Focus::HeaderValue(i) => { - handle_text_input(&mut model.headers[i].value, key); - } - } - Ok(Update::Next(model)) - } - }, - - Msg::Key(key) => match (model.screen.clone(), key.code) { - // Global - (_, KeyCode::Char('q')) => Ok(Update::Exit), - - // --- Dashboard --- - (Screen::Dashboard, KeyCode::Char('l')) => { - Ok(Update::NextWithEffect(model, Effect::LoadConfigFromDisk)) - } - (Screen::Dashboard, KeyCode::Char('n')) => { - model.screen = Screen::NewProject; - model.input_buffer.clear(); - Ok(Update::Next(model)) - } - (Screen::Dashboard, KeyCode::Down) => { - if !model.config.projects.is_empty() { - model.project_list_idx = - (model.project_list_idx + 1) % model.config.projects.len(); - } - Ok(Update::Next(model)) - } - (Screen::Dashboard, KeyCode::Up) => { - if !model.config.projects.is_empty() { - model.project_list_idx = if model.project_list_idx == 0 { - model.config.projects.len() - 1 - } else { - model.project_list_idx - 1 - }; - } - Ok(Update::Next(model)) - } - (Screen::Dashboard, KeyCode::Enter) => { - let project_opt = model.config.projects.get(model.project_list_idx).cloned(); - if let Some(proj) = project_opt { - model.selected_project_id = Some(proj.id); - model.screen = Screen::ServiceBrowser; - model.service_list_idx = 0; - model.status_message = Some("Fetching services...".into()); - Ok(Update::NextWithEffect(model, Effect::FetchServices(proj))) - } else { - Ok(Update::Next(model)) - } - } - - // --- New Project --- - (Screen::NewProject, KeyCode::Enter) => { - let new_proj = Project { - id: Uuid::new_v4(), - name: if model.input_buffer.is_empty() { - "Untitled".to_string() - } else { - model.input_buffer.clone() - }, - connection: ConnectionConfig::Reflection { - url: model.input_buffer.clone(), - }, - saved_requests: vec![], - }; - model.config.projects.push(new_proj); - model.screen = Screen::Dashboard; - model.project_list_idx = model.config.projects.len() - 1; - let effect = Effect::SaveConfigToDisk(model.config.clone()); - Ok(Update::NextWithEffect(model, effect)) - } - (Screen::NewProject, KeyCode::Char(c)) => { - model.input_buffer.push(c); - Ok(Update::Next(model)) - } - (Screen::NewProject, KeyCode::Backspace) => { - model.input_buffer.pop(); - Ok(Update::Next(model)) - } - (Screen::NewProject, KeyCode::Esc) => { - model.screen = Screen::Dashboard; - Ok(Update::Next(model)) - } - - // --- Service Browser --- - (Screen::ServiceBrowser, KeyCode::Down) => { - if !model.services.is_empty() { - model.service_list_idx = (model.service_list_idx + 1) % model.services.len(); - } - Ok(Update::Next(model)) - } - (Screen::ServiceBrowser, KeyCode::Up) => { - if !model.services.is_empty() { - model.service_list_idx = if model.service_list_idx == 0 { - model.services.len() - 1 - } else { - model.service_list_idx - 1 - }; - } - Ok(Update::Next(model)) - } - (Screen::ServiceBrowser, KeyCode::Enter) => { - if let (Some(svc), Some(proj)) = ( - model.services.get(model.service_list_idx).cloned(), - model.current_project().cloned(), - ) { - model.selected_service = Some(svc.clone()); - model.status_message = Some("Fetching methods...".into()); - Ok(Update::NextWithEffect( - model, - Effect::FetchMethods { - project: proj, - service: svc, - }, - )) - } else { - Ok(Update::Next(model)) - } - } - (Screen::ServiceBrowser, KeyCode::Esc) => { - model.screen = Screen::Dashboard; - Ok(Update::Next(model)) - } - - // --- Method Browser --- - (Screen::MethodBrowser, KeyCode::Down) => { - if !model.methods.is_empty() { - model.method_list_idx = (model.method_list_idx + 1) % model.methods.len(); - } - Ok(Update::Next(model)) - } - (Screen::MethodBrowser, KeyCode::Up) => { - if !model.methods.is_empty() { - model.method_list_idx = if model.method_list_idx == 0 { - model.methods.len() - 1 - } else { - model.method_list_idx - 1 - }; - } - Ok(Update::Next(model)) - } - (Screen::MethodBrowser, KeyCode::Enter) => { - if let Some(m) = model.methods.get(model.method_list_idx).cloned() { - model.selected_method = Some(m.name); - model.headers.clear(); - model.focus = Focus::Body; - model.screen = Screen::MethodView; - } - Ok(Update::Next(model)) - } - (Screen::MethodBrowser, KeyCode::Esc) => { - model.screen = Screen::ServiceBrowser; - Ok(Update::Next(model)) - } - - // --- Response View --- - (Screen::ResponseView, KeyCode::Esc) => { - model.screen = Screen::MethodView; - Ok(Update::Next(model)) - } - - _ => Ok(Update::Next(model)), - }, - - Msg::ServicesFetched { - project_id, - services, - } => { - if model.selected_project_id == Some(project_id) { - model.services = services; - model.service_list_idx = 0; - model.status_message = Some("Services loaded.".into()); - } - Ok(Update::Next(model)) - } - - Msg::MethodsFetched { service, methods } => { - if model.selected_service.as_deref() == Some(&service) { - model.methods = methods; - model.method_list_idx = 0; - model.screen = Screen::MethodBrowser; - model.status_message = Some("Methods loaded.".into()); - } - Ok(Update::Next(model)) - } - - Msg::CallResponse(res) => { - model.screen = Screen::ResponseView; - match res { - Ok(s) => { - model.response_output = s; - model.status_message = Some("Call success".into()); - } - Err(e) => { - model.response_output = format!("Error: {}", e); - model.status_message = Some("Call failed".into()); - } - } - Ok(Update::Next(model)) - } - } -} - -fn execute_request(mut model: Model) -> Result> { - let execution_data = if let (Some(p), Some(s), Some(m)) = ( - model.current_project(), - &model.selected_service, - &model.selected_method, - ) { - Some((p.clone(), s.clone(), m.clone())) - } else { - None - }; - - if let Some((p, s, m)) = execution_data { - let headers: Vec<(String, String)> = model - .headers - .iter() - .filter(|h| !h.key.is_empty()) - .map(|h| (h.key.clone(), h.value.clone())) - .collect(); - - let body_lines = model.body_editor.lines().to_vec(); - let body = body_lines.join("\n"); - - let effect = Effect::ExecuteCall { - project: p, - service: s, - method: m, - body, - headers, - }; - model.status_message = Some("Executing request...".into()); - Ok(Update::NextWithEffect(model, effect)) - } else { - model.status_message = - Some("Error: Missing execution context (project/service/method)".into()); - Ok(Update::Next(model)) - } -} - -fn handle_text_input(target: &mut String, key: crossterm::event::KeyEvent) { - match key.code { - KeyCode::Char(c) => target.push(c), - KeyCode::Backspace => { - target.pop(); - } - _ => {} - } -} - -fn to_ratatui_key(key: crossterm::event::KeyEvent) -> ratatui::crossterm::event::KeyEvent { - let code = match key.code { - crossterm::event::KeyCode::Backspace => ratatui::crossterm::event::KeyCode::Backspace, - crossterm::event::KeyCode::Enter => ratatui::crossterm::event::KeyCode::Enter, - crossterm::event::KeyCode::Left => ratatui::crossterm::event::KeyCode::Left, - crossterm::event::KeyCode::Right => ratatui::crossterm::event::KeyCode::Right, - crossterm::event::KeyCode::Up => ratatui::crossterm::event::KeyCode::Up, - crossterm::event::KeyCode::Down => ratatui::crossterm::event::KeyCode::Down, - crossterm::event::KeyCode::Home => ratatui::crossterm::event::KeyCode::Home, - crossterm::event::KeyCode::End => ratatui::crossterm::event::KeyCode::End, - crossterm::event::KeyCode::PageUp => ratatui::crossterm::event::KeyCode::PageUp, - crossterm::event::KeyCode::PageDown => ratatui::crossterm::event::KeyCode::PageDown, - crossterm::event::KeyCode::Tab => ratatui::crossterm::event::KeyCode::Tab, - crossterm::event::KeyCode::BackTab => ratatui::crossterm::event::KeyCode::BackTab, - crossterm::event::KeyCode::Delete => ratatui::crossterm::event::KeyCode::Delete, - crossterm::event::KeyCode::Insert => ratatui::crossterm::event::KeyCode::Insert, - crossterm::event::KeyCode::F(n) => ratatui::crossterm::event::KeyCode::F(n), - crossterm::event::KeyCode::Char(c) => ratatui::crossterm::event::KeyCode::Char(c), - crossterm::event::KeyCode::Null => ratatui::crossterm::event::KeyCode::Null, - crossterm::event::KeyCode::Esc => ratatui::crossterm::event::KeyCode::Esc, - _ => ratatui::crossterm::event::KeyCode::Null, - }; - - let modifiers = - ratatui::crossterm::event::KeyModifiers::from_bits_truncate(key.modifiers.bits()); - - ratatui::crossterm::event::KeyEvent { - code, - modifiers, - kind: ratatui::crossterm::event::KeyEventKind::Press, - state: ratatui::crossterm::event::KeyEventState::empty(), - } -} diff --git a/granc-tui/src/view.rs b/granc-tui/src/view.rs deleted file mode 100644 index fa7c43c..0000000 --- a/granc-tui/src/view.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::model::{Focus, Model, Screen}; -use color_eyre::Result; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Style, Stylize}, - widgets::ListState, - widgets::{Block, Borders, List, ListItem, Paragraph, StatefulWidget, Widget, WidgetRef}, -}; -use teatui::View; - -struct RootWidget { - model: Model, -} - -impl WidgetRef for RootWidget { - fn render_ref(&self, area: Rect, buf: &mut ratatui::prelude::Buffer) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(1)]) - .split(area); - - let main_area = chunks[0]; - let status_bar = chunks[1]; - - match self.model.screen { - Screen::Dashboard => draw_dashboard(&self.model, main_area, buf), - Screen::NewProject => draw_new_project(&self.model, main_area, buf), - Screen::ServiceBrowser => draw_services(&self.model, main_area, buf), - Screen::MethodBrowser => draw_method_browser(&self.model, main_area, buf), - Screen::MethodView => draw_method_execution(&self.model, main_area, buf), - Screen::ResponseView => draw_response(&self.model, main_area, buf), - } - - let msg = self.model.status_message.as_deref().unwrap_or("Ready"); - let status_text = format!( - " {} | Screen: {:?} | [Q] Quit | [L] Load", - msg, self.model.screen - ); - Paragraph::new(status_text) - .style(Style::default().bg(Color::Blue).fg(Color::White)) - .render_ref(status_bar, buf); - } -} - -pub fn view(model: &Model) -> Result { - Ok(View::new(RootWidget { - model: model.clone(), - })) -} - -// --- Helpers --- - -fn draw_dashboard(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { - let items: Vec = model - .config - .projects - .iter() - .map(|p| ListItem::new(p.name.as_str())) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .title("Projects (Press 'n' for new)") - .borders(Borders::ALL), - ) - .highlight_style(Style::default().bg(Color::DarkGray).bold()); - - let mut state = ListState::default().with_selected(Some(model.project_list_idx)); - StatefulWidget::render(list, area, buf, &mut state); -} - -fn draw_new_project(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { - let text = Paragraph::new(format!("Server URL: {}", model.input_buffer)).block( - Block::default() - .title("New Project (Enter URL)") - .borders(Borders::ALL), - ); - text.render_ref(area, buf); -} - -fn draw_services(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { - let items: Vec = model - .services - .iter() - .map(|s| ListItem::new(s.as_str())) - .collect(); - - let list = List::new(items) - .block(Block::default().title("Services").borders(Borders::ALL)) - .highlight_style(Style::default().bg(Color::DarkGray).bold()); - - let mut state = ListState::default().with_selected(Some(model.service_list_idx)); - StatefulWidget::render(list, area, buf, &mut state); -} - -fn draw_method_browser(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { - let items: Vec = model - .methods - .iter() - .map(|m| ListItem::new(m.signature.as_str())) - .collect(); - - let list = List::new(items) - .block( - Block::default() - .title(format!( - "Methods of {}", - model.selected_service.as_deref().unwrap_or("?") - )) - .borders(Borders::ALL), - ) - .highlight_style(Style::default().bg(Color::DarkGray).bold()); - - let mut state = ListState::default().with_selected(Some(model.method_list_idx)); - StatefulWidget::render(list, area, buf, &mut state); -} - -fn draw_method_execution(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(1), // Title - Constraint::Percentage(60), // Body - Constraint::Percentage(30), // Headers - Constraint::Length(1), // Hint - ]) - .split(area); - - let title = format!( - "Executing: {}", - model.selected_method.as_deref().unwrap_or("?") - ); - Paragraph::new(title).bold().render_ref(chunks[0], buf); - - // Body Editor - let mut editor = model.body_editor.clone(); - let body_block = Block::default() - .borders(Borders::ALL) - .title("Request Body (JSON)"); - - if model.focus == Focus::Body { - editor.set_style(Style::default()); - editor.set_block(body_block.border_style(Style::default().fg(Color::Yellow))); - } else { - editor.set_style(Style::default().fg(Color::DarkGray)); - editor.set_block(body_block); - } - - editor.render(chunks[1], buf); - - // Headers - let header_block = Block::default() - .borders(Borders::ALL) - .title("Headers (Ctrl+H to add)"); - header_block.render_ref(chunks[2], buf); - - let header_area = chunks[2].inner(ratatui::layout::Margin { - horizontal: 1, - vertical: 1, - }); - - let header_rows = Layout::default() - .direction(Direction::Vertical) - .constraints(vec![Constraint::Length(1); model.headers.len()]) - .split(header_area); - - for (i, header) in model.headers.iter().enumerate() { - if i >= header_rows.len() { - break; - } - - let row_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(45), - Constraint::Length(1), - Constraint::Percentage(45), - ]) - .split(header_rows[i]); - - let k_style = if model.focus == Focus::HeaderKey(i) { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }; - Paragraph::new(header.key.as_str()) - .style(k_style) - .render_ref(row_chunks[0], buf); - - Paragraph::new(":").render_ref(row_chunks[1], buf); - - let v_style = if model.focus == Focus::HeaderValue(i) { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }; - Paragraph::new(header.value.as_str()) - .style(v_style) - .render_ref(row_chunks[2], buf); - } - - Paragraph::new( - "[Tab] Cycle Focus | [Ctrl+Enter/S] Send | [Ctrl+H] Add Header | [Ctrl+D] Remove Header", - ) - .style(Style::default().fg(Color::Gray)) - .render_ref(chunks[3], buf); -} - -fn draw_response(model: &Model, area: Rect, buf: &mut ratatui::prelude::Buffer) { - let p = Paragraph::new(model.response_output.as_str()).block( - Block::default() - .title("Response (Esc to back)") - .borders(Borders::ALL), - ); - p.render_ref(area, buf); -}