diff --git a/Cargo.lock b/Cargo.lock index 2dcccaf..a460453 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,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 = "android_system_properties" version = "0.1.5" @@ -28,9 +34,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -43,15 +49,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -82,6 +88,26 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "ascend-tools-cli" version = "0.5.0" @@ -89,6 +115,7 @@ dependencies = [ "anyhow", "ascend-tools-core", "ascend-tools-mcp", + "ascend-tools-tui", "assert_cmd", "base64", "clap", @@ -138,6 +165,17 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "ascend-tools-tui" +version = "0.5.0" +dependencies = [ + "anyhow", + "arboard", + "ascend-tools-core", + "crossterm", + "ratatui", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -150,9 +188,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9" dependencies = [ "anstyle", "bstr", @@ -171,7 +209,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]] @@ -250,6 +297,27 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -282,17 +350,38 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[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" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -310,6 +399,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 = "chacha20" version = "0.10.0" @@ -337,9 +432,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -347,9 +442,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -359,27 +454,36 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -400,12 +504,35 @@ 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 = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[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 = "convert_case" version = "0.11.0" @@ -458,6 +585,39 @@ dependencies = [ "cfg-if", ] +[[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", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -480,6 +640,16 @@ 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 = "ctor" version = "0.6.3" @@ -520,7 +690,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -543,7 +713,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -554,9 +724,15 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "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 = "der" version = "0.7.10" @@ -577,6 +753,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 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "difflib" version = "0.4.0" @@ -595,6 +793,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dtor" version = "0.1.1" @@ -654,6 +871,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -691,12 +914,66 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "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]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -713,12 +990,35 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[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" @@ -750,6 +1050,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 = "form_urlencoded" version = "1.2.2" @@ -815,7 +1121,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -858,6 +1164,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -925,13 +1241,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -939,6 +1266,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" @@ -946,6 +1278,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 = "hkdf" version = "0.12.4" @@ -1082,6 +1420,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -1094,12 +1446,43 @@ 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 = "is_terminal_polyfill" 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" @@ -1161,6 +1544,23 @@ dependencies = [ "simple_asn1", ] +[[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 = "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" @@ -1178,9 +1578,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -1198,12 +1598,27 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[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" @@ -1219,6 +1634,25 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +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 = "matchers" version = "0.2.0" @@ -1240,12 +1674,33 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[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" @@ -1263,6 +1718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -1292,13 +1748,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "napi" version = "3.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6944d0bf100571cd6e1a98a316cdca262deb6fccf8d93f5ae1502ca3fc88bd3" dependencies = [ - "bitflags", + "bitflags 2.11.0", "ctor", "futures", "napi-build", @@ -1321,12 +1787,12 @@ version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c914b5e420182bfb73504e0607592cdb8e2e21437d450883077669fb72a114d" dependencies = [ - "convert_case", + "convert_case 0.11.0", "ctor", "napi-derive-backend", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1335,11 +1801,11 @@ version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0864cf6a82e2cfb69067374b64c9253d7e910e5b34db833ed7495dda56ccb18" dependencies = [ - "convert_case", + "convert_case 0.11.0", "proc-macro2", "quote", "semver", - "syn", + "syn 2.0.117", ] [[package]] @@ -1351,12 +1817,35 @@ dependencies = [ "libloading", ] +[[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 = "nohash-hasher" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[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 = "normalize-line-endings" version = "0.3.0" @@ -1404,6 +1893,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" @@ -1434,11 +1934,93 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1452,6 +2034,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[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 = "p256" version = "0.13.2" @@ -1525,10 +2116,105 @@ dependencies = [ ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "percent-encoding" +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 0.8.5", +] + +[[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 = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] [[package]] name = "pin-project-lite" @@ -1563,6 +2249,25 @@ dependencies = [ "spki", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1615,7 +2320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1636,6 +2341,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.45" @@ -1733,13 +2450,98 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[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 = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -1759,7 +2561,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1856,7 +2658,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn", + "syn 2.0.117", ] [[package]] @@ -1900,7 +2702,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2004,9 +2806,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2034,7 +2836,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] [[package]] @@ -2063,7 +2865,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "libc", @@ -2113,7 +2915,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2124,7 +2926,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2178,6 +2980,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" @@ -2222,6 +3045,12 @@ dependencies = [ "time", ] +[[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.12" @@ -2236,12 +3065,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2273,18 +3102,56 @@ dependencies = [ "pin-project-lite", ] +[[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.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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[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" @@ -2304,9 +3171,9 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -2315,12 +3182,75 @@ dependencies = [ "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 = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[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", + "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" @@ -2347,7 +3277,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2358,7 +3288,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2370,6 +3300,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -2378,7 +3322,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -2426,7 +3372,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2499,7 +3445,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2525,9 +3471,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -2547,6 +3493,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 = "unicode-ident" version = "1.0.24" @@ -2559,6 +3511,23 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2615,10 +3584,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ + "atomic", "getrandom 0.4.2", "js-sys", "wasm-bindgen", @@ -2636,6 +3606,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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 = "wait-timeout" version = "0.2.1" @@ -2711,7 +3690,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2752,7 +3731,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -2776,6 +3755,100 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[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", + "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" +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-util" version = "0.1.11" @@ -2785,6 +3858,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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-core" version = "0.62.2" @@ -2806,7 +3885,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2817,7 +3896,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3096,7 +4175,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3112,7 +4191,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3124,7 +4203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", @@ -3154,24 +4233,41 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3185,3 +4281,18 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 41d5662..7bbe13d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "src/ascend_tools/ascend-tools-core", "src/ascend_tools/ascend-tools-mcp", "src/ascend_tools/ascend-tools-cli", + "src/ascend_tools/ascend-tools-tui", ] resolver = "3" diff --git a/bin/check-js b/bin/check-js index 501e5c2..52027c2 100755 --- a/bin/check-js +++ b/bin/check-js @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/../src/ascend_tools/ascend-tools-js" +npm install --no-fund --no-audit npm run build npm test diff --git a/docs/cli.md b/docs/cli.md index a651f4f..5c7def7 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -237,6 +237,9 @@ ascend-tools otto run "Help me debug this pipeline" --provider "OpenAI" --model ascend-tools otto provider list ascend-tools otto model list ascend-tools otto model list --provider "OpenAI" + +# Interactive chat (Ctrl+C to exit) +ascend-tools otto tui --workspace "My Workspace" ``` ## Install AI assistant skills diff --git a/src/ascend_tools/ascend-tools-cli/Cargo.toml b/src/ascend_tools/ascend-tools-cli/Cargo.toml index a14289d..e2559a6 100644 --- a/src/ascend_tools/ascend-tools-cli/Cargo.toml +++ b/src/ascend_tools/ascend-tools-cli/Cargo.toml @@ -21,6 +21,7 @@ path = "src/main.rs" [dependencies] ascend-tools-core = { path = "../ascend-tools-core" } ascend-tools-mcp = { path = "../ascend-tools-mcp" } +ascend-tools-tui = { path = "../ascend-tools-tui" } clap = { version = "4.5", features = ["derive", "env"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src/ascend_tools/ascend-tools-cli/src/otto.rs b/src/ascend_tools/ascend-tools-cli/src/otto.rs index b765711..c900b4c 100644 --- a/src/ascend_tools/ascend-tools-cli/src/otto.rs +++ b/src/ascend_tools/ascend-tools-cli/src/otto.rs @@ -199,6 +199,28 @@ pub(crate) enum OttoCommands { #[command(subcommand)] command: Option, }, + /// Interactive multi-turn conversation with Otto (Ctrl+C to exit) + Tui { + /// Workspace to use for context + #[arg(long)] + workspace: Option, + + /// Deployment to use for context + #[arg(long)] + deployment: Option, + + /// Use UUID instead of title + #[arg(long)] + uuid: Option, + + /// LLM provider to use (requires --model) + #[arg(long, requires = "model")] + provider: Option, + + /// LLM model to use + #[arg(long)] + model: Option, + }, } #[derive(Subcommand)] @@ -370,5 +392,24 @@ pub(crate) fn handle_otto_cmd( } Ok(()) } + OttoCommands::Tui { + workspace, + deployment, + uuid, + provider, + model, + } => { + let runtime_uuid = client.resolve_optional_runtime_target( + workspace.as_deref(), + deployment.as_deref(), + uuid.as_deref(), + )?; + let otto_model = OttoModel::from_options(provider.as_deref(), model.as_deref()); + let context_label = workspace + .as_deref() + .map(|w| format!("workspace:{w}")) + .or(deployment.as_deref().map(|d| format!("deployment:{d}"))); + ascend_tools_tui::run_tui(client, runtime_uuid, otto_model, context_label) + } } } diff --git a/src/ascend_tools/ascend-tools-cli/src/skill-cli.md b/src/ascend_tools/ascend-tools-cli/src/skill-cli.md index 5debc2a..ca841d2 100644 --- a/src/ascend_tools/ascend-tools-cli/src/skill-cli.md +++ b/src/ascend_tools/ascend-tools-cli/src/skill-cli.md @@ -98,6 +98,7 @@ ascend-tools flow get-run --workspace | --deployment <TITLE> ascend-tools otto run "<PROMPT>" [--workspace <TITLE>] [--provider <NAME>] [--model <ID>] ascend-tools otto provider list ascend-tools otto model list [--provider <NAME>] +ascend-tools otto tui [--workspace <TITLE>] ``` ### Flow run spec diff --git a/src/ascend_tools/ascend-tools-py/Cargo.lock b/src/ascend_tools/ascend-tools-py/Cargo.lock index 831a00b..13fdd99 100644 --- a/src/ascend_tools/ascend-tools-py/Cargo.lock +++ b/src/ascend_tools/ascend-tools-py/Cargo.lock @@ -17,6 +17,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 = "android_system_properties" version = "0.1.5" @@ -82,6 +88,26 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "ascend-tools-cli" version = "0.5.0" @@ -89,6 +115,7 @@ dependencies = [ "anyhow", "ascend-tools-core", "ascend-tools-mcp", + "ascend-tools-tui", "clap", "libc", "serde", @@ -142,6 +169,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "ascend-tools-tui" +version = "1.0.0" +dependencies = [ + "anyhow", + "arboard", + "ascend-tools-core", + "crossterm", + "ratatui", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -150,7 +188,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]] @@ -229,6 +276,27 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -250,12 +318,33 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[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" @@ -278,6 +367,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 = "chacha20" version = "0.10.0" @@ -334,7 +429,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -343,6 +438,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -359,12 +463,35 @@ 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 = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[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 = "core-foundation" version = "0.10.1" @@ -408,6 +535,39 @@ dependencies = [ "cfg-if", ] +[[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", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -430,6 +590,16 @@ 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 = "curve25519-dalek" version = "4.1.3" @@ -454,7 +624,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -477,7 +647,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -488,9 +658,15 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "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 = "der" version = "0.7.10" @@ -511,6 +687,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" @@ -523,6 +721,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -567,6 +784,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -604,6 +827,60 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "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]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -620,12 +897,35 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[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" @@ -636,12 +936,24 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" 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" version = "0.3.32" @@ -698,7 +1010,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -741,6 +1053,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -752,6 +1074,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -760,7 +1094,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -777,13 +1111,24 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -791,6 +1136,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" @@ -798,6 +1148,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 = "hkdf" version = "0.12.4" @@ -933,6 +1289,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -945,12 +1315,43 @@ 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 = "is_terminal_polyfill" 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" @@ -1012,6 +1413,23 @@ dependencies = [ "simple_asn1", ] +[[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 = "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" @@ -1039,12 +1457,61 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[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.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +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 = "matchers" version = "0.2.0" @@ -1066,12 +1533,33 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[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" @@ -1089,10 +1577,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[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" @@ -1134,6 +1656,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" @@ -1164,6 +1697,88 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1182,6 +1797,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[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 = "p256" version = "0.13.2" @@ -1206,6 +1830,29 @@ dependencies = [ "sha2", ] +[[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 = "pastey" version = "0.2.1" @@ -1237,6 +1884,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 0.8.5", +] + +[[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-lite" version = "0.2.17" @@ -1270,6 +2012,19 @@ dependencies = [ "spki", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1298,7 +2053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1319,6 +2074,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + [[package]] name = "pyo3" version = "0.28.2" @@ -1361,7 +2122,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1374,7 +2135,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1387,6 +2148,12 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.45" @@ -1396,6 +2163,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1449,6 +2222,100 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -1466,7 +2333,19 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[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]] @@ -1551,7 +2430,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn", + "syn 2.0.117", ] [[package]] @@ -1583,6 +2462,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.37" @@ -1663,6 +2555,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -1704,9 +2602,15 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sec1" version = "0.7.3" @@ -1727,7 +2631,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "libc", @@ -1777,7 +2681,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1788,7 +2692,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1830,6 +2734,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" @@ -1868,6 +2793,12 @@ dependencies = [ "time", ] +[[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.12" @@ -1919,18 +2850,56 @@ dependencies = [ "pin-project-lite", ] +[[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.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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[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" @@ -1954,6 +2923,69 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" +[[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", + "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" @@ -1980,7 +3012,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1991,7 +3023,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2003,6 +3035,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -2011,7 +3057,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -2058,7 +3106,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2131,7 +3179,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2179,12 +3227,41 @@ 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 = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2245,6 +3322,7 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ + "atomic", "getrandom 0.4.2", "js-sys", "wasm-bindgen", @@ -2262,6 +3340,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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 = "walkdir" version = "2.5.0" @@ -2328,7 +3415,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2369,7 +3456,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -2393,6 +3480,100 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[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", + "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" +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-util" version = "0.1.11" @@ -2402,6 +3583,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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-core" version = "0.62.2" @@ -2423,7 +3610,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2434,7 +3621,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2713,7 +3900,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2729,7 +3916,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2741,7 +3928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", @@ -2771,6 +3958,23 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "zerocopy" version = "0.8.40" @@ -2788,7 +3992,7 @@ checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2802,3 +4006,18 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", +] diff --git a/src/ascend_tools/ascend-tools-tui/Cargo.toml b/src/ascend_tools/ascend-tools-tui/Cargo.toml new file mode 100644 index 0000000..7214144 --- /dev/null +++ b/src/ascend_tools/ascend-tools-tui/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ascend-tools-tui" +version = "0.5.0" +edition = "2024" +rust-version = "1.88" +authors = ["Cody <cody.peterson@ascend.io>"] +license = "MIT" +description = "Interactive TUI for the Ascend REST API" +repository = "https://github.com/ascend-io/ascend-tools" +homepage = "https://github.com/ascend-io/ascend-tools" +readme = "README.md" + +[lib] +name = "ascend_tools_tui" +path = "src/lib.rs" + +[dependencies] +ascend-tools-core = { path = "../ascend-tools-core" } +anyhow = "1" +ratatui = { version = "0.30", features = ["unstable-rendered-line-info"] } +crossterm = "0.29" +arboard = "3" diff --git a/src/ascend_tools/ascend-tools-tui/README.md b/src/ascend_tools/ascend-tools-tui/README.md new file mode 100644 index 0000000..5e87a22 --- /dev/null +++ b/src/ascend_tools/ascend-tools-tui/README.md @@ -0,0 +1,52 @@ +# ascend-tools-tui + +Interactive terminal UI for chatting with [Otto](https://www.ascend.io), the Ascend AI assistant. + +Built on [`ascend-tools-core`](../ascend-tools-core), [`ratatui`](https://crates.io/crates/ratatui), and [`crossterm`](https://crates.io/crates/crossterm). + +## Features + +- Vi keybindings (default) with Emacs mode toggle +- Multi-line input (Alt+Enter for newlines) +- Input history persisted across sessions +- Smooth streaming output (~200 chars/sec) +- Markdown rendering (code blocks, bold, inline code) +- Tab completion for slash commands +- Clipboard copy (`/copy`) +- Message timestamps (`/timestamps`) +- Scrollable chat with scrollbar +- Cursor shape changes (block/bar) for Vi modes + +## Usage + +The TUI is typically launched via the CLI: + +```bash +ascend-tools otto tui +ascend-tools otto tui --workspace my-ws +``` + +### As a library + +```rust +use ascend_tools::client::AscendClient; +use ascend_tools::config::Config; + +let config = Config::from_env()?; +let client = AscendClient::new(config)?; +ascend_tools_tui::run_tui(&client, None, None, None)?; +``` + +## Slash Commands + +| Command | Description | +|---------|-------------| +| `/help` | Show commands and keybindings | +| `/vim`, `/vi` | Switch to Vi keybindings | +| `/emacs` | Switch to Emacs keybindings | +| `/copy` | Copy last Otto response to clipboard | +| `/timestamps` | Toggle message timestamps | +| `/clear` | Clear chat and start new thread | +| `/quit`, `/exit` | Exit | + +See the [top-level README](../../../README.md) for full documentation. diff --git a/src/ascend_tools/ascend-tools-tui/src/lib.rs b/src/ascend_tools/ascend-tools-tui/src/lib.rs new file mode 100644 index 0000000..9dab3ca --- /dev/null +++ b/src/ascend_tools/ascend-tools-tui/src/lib.rs @@ -0,0 +1,1645 @@ +#![deny(unsafe_code)] + +//! Interactive TUI for Otto chat. +//! +//! Full-screen terminal interface using ratatui with: +//! - Scrollable chat history with scrollbar +//! - Streaming responses with spinner and smooth output +//! - Vi input mode (default) with `/emacs` toggle +//! - Multi-line input (Alt+Enter for newline) +//! - Input history (Up/Down, persisted to ~/.ascend-tools/history) +//! - Slash commands with tab completion +//! - Markdown rendering (code blocks, bold, inline code) +//! - Message timestamps (`/timestamps` to toggle) +//! - Clipboard copy (`/copy`) +//! - Vi yank/paste registers + +use std::collections::{HashMap, VecDeque}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Mutex, mpsc}; +use std::time::{Duration, Instant, SystemTime}; + +use anyhow::Result; +use crossterm::cursor::SetCursorStyle; +use crossterm::event::{ + self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, + Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind, +}; +use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; +use ratatui::prelude::*; +use ratatui::widgets::*; + +use ascend_tools::client::AscendClient; +use ascend_tools::models::{OttoChatRequest, OttoModel, StreamEvent}; +use std::ops::ControlFlow; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const SPINNER: &[&str] = &[ + "\u{280b}", "\u{2819}", "\u{2839}", "\u{2838}", "\u{283c}", "\u{2834}", "\u{2826}", "\u{2827}", + "\u{2807}", "\u{280f}", +]; +const POLL_DURATION: Duration = Duration::from_millis(16); +const SPINNER_INTERVAL: Duration = Duration::from_millis(80); + +#[rustfmt::skip] +const COMMANDS: &[&str] = &[ + "/clear", "/copy", "/emacs", "/exit", "/help", + "/q", "/quit", "/timestamps", "/vi", "/vim", +]; + +#[rustfmt::skip] +const SPLASH: &[&str] = &[ + " \u{2588}\u{2588} \u{2588}\u{2588}", + " \u{2588}\u{2588}\u{2588} \u{2588}\u{2588}\u{2588}", + " \u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}", + " \u{2588}\u{2588} . . \u{2588}\u{2588}", + " \u{2588}\u{2588} v \u{2588}\u{2588}", + " \u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}", + "", + " \u{2588}\u{2588}\u{2588}\u{2588} \u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588} \u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2588} \u{2588}\u{2588}\u{2588}\u{2588}", + " \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588}", + " \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588}", + " \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588}", + " \u{2588}\u{2588}\u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588} \u{2588}\u{2588}\u{2588}\u{2588}", + "", + " type /help for commands", +]; + +#[rustfmt::skip] +const EXPERIMENTAL_BANNER: &[&str] = &[ + "\u{26a0} EXPERIMENTAL \u{26a0}", + "", + "This feature is under active development.", + "Expect rough edges, bugs, and breaking changes.", + "Mascot below not finalized.", +]; + +const USER_COLOR: Color = Color::Rgb(80, 120, 200); // dark blue +const OTTO_COLOR: Color = Color::Rgb(232, 67, 67); // ascend red +const SYSTEM_COLOR: Color = Color::Rgb(160, 120, 200); // purple +const VI_NORMAL_COLOR: Color = Color::Rgb(255, 140, 80); // orange +const CODE_COLOR: Color = Color::Rgb(255, 140, 80); // orange (matches vi normal) +const DIM_COLOR: Color = Color::Rgb(100, 100, 100); +const WARNING_COLOR: Color = Color::Rgb(255, 200, 50); // yellow +const DIM_OTTO_COLOR: Color = Color::Rgb(120, 45, 45); // muted ascend red +const POPUP_BG: Color = Color::Rgb(50, 50, 50); +const TEXT_COLOR: Color = Color::White; +const TIMESTAMP_COLOR: Color = Color::Rgb(80, 80, 80); + +/// Characters per second for smoothed streaming output. +const STREAM_CPS: f64 = 200.0; +/// Above this pending count, flush in bulk to catch up. +const STREAM_BULK_THRESHOLD: usize = 200; +/// Above this pending count, skip smoothing entirely. +const STREAM_FAST_THRESHOLD: usize = 50; + +const MAX_HISTORY: usize = 1000; +const MAX_INPUT_LINES: u16 = 8; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +enum StreamMsg { + ProviderInfo { + provider_label: Option<String>, + model_label: String, + }, + /// Stream messages tagged with a generation to discard stale messages + /// from cancelled requests. + Stream { + generation: u64, + kind: StreamMsgKind, + }, +} + +enum StreamMsgKind { + ThreadId(String), + Delta(String), + ToolCallStart { name: String }, + ToolCallOutput { name: String, output: String }, + Done, + Error(String), +} + +#[derive(Clone, Copy, PartialEq)] +enum InputMode { + Emacs, + ViInsert, + ViNormal, +} + +#[derive(Clone, Copy, PartialEq)] +enum Role { + User, + Otto, + System, +} + +struct Message { + role: Role, + content: String, + timestamp: SystemTime, +} + +// --------------------------------------------------------------------------- +// History +// --------------------------------------------------------------------------- + +struct History { + entries: Vec<String>, + position: Option<usize>, + saved_input: Vec<char>, +} + +impl History { + fn load() -> Self { + let entries = Self::history_path() + .and_then(|p| std::fs::read_to_string(p).ok()) + .map(|s| { + s.lines() + .filter(|l| !l.is_empty()) + .map(String::from) + .collect() + }) + .unwrap_or_default(); + Self { + entries, + position: None, + saved_input: Vec::new(), + } + } + + fn push(&mut self, entry: &str) { + let entry = entry.trim().replace('\n', "\\n"); + if entry.is_empty() { + return; + } + // Deduplicate consecutive + if self.entries.last().is_some_and(|last| *last == entry) { + return; + } + self.entries.push(entry.clone()); + if self.entries.len() > MAX_HISTORY { + self.entries.remove(0); + } + self.position = None; + // Append to file + if let Some(path) = Self::history_path() { + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + { + let _ = writeln!(f, "{entry}"); + } + } + } + + fn decode(entry: &str) -> Vec<char> { + entry.replace("\\n", "\n").chars().collect() + } + + fn prev(&mut self, current_input: &[char]) -> Option<Vec<char>> { + if self.entries.is_empty() { + return None; + } + let new_pos = match self.position { + None => { + self.saved_input = current_input.to_vec(); + self.entries.len() - 1 + } + Some(0) => return None, + Some(p) => p - 1, + }; + self.position = Some(new_pos); + Some(Self::decode(&self.entries[new_pos])) + } + + fn next(&mut self) -> Option<Vec<char>> { + let pos = self.position?; + if pos + 1 >= self.entries.len() { + self.position = None; + Some(self.saved_input.clone()) + } else { + self.position = Some(pos + 1); + Some(Self::decode(&self.entries[pos + 1])) + } + } + + fn history_path() -> Option<std::path::PathBuf> { + std::env::var("HOME").ok().map(|h| { + std::path::PathBuf::from(h) + .join(".ascend-tools") + .join("history") + }) + } +} + +// --------------------------------------------------------------------------- +// App +// --------------------------------------------------------------------------- + +struct App { + messages: Vec<Message>, + input: Vec<char>, + cursor: usize, + input_mode: InputMode, + /// Lines scrolled up from the bottom (0 = pinned to newest). + scroll: usize, + auto_scroll: bool, + streaming: bool, + stream_buffer: String, + stream_pending: VecDeque<char>, + last_stream_tick: Instant, + stream_start: Option<Instant>, + thread_id: Option<String>, + runtime_uuid: Option<String>, + otto_model: Option<OttoModel>, + provider_label: Option<String>, + model_label: String, + context_label: Option<String>, + pending_request: Option<OttoChatRequest>, + should_quit: bool, + spinner_frame: usize, + last_spinner: Instant, + vi_pending: Option<char>, + yank_register: String, + completion_index: Option<usize>, + history: History, + show_timestamps: bool, + active_tool_call: Option<String>, + stream_generation: u64, + /// Set when cancel fires; the main loop spawns a thread to stop the backend. + stop_pending: bool, +} + +impl App { + fn new( + runtime_uuid: Option<String>, + otto_model: Option<OttoModel>, + provider_label: Option<String>, + model_label: String, + context_label: Option<String>, + ) -> Self { + Self { + messages: Vec::new(), + input: Vec::new(), + cursor: 0, + input_mode: InputMode::ViInsert, + scroll: 0, + auto_scroll: true, + streaming: false, + stream_buffer: String::new(), + stream_pending: VecDeque::new(), + last_stream_tick: Instant::now(), + stream_start: None, + thread_id: None, + runtime_uuid, + otto_model, + provider_label, + model_label, + context_label, + pending_request: None, + should_quit: false, + spinner_frame: 0, + last_spinner: Instant::now(), + vi_pending: None, + yank_register: String::new(), + completion_index: None, + history: History::load(), + show_timestamps: false, + active_tool_call: None, + stream_generation: 0, + stop_pending: false, + } + } + + // -- Input helpers ------------------------------------------------------ + + fn input_line_count(&self) -> u16 { + let count = 1 + self.input.iter().filter(|c| **c == '\n').count(); + (count as u16).min(MAX_INPUT_LINES) + } + + fn handle_paste(&mut self, text: &str) { + if self.input_mode == InputMode::ViNormal { + self.input_mode = InputMode::ViInsert; + } + let chars: Vec<char> = text.chars().collect(); + let count = chars.len(); + self.input.splice(self.cursor..self.cursor, chars); + self.cursor += count; + self.completion_index = None; + } + + // -- Key handling ------------------------------------------------------- + + fn handle_key(&mut self, key: KeyEvent, cancel: &AtomicBool) { + // Ctrl+C: cancel stream or quit + if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { + if self.streaming { + self.cancel_stream(cancel); + } else { + self.should_quit = true; + } + return; + } + + // Escape: cancel stream (if streaming), otherwise normal key handling + if key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE && self.streaming { + self.cancel_stream(cancel); + return; + } + + match self.input_mode { + InputMode::Emacs => self.handle_key_emacs(key), + InputMode::ViInsert => self.handle_key_vi_insert(key), + InputMode::ViNormal => self.handle_key_vi_normal(key), + } + } + + fn handle_key_emacs(&mut self, key: KeyEvent) { + // Tab: cycle completions + if key.code == KeyCode::Tab && key.modifiers == KeyModifiers::NONE { + self.complete_tab(); + return; + } + self.reset_completion(); + + match (key.modifiers, key.code) { + (KeyModifiers::NONE, KeyCode::Enter) => self.submit(), + // Alt+Enter or Shift+Enter: insert newline + (KeyModifiers::ALT, KeyCode::Enter) | (KeyModifiers::SHIFT, KeyCode::Enter) => { + self.input.insert(self.cursor, '\n'); + self.cursor += 1; + } + + (KeyModifiers::NONE | KeyModifiers::SHIFT, KeyCode::Char(c)) => { + self.input.insert(self.cursor, c); + self.cursor += 1; + self.history.position = None; + } + + (KeyModifiers::NONE, KeyCode::Backspace) => { + if self.cursor > 0 { + self.cursor -= 1; + self.input.remove(self.cursor); + self.history.position = None; + } + } + (KeyModifiers::NONE, KeyCode::Delete) => { + if self.cursor < self.input.len() { + self.input.remove(self.cursor); + self.history.position = None; + } + } + + (KeyModifiers::NONE, KeyCode::Left) => { + self.cursor = self.cursor.saturating_sub(1); + } + (KeyModifiers::NONE, KeyCode::Right) => { + self.cursor = (self.cursor + 1).min(self.input.len()); + } + + // Word-wise movement + (KeyModifiers::ALT, KeyCode::Left) => self.cursor = self.word_back(), + (KeyModifiers::ALT, KeyCode::Right) => self.cursor = self.word_fwd(), + + (KeyModifiers::NONE, KeyCode::Home) | (KeyModifiers::CONTROL, KeyCode::Char('a')) => { + self.cursor = 0; + } + (KeyModifiers::NONE, KeyCode::End) | (KeyModifiers::CONTROL, KeyCode::Char('e')) => { + self.cursor = self.input.len(); + } + + // Kill to start + (KeyModifiers::CONTROL, KeyCode::Char('u')) => { + self.input.drain(..self.cursor); + self.cursor = 0; + } + // Kill to end + (KeyModifiers::CONTROL, KeyCode::Char('k')) => { + self.input.truncate(self.cursor); + } + // Kill word backward + (KeyModifiers::CONTROL, KeyCode::Char('w')) => { + let new_cursor = self.word_back(); + self.input.drain(new_cursor..self.cursor); + self.cursor = new_cursor; + } + + // History + (KeyModifiers::NONE, KeyCode::Up) => { + if let Some(chars) = self.history.prev(&self.input) { + self.input = chars; + self.cursor = self.input.len(); + self.completion_index = None; + } + } + (KeyModifiers::NONE, KeyCode::Down) => { + if let Some(chars) = self.history.next() { + self.input = chars; + self.cursor = self.input.len(); + self.completion_index = None; + } + } + + // Scroll + (KeyModifiers::NONE, KeyCode::PageUp) => self.scroll_up(10), + (KeyModifiers::NONE, KeyCode::PageDown) => self.scroll_down(10), + + _ => {} + } + } + + fn handle_key_vi_insert(&mut self, key: KeyEvent) { + if key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE { + self.input_mode = InputMode::ViNormal; + if self.cursor > 0 { + self.cursor -= 1; + } + return; + } + self.handle_key_emacs(key); + } + + fn handle_key_vi_normal(&mut self, key: KeyEvent) { + // Multi-char commands (dd, yy) + if let Some(pending) = self.vi_pending.take() { + match (pending, key.code) { + ('d', KeyCode::Char('d')) => { + self.yank_register = self.input.iter().collect(); + self.input.clear(); + self.cursor = 0; + } + ('y', KeyCode::Char('y')) => { + self.yank_register = self.input.iter().collect(); + } + _ => {} + } + return; + } + + match (key.modifiers, key.code) { + // Enter insert mode + (KeyModifiers::NONE, KeyCode::Char('i')) => { + self.input_mode = InputMode::ViInsert; + } + (KeyModifiers::NONE, KeyCode::Char('a')) => { + self.input_mode = InputMode::ViInsert; + self.cursor = (self.cursor + 1).min(self.input.len()); + } + (KeyModifiers::SHIFT, KeyCode::Char('I')) => { + self.input_mode = InputMode::ViInsert; + self.cursor = 0; + } + (KeyModifiers::SHIFT, KeyCode::Char('A')) => { + self.input_mode = InputMode::ViInsert; + self.cursor = self.input.len(); + } + + // Motion + (KeyModifiers::NONE, KeyCode::Char('h') | KeyCode::Left) => { + self.cursor = self.cursor.saturating_sub(1); + } + (KeyModifiers::NONE, KeyCode::Char('l') | KeyCode::Right) => { + let max = self.input.len().saturating_sub(1); + self.cursor = (self.cursor + 1).min(max); + } + (KeyModifiers::NONE, KeyCode::Char('0')) => self.cursor = 0, + (KeyModifiers::SHIFT, KeyCode::Char('$')) => { + self.cursor = self.input.len().saturating_sub(1); + } + (KeyModifiers::NONE, KeyCode::Char('w')) => self.cursor = self.word_fwd(), + (KeyModifiers::NONE, KeyCode::Char('b')) => self.cursor = self.word_back(), + (KeyModifiers::NONE, KeyCode::Char('e')) => self.cursor = self.word_end(), + + // Editing + (KeyModifiers::NONE, KeyCode::Char('x')) => { + if self.cursor < self.input.len() { + let ch = self.input.remove(self.cursor); + self.yank_register = ch.to_string(); + if self.cursor > 0 && self.cursor >= self.input.len() { + self.cursor = self.input.len().saturating_sub(1); + } + } + } + (KeyModifiers::NONE, KeyCode::Char('d')) => { + self.vi_pending = Some('d'); + } + (KeyModifiers::NONE, KeyCode::Char('y')) => { + self.vi_pending = Some('y'); + } + // Paste after cursor + (KeyModifiers::NONE, KeyCode::Char('p')) => { + if !self.yank_register.is_empty() { + let pos = (self.cursor + 1).min(self.input.len()); + let chars: Vec<char> = self.yank_register.chars().collect(); + let count = chars.len(); + self.input.splice(pos..pos, chars); + self.cursor = pos + count - 1; + } + } + // Paste before cursor + (KeyModifiers::SHIFT, KeyCode::Char('P')) => { + if !self.yank_register.is_empty() { + let chars: Vec<char> = self.yank_register.chars().collect(); + let count = chars.len(); + self.input.splice(self.cursor..self.cursor, chars); + self.cursor += count.saturating_sub(1); + } + } + + // Submit + (KeyModifiers::NONE, KeyCode::Enter) => self.submit(), + + // History + (KeyModifiers::NONE, KeyCode::Char('k') | KeyCode::Up) if self.input.is_empty() => { + if let Some(chars) = self.history.prev(&self.input) { + self.input = chars; + self.cursor = self.input.len().saturating_sub(1); + self.completion_index = None; + } + } + (KeyModifiers::NONE, KeyCode::Char('j') | KeyCode::Down) if self.input.is_empty() => { + if let Some(chars) = self.history.next() { + self.input = chars; + self.cursor = self.input.len().saturating_sub(1); + self.completion_index = None; + } + } + + // Scroll + (KeyModifiers::NONE, KeyCode::PageUp) => self.scroll_up(10), + (KeyModifiers::NONE, KeyCode::PageDown) => self.scroll_down(10), + (KeyModifiers::CONTROL, KeyCode::Char('u')) => self.scroll_up(15), + (KeyModifiers::CONTROL, KeyCode::Char('d')) => self.scroll_down(15), + + _ => {} + } + } + + // -- Completions -------------------------------------------------------- + + fn input_str(&self) -> String { + self.input.iter().collect() + } + + fn completions(&self) -> Vec<&'static str> { + let text = self.input_str(); + if !text.starts_with('/') { + return Vec::new(); + } + COMMANDS + .iter() + .filter(|cmd| cmd.starts_with(&text) && **cmd != text) + .copied() + .collect() + } + + fn complete_tab(&mut self) { + let matches = self.completions(); + if matches.is_empty() { + self.completion_index = None; + return; + } + let idx = match self.completion_index { + Some(i) => (i + 1) % matches.len(), + None => 0, + }; + self.completion_index = Some(idx); + let cmd = matches[idx]; + self.input = cmd.chars().collect(); + self.cursor = self.input.len(); + } + + fn reset_completion(&mut self) { + self.completion_index = None; + } + + // -- Word boundaries ---------------------------------------------------- + + fn word_fwd(&self) -> usize { + let mut i = self.cursor; + while i < self.input.len() && !self.input[i].is_whitespace() { + i += 1; + } + while i < self.input.len() && self.input[i].is_whitespace() { + i += 1; + } + i + } + + fn word_back(&self) -> usize { + if self.cursor == 0 { + return 0; + } + let mut i = self.cursor - 1; + while i > 0 && self.input[i].is_whitespace() { + i -= 1; + } + while i > 0 && !self.input[i - 1].is_whitespace() { + i -= 1; + } + i + } + + fn word_end(&self) -> usize { + if self.input.is_empty() { + return 0; + } + let last = self.input.len() - 1; + let mut i = self.cursor; + if i < last { + i += 1; + } + while i < last && self.input[i].is_whitespace() { + i += 1; + } + while i < last && !self.input[i + 1].is_whitespace() { + i += 1; + } + i + } + + // -- Scroll helpers ----------------------------------------------------- + + fn scroll_up(&mut self, n: usize) { + self.scroll = self.scroll.saturating_add(n); + self.auto_scroll = false; + } + + fn scroll_down(&mut self, n: usize) { + self.scroll = self.scroll.saturating_sub(n); + if self.scroll == 0 { + self.auto_scroll = true; + } + } + + // -- Submit & commands -------------------------------------------------- + + fn submit(&mut self) { + if self.streaming { + self.push_system("Waiting for response..."); + return; + } + let text: String = self.input.drain(..).collect(); + self.cursor = 0; + let text = text.trim().to_string(); + if text.is_empty() { + return; + } + + if text.starts_with('/') { + self.handle_command(&text); + return; + } + + self.history.push(&text); + + self.messages.push(Message { + role: Role::User, + content: text.clone(), + timestamp: SystemTime::now(), + }); + + self.pending_request = Some(OttoChatRequest { + prompt: text, + runtime_uuid: self.runtime_uuid.clone(), + thread_id: self.thread_id.clone(), + model: self.otto_model.clone(), + }); + self.streaming = true; + self.stream_buffer.clear(); + self.stream_pending.clear(); + self.last_stream_tick = Instant::now(); + self.stream_start = Some(Instant::now()); + self.auto_scroll = true; + self.scroll = 0; + + if self.input_mode == InputMode::ViNormal { + self.input_mode = InputMode::ViInsert; + } + } + + fn push_system(&mut self, content: impl Into<String>) { + self.messages.push(Message { + role: Role::System, + content: content.into(), + timestamp: SystemTime::now(), + }); + } + + fn handle_command(&mut self, cmd: &str) { + let parts: Vec<&str> = cmd.splitn(2, ' ').collect(); + match parts[0] { + "/vim" | "/vi" => { + self.input_mode = InputMode::ViNormal; + self.push_system("Vi mode"); + } + "/emacs" => { + self.input_mode = InputMode::Emacs; + self.push_system("Emacs mode"); + } + "/clear" => { + self.messages.clear(); + self.scroll = 0; + self.thread_id = None; + self.push_system("Thread cleared"); + } + "/copy" => { + let last_otto = self + .messages + .iter() + .rev() + .find(|m| m.role == Role::Otto) + .map(|m| m.content.clone()); + match last_otto { + Some(text) => { + match arboard::Clipboard::new().and_then(|mut cb| cb.set_text(text)) { + Ok(()) => self.push_system("Copied to clipboard"), + Err(e) => self.push_system(format!("Clipboard error: {e}")), + } + } + None => self.push_system("No Otto message to copy"), + } + } + "/timestamps" => { + self.show_timestamps = !self.show_timestamps; + let state = if self.show_timestamps { "on" } else { "off" }; + self.push_system(format!("Timestamps {state}")); + } + "/quit" | "/exit" | "/q" => { + self.should_quit = true; + } + "/help" => { + self.push_system(concat!( + "Commands:\n", + " /emacs Switch to Emacs keybindings\n", + " /vim Switch to Vi keybindings (default)\n", + " /copy Copy last Otto response to clipboard\n", + " /timestamps Toggle message timestamps\n", + " /clear Clear chat and start new thread\n", + " /quit, /exit Exit\n", + " /help Show this help\n", + "\n", + "Keys:\n", + " Enter Send message\n", + " Alt+Enter Insert newline\n", + " Esc Vi normal mode\n", + " Up/Down Input history\n", + " PageUp/Down Scroll chat\n", + " Tab Complete /command\n", + " Ctrl+C Cancel stream / Exit", + )); + } + other => { + self.push_system(format!("Unknown command: {other}")); + } + } + } + + // -- Streaming ---------------------------------------------------------- + + fn handle_stream_msg(&mut self, msg: StreamMsg) { + match msg { + StreamMsg::ProviderInfo { + provider_label: provider, + model_label: model, + } => { + self.provider_label = provider; + self.model_label = model; + } + StreamMsg::Stream { generation, kind } => { + // Discard stale messages from cancelled requests + if generation != self.stream_generation { + return; + } + self.handle_stream_kind(kind); + } + } + } + + fn handle_stream_kind(&mut self, kind: StreamMsgKind) { + match kind { + StreamMsgKind::ThreadId(tid) => { + self.thread_id = Some(tid); + } + StreamMsgKind::Delta(text) => { + self.stream_pending.extend(text.chars()); + } + StreamMsgKind::ToolCallStart { name, .. } => { + self.flush_stream_text(); + self.active_tool_call = Some(name); + } + StreamMsgKind::ToolCallOutput { name, output } => { + self.active_tool_call = None; + let output_summary = truncate(&output, 80); + self.push_system(format!("\u{2699} {name} \u{2192} {output_summary}")); + } + StreamMsgKind::Done => { + self.finish_stream(); + // Bell if response took >3s + if self + .stream_start + .is_some_and(|s| s.elapsed() > Duration::from_secs(3)) + { + let _ = crossterm::execute!(std::io::stderr(), crossterm::style::Print("\x07")); + } + self.stream_start = None; + } + StreamMsgKind::Error(err) => { + self.finish_stream(); + self.push_system(format!("Error: {err}")); + self.stream_start = None; + } + } + } + + fn flush_stream_text(&mut self) { + let remaining: String = self.stream_pending.drain(..).collect(); + self.stream_buffer.push_str(&remaining); + let content = std::mem::take(&mut self.stream_buffer); + if !content.is_empty() { + self.messages.push(Message { + role: Role::Otto, + content, + timestamp: SystemTime::now(), + }); + } + } + + fn cancel_stream(&mut self, cancel: &AtomicBool) { + cancel.store(true, Ordering::Relaxed); + self.finish_stream(); + self.push_system("Cancelled"); + self.stream_start = None; + self.stop_pending = true; + } + + fn finish_stream(&mut self) { + self.flush_stream_text(); + self.streaming = false; + self.active_tool_call = None; + } + + fn tick_stream(&mut self) { + if self.stream_pending.is_empty() { + return; + } + + let elapsed = self.last_stream_tick.elapsed(); + let chars_due = (elapsed.as_secs_f64() * STREAM_CPS) as usize; + + if chars_due == 0 { + return; + } + + self.last_stream_tick = Instant::now(); + let pending = self.stream_pending.len(); + + let n = if pending > STREAM_BULK_THRESHOLD { + pending.min(chars_due + 100) + } else if pending > STREAM_FAST_THRESHOLD { + chars_due * 3 + } else { + chars_due + }; + + let n = n.min(pending); + let chunk: String = self.stream_pending.drain(..n).collect(); + self.stream_buffer.push_str(&chunk); + } + + fn take_pending_request(&mut self) -> Option<OttoChatRequest> { + self.pending_request.take() + } + + fn tick_spinner(&mut self) { + if self.streaming && self.last_spinner.elapsed() >= SPINNER_INTERVAL { + self.spinner_frame = (self.spinner_frame + 1) % SPINNER.len(); + self.last_spinner = Instant::now(); + } + } + + // -- Rendering ---------------------------------------------------------- + + fn render(&self, frame: &mut Frame) { + let area = frame.area(); + if area.height < 5 { + return; + } + + let input_height = self.input_line_count(); + let chunks = Layout::vertical([ + Constraint::Min(1), // chat + Constraint::Length(1), // top rule + Constraint::Length(input_height), // input + Constraint::Length(1), // bottom rule + Constraint::Length(1), // status + ]) + .split(area); + + self.render_chat(frame, chunks[0]); + self.render_rule(frame, chunks[1]); + self.render_input(frame, chunks[2]); + self.render_rule(frame, chunks[3]); + self.render_completions(frame, chunks[0], chunks[1]); + self.render_status(frame, chunks[4]); + + // Cursor shape + let cursor_style = match self.input_mode { + InputMode::ViNormal => SetCursorStyle::SteadyBlock, + _ => SetCursorStyle::BlinkingBar, + }; + let _ = crossterm::execute!(std::io::stderr(), cursor_style); + } + + fn render_chat(&self, frame: &mut Frame, area: Rect) { + let has_content = self.streaming || !self.messages.is_empty(); + if !has_content { + self.render_splash(frame, area); + return; + } + + let mut lines: Vec<Line<'_>> = Vec::new(); + + for msg in &self.messages { + if !lines.is_empty() { + lines.push(Line::raw("")); + } + + let (label, color) = match msg.role { + Role::User => (" you", USER_COLOR), + Role::Otto => (" otto", OTTO_COLOR), + Role::System => ("", SYSTEM_COLOR), + }; + + if !label.is_empty() { + let mut label_spans = vec![Span::styled(label, Style::default().fg(color).bold())]; + if self.show_timestamps { + label_spans.push(Span::styled( + format!(" {}", format_time(msg.timestamp)), + Style::default().fg(TIMESTAMP_COLOR), + )); + } + lines.push(Line::from(label_spans)); + } else if self.show_timestamps { + // System messages: show timestamp inline + lines.push(Line::from(Span::styled( + format!(" {}", format_time(msg.timestamp)), + Style::default().fg(TIMESTAMP_COLOR), + ))); + } + + lines.extend(render_markdown(&msg.content, msg.role)); + } + + // Streaming: show current buffer or spinner + if self.streaming { + if !lines.is_empty() { + lines.push(Line::raw("")); + } + lines.push(Line::from(Span::styled( + " otto", + Style::default().fg(OTTO_COLOR).bold(), + ))); + + if self.stream_buffer.is_empty() && self.stream_pending.is_empty() { + let label = if let Some(tool) = &self.active_tool_call { + format!(" {} \u{2699} {tool}...", SPINNER[self.spinner_frame]) + } else { + format!(" {} Ascending...", SPINNER[self.spinner_frame]) + }; + lines.push(Line::from(Span::styled( + label, + Style::default().fg(DIM_OTTO_COLOR), + ))); + } else if let Some(tool) = &self.active_tool_call { + lines.extend(render_markdown(&self.stream_buffer, Role::Otto)); + lines.push(Line::from(Span::styled( + format!(" {} \u{2699} {tool}...", SPINNER[self.spinner_frame]), + Style::default().fg(DIM_OTTO_COLOR), + ))); + } else { + lines.extend(render_markdown(&self.stream_buffer, Role::Otto)); + } + } + + // Trailing padding + lines.push(Line::raw("")); + + // Exact rendered line count via Paragraph::line_count + let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false }); + let total_rendered = paragraph.line_count(area.width); + let visible = area.height as usize; + let max_scroll = total_rendered.saturating_sub(visible); + let clamped_scroll = self.scroll.min(max_scroll); + let scroll_y = max_scroll.saturating_sub(clamped_scroll); + + let paragraph = paragraph.scroll((scroll_y.min(u16::MAX as usize) as u16, 0)); + frame.render_widget(paragraph, area); + + // Scrollbar + if total_rendered > visible { + let mut scrollbar_state = ScrollbarState::new(max_scroll).position(clamped_scroll); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .style(Style::default().fg(DIM_COLOR)), + area, + &mut scrollbar_state, + ); + } + } + + fn render_splash(&self, frame: &mut Frame, area: Rect) { + let banner_height = EXPERIMENTAL_BANNER.len() as u16 + 2; // +2 for blank lines around it + let splash_height = SPLASH.len() as u16; + let total_height = splash_height + banner_height; + let y_offset = area.height.saturating_sub(total_height) / 2; + + let warning_style = Style::default().fg(WARNING_COLOR).bold(); + + let mut lines: Vec<Line<'_>> = Vec::new(); + + // Experimental banner + lines.push(Line::raw("")); + for &line in EXPERIMENTAL_BANNER { + let display_width = line.chars().count(); + let pad = (area.width as usize).saturating_sub(display_width) / 2; + let padded = format!("{:>width$}{}", "", line, width = pad); + lines.push(Line::from(Span::styled(padded, warning_style))); + } + lines.push(Line::raw("")); + + // Otto splash + for &line in SPLASH { + let display_width = line.chars().count(); + let pad = (area.width as usize).saturating_sub(display_width) / 2; + let padded = format!("{:>width$}{}", "", line, width = pad); + if line.contains("/help") { + lines.push(Line::from(Span::styled( + padded, + Style::default().fg(DIM_COLOR), + ))); + } else { + lines.push(Line::from(Span::styled( + padded, + Style::default().fg(OTTO_COLOR), + ))); + } + } + + let clamped_height = total_height.min(area.height); + let splash_area = Rect::new(area.x, area.y + y_offset, area.width, clamped_height); + frame.render_widget(Paragraph::new(lines), splash_area); + } + + fn render_rule(&self, frame: &mut Frame, area: Rect) { + let rule_color = match self.input_mode { + InputMode::ViNormal => VI_NORMAL_COLOR, + _ if self.streaming => DIM_COLOR, + _ => OTTO_COLOR, + }; + let rule = "\u{2500}".repeat(area.width as usize); + frame.render_widget( + Paragraph::new(Line::from(Span::styled( + rule, + Style::default().fg(rule_color), + ))), + area, + ); + } + + fn render_input(&self, frame: &mut Frame, area: Rect) { + let prompt = match self.input_mode { + InputMode::ViNormal => " \u{2502} ", + _ => " \u{276f} ", + }; + let prompt_len = 3; + + let prompt_color = match self.input_mode { + InputMode::ViNormal => VI_NORMAL_COLOR, + _ if self.streaming => DIM_COLOR, + _ => OTTO_COLOR, + }; + + // Multi-line: split input on \n + let input_str: String = self.input.iter().collect(); + let input_lines: Vec<&str> = input_str.split('\n').collect(); + + // Find which line the cursor is on and the column within that line + let mut cursor_line = 0; + let mut cursor_col = 0; + let mut chars_so_far = 0; + for (i, line) in input_lines.iter().enumerate() { + let line_len = line.chars().count(); + let is_last = i == input_lines.len() - 1; + if self.cursor <= chars_so_far + line_len || is_last { + cursor_line = i; + cursor_col = self.cursor.saturating_sub(chars_so_far); + break; + } + chars_so_far += line_len + 1; // +1 for \n + } + + let mut render_lines: Vec<Line<'_>> = Vec::new(); + for (i, line) in input_lines.iter().enumerate() { + let p = if i == 0 { prompt } else { " " }; + let p_style = if i == 0 { + Style::default().fg(prompt_color) + } else { + Style::default().fg(DIM_COLOR) + }; + render_lines.push(Line::from(vec![ + Span::styled(p, p_style), + Span::raw((*line).to_string()), + ])); + } + + frame.render_widget(Paragraph::new(render_lines), area); + + if !self.streaming { + let cx = area.x + prompt_len as u16 + cursor_col as u16; + let cy = area.y + cursor_line as u16; + frame.set_cursor_position((cx, cy)); + } + } + + fn render_completions(&self, frame: &mut Frame, chat_area: Rect, rule_area: Rect) { + let matches = self.completions(); + if matches.is_empty() { + return; + } + + let height = matches.len().min(8) as u16; + let width = matches.iter().map(|s| s.len()).max().unwrap_or(0) as u16 + 4; + + let x = rule_area.x + 1; + let y = chat_area.bottom().saturating_sub(height); + let popup = Rect::new(x, y, width.min(rule_area.width), height); + + frame.render_widget(Clear, popup); + + let items: Vec<Line<'_>> = matches + .iter() + .enumerate() + .map(|(i, cmd)| { + let style = if self.completion_index == Some(i) { + Style::default().fg(TEXT_COLOR).bg(OTTO_COLOR).bold() + } else { + Style::default().fg(TEXT_COLOR).bg(POPUP_BG) + }; + Line::from(Span::styled(format!(" {cmd} "), style)) + }) + .collect(); + + let block = Block::default() + .borders(Borders::NONE) + .style(Style::default().bg(POPUP_BG)); + let paragraph = Paragraph::new(items).block(block); + frame.render_widget(paragraph, popup); + } + + fn render_status(&self, frame: &mut Frame, area: Rect) { + let (mode, mode_color) = match self.input_mode { + InputMode::Emacs => ("emacs", SYSTEM_COLOR), + InputMode::ViInsert => ("INSERT", VI_NORMAL_COLOR), + InputMode::ViNormal => ("NORMAL", VI_NORMAL_COLOR), + }; + + let mut parts = vec![Span::styled( + format!(" {mode}"), + Style::default().fg(mode_color), + )]; + + let pill_style = Style::default().fg(DIM_OTTO_COLOR); + + if let Some(label) = &self.context_label { + parts.push(Span::raw(" ")); + parts.push(Span::styled(format!(" {label} "), pill_style)); + } + + if let Some(provider) = &self.provider_label { + parts.push(Span::raw(" ")); + parts.push(Span::styled(format!(" provider:{provider} "), pill_style)); + } + + if !self.model_label.is_empty() { + parts.push(Span::raw(" ")); + parts.push(Span::styled( + format!(" model:{} ", self.model_label), + pill_style, + )); + } + + if let Some(tid) = &self.thread_id { + let short: String = tid.chars().take(12).collect(); + parts.push(Span::raw(" ")); + parts.push(Span::styled(format!(" thread:{short} "), pill_style)); + } + + let msg_count = self + .messages + .iter() + .filter(|m| m.role != Role::System) + .count(); + if msg_count > 0 { + parts.push(Span::raw(" ")); + parts.push(Span::styled(format!(" {msg_count} messages "), pill_style)); + } + + // Truncate pills to fit terminal width + let total_width: usize = parts.iter().map(|s| s.width()).sum(); + if total_width > area.width as usize { + let mut width = 0; + let mut truncated = Vec::new(); + for span in parts { + width += span.width(); + if width > area.width as usize { + break; + } + truncated.push(span); + } + frame.render_widget(Paragraph::new(Line::from(truncated)), area); + } else { + frame.render_widget(Paragraph::new(Line::from(parts)), area); + } + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Truncate a string for display, adding "..." if it exceeds `max_len`. +fn truncate(s: &str, max_len: usize) -> String { + if s.chars().count() <= max_len { + s.to_string() + } else { + let truncated: String = s.chars().take(max_len).collect(); + format!("{truncated}...") + } +} + +fn format_time(time: SystemTime) -> String { + // Elapsed since message was created — avoids UTC vs local time issues + let elapsed = time.elapsed().unwrap_or_default(); + let secs = elapsed.as_secs(); + if secs < 60 { + "just now".to_string() + } else if secs < 3600 { + format!("{}m ago", secs / 60) + } else { + format!("{}h ago", secs / 3600) + } +} + +// --------------------------------------------------------------------------- +// Markdown rendering (code blocks, inline code, bold) +// --------------------------------------------------------------------------- + +fn render_markdown(text: &str, role: Role) -> Vec<Line<'static>> { + let indent = " "; + let mut lines = Vec::new(); + let mut in_code_block = false; + + for line in text.lines() { + let trimmed = line.trim_start(); + if trimmed.starts_with("```") { + in_code_block = !in_code_block; + if in_code_block { + let lang = trimmed.trim_start_matches('`').trim(); + let header = if lang.is_empty() { + format!("{indent}\u{256d}\u{2500}\u{2500}") + } else { + format!("{indent}\u{256d}\u{2500} {lang} \u{2500}") + }; + lines.push(Line::from(Span::styled( + header, + Style::default().fg(DIM_COLOR), + ))); + } else { + lines.push(Line::from(Span::styled( + format!("{indent}\u{2570}\u{2500}\u{2500}"), + Style::default().fg(DIM_COLOR), + ))); + } + continue; + } + + if in_code_block { + lines.push(Line::from(Span::styled( + format!("{indent}\u{2502} {line}"), + Style::default().fg(TEXT_COLOR), + ))); + } else { + lines.push(parse_inline(line, indent, role)); + } + } + lines +} + +fn parse_inline(text: &str, indent: &str, role: Role) -> Line<'static> { + let base_style = match role { + Role::System => Style::default().fg(SYSTEM_COLOR).italic(), + _ => Style::default(), + }; + let code_style = Style::default().fg(CODE_COLOR); + let bold_style = base_style.bold(); + + let chars: Vec<char> = text.chars().collect(); + let mut spans: Vec<Span<'static>> = vec![Span::raw(indent.to_string())]; + let mut i = 0; + let mut buf = String::new(); + + while i < chars.len() { + // **bold** + if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' { + if !buf.is_empty() { + spans.push(Span::styled(std::mem::take(&mut buf), base_style)); + } + i += 2; + let mut bold = String::new(); + while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '*') { + bold.push(chars[i]); + i += 1; + } + if i + 1 < chars.len() { + i += 2; + } + spans.push(Span::styled(bold, bold_style)); + continue; + } + + // `code` + if chars[i] == '`' { + if !buf.is_empty() { + spans.push(Span::styled(std::mem::take(&mut buf), base_style)); + } + i += 1; + let mut code = String::new(); + while i < chars.len() && chars[i] != '`' { + code.push(chars[i]); + i += 1; + } + if i < chars.len() { + i += 1; + } + spans.push(Span::styled(code, code_style)); + continue; + } + + buf.push(chars[i]); + i += 1; + } + + if !buf.is_empty() { + spans.push(Span::styled(buf, base_style)); + } + + Line::from(spans) +} + +// --------------------------------------------------------------------------- +// Provider resolution +// --------------------------------------------------------------------------- + +/// Resolve provider/model labels from the API, mapping IDs to friendly names. +fn resolve_provider_labels( + client: &AscendClient, + otto_model: &Option<OttoModel>, +) -> (Option<String>, String) { + let providers = client.list_otto_providers().ok().unwrap_or_default(); + match otto_model { + Some(OttoModel::Name(name)) => { + let friendly = providers + .iter() + .flat_map(|p| { + p.models + .iter() + .filter(|m| m.id == *name) + .map(|m| m.name.clone()) + }) + .next() + .unwrap_or_else(|| name.clone()); + (None, friendly) + } + Some(OttoModel::ProviderModel { + provider_id, + model_id, + }) => { + let p = providers.iter().find(|p| p.id == *provider_id); + let provider_name = p + .map(|p| p.name.clone()) + .unwrap_or_else(|| provider_id.clone()); + let model_name = p + .and_then(|p| p.models.iter().find(|m| m.id == *model_id)) + .map(|m| m.name.clone()) + .unwrap_or_else(|| model_id.clone()); + (Some(provider_name), model_name) + } + None => providers + .first() + .map(|p| { + let model_name = p + .models + .iter() + .find(|m| m.id == p.default_model) + .map(|m| m.name.clone()) + .unwrap_or_else(|| p.default_model.clone()); + (Some(p.name.clone()), model_name) + }) + .unwrap_or_default(), + } +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +pub fn run_tui( + client: &AscendClient, + runtime_uuid: Option<String>, + otto_model: Option<OttoModel>, + context_label: Option<String>, +) -> Result<()> { + // Setup terminal + terminal::enable_raw_mode()?; + let mut stderr = std::io::stderr(); + crossterm::execute!( + stderr, + EnterAlternateScreen, + EnableMouseCapture, + EnableBracketedPaste + )?; + let backend = CrosstermBackend::new(stderr); + let mut terminal = Terminal::new(backend)?; + + // Panic hook to restore terminal on crash + let original_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + let _ = terminal::disable_raw_mode(); + let _ = crossterm::execute!( + std::io::stderr(), + LeaveAlternateScreen, + DisableMouseCapture, + DisableBracketedPaste, + SetCursorStyle::DefaultUserShape + ); + original_hook(info); + })); + + let (stream_tx, stream_rx) = mpsc::channel::<StreamMsg>(); + let cancel = AtomicBool::new(false); + let gen_counter = AtomicU64::new(0); + let active_thread_id: Mutex<Option<String>> = Mutex::new(None); + + let result = std::thread::scope(|scope| { + // Resolve provider/model labels in the background so the TUI loads instantly + let bg_tx = stream_tx.clone(); + let bg_model = otto_model.clone(); + scope.spawn(move || { + let (provider_label, model_label) = resolve_provider_labels(client, &bg_model); + let _ = bg_tx.send(StreamMsg::ProviderInfo { + provider_label, + model_label, + }); + }); + + let mut app = App::new(runtime_uuid, otto_model, None, String::new(), context_label); + + loop { + app.tick_spinner(); + app.tick_stream(); + terminal.draw(|frame| app.render(frame))?; + + if event::poll(POLL_DURATION)? { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => { + app.handle_key(key, &cancel); + } + Event::Paste(text) => { + app.handle_paste(&text); + } + Event::Mouse(mouse) => match mouse.kind { + MouseEventKind::ScrollUp => app.scroll_up(3), + MouseEventKind::ScrollDown => app.scroll_down(3), + _ => {} + }, + Event::Resize(_, _) => { + // ratatui handles re-layout; just ensure scroll is valid + if app.auto_scroll { + app.scroll = 0; + } + } + _ => {} + } + } + + // Process stream messages + while let Ok(msg) = stream_rx.try_recv() { + app.handle_stream_msg(msg); + } + + // If the user cancelled, tell the backend to stop the thread. + // Spawns a background thread so the TUI stays responsive. + if app.stop_pending { + app.stop_pending = false; + if let Some(tid) = active_thread_id + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone() + { + scope.spawn(move || { + let _ = client.stop_thread_and_wait(&tid); + }); + } + } + + // Launch streaming request if pending + if let Some(request) = app.take_pending_request() { + let generation = gen_counter.fetch_add(1, Ordering::Relaxed) + 1; + app.stream_generation = generation; + cancel.store(false, Ordering::Relaxed); + *active_thread_id.lock().unwrap_or_else(|e| e.into_inner()) = None; + let tx = stream_tx.clone(); + let cancel_ref = &cancel; + let active_tid = &active_thread_id; + scope.spawn(move || { + let tx2 = tx.clone(); + let mut tool_names: HashMap<String, String> = HashMap::new(); + let send = |kind: StreamMsgKind| { + let _ = tx.send(StreamMsg::Stream { generation, kind }); + }; + let result = client.otto_streaming( + &request, + |event| { + if cancel_ref.load(Ordering::Relaxed) { + return ControlFlow::Break(()); + } + match event { + StreamEvent::TextDelta(delta) => { + send(StreamMsgKind::Delta(delta)); + } + StreamEvent::ToolCallStart { call_id, name, .. } => { + tool_names.insert(call_id, name.clone()); + send(StreamMsgKind::ToolCallStart { name }); + } + StreamEvent::ToolCallOutput { call_id, output } => { + let name = + tool_names.get(&call_id).cloned().unwrap_or_default(); + send(StreamMsgKind::ToolCallOutput { name, output }); + } + } + ControlFlow::Continue(()) + }, + |tid: &str| { + *active_tid.lock().unwrap_or_else(|e| e.into_inner()) = + Some(tid.to_string()); + let _ = tx2.send(StreamMsg::Stream { + generation, + kind: StreamMsgKind::ThreadId(tid.to_string()), + }); + }, + ); + match result { + Ok(_) => send(StreamMsgKind::Done), + Err(e) => send(StreamMsgKind::Error(format!("{e}"))), + } + }); + } + + if app.should_quit { + // Signal all spawned threads to stop so thread::scope + // can join them without blocking indefinitely. + cancel.store(true, Ordering::Relaxed); + break; + } + } + + Ok::<(), anyhow::Error>(()) + }); + + // Restore terminal after all spawned threads have joined + let _ = terminal::disable_raw_mode(); + let _ = crossterm::execute!( + std::io::stderr(), + LeaveAlternateScreen, + DisableMouseCapture, + DisableBracketedPaste, + SetCursorStyle::DefaultUserShape + ); + let _ = terminal.show_cursor(); + let _ = std::panic::take_hook(); + + result +}