diff --git a/clients/voce-tui/Cargo.lock b/clients/voce-tui/Cargo.lock index 5242b72..685c866 100644 --- a/clients/voce-tui/Cargo.lock +++ b/clients/voce-tui/Cargo.lock @@ -63,6 +63,19 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi-to-tui" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42366bb9d958f042bf58f0a85e1b2d091997c1257ca49bddd7e4827aadc65fd" +dependencies = [ + "nom 8.0.0", + "ratatui-core", + "simdutf8", + "smallvec", + "thiserror 2.0.18", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -111,6 +124,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.72.1" @@ -209,7 +231,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -254,13 +276,28 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" dependencies = [ "castaway", "cfg-if", "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[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", ] @@ -343,6 +380,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -374,6 +420,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "mio 1.1.1", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -393,6 +455,40 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + [[package]] name = "dasp_sample" version = "0.11.0" @@ -414,6 +510,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -500,6 +602,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -512,6 +624,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 = "foreign-types" version = "0.3.2" @@ -580,6 +698,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.32" @@ -604,6 +728,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.0", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -679,7 +812,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -687,6 +820,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" @@ -923,6 +1061,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -956,6 +1100,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -974,18 +1140,18 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -1060,6 +1226,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1088,6 +1264,18 @@ dependencies = [ "windows-link", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1124,6 +1312,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[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 = "mach2" version = "0.4.3" @@ -1167,6 +1364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1188,6 +1386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -1248,6 +1447,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1361,6 +1569,28 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.11.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "openssl" version = "0.10.76" @@ -1458,6 +1688,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1497,6 +1740,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1534,6 +1787,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" +dependencies = [ + "bitflags 2.11.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -1587,22 +1868,43 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags 2.11.0", "cassowary", - "compact_str", - "crossterm", - "itertools 0.12.1", - "lru", + "compact_str 0.8.1", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", + "lru 0.12.5", "paste", - "stability", - "strum", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate 1.1.0", + "unicode-width 0.2.0", +] + +[[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 0.9.0", + "hashbrown 0.16.1", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru 0.16.3", + "strum 0.27.2", + "thiserror 2.0.18", "unicode-segmentation", - "unicode-truncate", - "unicode-width", + "unicode-truncate 2.0.1", + "unicode-width 0.2.0", ] [[package]] @@ -1643,6 +1945,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.28" @@ -1708,6 +2016,35 @@ dependencies = [ "portable-atomic-util", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.117", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -1720,6 +2057,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustfft" version = "6.4.1" @@ -1734,6 +2080,19 @@ dependencies = [ "transpose", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1743,7 +2102,7 @@ dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -1953,6 +2312,7 @@ checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio 0.8.11", + "mio 1.1.1", "signal-hook", ] @@ -1966,6 +2326,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -1988,16 +2360,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "stability" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" -dependencies = [ - "quote", - "syn 2.0.117", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2016,13 +2378,28 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -2038,6 +2415,18 @@ dependencies = [ "syn 2.0.117", ] +[[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" @@ -2098,6 +2487,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.18", + "walkdir", + "yaml-rust", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -2128,7 +2538,7 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -2502,6 +2912,22 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-markdown" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e766339aabad4528c3fccddf4acf03bc2b7ae6642def41e43c7af1a11f183122" +dependencies = [ + "ansi-to-tui", + "itertools 0.14.0", + "pretty_assertions", + "pulldown-cmark", + "ratatui-core", + "rstest", + "syntect", + "tracing", +] + [[package]] name = "tungstenite" version = "0.21.0" @@ -2528,6 +2954,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -2548,7 +2980,18 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools 0.13.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools 0.14.0", + "unicode-segmentation", + "unicode-width 0.2.0", ] [[package]] @@ -2557,6 +3000,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2619,10 +3068,11 @@ dependencies = [ "bytes", "chrono", "cpal", - "crossterm", + "crossterm 0.27.0", "futures-util", "once_cell", "ratatui", + "ratatui-core", "reqwest", "ringbuf", "rustfft", @@ -2634,7 +3084,8 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", - "unicode-width", + "tui-markdown", + "unicode-width 0.1.14", "webrtc-audio-processing", ] @@ -3264,6 +3715,21 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.1" diff --git a/clients/voce-tui/Cargo.toml b/clients/voce-tui/Cargo.toml index 08adcec..57a2477 100644 --- a/clients/voce-tui/Cargo.toml +++ b/clients/voce-tui/Cargo.toml @@ -10,7 +10,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] } futures-util = "0.3.30" -ratatui = "0.26.2" +ratatui = "0.29.0" crossterm = "0.27.0" cpal = "0.15.3" ringbuf = "0.4.1" @@ -25,3 +25,5 @@ webrtc-audio-processing = { version = "0.3.0", features = ["bundled"] } toml = "0.8" unicode-width = "0.1.11" once_cell = "1.19" +ratatui-core = "0.1.0" +tui-markdown = "0.3.7" diff --git a/clients/voce-tui/src/pages/chat.rs b/clients/voce-tui/src/pages/chat.rs index 6d86c47..ae8ca3d 100644 --- a/clients/voce-tui/src/pages/chat.rs +++ b/clients/voce-tui/src/pages/chat.rs @@ -22,6 +22,7 @@ pub struct Subtitle { pub role: String, pub text: String, pub is_final: bool, + pub cached_lines: Vec>, } pub struct Chat { @@ -242,11 +243,42 @@ impl Chat { } fn push_subtitle(&mut self, role: String, text: String, is_final: bool) { + // 0. Normalize text to avoid common markdown parsing glitches (like \r\n or trailing whitespace) + let normalized_text = text.replace('\r', "").trim_end().to_string(); + if normalized_text.is_empty() && !is_final { + return; + } + + // 1. Markdown 解析 + let rendered = tui_markdown::from_str(&normalized_text); + let mut cached_lines: Vec> = rendered.lines.into_iter().map(|line| { + let spans: Vec> = line.spans.into_iter().map(|s| { + let mut span = Span::from(s.content.into_owned()); + span.style = convert_style(s.style); + span + }).collect(); + Line::from(spans) + }).collect(); + + // 2. 注入 Role 前缀 (保留配色) + if !cached_lines.is_empty() { + let color = if role == "user" { Color::Green } else { Color::Magenta }; + let prefix = Span::styled(format!("{}: ", role), Style::default().fg(color).add_modifier(Modifier::BOLD)); + cached_lines[0].spans.insert(0, prefix); + } + + // 3. 更新或推入 if let Some(last) = self.subtitles.back_mut() { - if last.role == role && !last.is_final { last.text = text; last.is_final = is_final; return; } + if last.role == role && !last.is_final { + last.text = normalized_text; + last.is_final = is_final; + last.cached_lines = cached_lines; + return; + } } + if self.subtitles.len() >= 50 { self.subtitles.pop_front(); } - self.subtitles.push_back(Subtitle { role, text, is_final }); + self.subtitles.push_back(Subtitle { role, text: normalized_text, is_final, cached_lines }); } } @@ -270,7 +302,7 @@ impl Page for Chat { Constraint::Min(10), // Subtitles Constraint::Length(3), // Footer ]) - .split(f.size()); + .split(f.area()); let display_agent_speaking = self.agent_speaking || self.agent_playback_active; @@ -319,30 +351,14 @@ impl Page for Chat { ]) .split(chunks[2]); - // --- Subtitles Rendering (Deterministic Physical Wrapping) --- + // --- Subtitles Rendering (Markdown Display & Dynamic Wrapping) --- let area_width = mid_chunks[0].width.saturating_sub(2).max(1) as usize; let area_height = mid_chunks[0].height.saturating_sub(2).max(1); + let mut physical_lines = Vec::new(); - for s in &self.subtitles { - let (name, color) = if s.role == "user" { ("User: ", Color::Green) } else { ("Agent: ", Color::Magenta) }; - let name_style = Style::default().fg(color).add_modifier(Modifier::BOLD); - - let mut current_line_spans = vec![Span::styled(name, name_style)]; - let mut current_width = name.width(); - - for c in s.text.chars() { - let cw = UnicodeWidthStr::width(c.to_string().as_str()); - if current_width + cw > area_width { - physical_lines.push(Line::from(current_line_spans)); - current_line_spans = Vec::new(); - current_width = 0; - } - current_line_spans.push(Span::raw(c.to_string())); - current_width += cw; - } - if !current_line_spans.is_empty() { - physical_lines.push(Line::from(current_line_spans)); + for line in &s.cached_lines { + physical_lines.extend(self.wrap_line(line.clone(), area_width)); } } @@ -426,4 +442,89 @@ impl Chat { f.render_widget(Paragraph::new(Line::from(spans)), Rect::new(area.x, area.y + row as u16, area.width, 1)); } } + + /// Helper to wrap a Line (potentially with multiple Spans) to a specific width. + fn wrap_line<'a>(&self, line: Line<'a>, width: usize) -> Vec> { + let mut result = Vec::new(); + let mut current_spans = Vec::>::new(); + let mut current_width = 0; + + for span in line.spans { + let style = span.style; + for c in span.content.chars() { + let char_str = c.to_string(); + let cw = UnicodeWidthStr::width(char_str.as_str()); + + // Wrap if adding this char exceeds width + if current_width + cw > width && !current_spans.is_empty() { + result.push(Line::from(std::mem::take(&mut current_spans))); + current_width = 0; + } + + // Append to existing span if style matches, otherwise push new one + if let Some(last) = current_spans.last_mut().filter(|s| s.style == style) { + last.content.to_mut().push(c); + } else { + current_spans.push(Span::styled(char_str, style)); + } + + current_width += cw; + } + } + + if !current_spans.is_empty() { + result.push(Line::from(current_spans)); + } + + if result.is_empty() { + result.push(Line::from("")); + } + result + } +} + +/// Helper to convert ratatui-core Style to ratatui Style. +fn convert_style(s: ratatui_core::style::Style) -> Style { + let mut style = Style::default() + .add_modifier(convert_modifier(s.add_modifier)) + .remove_modifier(convert_modifier(s.sub_modifier)); + + if let Some(fg) = s.fg { + style = style.fg(convert_color(fg)); + } + if let Some(bg) = s.bg { + style = style.bg(convert_color(bg)); + } + style +} + +/// Helper to convert ratatui-core Color to ratatui Color. +fn convert_color(c: ratatui_core::style::Color) -> Color { + use ratatui_core::style::Color::*; + match c { + Reset => Color::Reset, + Black => Color::Black, + Red => Color::Red, + Green => Color::Green, + Yellow => Color::Yellow, + Blue => Color::Blue, + Magenta => Color::Magenta, + Cyan => Color::Cyan, + Gray => Color::Gray, + DarkGray => Color::DarkGray, + LightRed => Color::LightRed, + LightGreen => Color::LightGreen, + LightYellow => Color::LightYellow, + LightBlue => Color::LightBlue, + LightMagenta => Color::LightMagenta, + LightCyan => Color::LightCyan, + White => Color::White, + Rgb(r, g, b) => Color::Rgb(r, g, b), + Indexed(i) => Color::Indexed(i), + } +} + +/// Helper to convert ratatui-core Modifier to ratatui Modifier. +fn convert_modifier(m: ratatui_core::style::Modifier) -> Modifier { + Modifier::from_bits_truncate(m.bits()) } diff --git a/clients/voce-tui/src/pages/monitor_dashboard.rs b/clients/voce-tui/src/pages/monitor_dashboard.rs index dd4b6be..15b6ac1 100644 --- a/clients/voce-tui/src/pages/monitor_dashboard.rs +++ b/clients/voce-tui/src/pages/monitor_dashboard.rs @@ -46,7 +46,7 @@ impl Page for MonitorDashboard { Constraint::Min(10), // Full-width Traffic Chart Constraint::Length(10), // Bottom Stats (Mem, GC, etc) ]) - .split(f.size()); + .split(f.area()); // 1. Header f.render_widget(Paragraph::new(" [ESC/q] Back to List | SYSTEM WIDE MONITORING ").block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::Cyan))), chunks[0]); diff --git a/clients/voce-tui/src/pages/property_editor.rs b/clients/voce-tui/src/pages/property_editor.rs index 6de8a83..7fb7870 100644 --- a/clients/voce-tui/src/pages/property_editor.rs +++ b/clients/voce-tui/src/pages/property_editor.rs @@ -55,7 +55,7 @@ impl Page for PropertyEditor { } fn draw(&mut self, f: &mut Frame) { - let area = centered_rect(60, 30, f.size()); + let area = centered_rect(60, 30, f.area()); let input = Paragraph::new(self.input.as_str()) .block(Block::default().title(format!(" 2. Config for {} ", self.workflow.name)).borders(Borders::ALL).border_style(Style::default().fg(Color::Green))) .wrap(Wrap { trim: false }); diff --git a/clients/voce-tui/src/pages/workflow_list.rs b/clients/voce-tui/src/pages/workflow_list.rs index 78aa534..3771180 100644 --- a/clients/voce-tui/src/pages/workflow_list.rs +++ b/clients/voce-tui/src/pages/workflow_list.rs @@ -72,7 +72,7 @@ impl Page for WorkflowList { } fn draw(&mut self, f: &mut Frame) { - let area = centered_rect(60, 40, f.size()); + let area = centered_rect(60, 40, f.area()); let items: Vec = self.workflows.iter().map(|w| ListItem::new(format!(" {} ", w.name))).collect(); let list = List::new(items) .block(Block::default().title(" 1. Select Workflow ").borders(Borders::ALL).border_style(Style::default().fg(Color::Cyan))) diff --git a/docs/plugins_list.md b/docs/plugins_list.md index 1598534..bff9fce 100644 --- a/docs/plugins_list.md +++ b/docs/plugins_list.md @@ -30,6 +30,7 @@ Voce 采用插件化服务,通过不同的插件组合实现多模态能力的 - **interrupter**: 实时打断控制器,负责发送打断信号。 - **caption**: 字幕传输插件,负责实时下发 ASR 或 LLM 产生的文本内容。 +- **markdown_filter**: 实时过滤文本中的 Markdown 标记代码(如标题、粗体、链接、代码块等),通常用于 TTS 前置转换。 - **sink**: 统一数据出口。 --- diff --git a/internal/plugins/init.go b/internal/plugins/init.go index d7bedc8..c859578 100644 --- a/internal/plugins/init.go +++ b/internal/plugins/init.go @@ -7,6 +7,7 @@ import ( _ "github.com/wnnce/voce/internal/plugins/elevenlabs_tts" _ "github.com/wnnce/voce/internal/plugins/google_asr" _ "github.com/wnnce/voce/internal/plugins/interrupter" + _ "github.com/wnnce/voce/internal/plugins/md_filter" _ "github.com/wnnce/voce/internal/plugins/minimax_tts" _ "github.com/wnnce/voce/internal/plugins/openai_llm" _ "github.com/wnnce/voce/internal/plugins/openai_tts" diff --git a/internal/plugins/md_filter/plugin.go b/internal/plugins/md_filter/plugin.go new file mode 100644 index 0000000..f434203 --- /dev/null +++ b/internal/plugins/md_filter/plugin.go @@ -0,0 +1,133 @@ +package md_filter + +import ( + "context" + "strings" + + "github.com/wnnce/voce/internal/engine" + "github.com/wnnce/voce/internal/schema" +) + +const ( + stateNormal int = iota + stateInCodeBlock + stateInLinkUrl +) + +type Plugin struct { + engine.BuiltinPlugin + state int + backtickBuf strings.Builder + seenCloseBracket bool +} + +func NewPlugin(_ engine.EmptyPluginConfig) engine.Plugin { + return &Plugin{} +} + +func (p *Plugin) OnSignal(ctx context.Context, flow engine.Flow, signal schema.Signal) { + if signal.Name() == schema.SignalInterrupter { + p.state = stateNormal + p.backtickBuf.Reset() + p.seenCloseBracket = false + } + flow.SendSignal(signal) +} + +func (p *Plugin) OnPayload(ctx context.Context, flow engine.Flow, payload schema.Payload) { + if payload.Name() != schema.PayloadLLMChunk { + flow.SendPayload(payload) + return + } + + text := schema.GetAs(payload, "sentence", "") + isFinal := schema.GetAs(payload, "is_final", false) + + filtered := p.filter(text) + + if strings.TrimSpace(filtered) == "" && !isFinal { + return + } + + out := schema.NewPayload(schema.PayloadLLMChunk) + _ = out.Set("sentence", filtered) + _ = out.Set("is_final", isFinal) + flow.SendPayload(out.ReadOnly()) +} + +func (p *Plugin) filter(input string) string { + var out strings.Builder + runes := []rune(input) + + for i := 0; i < len(runes); i++ { + r := runes[i] + + if r == '`' { + p.backtickBuf.WriteRune(r) + p.seenCloseBracket = false + continue + } + + if p.backtickBuf.Len() > 0 { + count := p.backtickBuf.Len() + p.backtickBuf.Reset() + if count >= 3 { + if p.state == stateInCodeBlock { + p.state = stateNormal + } else { + p.state = stateInCodeBlock + } + } + i-- + continue + } + + if p.state == stateInCodeBlock { + continue + } + + if p.state == stateInLinkUrl { + if r == ')' { + p.state = stateNormal + } + continue + } + + if r == '(' && p.seenCloseBracket { + p.state = stateInLinkUrl + p.seenCloseBracket = false + continue + } + p.seenCloseBracket = false + + if r == '*' || r == '_' || r == '~' || r == '#' || r == '>' || + r == '[' || r == '\\' { + continue + } + + if r == ']' { + p.seenCloseBracket = true + continue + } + + out.WriteRune(r) + } + + return out.String() +} + +func init() { + if err := engine.RegisterPlugin(NewPlugin, engine.PluginMetadata{ + Name: "markdown_filter", + Inputs: engine.NewPropertyBuilder(). + AddPayload(schema.PayloadLLMChunk, "sentence", engine.TypeString, true). + AddPayload(schema.PayloadLLMChunk, "is_final", engine.TypeBoolean, true). + Build(), + Outputs: engine.NewPropertyBuilder(). + AddPayload(schema.PayloadLLMChunk, "sentence", engine.TypeString, true). + AddPayload(schema.PayloadLLMChunk, "is_final", engine.TypeBoolean, true). + Build(), + }); err != nil { + panic(err) + } +} diff --git a/internal/plugins/md_filter/plugin_test.go b/internal/plugins/md_filter/plugin_test.go new file mode 100644 index 0000000..618773c --- /dev/null +++ b/internal/plugins/md_filter/plugin_test.go @@ -0,0 +1,177 @@ +package md_filter + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/wnnce/voce/internal/engine" + "github.com/wnnce/voce/internal/schema" +) + +func TestMdFilter_Basic(t *testing.T) { + ext := NewPlugin(engine.EmptyPluginConfig{}) + tester := engine.NewPluginTester(t, ext) + + var lastText string + tester.OnPayload(func(port int, payload schema.Payload) { + lastText = schema.GetAs(payload, "sentence", "") + }) + + tester.Start() + + // 1. Simple Bold/Italic/Nesting + d1 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d1.Set("sentence", "Hello **World** and _Rust_ with __nesting__.") + tester.InjectPayload(d1.ReadOnly()) + assert.Equal(t, "Hello World and Rust with nesting.", lastText) + + // 2. Links + d2 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d2.Set("sentence", "Visit [Google](https://google.com).") + tester.InjectPayload(d2.ReadOnly()) + assert.Equal(t, "Visit Google.", lastText) + + tester.Done() + tester.Wait() + tester.Stop() +} + +func TestMdFilter_CrossChunkLink(t *testing.T) { + ext := NewPlugin(engine.EmptyPluginConfig{}) + tester := engine.NewPluginTester(t, ext) + + var lastText string + tester.OnPayload(func(port int, payload schema.Payload) { + lastText = schema.GetAs(payload, "sentence", "") + }) + + tester.Start() + + // Chunk 1: [Link + d1 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d1.Set("sentence", "[Link") + tester.InjectPayload(d1.ReadOnly()) + assert.Equal(t, "Link", lastText) + + // Chunk 2: text] + d2 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d2.Set("sentence", " text]") + tester.InjectPayload(d2.ReadOnly()) + assert.Equal(t, " text", lastText) + + // Chunk 3: (https://example.com) + d3 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d3.Set("sentence", "(https://example.com) is here.") + _ = d3.Set("is_final", true) + tester.InjectPayload(d3.ReadOnly()) + + tester.Done() + tester.Wait() + assert.Equal(t, " is here.", lastText) + tester.Stop() +} + +func TestMdFilter_CodeBlock(t *testing.T) { + ext := NewPlugin(engine.EmptyPluginConfig{}) + tester := engine.NewPluginTester(t, ext) + + var lastText string + tester.OnPayload(func(port int, payload schema.Payload) { + lastText = schema.GetAs(payload, "sentence", "") + }) + + tester.Start() + + d1 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d1.Set("sentence", "Code: ```go\n") + tester.InjectPayload(d1.ReadOnly()) + + d2 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d2.Set("sentence", "println(\"hi\")\n``` and post.") + _ = d2.Set("is_final", true) + tester.InjectPayload(d2.ReadOnly()) + + tester.Done() + tester.Wait() + assert.Equal(t, " and post.", lastText) + tester.Stop() +} + +func TestMdFilter_Interruption(t *testing.T) { + ext := NewPlugin(engine.EmptyPluginConfig{}) + tester := engine.NewPluginTester(t, ext) + + var lastText string + tester.OnPayload(func(port int, payload schema.Payload) { + lastText = schema.GetAs(payload, "sentence", "") + }) + + tester.Start() + + d1 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d1.Set("sentence", "```broken block") + tester.InjectPayload(d1.ReadOnly()) + + tester.InjectSignal(schema.NewSignal(schema.SignalInterrupter).ReadOnly()) + + d2 := schema.NewPayload(schema.PayloadLLMChunk) + _ = d2.Set("sentence", "Normal text") + _ = d2.Set("is_final", true) + tester.InjectPayload(d2.ReadOnly()) + + tester.Done() + tester.Wait() + assert.Equal(t, "Normal text", lastText) + tester.Stop() +} + +// --- Benchmarks --- + +func BenchmarkFilterNormalText(b *testing.B) { + p := &Plugin{} + text := "This is a normal sentence without any markdown symbols." + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + p.filter(text) + } +} + +func BenchmarkFilterHeavyMarkdown(b *testing.B) { + p := &Plugin{} + text := "Hello **World**, [this](https://example.com) is a _heavy_ markdown `text` with # headers and > quotes." + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + p.filter(text) + } +} + +func BenchmarkFilterLargeCodeBlock(b *testing.B) { + p := &Plugin{} + var sb strings.Builder + sb.WriteString("Start of code block: ```go\n") + for i := 0; i < 100; i++ { + sb.WriteString(fmt.Sprintf("func test%d() { println(\"testing\") }\n", i)) + } + sb.WriteString("``` End of code block.") + text := sb.String() + + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + p.filter(text) + } +} + +func BenchmarkFilterLongLinks(b *testing.B) { + p := &Plugin{} + text := "[Link Text](https://very-long-url-with-lots-of-parameters.com/v1/resource?id=123456789&token=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ)" + b.ResetTimer() + b.ReportAllocs() + for b.Loop() { + p.filter(text) + } +}