diff --git a/Cargo.lock b/Cargo.lock index 3abc1e3..25afce9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.21" @@ -237,6 +243,27 @@ dependencies = [ "console 0.15.11", ] +[[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" @@ -314,6 +341,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[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" @@ -419,6 +455,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "compression-codecs" version = "0.4.37" @@ -551,6 +601,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "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 = "crypto-common" version = "0.1.7" @@ -561,6 +638,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" @@ -588,6 +675,46 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + [[package]] name = "der" version = "0.7.10" @@ -691,6 +818,15 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dunce" version = "1.0.5" @@ -762,6 +898,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +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" @@ -788,6 +943,17 @@ dependencies = [ "version_check", ] +[[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 = "filetime" version = "0.2.27" @@ -805,6 +971,18 @@ 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" @@ -836,6 +1014,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" @@ -1026,7 +1210,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags", + "bitflags 2.11.0", "ignore", "walkdir", ] @@ -1056,7 +1240,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1064,6 +1248,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" @@ -1086,6 +1275,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.0" @@ -1290,6 +1485,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" @@ -1352,19 +1553,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "indicatif" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" -dependencies = [ - "console 0.16.2", - "portable-atomic", - "unicode-width", - "unit-prefix", - "web-time", -] - [[package]] name = "indoc" version = "2.0.7" @@ -1380,6 +1568,19 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[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.11.0" @@ -1402,6 +1603,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -1450,6 +1660,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "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" @@ -1474,9 +1701,18 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.11.0", "libc", - "redox_syscall", + "redox_syscall 0.7.1", +] + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -1491,24 +1727,79 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[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 = "memchr" 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 = "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" @@ -1526,10 +1817,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] +[[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 = "normalize-line-endings" version = "0.3.0" @@ -1593,6 +1908,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" @@ -1643,6 +1969,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -1655,7 +1990,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -1696,25 +2031,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "pear" -version = "0.2.9" +name = "ordered-float" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi", + "num-traits", ] [[package]] -name = "pear_codegen" -version = "0.2.9" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", + "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 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", "syn 2.0.117", ] @@ -1724,6 +2091,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.6", +] + +[[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.16" @@ -1896,7 +2358,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.2", "ring", "rustc-hash", "rustls", @@ -1937,6 +2399,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" @@ -1975,13 +2446,107 @@ dependencies = [ "getrandom 0.3.4", ] +[[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 = "redox_syscall" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -2166,7 +2731,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2279,13 +2844,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" 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", @@ -2322,7 +2893,7 @@ dependencies = [ "either", "flate2", "hyper", - "indicatif 0.17.11", + "indicatif", "log", "quick-xml", "regex", @@ -2456,6 +3027,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" @@ -2482,6 +3074,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" @@ -2520,12 +3118,39 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[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" @@ -2626,12 +3251,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" @@ -2689,7 +3377,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -2900,7 +3590,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -3025,6 +3715,7 @@ dependencies = [ "clap-verbosity-flag", "console 0.16.2", "const-str", + "crossterm", "derive_more", "diff-struct", "enum_dispatch", @@ -3032,12 +3723,12 @@ dependencies = [ "futures", "human-panic", "ignore", - "indicatif 0.18.4", "indoc", "log", "num_cpus", "predicates", "pretty_assertions", + "ratatui", "reqwest 0.13.2", "rstest", "self_update", @@ -3063,6 +3754,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 = "uncased" version = "0.9.10" @@ -3084,6 +3781,17 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.2.2" @@ -3096,12 +3804,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - [[package]] name = "untrusted" version = "0.9.0" @@ -3145,7 +3847,10 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ + "atomic", "getrandom 0.4.1", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -3160,6 +3865,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" @@ -3299,7 +4013,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", @@ -3343,6 +4057,78 @@ dependencies = [ "rustls-pki-types", ] +[[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" @@ -3871,7 +4657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 84614a5..c688c9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ better-panic = "0.3" clap = { version = "4.5", features = ["cargo", "derive", "env"] } clap-verbosity-flag = "3.0" console = "0.16" +crossterm = "0.29" derive_more = { version = "2", features = ["as_ref", "deref", "display"] } diff-struct = "0.5" enum_dispatch = "0.3" @@ -53,7 +54,7 @@ figment = { version = "0.10", features = ["toml", "env"] } futures = "0.3" human-panic = "2.0" ignore = "0.4" -indicatif = "0.18" +ratatui = { version = "0.30", default-features = false, features = ["crossterm", "underline-color", "macros"] } log = "0.4" num_cpus = "1.17" reqwest = { version = "0.13", default-features = false, features = [ diff --git a/src/actors/display.rs b/src/actors/display.rs index 00076d7..eba5d1e 100644 --- a/src/actors/display.rs +++ b/src/actors/display.rs @@ -1,23 +1,23 @@ -use std::{collections::HashMap, sync::Arc}; +use std::io; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use ratatui::widgets::Paragraph; use tokio::sync::{mpsc, oneshot}; +use tokio::time; -use crate::{ - actors::{Addr, Response}, - display::{Progress, ProgressBar, UpdateKind}, - git::GitRef, - TsdlResult, -}; +use crate::actors::Addr; +use crate::display::{DisplayState, GrammarEntry, ItemStatus, Mode, RepoEntry}; +use crate::git::GitRef; -#[derive(Debug)] -#[allow(dead_code)] -enum DisplayResponseKind<'a> { - RegisterGrammar { language: &'a str, name: &'a str }, - RegisterLanguage { name: &'a str }, -} +// --------------------------------------------------------------------------- +// Message types +// --------------------------------------------------------------------------- #[derive(Debug)] pub enum DisplayMessage { + /// Register a repo-level progress line. Returns a ProgressAddr. RegisterLanguage { git_ref: GitRef, name: Arc, @@ -25,10 +25,7 @@ pub enum DisplayMessage { tx: oneshot::Sender, }, - Println { - msg: Arc, - }, - + /// Register a grammar-level progress line. Returns a ProgressAddr. RegisterGrammar { git_ref: GitRef, language: Arc, @@ -37,23 +34,34 @@ pub enum DisplayMessage { tx: oneshot::Sender, }, - UnregisterLanguage { - name: Arc, - }, - + /// Update a specific bar. Update { id: u64, kind: UpdateKind, - msg: String, + msg: Arc, }, + /// Advance the elapsed-time display and trigger a re-render. Tick, } -/// The Manager Handle: Only used to register/unregister tasks. +#[derive(Debug, Clone, Copy)] +pub enum UpdateKind { + Msg, + Step, + Fin, + Err, +} + +// --------------------------------------------------------------------------- +// Handles +// --------------------------------------------------------------------------- + #[derive(Debug, Clone)] pub struct DisplayAddr { tx: mpsc::Sender, + #[allow(dead_code)] + mode: Mode, } impl Addr for DisplayAddr { @@ -70,20 +78,18 @@ impl Addr for DisplayAddr { impl DisplayAddr { #[must_use] - pub fn new(tx: mpsc::Sender) -> Self { - Self { tx } + pub fn new(tx: mpsc::Sender, mode: Mode) -> Self { + Self { tx, mode } } - pub async fn add_grammar>>( + pub async fn add_language>>( &self, git_ref: GitRef, - language: S, name: S, num_tasks: usize, ) -> ProgressAddr { - self.request(|tx| DisplayMessage::RegisterGrammar { + self.request(|tx| DisplayMessage::RegisterLanguage { git_ref, - language: language.into(), name: name.into(), num_tasks, tx, @@ -91,14 +97,16 @@ impl DisplayAddr { .await } - pub async fn add_language>>( + pub async fn add_grammar>>( &self, git_ref: GitRef, + language: S, name: S, num_tasks: usize, ) -> ProgressAddr { - self.request(|tx| DisplayMessage::RegisterLanguage { + self.request(|tx| DisplayMessage::RegisterGrammar { git_ref, + language: language.into(), name: name.into(), num_tasks, tx, @@ -106,22 +114,12 @@ impl DisplayAddr { .await } - pub async fn println>>(&self, msg: S) { - self.fire(DisplayMessage::Println { msg: msg.into() }).await; - } - - pub async fn remove_language>>(&self, name: S) -> TsdlResult<()> { - self.fire(DisplayMessage::UnregisterLanguage { name: name.into() }) - .await; - Ok(()) - } - pub async fn tick(&self) { - self.fire(DisplayMessage::Tick {}).await; + self.fire(DisplayMessage::Tick).await; } } -/// The Task Handle: Dedicated to controlling a specific progress bar. +/// Handle for updating a specific progress bar (repo or grammar). #[derive(Debug, Clone)] pub struct ProgressAddr { id: u64, @@ -129,8 +127,7 @@ pub struct ProgressAddr { } impl ProgressAddr { - /// Takes Into directly as the message must be owned to be sent - pub fn msg>(&self, msg: S) { + pub fn msg>>(&self, msg: S) { let _ = self.tx.try_send(DisplayMessage::Update { id: self.id, kind: UpdateKind::Msg, @@ -138,7 +135,7 @@ impl ProgressAddr { }); } - pub fn step>(&self, msg: S) { + pub fn step>>(&self, msg: S) { let _ = self.tx.try_send(DisplayMessage::Update { id: self.id, kind: UpdateKind::Step, @@ -146,7 +143,7 @@ impl ProgressAddr { }); } - pub fn fin>(&self, msg: S) { + pub fn fin>>(&self, msg: S) { let _ = self.tx.try_send(DisplayMessage::Update { id: self.id, kind: UpdateKind::Fin, @@ -154,7 +151,7 @@ impl ProgressAddr { }); } - pub fn err>(&self, msg: S) { + pub fn err>>(&self, msg: S) { let _ = self.tx.try_send(DisplayMessage::Update { id: self.id, kind: UpdateKind::Err, @@ -163,124 +160,360 @@ impl ProgressAddr { } } +// --------------------------------------------------------------------------- +// DisplayActor +// --------------------------------------------------------------------------- + pub struct DisplayActor { - handles: HashMap, + state: DisplayState, next_id: u64, - progress: Progress, rx: mpsc::Receiver, tx: mpsc::Sender, } impl DisplayActor { - fn finish(&mut self, id: u64, f: F) - where - F: FnOnce(&ProgressBar), - { - self.forward(id, f); - self.handles.remove(&id); + #[must_use] + pub fn spawn(mode: Mode, build_dir: Arc, out_dir: Arc) -> DisplayAddr { + let (tx, rx) = mpsc::channel(256); + let actor = Self { + state: DisplayState::new(mode, build_dir, out_dir), + next_id: 1, + rx, + tx: tx.clone(), + }; + + tokio::spawn(async move { + actor.run().await; + }); + + DisplayAddr::new(tx, mode) } - fn forward(&self, id: u64, f: F) - where - F: FnOnce(&ProgressBar), - { - if let Some(h) = self.handles.get(&id) { - f(h); + async fn run(mut self) { + if self.state.mode == Mode::Fancy { + self.run_fancy().await; + } else { + self.run_plain().await; } } - async fn run(mut self) { + // ── Fancy mode ──────────────────────────────────────────────────── + + async fn run_fancy(&mut self) { + let term_height = crossterm::terminal::size() + .map(|(_, h)| h as usize) + .unwrap_or(40); + let viewport_height = term_height.min(40); + + // for _ in 0..viewport_height { + // println!(); + // } + + let mut terminal = ratatui::Terminal::with_options( + ratatui::backend::CrosstermBackend::new(io::stderr()), + ratatui::TerminalOptions { + viewport: ratatui::Viewport::Inline(viewport_height as u16), + }, + ) + .expect("Failed to initialize ratatui terminal"); + + let _ = terminal.draw(|frame| self.render(frame)); + + let mut tick_interval = time::interval(Duration::from_millis(100)); + + loop { + tokio::select! { + msg = self.rx.recv() => { + match msg { + Some(msg) => self.handle_message(msg), + None => break, + } + } + _ = tick_interval.tick() => { + // Tick advances nothing visible — just triggers re-render + // for live elapsed-time updates on in-progress items. + } + } + + let _ = terminal.draw(|frame| self.render(frame)); + } + + ratatui::restore(); + // for _ in 0..viewport_height { + // println!(); + // } + } + + fn render(&self, frame: &mut ratatui::Frame) { + let area = frame.area(); + let lines = self.state.render_lines(area.width); + let paragraph = Paragraph::new(lines); + frame.render_widget(paragraph, area); + } + + // ── Plain mode ──────────────────────────────────────────────────── + + async fn run_plain(&mut self) { while let Some(msg) = self.rx.recv().await { match msg { DisplayMessage::RegisterLanguage { git_ref, - ref name, + name, num_tasks, tx, } => { - let res = self.register({ - let name = name.clone(); - move |p| p.register(name, git_ref, num_tasks) - }); - - Response { - tx, - kind: DisplayResponseKind::RegisterLanguage { name }, - } - .send(res); + let addr = self.register_repo(name, git_ref, num_tasks); + let _ = tx.send(addr); } - - DisplayMessage::Println { msg } => { - self.progress.prinltn(msg); - } - DisplayMessage::RegisterGrammar { git_ref, - ref language, - ref name, + language, + name, num_tasks, tx, } => { - let res = self.register(|p| { - let language = language.clone(); - let name = name.clone(); - p.register(format!("{language}/{name}").into(), git_ref, num_tasks) - }); - Response { - tx, - kind: DisplayResponseKind::RegisterGrammar { language, name }, - } - .send(res); + let addr = self.register_grammar(language, name, git_ref, num_tasks); + let _ = tx.send(addr); } - - DisplayMessage::UnregisterLanguage { name } => { - self.handles.retain(|_, h| name != h.name); + DisplayMessage::Update { id, kind, msg } => { + self.apply_update(id, kind, msg); + if let Some(repo) = self.state.repos.get(&id) { + println!( + " {} {} [{}/{}] {}", + repo.name, repo.git_ref, repo.step, repo.total, repo.msg, + ); + } else if let Some(grammar) = self.state.grammars.get(&id) { + println!( + " {}/{} {} [{}/{}] {}", + grammar.repo, + grammar.name, + grammar.git_ref, + grammar.step, + grammar.total, + grammar.msg, + ); + } } + DisplayMessage::Tick => {} + } + } + } - DisplayMessage::Update { id, kind, ref msg } => match kind { - UpdateKind::Msg => self.forward(id, |h| h.msg(msg)), - UpdateKind::Step => self.forward(id, |h| h.step(msg)), - UpdateKind::Fin => self.finish(id, |h| h.fin(msg)), - UpdateKind::Err => self.finish(id, |h| h.err(msg)), - }, - - DisplayMessage::Tick => { - self.progress.tick(); - } + // ── Message handling ────────────────────────────────────────────── + + fn handle_message(&mut self, msg: DisplayMessage) { + match msg { + DisplayMessage::RegisterLanguage { + git_ref, + name, + num_tasks, + tx, + } => { + let addr = self.register_repo(name, git_ref, num_tasks); + let _ = tx.send(addr); } + DisplayMessage::RegisterGrammar { + git_ref, + language, + name, + num_tasks, + tx, + } => { + let addr = self.register_grammar(language, name, git_ref, num_tasks); + let _ = tx.send(addr); + } + DisplayMessage::Update { id, kind, msg } => { + self.apply_update(id, kind, msg); + } + DisplayMessage::Tick => {} } } - fn register(&mut self, create: F) -> ProgressAddr - where - F: FnOnce(&mut Progress) -> ProgressBar, - { - // 1. Create inner handle - let inner = create(&mut self.progress); + fn register_repo(&mut self, name: Arc, git_ref: GitRef, num_tasks: usize) -> ProgressAddr { + let id = self.next_id; + self.next_id += 1; + + self.state.repos.insert( + id, + RepoEntry { + name, + git_ref, + status: ItemStatus::New, + msg: Arc::from(""), + step: 0, + total: num_tasks, + t_start: Instant::now(), + frozen_elapsed: None, + }, + ); - // 2. Register in actor state + ProgressAddr { + id, + tx: self.tx.clone(), + } + } + + fn register_grammar( + &mut self, + language: Arc, + name: Arc, + git_ref: GitRef, + num_tasks: usize, + ) -> ProgressAddr { let id = self.next_id; self.next_id += 1; - self.handles.insert(id, inner); - // 3. Return client handle + // Find parent repo + let repo_id = self + .state + .repos + .iter() + .find(|(_, r)| r.name == language) + .map(|(id, _)| *id) + .unwrap_or(0); + + // Update parent repo state when grammars are registered + if let Some(repo) = self.state.repos.get_mut(&repo_id) { + repo.msg = Arc::from("building"); + repo.status = ItemStatus::InProgress; + repo.total = repo.step + num_tasks; + } + + self.state.grammars.insert( + id, + GrammarEntry { + repo: language, + repo_id, + name, + git_ref, + status: ItemStatus::New, + msg: Arc::from(""), + step: 0, + total: num_tasks, + t_start: Instant::now(), + frozen_elapsed: None, + }, + ); + ProgressAddr { id, tx: self.tx.clone(), } } - #[must_use] - pub fn spawn(progress: Progress) -> DisplayAddr { - let (tx, rx) = mpsc::channel(64); - let actor = Self { - handles: HashMap::new(), - next_id: 1, - progress, - rx, - tx: tx.clone(), - }; - tokio::spawn(actor.run()); - DisplayAddr::new(tx) + fn apply_update(&mut self, id: u64, kind: UpdateKind, msg: Arc) { + // Update repo entry + if let Some(repo) = self.state.repos.get_mut(&id) { + match kind { + UpdateKind::Msg => { + repo.msg = msg; + } + UpdateKind::Step => { + repo.status = ItemStatus::InProgress; + repo.step += 1; + repo.msg = msg; + repo.t_start = Instant::now(); + } + UpdateKind::Fin => { + // Freeze elapsed — repo stays in display + repo.frozen_elapsed = Some(format_elapsed_str(repo.t_start)); + if repo.total > 0 { + repo.step = repo.total; + } + repo.msg = msg; + } + UpdateKind::Err => { + repo.frozen_elapsed = Some(format_elapsed_str(repo.t_start)); + repo.status = ItemStatus::Failed; + repo.msg = msg; + self.state.failed_count += 1; + } + } + return; + } + + // Update grammar entry + let mut maybe_parent_id: Option = None; + if let Some(grammar) = self.state.grammars.get_mut(&id) { + match kind { + UpdateKind::Msg => { + grammar.msg = msg; + } + UpdateKind::Step => { + grammar.status = ItemStatus::InProgress; + grammar.step += 1; + grammar.msg = msg; + grammar.t_start = Instant::now(); + } + UpdateKind::Fin => { + grammar.frozen_elapsed = Some(format_elapsed_str(grammar.t_start)); + let is_cached = msg.as_ref() == "cached"; + grammar.status = if is_cached { + ItemStatus::Cached + } else { + ItemStatus::Done + }; + grammar.step = grammar.total; + grammar.msg = msg; + if is_cached { + self.state.cached_count += 1; + } else { + self.state.built_count += 1; + } + maybe_parent_id = Some(grammar.repo_id); + } + UpdateKind::Err => { + grammar.frozen_elapsed = Some(format_elapsed_str(grammar.t_start)); + grammar.status = ItemStatus::Failed; + grammar.msg = msg; + self.state.failed_count += 1; + } + } + } + if let Some(repo_id) = maybe_parent_id { + self.sync_parent_repo(repo_id); + } + } + + fn sync_parent_repo(&mut self, repo_id: u64) { + // If all grammars for this repo are terminal (Done/Cached/Failed), + // update the repo's status to Done (or keep Failed if any failed). + let has_any = self.state.grammars.values().any(|g| g.repo_id == repo_id); + if !has_any { + return; + } + let any_failed = self + .state + .grammars + .values() + .any(|g| g.repo_id == repo_id && g.status == ItemStatus::Failed); + let any_active = self.state.grammars.values().any(|g| { + g.repo_id == repo_id + && (g.status == ItemStatus::New || g.status == ItemStatus::InProgress) + }); + + if let Some(repo) = self.state.repos.get_mut(&repo_id) { + if any_failed { + repo.status = ItemStatus::Failed; + repo.frozen_elapsed + .get_or_insert_with(|| format_elapsed_str(repo.t_start)); + } else if !any_active { + repo.status = ItemStatus::Done; + repo.frozen_elapsed + .get_or_insert_with(|| format_elapsed_str(repo.t_start)); + } + } + } +} + +fn format_elapsed_str(start: Instant) -> String { + let dur = start.elapsed(); + let secs = dur.as_secs(); + if secs < 60 { + format!("{}.{:02}s", secs, dur.subsec_millis() / 10) + } else if secs < 3600 { + format!("{}m{}s", secs / 60, secs % 60) + } else { + format!("{}h{}m", secs / 3600, (secs % 3600) / 60) } } diff --git a/src/app.rs b/src/app.rs index 9824f9c..387a07f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,7 @@ use crate::{args::Args, args::BuildCommand, config, display, TsdlResult}; pub struct App { pub command: BuildCommand, pub config_path: PathBuf, - pub progress: display::Progress, + pub progress_mode: display::Mode, pub verbose: Verbosity, } @@ -17,11 +17,11 @@ impl App { /// This resolves and merges all configuration sources (CLI, config file, defaults). pub fn new(args: &Args) -> TsdlResult { let command = config::current(&args.config, args.command.as_build())?; - let progress = display::current(&args.progress, &args.verbose); + let progress_mode = display::mode_from_args(&args.progress, &args.verbose); Ok(Self { command, - progress, + progress_mode, config_path: args.config.clone(), verbose: args.verbose, }) diff --git a/src/build.rs b/src/build.rs index 8b77cda..4024be6 100644 --- a/src/build.rs +++ b/src/build.rs @@ -6,16 +6,14 @@ use std::{ }; use serde::{Deserialize, Serialize}; -use tokio::time; use url::Url; use crate::{ - actors::{self, CacheActor, DisplayActor, DisplayAddr}, + actors::{self, CacheActor, DisplayActor}, app::App, args::{ParserConfig, Target, TreeSitter}, cache::Db, consts::TSDL_FROM, - display::{self, Progress, ProgressBar, TICK_CHARS}, error::{self, TsdlError}, git::GitRef, lock::{Lock, LockStatus}, @@ -43,50 +41,6 @@ pub struct OutputConfig { pub struct BuildContext { pub cache_hit: bool, pub force: bool, - pub progress: Option, -} - -impl BuildContext { - pub fn err(&self, msg: &str) { - if let Some(ref progress) = self.progress { - progress.err(msg); - } - } - - pub fn fin(&self, msg: &str) { - if let Some(ref progress) = self.progress { - progress.fin(msg); - } - } - - pub fn msg(&self, msg: &str) { - if let Some(ref progress) = self.progress { - progress.msg(msg); - } - } - - pub fn step(&self, msg: &str) { - if let Some(ref progress) = self.progress { - progress.step(msg); - } - } - - #[must_use] - pub fn is_done(&self) -> bool { - self.progress.as_ref().is_none_or(ProgressBar::is_done) - } - - pub fn start(&mut self, msg: &str) { - if let Some(ref mut progress) = self.progress { - progress.step(msg); - } - } - - pub fn tick(&self) { - if let Some(ref progress) = self.progress { - progress.tick(); - } - } } pub fn run(app: &mut App) -> TsdlResult<()> { @@ -94,15 +48,12 @@ pub fn run(app: &mut App) -> TsdlResult<()> { crate::config::show(&app.command)?; } - // Initialize the manager first with the build directory let lock = Lock::new(&app.command.build_dir); if app.command.unlock { lock.force_unlock()?; } - // Check lock status before clearing anything - let _guard = match lock.try_acquire()? { LockStatus::Acquired(lock) => lock, @@ -114,7 +65,6 @@ pub fn run(app: &mut App) -> TsdlResult<()> { LockStatus::LockedBy { pid, exe } => { eprintln!("Lock owned by different process: PID {pid} ({exe})"); if prompt_user("Proceed anyway?", false)? { - // Use the manager instance to force acquire lock.force_acquire()? } else { return Err(TsdlError::message("Lock acquisition cancelled by user")); @@ -147,9 +97,8 @@ pub fn run(app: &mut App) -> TsdlResult<()> { fn clear(app: &mut App) -> TsdlResult<()> { if app.command.fresh && app.command.build_dir.exists() { - let bar = app.progress.register("Fresh Build".into(), "".into(), 1); fs::remove_dir_all(&app.command.build_dir)?; - bar.fin(format!("Cleaned {}", app.command.build_dir.display())); + eprintln!("Cleaned {}", app.command.build_dir.display()); } fs::create_dir_all(&app.command.build_dir)?; @@ -180,7 +129,6 @@ fn get_language_coords( language: &str, defined_parsers: Option<&BTreeMap>, ) -> (Option, GitRef, TsdlResult) { - // Attempt to find the config; defaults to None if map or key is missing let config = defined_parsers.and_then(|parsers| parsers.get(language)); match config { @@ -221,12 +169,9 @@ fn ignite(app: &App) -> TsdlResult<()> { let result = rt.block_on(async move { let cache = CacheActor::spawn(db, app.command.force); - let display = DisplayActor::spawn(Progress::new(app.progress.mode)); - - let display2 = display.clone(); - tokio::spawn(async { - update_screen(display2).await; - }); + let build_dir: Arc = app.command.build_dir.canon()?.into(); + let out_dir: Arc = app.command.out_dir.canon()?.into(); + let display = DisplayActor::spawn(app.progress_mode, build_dir, out_dir); actors::run( &app.command.build_dir, @@ -281,7 +226,6 @@ fn unique_languages(app: &App) -> Vec> { BuildContext { force: app.command.force || app.command.fresh, cache_hit: false, - progress: None, // Progress is handled by DisplayActor }, Arc::new(BuildSpec { build_script, @@ -315,14 +259,3 @@ fn unique_languages(app: &App) -> Vec> { results } - -async fn update_screen(display: DisplayAddr) { - let mut interval = time::interval(time::Duration::from_millis( - 1000 / TICK_CHARS.chars().count() as u64, - )); - - loop { - interval.tick().await; - display.tick().await; - } -} diff --git a/src/display.rs b/src/display.rs index ace4dfa..9c0e2d5 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,26 +1,19 @@ -use std::{ - sync::atomic::Ordering, - sync::{atomic::AtomicU64, Arc}, - time, -}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Instant; use clap_verbosity_flag::{InfoLevel, Verbosity}; -use console::style; use log::Level; -use tokio::sync::OnceCell; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span}; -use crate::{args::ProgressStyle, error::TsdlError, format_duration, git::GitRef, TsdlResult}; +use crate::args::ProgressStyle; +use crate::git::GitRef; -#[derive(Debug, Clone, Copy)] -pub enum UpdateKind { - Msg, - Step, - Fin, - Err, -} - -/// Spinning sprite. -pub const TICK_CHARS: &str = "⠷⠯⠟⠻⠽⠾⠿"; +// --------------------------------------------------------------------------- +// Mode +// --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq)] pub enum Mode { @@ -28,306 +21,452 @@ pub enum Mode { Plain, } -#[derive(Debug, Clone)] -pub struct Progress { - multi: indicatif::MultiProgress, - pub mode: Mode, - // We store handles to ensure they aren't dropped prematurely if needed, - // mimicking the original `handles` vectors. - handles: Vec, -} - -impl Progress { - #[must_use] - pub fn new(mode: Mode) -> Self { - Self { - multi: indicatif::MultiProgress::new(), - mode, - handles: Vec::new(), - } - } - - pub fn clear(&self) -> TsdlResult<()> { - if self.mode == Mode::Fancy { - self.multi - .clear() - .map_err(|e| TsdlError::context("Clearing the multi-progress bar", e))?; +/// Determine the display mode from CLI flags and environment. +#[must_use] +pub fn mode_from_args(progress: &ProgressStyle, verbose: &Verbosity) -> Mode { + let mut mode = match progress { + ProgressStyle::Auto => { + if atty::is(atty::Stream::Stdout) { + Mode::Fancy + } else { + Mode::Plain + } } - Ok(()) - } - - pub fn is_done(&self) -> bool { - self.handles.iter().all(ProgressBar::is_done) - } + ProgressStyle::Fancy => Mode::Fancy, + ProgressStyle::Plain => Mode::Plain, + }; - pub fn prinltn(&self, msg: impl AsRef) { - println!("{}", msg.as_ref()); + if matches!(verbose.log_level(), Some(Level::Debug | Level::Trace)) { + mode = Mode::Plain; } - /// # Panics - /// - /// Will panic indicatif errs. - pub fn register(&mut self, name: Arc, git_ref: GitRef, num_tasks: usize) -> ProgressBar { - let bar = match self.mode { - Mode::Fancy => { - let bar = indicatif::ProgressBar::new(num_tasks as u64); - let bar = self.multi.add(bar); - let style = indicatif::ProgressStyle::with_template( - "{prefix:.bold.dim} {spinner} {wide_msg}", - ) - .unwrap_or_else(|_| { - panic!("cannot create spinner [?/{num_tasks}] {name} @ {git_ref}") - }) - .tick_chars(TICK_CHARS); - bar.set_style(style); - bar.set_prefix(format!("[?/{num_tasks}]")); - Some(bar) - } - Mode::Plain => None, - }; + mode +} - let handle = ProgressBar { - bar, - name, - git_ref, - num_tasks, - t_start: OnceCell::new(), - mode: self.mode, - current_step: Arc::new(AtomicU64::new(0)), - }; +// --------------------------------------------------------------------------- +// Item status +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ItemStatus { + /// Just registered, not yet started (blue). + New, + /// In progress — cloning, generating, building, etc. (yellow). + InProgress, + /// Cache hit, no rebuild needed (green). + Cached, + /// Successfully built (green). + Done, + /// Failed (red). + Failed, +} - self.handles.push(handle.clone()); - handle +impl ItemStatus { + fn color(self) -> Color { + match self { + ItemStatus::New => Color::Blue, + ItemStatus::InProgress => Color::Yellow, + ItemStatus::Cached => Color::Green, + ItemStatus::Done => Color::Green, + ItemStatus::Failed => Color::Red, + } } - pub fn tick(&self) { - // Only necessary for fancy bars in some terminals/configs, plain bars do nothing - if self.mode == Mode::Fancy { - for handle in &self.handles { - handle.tick(); - } + fn icon(self) -> &'static str { + match self { + ItemStatus::New => "●", + ItemStatus::InProgress => "●", + ItemStatus::Cached => "✓", + ItemStatus::Done => "✓", + ItemStatus::Failed => "✗", } } } -// Ensure bars are finished on drop -impl Drop for Progress { - fn drop(&mut self) { - for handle in &self.handles { - if !handle.is_done() { - if let Some(bar) = &handle.bar { - bar.finish(); - } - } - } - } +// --------------------------------------------------------------------------- +// State entries +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub(crate) struct RepoEntry { + pub name: Arc, + pub git_ref: GitRef, + pub status: ItemStatus, + pub msg: Arc, + pub step: usize, + pub total: usize, + pub t_start: Instant, + /// Frozen elapsed string for Done/Failed states. + pub frozen_elapsed: Option, } #[derive(Debug, Clone)] -pub struct ProgressBar { - bar: Option, +pub(crate) struct GrammarEntry { + pub repo: Arc, + pub repo_id: u64, pub name: Arc, - git_ref: GitRef, - num_tasks: usize, - t_start: OnceCell, - mode: Mode, - current_step: Arc, + pub git_ref: GitRef, + pub status: ItemStatus, + pub msg: Arc, + pub step: usize, + pub total: usize, + pub t_start: Instant, + /// Frozen elapsed string for Done/Cached/Failed states. + pub frozen_elapsed: Option, } -impl PartialEq for ProgressBar { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.git_ref == other.git_ref - && self.num_tasks == other.num_tasks - } +// --------------------------------------------------------------------------- +// DisplayState — the full renderable state +// --------------------------------------------------------------------------- + +pub(crate) struct DisplayState { + pub mode: Mode, + pub repos: HashMap, + pub grammars: HashMap, + pub cached_count: usize, + pub built_count: usize, + pub failed_count: usize, + pub build_dir: Arc, + pub out_dir: Arc, + pub total_start: Instant, } -impl ProgressBar { - fn format_elapsed(&self) -> String { - self.t_start - .get() - .map(|start| { - let dur = format_duration(time::Instant::now().duration_since(*start)); - if self.mode == Mode::Fancy { - format!(" in {}", style(dur).yellow()) - } else { - format!(" in {dur}") - } - }) - .unwrap_or_default() +impl DisplayState { + pub fn new(mode: Mode, build_dir: Arc, out_dir: Arc) -> Self { + Self { + mode, + repos: HashMap::new(), + grammars: HashMap::new(), + cached_count: 0, + built_count: 0, + failed_count: 0, + build_dir, + out_dir, + total_start: Instant::now(), + } } - fn name_with_version(&self) -> String { - if self.git_ref.is_empty() { - self.name.to_string() - } else { - format!("{} {}", self.name, style(&self.git_ref).blue()) + /// Render the entire display as ratatui `Line`s. + pub fn render_lines(&self, width: u16) -> Vec> { + let w = width as usize; + let mut lines: Vec = Vec::new(); + + // ── Legend (always at top) ── + lines.push(self.format_legend(w)); + + // ── Repos + grammars (persistent, never removed) ── + let sorted_repos = self.sorted_repos(); + if !sorted_repos.is_empty() { + lines.push(Line::from("")); + + for repo in &sorted_repos { + let repo_lines = self.format_repo_with_grammars(repo, w); + lines.extend(repo_lines); + } } + + // ── Footer ── + lines.push(self.separator(w, "─")); + lines.push(self.format_footer_build(w)); + lines.push(self.format_footer_out(w)); + lines.push(Line::from("")); + lines.push(self.format_footer_counts(w)); + + lines } - /// Helper to print log lines in Plain mode (using bar.println to coordinate with `MultiProgress`) - pub fn println(&self, msg: String) { - match &self.bar { - Some(bar) => bar.println(msg), - None => println!("{msg}"), - } + fn sorted_repos(&self) -> Vec<&RepoEntry> { + let mut repos: Vec<&RepoEntry> = self.repos.values().collect(); + repos.sort_by_key(|r| r.t_start); + repos } -} -impl ProgressBar { - pub fn err(&self, msg: impl AsRef) { - if let Some(bar) = &self.bar { - bar.abandon_with_message(format!( - "{} {} {}{}", - self.name_with_version(), - style(msg.as_ref()).blue(), - style("failed").red(), - self.format_elapsed() + fn format_repo_with_grammars(&self, repo: &RepoEntry, w: usize) -> Vec> { + let mut lines: Vec = Vec::new(); + let repo_grammars: Vec<&GrammarEntry> = self + .grammars + .values() + .filter(|g| g.repo_id == self.repo_id_by_name(&repo.name)) + .collect(); + + if repo_grammars.is_empty() { + lines.push(self.format_item_line( + repo.status, + &repo.name, + &repo.git_ref.to_string(), + &repo.msg, + repo.step, + repo.total, + repo.t_start, + &repo.frozen_elapsed, + w, + "", )); + } else if repo_grammars.len() == 1 { + let g = repo_grammars[0]; + if g.name == repo.name { + lines.push(self.format_item_line( + g.status, + &repo.name, + &repo.git_ref.to_string(), + &g.msg, + g.step, + g.total, + g.t_start, + &g.frozen_elapsed, + w, + "", + )); + } else { + let label = format!("{}/{}", repo.name, g.name); + lines.push(self.format_item_line( + g.status, + &label, + &g.git_ref.to_string(), + &g.msg, + g.step, + g.total, + g.t_start, + &g.frozen_elapsed, + w, + "", + )); + } } else { - let cur = self.current_step.load(Ordering::SeqCst); - self.println(format!( - "[{}/{}] {} {} {}{}", - cur, - self.num_tasks, - self.name_with_version(), - msg.as_ref(), - style("failed").red(), - self.format_elapsed() + lines.push(self.format_item_line( + repo.status, + &repo.name, + &repo.git_ref.to_string(), + &repo.msg, + repo.step, + repo.total, + repo.t_start, + &repo.frozen_elapsed, + w, + "", )); + for g in &repo_grammars { + lines.push(self.format_item_line( + g.status, + &g.name, + &g.git_ref.to_string(), + &g.msg, + g.step, + g.total, + g.t_start, + &g.frozen_elapsed, + w, + " ", + )); + } } + + lines } - pub fn fin(&self, msg: impl AsRef) { - if let Some(bar) = &self.bar { - bar.inc(1); - } else { - self.current_step.fetch_add(1, Ordering::SeqCst); - } + fn repo_id_by_name(&self, name: &str) -> u64 { + self.repos + .iter() + .find(|(_, r)| r.name.as_ref() == name) + .map(|(id, _)| *id) + .unwrap_or(0) + } - if let Some(bar) = &self.bar { - let position = usize::try_from(bar.position()) - .unwrap_or(self.num_tasks) - .min(self.num_tasks); - bar.set_prefix(format!("[{}/{}]", position, self.num_tasks)); - - let message = if msg.as_ref().is_empty() { - format!( - "{} {}{}", - self.name_with_version(), - style("done").green(), - self.format_elapsed() - ) - } else { - format!( - "{} {} {}{}", - self.name_with_version(), - msg.as_ref(), - style("done").green(), - self.format_elapsed() - ) - }; - bar.finish_with_message(message); - } else { - let cur = self.current_step.load(Ordering::SeqCst); - if msg.as_ref().is_empty() { - self.println(format!( - "[{}/{}] {} {}{}", - cur, - self.num_tasks, - self.name_with_version(), - style("done").green(), - self.format_elapsed() - )); - } else { - self.println(format!( - "[{}/{}] {} {} {}{}", - cur, - self.num_tasks, - self.name_with_version(), - style(msg.as_ref()).blue(), - style("done").green(), - self.format_elapsed() - )); - } - } + fn separator(&self, width: usize, ch: &str) -> Line<'static> { + let sep = ch.repeat(width.min(120)); + Line::from(Span::styled( + sep, + Style::default() + .fg(Color::DarkGray) + .add_modifier(Modifier::DIM), + )) } - pub fn is_done(&self) -> bool { - self.bar - .as_ref() - .is_some_and(indicatif::ProgressBar::is_finished) + fn dim_line(&self, text: String) -> Line<'static> { + Line::from(Span::styled( + text, + Style::default() + .fg(Color::DarkGray) + .add_modifier(Modifier::DIM), + )) } - pub fn msg(&self, msg: impl AsRef) { - if let Some(bar) = &self.bar { - let position = usize::try_from(bar.position()) - .unwrap_or(self.num_tasks) - .min(self.num_tasks); - bar.set_prefix(format!("[{}/{}]", position, self.num_tasks)); - bar.set_message(format!("{} {}", self.name_with_version(), msg.as_ref())); - } else { - let cur = self.current_step.load(Ordering::SeqCst); - self.println(format!( - "[{}/{}] {}: {}", - cur, - self.num_tasks, - self.name_with_version(), - msg.as_ref() - )); - } + fn format_legend(&self, _width: usize) -> Line<'static> { + let mut spans: Vec = Vec::new(); + spans.push(Span::styled( + " cached", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + )); + spans.push(Span::styled( + " done", + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + )); + spans.push(Span::styled( + " failed", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )); + Line::from(spans) } - pub fn step(&self, msg: impl AsRef) { - let _ = self.t_start.set(time::Instant::now()); - if let Some(bar) = &self.bar { - bar.inc(1); + fn format_item_line( + &self, + status: ItemStatus, + name: &str, + git_ref: &str, + msg: &str, + step: usize, + total: usize, + t_start: Instant, + frozen_elapsed: &Option, + width: usize, + indent: &str, + ) -> Line<'static> { + let icon = status.icon(); + let name_style = Style::default() + .fg(status.color()) + .add_modifier(Modifier::BOLD); + let msg_style = Style::default().fg(Color::White); + + let step_info = if total > 0 { + format!("[{}/{}] ", step.min(total), total) } else { - self.current_step.fetch_add(1, Ordering::SeqCst); - } + String::new() + }; - if let Some(bar) = &self.bar { - let position = usize::try_from(bar.position()) - .unwrap_or(self.num_tasks) - .min(self.num_tasks); - bar.set_prefix(format!("[{}/{}]", position, self.num_tasks)); - bar.set_message(format!("{}: {}", self.name_with_version(), msg.as_ref())); + // Elapsed: frozen for terminal states, live otherwise + let elapsed = if let Some(ref frozen) = frozen_elapsed { + format!("in {frozen}") + } else if status == ItemStatus::InProgress || status == ItemStatus::New { + format_elapsed(t_start) } else { - let cur = self.current_step.load(Ordering::SeqCst); - self.println(format!( - "[{}/{}] {} {}", - cur, - self.num_tasks, - self.name_with_version(), - msg.as_ref() + String::new() + }; + + let line_content = format!("{step_info}{}", truncate_str(msg, width.saturating_sub(40))); + + let prefix = format!( + "{indent} {} {:<28} {}", + icon, + truncate_str(name, 26), + truncate_str(git_ref, 12) + ); + + let mut spans: Vec = Vec::new(); + spans.push(Span::styled(prefix, name_style)); + spans.push(Span::styled(line_content, msg_style)); + if !elapsed.is_empty() { + let elapsed_color = match status { + ItemStatus::Done | ItemStatus::Cached => Color::Green, + ItemStatus::Failed => Color::Red, + _ => Color::DarkGray, + }; + spans.push(Span::styled( + format!(" {elapsed}"), + Style::default() + .fg(elapsed_color) + .add_modifier(if frozen_elapsed.is_some() { + Modifier::empty() + } else { + Modifier::DIM + }), )); } + + Line::from(spans) } - pub fn tick(&self) { - if let Some(bar) = &self.bar { - bar.tick(); - } + fn format_footer_build(&self, _width: usize) -> Line<'static> { + self.dim_line(format!(" build: {}", self.build_dir.display())) } -} -#[must_use] -pub fn current(progress: &ProgressStyle, verbose: &Verbosity) -> Progress { - let mut mode = match progress { - ProgressStyle::Auto => { - if atty::is(atty::Stream::Stdout) { - Mode::Fancy + fn format_footer_out(&self, _width: usize) -> Line<'static> { + self.dim_line(format!(" out: {}", self.out_dir.display())) + } + + fn format_footer_counts(&self, _width: usize) -> Line<'static> { + let in_progress = self + .repos + .values() + .filter(|r| r.status == ItemStatus::New || r.status == ItemStatus::InProgress) + .count() + + self + .grammars + .values() + .filter(|g| g.status == ItemStatus::New || g.status == ItemStatus::InProgress) + .count(); + + let total_elapsed = format_elapsed(self.total_start); + + let mut spans: Vec = Vec::new(); + spans.push(Span::styled( + format!(" ✓ {} cached ", self.cached_count), + Style::default().fg(if self.cached_count > 0 { + Color::Green } else { - Mode::Plain - } - } - ProgressStyle::Fancy => Mode::Fancy, - ProgressStyle::Plain => Mode::Plain, - }; + Color::DarkGray + }), + )); + spans.push(Span::styled( + format!("● {} done ", self.built_count), + Style::default().fg(if self.built_count > 0 { + Color::Blue + } else { + Color::DarkGray + }), + )); + spans.push(Span::styled( + format!("● {} building ", in_progress), + Style::default().fg(if in_progress > 0 { + Color::Yellow + } else { + Color::DarkGray + }), + )); + spans.push(Span::styled( + format!("✗ {} failed ", self.failed_count), + Style::default().fg(if self.failed_count > 0 { + Color::Red + } else { + Color::DarkGray + }), + )); + spans.push(Span::styled( + format!("Done in {total_elapsed}"), + Style::default() + .fg(Color::White) + .add_modifier(Modifier::DIM), + )); + + Line::from(spans) + } +} - if matches!(verbose.log_level(), Some(Level::Debug | Level::Trace)) { - mode = Mode::Plain; +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn truncate_str(s: &str, max: usize) -> &str { + if s.len() <= max { + s + } else if max <= 3 { + &s[..max] + } else { + &s[..max - 3] } +} - Progress::new(mode) +fn format_elapsed(start: Instant) -> String { + let dur = start.elapsed(); + let secs = dur.as_secs(); + if secs < 60 { + format!("{}.{:02}s", secs, dur.subsec_millis() / 10) + } else if secs < 3600 { + format!("{}m{}s", secs / 60, secs % 60) + } else { + format!("{}h{}m", secs / 3600, (secs % 3600) / 60) + } } diff --git a/src/main.rs b/src/main.rs index 31a17f2..173381b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,13 +38,12 @@ fn run(app: &mut App, args: &args::Args) -> TsdlResult<()> { } } -fn selfupdate(app: &mut App) -> TsdlResult<()> { +fn selfupdate(_app: &mut App) -> TsdlResult<()> { let tsdl = env!("CARGO_BIN_NAME"); let current_version = Version::parse(env!("CARGO_PKG_VERSION")) .map_err(|e| TsdlError::context("Failed to parse current version", e))?; - let handle = app.progress.register("selfupdate".into(), "".into(), 4); - handle.step("fetching releases"); + eprintln!("Fetching releases..."); let releases = self_update::backends::github::ReleaseList::configure() .repo_owner("stackmystack") .repo_name(tsdl) @@ -64,11 +63,11 @@ fn selfupdate(app: &mut App) -> TsdlResult<()> { let latest_version = Version::parse(&releases[0].version) .map_err(|e| TsdlError::context("Failed to parse latest version", e))?; if latest_version <= current_version { - handle.msg("already at the latest version"); + eprintln!("Already at the latest version ({current_version})"); return Ok(()); } - handle.step(format!("downloading {latest_version}")); + eprintln!("Downloading {latest_version}..."); let asset = asset.unwrap(); let tmp_dir = tempfile::tempdir() .map_err(|e| TsdlError::context("Failed to create temporary directory", e))?; @@ -86,7 +85,7 @@ fn selfupdate(app: &mut App) -> TsdlResult<()> { .download_to(&tmp_gz) .map_err(|e| TsdlError::context("Failed to download release asset", e))?; - handle.step(format!("extracting {latest_version}")); + eprintln!("Extracting {latest_version}..."); let tsdl_bin = PathBuf::from(tsdl); self_update::Extract::from_source(&tmp_gz_path) .archive(self_update::ArchiveKind::Plain(Some( @@ -99,7 +98,7 @@ fn selfupdate(app: &mut App) -> TsdlResult<()> { self_replace::self_replace(new_exe) .map_err(|e| TsdlError::context("Failed to replace current executable", e))?; - handle.fin(format!("{latest_version}")); + eprintln!("Updated to {latest_version}"); Ok(()) } diff --git a/src/parser.rs b/src/parser.rs index ff0e235..e1f9e2f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -273,9 +273,7 @@ impl GrammarBuild { // Report reinstallation when fixing broken hardlink if hardlink_broken { - if let Some(hnd) = self.context.progress.as_ref() { - hnd.msg("Reinstalled"); - } + self.progress.msg("Reinstalled"); } // Create the hardlink after removing the old one