diff --git a/Cargo.lock b/Cargo.lock index d2ca9c9..08499b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -373,7 +373,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -408,7 +408,16 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", ] [[package]] @@ -878,15 +887,30 @@ dependencies = [ "vsimd", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -977,7 +1001,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1053,6 +1077,15 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.56" @@ -1128,7 +1161,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1188,6 +1221,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1207,6 +1254,15 @@ dependencies = [ "wasm-bindgen", ] +[[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 = "copypasta" version = "0.10.2" @@ -1318,7 +1374,9 @@ dependencies = [ "aws-sdk-secretsmanager", "chrono", "clap", + "crossterm", "dirs", + "ratatui", "serde", "serde-envfile", "serde_json", @@ -1333,6 +1391,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "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 = "crunchy" version = "0.2.4" @@ -1349,12 +1434,62 @@ dependencies = [ "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 = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[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 2.0.117", +] + +[[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 2.0.117", +] + +[[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.8" @@ -1364,6 +1499,28 @@ dependencies = [ "powerfmt", ] +[[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 2.0.117", +] + [[package]] name = "digest" version = "0.10.7" @@ -1410,7 +1567,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1485,7 +1642,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1549,6 +1706,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set 0.5.3", + "regex", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1564,12 +1731,35 @@ dependencies = [ "simd-adler32", ] +[[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 = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[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 = "flate2" version = "1.1.9" @@ -1657,7 +1847,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2183,7 +2373,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.2", "tokio", "tower-service", "tracing", @@ -2301,6 +2491,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -2347,6 +2543,28 @@ dependencies = [ "serde_core", ] +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -2359,6 +2577,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -2407,6 +2634,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + [[package]] name = "keyboard-types" version = "0.8.3" @@ -2445,6 +2683,12 @@ dependencies = [ "smallvec", ] +[[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" @@ -2509,6 +2753,15 @@ dependencies = [ "redox_syscall 0.7.3", ] +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "linebender_include_doc_path" version = "0.1.0" @@ -2563,6 +2816,25 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "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]] name = "malloc_buf" version = "0.0.6" @@ -2665,6 +2937,12 @@ dependencies = [ "libc", ] +[[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" @@ -2689,6 +2967,12 @@ dependencies = [ "paste", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2706,6 +2990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -2727,7 +3012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.8.0", "bitflags 2.11.0", "cfg-if", "cfg_aliases", @@ -2776,6 +3061,29 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.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 = "nu-ansi-term" version = "0.50.3" @@ -2791,6 +3099,17 @@ 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.117", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -2829,7 +3148,16 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", ] [[package]] @@ -3116,6 +3444,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-float" version = "5.1.0" @@ -3232,6 +3569,101 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[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.117", +] + +[[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.11" @@ -3249,7 +3681,7 @@ checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3376,7 +3808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -3446,12 +3878,112 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +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 = "range-alloc" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "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.11.0", + "compact_str", + "hashbrown 0.16.1", + "indoc", + "itertools", + "kasuari", + "lru", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "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.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3517,6 +4049,18 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -3828,7 +4372,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3852,7 +4396,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3881,6 +4425,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[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" @@ -3897,6 +4462,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "skrifa" version = "0.37.0" @@ -4063,6 +4634,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4086,6 +4678,17 @@ dependencies = [ "zeno", ] +[[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]] name = "syn" version = "2.0.117" @@ -4105,7 +4708,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4136,6 +4739,69 @@ dependencies = [ "winapi-util", ] +[[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.11.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "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" @@ -4162,7 +4828,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4173,7 +4839,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4193,7 +4859,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -4276,7 +4944,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4395,7 +5063,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4489,6 +5157,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.1.0" @@ -4534,6 +5208,17 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.2.2" @@ -4588,6 +5273,8 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ + "atomic", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -4656,6 +5343,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4745,7 +5441,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -4947,6 +5643,78 @@ dependencies = [ "wasm-bindgen", ] +[[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 0.3.4", + "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 = "wgpu" version = "27.0.1" @@ -4983,8 +5751,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" dependencies = [ "arrayvec", - "bit-set", - "bit-vec", + "bit-set 0.8.0", + "bit-vec 0.8.0", "bitflags 2.11.0", "bytemuck", "cfg_aliases", @@ -5044,7 +5812,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", + "bit-set 0.8.0", "bitflags 2.11.0", "block", "bytemuck", @@ -5067,7 +5835,7 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "ordered-float", + "ordered-float 5.1.0", "parking_lot", "portable-atomic", "portable-atomic-util", @@ -5206,7 +5974,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5217,7 +5985,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5228,7 +5996,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5239,7 +6007,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5632,7 +6400,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -5648,7 +6416,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -5857,7 +6625,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -5914,7 +6682,7 @@ checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "zbus-lockstep", "zbus_xml", "zvariant", @@ -5929,7 +6697,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -5981,7 +6749,7 @@ checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -6001,7 +6769,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -6022,7 +6790,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -6056,7 +6824,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -6088,7 +6856,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "zvariant_utils", ] @@ -6101,6 +6869,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.117", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 3b81a8f..ebd2f39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,10 @@ version = "0.1.0" edition = "2024" [features] +default = ["gui"] dev-store = [] +gui = ["dep:xilem"] +tui = ["dep:ratatui", "dep:crossterm"] [dependencies] anyhow = "1" @@ -19,4 +22,7 @@ serde-envfile = "0.3" tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } zeroize = { version = "1", features = ["derive"] } -xilem = { git = "https://github.com/linebender/xilem", package = "xilem" } +xilem = { git = "https://github.com/linebender/xilem", package = "xilem", optional = true } + +ratatui = { version = "0.30", optional = true } +crossterm = { version = "0.29", optional = true } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9e436d7 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +.PHONY: all check fmt clippy test build build-gui build-tui build-all run run-tui clean + +all: check fmt clippy test + +check: + cargo check --no-default-features --features gui + cargo check --no-default-features --features tui + cargo check --no-default-features --features "gui tui" + +fmt: + cargo fmt --all --check + +clippy: + cargo clippy --no-default-features --features gui -- -D warnings + cargo clippy --no-default-features --features tui -- -D warnings + cargo clippy --no-default-features --features "gui tui" -- -D warnings + +test: + cargo test --no-default-features --features gui + cargo test --no-default-features --features tui + cargo test --no-default-features --features "gui tui" + +# Default build: GUI (xilem). Same as `make build-gui`. +build: build-gui + +build-gui: + cargo build --release --no-default-features --features gui + +# TUI-only build: no xilem, no fontconfig, no wgpu. +build-tui: + cargo build --release --no-default-features --features tui + +# Both editors in one binary. +build-all: + cargo build --release --no-default-features --features "gui tui" + +run: + cargo run --release --no-default-features --features gui -- + +run-tui: + cargo run --release --no-default-features --features tui -- --editor tui + +clean: + cargo clean diff --git a/src/cli.rs b/src/cli.rs index 468b449..c318c81 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,16 +1,32 @@ -use clap::{Parser, Subcommand}; +use clap::Parser; +use clap::Subcommand; +use clap::ValueEnum; #[derive(Parser)] -#[command(name = "credctl", about = "Minimal secret manager", disable_help_subcommand = true)] +#[command( + name = "credctl", + about = "Minimal secret manager", + disable_help_subcommand = true +)] pub struct Cli { /// Override the active context for this invocation #[arg(long, global = true)] pub context: Option, + /// Editor UI to use (default: gui). Requires --features tui for the tui option. + #[arg(long, global = true, value_enum, default_value_t = EditorKind::Gui)] + pub editor: EditorKind, + #[command(subcommand)] pub command: Commands, } +#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)] +pub enum EditorKind { + Gui, + Tui, +} + #[derive(Subcommand)] pub enum Commands { /// List all secrets @@ -114,6 +130,7 @@ COMMANDS: GLOBAL FLAGS: --context Override the active context for this invocation + --editor Editor UI (default: gui; tui requires --features tui) EXAMPLES: credctl create my-secret diff --git a/src/config.rs b/src/config.rs index 35c0d5f..e1e7cc9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -83,14 +83,22 @@ pub fn save_config(config: &Config) -> Result<()> { Ok(()) } -pub fn resolve_context<'a>(config: &'a Config, override_name: Option<&'a str>) -> Result<(&'a str, &'a StoreType)> { +pub fn resolve_context<'a>( + config: &'a Config, + override_name: Option<&'a str>, +) -> Result<(&'a str, &'a StoreType)> { let name = override_name.unwrap_or(&config.current_context); - let store_type = config.contexts.get(name) + let store_type = config + .contexts + .get(name) .ok_or_else(|| anyhow::anyhow!("context '{}' not found", name))?; Ok((name, store_type)) } -pub fn handle_context_command(config: &mut Config, action: &crate::cli::ContextAction) -> Result<()> { +pub fn handle_context_command( + config: &mut Config, + action: &crate::cli::ContextAction, +) -> Result<()> { match action { crate::cli::ContextAction::List => { if config.contexts.is_empty() { @@ -99,7 +107,11 @@ pub fn handle_context_command(config: &mut Config, action: &crate::cli::ContextA let mut names: Vec<_> = config.contexts.keys().collect(); names.sort(); for name in names { - let marker = if name == &config.current_context { " *" } else { "" }; + let marker = if name == &config.current_context { + " *" + } else { + "" + }; let store = &config.contexts[name]; println!("{:<20} {}{}", name, store, marker); } @@ -119,11 +131,18 @@ pub fn handle_context_command(config: &mut Config, action: &crate::cli::ContextA println!("switched to context '{}'", name); } - crate::cli::ContextAction::Set { name, store, path, profile } => { + crate::cli::ContextAction::Set { + name, + store, + path, + profile, + } => { let store_type = match store.as_str() { "file" => StoreType::File { path: path.clone() }, "env" => StoreType::Env { path: path.clone() }, - "aws" => StoreType::Aws { profile: profile.clone() }, + "aws" => StoreType::Aws { + profile: profile.clone(), + }, other => bail!("unknown store type '{}' (expected: file, env, aws)", other), }; config.contexts.insert(name.clone(), store_type); @@ -133,7 +152,10 @@ pub fn handle_context_command(config: &mut Config, action: &crate::cli::ContextA crate::cli::ContextAction::Delete { name } => { if name == &config.current_context { - bail!("cannot delete the current context '{}'. Switch first.", name); + bail!( + "cannot delete the current context '{}'. Switch first.", + name + ); } if config.contexts.remove(name).is_none() { bail!("context '{}' not found", name); diff --git a/src/main.rs b/src/main.rs index 2244b9f..349a3fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +#[cfg(not(any(feature = "gui", feature = "tui")))] +compile_error!("credctl requires at least one editor feature: `gui` and/or `tui`"); + mod cli; mod config; mod store; @@ -7,12 +10,13 @@ use std::io::{BufRead, Write}; use anyhow::Result; use clap::Parser; -use cli::{Cli, Commands}; +use cli::{Cli, Commands, EditorKind}; use config::StoreType; #[tokio::main] async fn main() -> Result<()> { // Suppress xilem/wgpu log noise unless the user explicitly sets RUST_LOG. + #[cfg(feature = "gui")] if std::env::var_os("RUST_LOG").is_none() { // SAFETY: called before tokio spawns any threads. unsafe { std::env::set_var("RUST_LOG", "error") }; @@ -41,7 +45,7 @@ async fn main() -> Result<()> { StoreType::File { path } => { let dir = path.unwrap_or_else(|| ".credctl/secrets".to_string()); let s = store::file::FileStore::new(dir); - run_with_store(&s, &ui::XilemEditor, cli.command).await + dispatch_editor(&s, cli.editor, cli.command).await } #[cfg(not(feature = "dev-store"))] StoreType::File { .. } => { @@ -50,23 +54,44 @@ async fn main() -> Result<()> { StoreType::Env { path } => { let p = path.unwrap_or_else(|| ".env".to_string()); let s = store::env::EnvStore::new(p); - run_with_store(&s, &ui::XilemEditor, cli.command).await + dispatch_editor(&s, cli.editor, cli.command).await } StoreType::Aws { profile } => { let s = store::aws::AwsStore::new(profile).await?; - run_with_store(&s, &ui::XilemEditor, cli.command).await + dispatch_editor(&s, cli.editor, cli.command).await + } + } +} + +async fn dispatch_editor( + store: &impl store::SecretStore, + editor: EditorKind, + cmd: Commands, +) -> Result<()> { + match editor { + #[cfg(feature = "gui")] + EditorKind::Gui => run_with_store(store, &ui::XilemEditor, cmd).await, + #[cfg(not(feature = "gui"))] + EditorKind::Gui => { + anyhow::bail!("gui editor requires building with --features gui") + } + #[cfg(feature = "tui")] + EditorKind::Tui => run_with_store(store, &ui::TuiEditor, cmd).await, + #[cfg(not(feature = "tui"))] + EditorKind::Tui => { + anyhow::bail!("tui editor requires building with --features tui") } } } /// If the value is a JSON object with all-string values, treat it as structured. fn detect_kind(value: &str) -> store::SecretKind { - if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(value) { - if map.values().all(|v| v.is_string()) { - let mut keys: Vec = map.keys().cloned().collect(); - keys.sort(); - return store::SecretKind::Structured { keys }; - } + if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(value) + && map.values().all(|v| v.is_string()) + { + let mut keys: Vec = map.keys().cloned().collect(); + keys.sort(); + return store::SecretKind::Structured { keys }; } store::SecretKind::PlainText } @@ -181,7 +206,10 @@ async fn run_with_store( let name = resolve(name)?; print!("Type '{}' to confirm deletion: ", name); std::io::stdout().flush()?; - let line = std::io::stdin().lock().lines().next() + let line = std::io::stdin() + .lock() + .lines() + .next() .unwrap_or_else(|| Ok(String::new()))?; if line.trim() != name { anyhow::bail!("confirmation did not match, aborting"); diff --git a/src/store/aws.rs b/src/store/aws.rs index 975af03..e067ced 100644 --- a/src/store/aws.rs +++ b/src/store/aws.rs @@ -42,8 +42,7 @@ impl AwsStore { } fn aws_ts_to_chrono(ts: &aws_sdk_secretsmanager::primitives::DateTime) -> DateTime { - DateTime::from_timestamp(ts.secs(), ts.subsec_nanos() as u32) - .unwrap_or_else(Utc::now) + DateTime::from_timestamp(ts.secs(), ts.subsec_nanos()).unwrap_or_else(Utc::now) } fn kind_tag_value(kind: &SecretKind) -> &'static str { @@ -80,10 +79,12 @@ impl SecretStore for AwsStore { let tags = entry.tags(); let kind = self.parse_tags(tags); - let created_at = entry.created_date() + let created_at = entry + .created_date() .map(Self::aws_ts_to_chrono) .unwrap_or_else(Utc::now); - let updated_at = entry.last_changed_date() + let updated_at = entry + .last_changed_date() .map(Self::aws_ts_to_chrono) .unwrap_or(created_at); @@ -106,7 +107,9 @@ impl SecretStore for AwsStore { } async fn describe(&self, name: &str) -> anyhow::Result { - let resp = self.client.describe_secret() + let resp = self + .client + .describe_secret() .secret_id(name) .send() .await @@ -115,10 +118,12 @@ impl SecretStore for AwsStore { let tags = resp.tags(); let kind = self.parse_tags(tags); - let created_at = resp.created_date() + let created_at = resp + .created_date() .map(Self::aws_ts_to_chrono) .unwrap_or_else(Utc::now); - let updated_at = resp.last_changed_date() + let updated_at = resp + .last_changed_date() .map(Self::aws_ts_to_chrono) .unwrap_or(created_at); @@ -131,11 +136,7 @@ impl SecretStore for AwsStore { } async fn get_value(&self, name: &str) -> anyhow::Result { - let resp = match self.client.get_secret_value() - .secret_id(name) - .send() - .await - { + let resp = match self.client.get_secret_value().secret_id(name).send().await { Ok(r) => r, Err(e) => { // Secret exists but has no value version yet — treat as empty. @@ -144,18 +145,24 @@ impl SecretStore for AwsStore { { return Ok(SecretValue::new(String::new())); } - return Err(e).with_context(|| format!("failed to get value for secret '{}'", name)); + return Err(e) + .with_context(|| format!("failed to get value for secret '{}'", name)); } }; - let value = resp.secret_string() - .ok_or_else(|| anyhow::anyhow!("secret '{}' has no string value (binary secrets not supported)", name))?; + let value = resp.secret_string().ok_or_else(|| { + anyhow::anyhow!( + "secret '{}' has no string value (binary secrets not supported)", + name + ) + })?; Ok(SecretValue::new(value.to_string())) } async fn put_value(&self, name: &str, value: &SecretValue) -> anyhow::Result<()> { - self.client.put_secret_value() + self.client + .put_secret_value() .secret_id(name) .secret_string(value.expose()) .send() @@ -164,8 +171,15 @@ impl SecretStore for AwsStore { Ok(()) } - async fn create(&self, name: &str, kind: SecretKind, value: &SecretValue) -> anyhow::Result<()> { - let mut req = self.client.create_secret() + async fn create( + &self, + name: &str, + kind: SecretKind, + value: &SecretValue, + ) -> anyhow::Result<()> { + let mut req = self + .client + .create_secret() .name(name) .secret_string(value.expose()); @@ -185,13 +199,15 @@ impl SecretStore for AwsStore { ); } - req.send().await + req.send() + .await .with_context(|| format!("failed to create secret '{}'", name))?; Ok(()) } async fn delete(&self, name: &str) -> anyhow::Result<()> { - self.client.delete_secret() + self.client + .delete_secret() .secret_id(name) .force_delete_without_recovery(true) .send() @@ -199,5 +215,4 @@ impl SecretStore for AwsStore { .with_context(|| format!("failed to delete secret '{}'", name))?; Ok(()) } - } diff --git a/src/store/env.rs b/src/store/env.rs index ba0e219..1a1153a 100644 --- a/src/store/env.rs +++ b/src/store/env.rs @@ -28,7 +28,8 @@ impl EnvStore { .ok() .and_then(|t| { let dur = t.duration_since(std::time::UNIX_EPOCH).ok()?; - Utc.timestamp_opt(dur.as_secs() as i64, dur.subsec_nanos()).single() + Utc.timestamp_opt(dur.as_secs() as i64, dur.subsec_nanos()) + .single() }) .unwrap_or_else(Utc::now) } @@ -75,7 +76,11 @@ impl SecretStore for EnvStore { } let expected = self.secret_name(); if name != expected { - bail!("secret '{}' not found (this env store manages '{}')", name, expected); + bail!( + "secret '{}' not found (this env store manages '{}')", + name, + expected + ); } let map = self.parse_env()?; Ok(self.meta_from_map(&map)) @@ -84,7 +89,11 @@ impl SecretStore for EnvStore { async fn get_value(&self, name: &str) -> anyhow::Result { let expected = self.secret_name(); if name != expected { - bail!("secret '{}' not found (this env store manages '{}')", name, expected); + bail!( + "secret '{}' not found (this env store manages '{}')", + name, + expected + ); } let map = self.parse_env()?; let json = serde_json::to_string(&map)?; @@ -94,17 +103,26 @@ impl SecretStore for EnvStore { async fn put_value(&self, name: &str, value: &SecretValue) -> anyhow::Result<()> { let expected = self.secret_name(); if name != expected { - bail!("secret '{}' not found (this env store manages '{}')", name, expected); + bail!( + "secret '{}' not found (this env store manages '{}')", + name, + expected + ); } let map: HashMap = serde_json::from_str(value.expose()) .context("value must be a JSON object with string values")?; - let env_str: String = serde_envfile::to_string(&map) - .context("failed to serialize to env format")?; + let env_str: String = + serde_envfile::to_string(&map).context("failed to serialize to env format")?; std::fs::write(&self.path, env_str)?; Ok(()) } - async fn create(&self, name: &str, _kind: SecretKind, value: &SecretValue) -> anyhow::Result<()> { + async fn create( + &self, + name: &str, + _kind: SecretKind, + value: &SecretValue, + ) -> anyhow::Result<()> { if self.path.exists() { bail!("env file '{}' already exists", self.path.display()); } @@ -113,10 +131,9 @@ impl SecretStore for EnvStore { bail!("env store name is '{}', cannot create '{}'", expected, name); } // Write the initial value - let map: HashMap = serde_json::from_str(value.expose()) - .unwrap_or_default(); - let env_str: String = serde_envfile::to_string(&map) - .context("failed to serialize to env format")?; + let map: HashMap = serde_json::from_str(value.expose()).unwrap_or_default(); + let env_str: String = + serde_envfile::to_string(&map).context("failed to serialize to env format")?; if let Some(parent) = self.path.parent() { std::fs::create_dir_all(parent)?; } @@ -127,7 +144,11 @@ impl SecretStore for EnvStore { async fn delete(&self, name: &str) -> anyhow::Result<()> { let expected = self.secret_name(); if name != expected { - bail!("secret '{}' not found (this env store manages '{}')", name, expected); + bail!( + "secret '{}' not found (this env store manages '{}')", + name, + expected + ); } if !self.path.exists() { bail!("env file '{}' not found", self.path.display()); @@ -135,5 +156,4 @@ impl SecretStore for EnvStore { std::fs::remove_file(&self.path)?; Ok(()) } - } diff --git a/src/store/file.rs b/src/store/file.rs index 54b9cd4..dd97b45 100644 --- a/src/store/file.rs +++ b/src/store/file.rs @@ -31,7 +31,8 @@ impl FileStore { let path = self.secret_path(name); let data = std::fs::read_to_string(&path) .with_context(|| format!("secret '{}' not found", name))?; - serde_json::from_str(&data).with_context(|| format!("corrupt secret file: {}", path.display())) + serde_json::from_str(&data) + .with_context(|| format!("corrupt secret file: {}", path.display())) } fn write_file(&self, sf: &SecretFile) -> anyhow::Result<()> { @@ -80,7 +81,12 @@ impl SecretStore for FileStore { self.write_file(&sf) } - async fn create(&self, name: &str, kind: SecretKind, value: &SecretValue) -> anyhow::Result<()> { + async fn create( + &self, + name: &str, + kind: SecretKind, + value: &SecretValue, + ) -> anyhow::Result<()> { let path = self.secret_path(name); if path.exists() { bail!("secret '{}' already exists", name); @@ -106,5 +112,4 @@ impl SecretStore for FileStore { std::fs::remove_file(&path)?; Ok(()) } - } diff --git a/src/store/mod.rs b/src/store/mod.rs index 7020ee2..8f4dd80 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -39,7 +39,9 @@ impl std::fmt::Debug for SecretValue { pub enum SecretKind { PlainText, #[serde(alias = "Json")] - Structured { keys: Vec }, + Structured { + keys: Vec, + }, } impl std::fmt::Display for SecretKind { @@ -63,12 +65,26 @@ pub struct SecretMeta { pub trait SecretStore { /// If the store manages a single implicit secret, return its name. - fn default_name(&self) -> Option { None } + fn default_name(&self) -> Option { + None + } fn list(&self) -> impl std::future::Future>> + Send; fn describe(&self, name: &str) -> impl std::future::Future> + Send; - fn get_value(&self, name: &str) -> impl std::future::Future> + Send; - fn put_value(&self, name: &str, value: &SecretValue) -> impl std::future::Future> + Send; - fn create(&self, name: &str, kind: SecretKind, value: &SecretValue) -> impl std::future::Future> + Send; + fn get_value( + &self, + name: &str, + ) -> impl std::future::Future> + Send; + fn put_value( + &self, + name: &str, + value: &SecretValue, + ) -> impl std::future::Future> + Send; + fn create( + &self, + name: &str, + kind: SecretKind, + value: &SecretValue, + ) -> impl std::future::Future> + Send; fn delete(&self, name: &str) -> impl std::future::Future> + Send; } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 22cfc45..3a4189c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -22,6 +22,14 @@ pub trait SecretEditor { fn edit(&self, kind: &SecretKind, current: Option) -> Result; } +#[cfg(feature = "gui")] pub mod xilem_editor; +#[cfg(feature = "gui")] pub use xilem_editor::XilemEditor; + +#[cfg(feature = "tui")] +pub mod tui_editor; + +#[cfg(feature = "tui")] +pub use tui_editor::TuiEditor; diff --git a/src/ui/tui_editor.rs b/src/ui/tui_editor.rs new file mode 100644 index 0000000..62646ce --- /dev/null +++ b/src/ui/tui_editor.rs @@ -0,0 +1,427 @@ +use std::io; +use std::io::Stdout; + +use anyhow::Result; +use crossterm::event; +use crossterm::event::Event; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; +use crossterm::execute; +use crossterm::terminal::EnterAlternateScreen; +use crossterm::terminal::LeaveAlternateScreen; +use crossterm::terminal::disable_raw_mode; +use crossterm::terminal::enable_raw_mode; +use ratatui::Frame; +use ratatui::Terminal; +use ratatui::backend::CrosstermBackend; +use ratatui::layout::Constraint; +use ratatui::layout::Direction; +use ratatui::layout::Layout; +use ratatui::layout::Rect; +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::Block; +use ratatui::widgets::Borders; +use ratatui::widgets::Paragraph; +use serde_json::Map; +use serde_json::Value; + +use crate::store::SecretKind; +use crate::store::SecretValue; +use crate::ui::Cancelled; +use crate::ui::SecretEditor; + +pub struct TuiEditor; + +impl SecretEditor for TuiEditor { + fn edit(&self, kind: &SecretKind, current: Option) -> Result { + let app = App::new(kind, current); + match run_app(app)? { + Outcome::Saved(s) => Ok(SecretValue::new(s)), + Outcome::Cancelled => Err(Cancelled.into()), + } + } +} + +enum Outcome { + Saved(String), + Cancelled, +} + +// Editor modes mirror SecretKind: plain string or key-value rows. +enum Mode { + PlainText { content: String, masked: bool }, + Structured(Structured), +} + +struct Structured { + rows: Vec, + selected: usize, + editing: EditField, + masked_all: bool, +} + +struct Row { + key: String, + value: String, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum EditField { + None, + Key, + Value, +} + +struct App { + mode: Mode, +} + +impl App { + fn new(kind: &SecretKind, current: Option) -> Self { + let mode = match kind { + SecretKind::PlainText => { + let content = current + .as_ref() + .map(|v| v.expose().to_owned()) + .unwrap_or_default(); + Mode::PlainText { + content, + masked: true, + } + } + SecretKind::Structured { keys } => { + let existing: Map = current + .as_ref() + .and_then(|v| serde_json::from_str(v.expose()).ok()) + .unwrap_or_default(); + + let mut rows: Vec = Vec::new(); + // Schema keys first (preserve order), then any extras from the stored value. + for k in keys { + let value = existing + .get(k) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_owned(); + rows.push(Row { + key: k.clone(), + value, + }); + } + for (k, v) in &existing { + if !keys.contains(k) { + rows.push(Row { + key: k.clone(), + value: v.as_str().unwrap_or("").to_owned(), + }); + } + } + if rows.is_empty() { + rows.push(Row { + key: String::new(), + value: String::new(), + }); + } + Mode::Structured(Structured { + rows, + selected: 0, + editing: EditField::None, + masked_all: true, + }) + } + }; + Self { mode } + } +} + +fn run_app(mut app: App) -> Result { + let mut terminal = setup_terminal()?; + let outcome = event_loop(&mut terminal, &mut app); + restore_terminal(&mut terminal)?; + outcome +} + +fn setup_terminal() -> Result>> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + Ok(terminal) +} + +fn restore_terminal(terminal: &mut Terminal>) -> Result<()> { + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + Ok(()) +} + +fn event_loop(terminal: &mut Terminal>, app: &mut App) -> Result { + loop { + terminal.draw(|f| draw(f, app))?; + + let Event::Key(key) = event::read()? else { + continue; + }; + if key.kind != KeyEventKind::Press { + continue; + } + + // Global cancel. + if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { + return Ok(Outcome::Cancelled); + } + + if let Some(out) = handle_key(&mut app.mode, key) { + return Ok(out); + } + } +} + +fn handle_key(mode: &mut Mode, key: KeyEvent) -> Option { + match mode { + Mode::PlainText { content, masked } => handle_plain_key(content, masked, key), + Mode::Structured(s) => handle_structured_key(s, key), + } +} + +fn handle_plain_key(content: &mut String, masked: &mut bool, key: KeyEvent) -> Option { + match key.code { + KeyCode::Esc => return Some(Outcome::Cancelled), + KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => { + return Some(Outcome::Saved(content.clone())); + } + KeyCode::Tab => { + *masked = !*masked; + } + KeyCode::Backspace => { + content.pop(); + } + KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => { + content.push(c); + } + _ => {} + } + None +} + +fn handle_structured_key(s: &mut Structured, key: KeyEvent) -> Option { + // While editing a field, all printable keys go to the buffer. + if s.editing != EditField::None { + let row = &mut s.rows[s.selected]; + let buf = match s.editing { + EditField::Key => &mut row.key, + EditField::Value => &mut row.value, + EditField::None => unreachable!(), + }; + match key.code { + KeyCode::Esc | KeyCode::Enter | KeyCode::Tab => s.editing = EditField::None, + KeyCode::Backspace => { + buf.pop(); + } + KeyCode::Char(c) + if !key + .modifiers + .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => + { + buf.push(c); + } + _ => {} + } + return None; + } + + match key.code { + KeyCode::Esc => return Some(Outcome::Cancelled), + KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => { + return Some(Outcome::Saved(serialize_rows(&s.rows))); + } + KeyCode::Char('m') => { + s.masked_all = !s.masked_all; + } + KeyCode::Up | KeyCode::Char('k') => { + if s.selected > 0 { + s.selected -= 1; + } + } + KeyCode::Down | KeyCode::Char('j') => { + if s.selected + 1 < s.rows.len() { + s.selected += 1; + } + } + KeyCode::Enter | KeyCode::Char('e') => s.editing = EditField::Value, + KeyCode::Char('r') => s.editing = EditField::Key, + KeyCode::Char('a') => { + s.rows.push(Row { + key: String::new(), + value: String::new(), + }); + s.selected = s.rows.len() - 1; + s.editing = EditField::Key; + } + KeyCode::Char('d') => { + s.rows.remove(s.selected); + if s.rows.is_empty() { + s.rows.push(Row { + key: String::new(), + value: String::new(), + }); + } + if s.selected >= s.rows.len() { + s.selected = s.rows.len() - 1; + } + } + _ => {} + } + None +} + +fn serialize_rows(rows: &[Row]) -> String { + let mut map = Map::new(); + for r in rows { + if !r.key.is_empty() { + map.insert(r.key.clone(), Value::String(r.value.clone())); + } + } + serde_json::to_string(&map).unwrap_or_else(|_| "{}".to_string()) +} + +// -- Rendering -- + +const ACCENT: Color = Color::Rgb(0x3b, 0x82, 0xf6); +const MUTED: Color = Color::Rgb(0x80, 0x80, 0x98); + +fn draw(f: &mut Frame<'_>, app: &App) { + let area = f.area(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(3), + Constraint::Length(2), + ]) + .split(area); + + draw_header(f, chunks[0]); + match &app.mode { + Mode::PlainText { content, masked } => draw_plain(f, chunks[1], content, *masked), + Mode::Structured(s) => draw_structured(f, chunks[1], s), + } + draw_footer(f, chunks[2], &app.mode); +} + +fn draw_header(f: &mut Frame<'_>, area: Rect) { + let title = Paragraph::new(Line::from(Span::styled( + " credctl - Edit Secret ", + Style::default().fg(ACCENT).add_modifier(Modifier::BOLD), + ))) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(title, area); +} + +fn draw_plain(f: &mut Frame<'_>, area: Rect, content: &str, masked: bool) { + let display = if masked { + if content.is_empty() { + "(empty)".to_string() + } else { + "*".repeat(content.chars().count().min(64)) + } + } else { + content.to_string() + }; + let title = if masked { + " Value (masked) " + } else { + " Value " + }; + let block = Block::default().borders(Borders::ALL).title(title); + let style = if masked { + Style::default().fg(MUTED) + } else { + Style::default() + }; + f.render_widget(Paragraph::new(display).style(style).block(block), area); +} + +fn draw_structured(f: &mut Frame<'_>, area: Rect, s: &Structured) { + let block = Block::default().borders(Borders::ALL).title(" Variables "); + let inner = block.inner(area); + f.render_widget(block, area); + + let mut lines: Vec = Vec::with_capacity(s.rows.len() + 1); + lines.push(Line::from(vec![ + Span::styled( + format!(" {:<30} ", "KEY"), + Style::default().fg(MUTED).add_modifier(Modifier::BOLD), + ), + Span::styled( + "VALUE", + Style::default().fg(MUTED).add_modifier(Modifier::BOLD), + ), + ])); + + for (i, r) in s.rows.iter().enumerate() { + let is_sel = i == s.selected; + let marker = if is_sel { "> " } else { " " }; + let key_str = if r.key.is_empty() { + "(empty)".to_string() + } else { + r.key.clone() + }; + let value_str = if s.masked_all && !r.value.is_empty() { + "*".repeat(r.value.chars().count().min(24)) + } else if r.value.is_empty() { + "(empty)".to_string() + } else { + r.value.clone() + }; + + let key_style = field_style(is_sel, s.editing == EditField::Key, false); + let value_style = field_style(is_sel, s.editing == EditField::Value, s.masked_all); + + lines.push(Line::from(vec![ + Span::raw(marker), + Span::styled(format!("{:<30} ", key_str), key_style), + Span::styled(value_str, value_style), + ])); + } + + f.render_widget(Paragraph::new(lines), inner); +} + +fn field_style(is_selected: bool, is_editing: bool, masked: bool) -> Style { + if is_selected && is_editing { + Style::default().fg(Color::Black).bg(ACCENT) + } else if is_selected { + Style::default().add_modifier(Modifier::BOLD) + } else if masked { + Style::default().fg(MUTED) + } else { + Style::default() + } +} + +fn draw_footer(f: &mut Frame<'_>, area: Rect, mode: &Mode) { + let hint = match mode { + Mode::PlainText { masked, .. } => { + let m = if *masked { "show" } else { "hide" }; + format!(" type to edit | Tab {} | Ctrl+S save | Esc cancel", m) + } + Mode::Structured(s) if s.editing != EditField::None => { + " typing... | Enter/Esc done ".to_string() + } + Mode::Structured(_) => " j/k move | e edit value | r rename key | a add | d delete | \ + m mask | Ctrl+S save | Esc cancel" + .to_string(), + }; + let p = Paragraph::new(Span::styled(hint, Style::default().fg(MUTED))) + .block(Block::default().borders(Borders::TOP)); + f.render_widget(p, area); +} diff --git a/src/ui/xilem_editor.rs b/src/ui/xilem_editor.rs index 39d9469..01db704 100644 --- a/src/ui/xilem_editor.rs +++ b/src/ui/xilem_editor.rs @@ -1,16 +1,14 @@ use std::sync::{Arc, Mutex}; use anyhow::Result; +use xilem::core::one_of::Either; use xilem::masonry::layout::Length; use xilem::masonry::parley::FontWeight; use xilem::masonry::peniko::Color; use xilem::style::Style as _; -use xilem::core::one_of::Either; use xilem::view::{FlexExt, flex_col, flex_row, label, portal, sized_box, text_button, text_input}; use xilem::winit::dpi::LogicalSize; -use xilem::{ - AppState, EventLoop, WidgetView, WindowId, WindowOptions, WindowView, Xilem, window, -}; +use xilem::{AppState, EventLoop, WidgetView, WindowId, WindowOptions, WindowView, Xilem, window}; use crate::store::{SecretKind, SecretValue}; @@ -273,9 +271,7 @@ fn plain_text_body(content: String, masked: bool) -> impl WidgetView )) .gap(Length::px(8.)); - flex_col((header, editor)) - .gap(Length::px(6.)) - .padding(16.0) + flex_col((header, editor)).gap(Length::px(6.)).padding(16.0) } // ── JSON body ──