diff --git a/Cargo.lock b/Cargo.lock index b47545f..ffe5a58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,25 @@ dependencies = [ "equator", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ansi-to-tui" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42366bb9d958f042bf58f0a85e1b2d091997c1257ca49bddd7e4827aadc65fd" +dependencies = [ + "nom", + "ratatui-core", + "simdutf8", + "smallvec", + "thiserror 2.0.18", +] + [[package]] name = "ansi_colours" version = "1.2.3" @@ -70,9 +89,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -89,7 +108,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -100,14 +119,23 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "approx" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] [[package]] name = "arbitrary" @@ -128,9 +156,9 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "as-slice" @@ -143,9 +171,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "av-scenechange" @@ -162,7 +190,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror", + "thiserror 2.0.18", "v_frame", "y4m", ] @@ -183,9 +211,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" dependencies = [ "arrayvec", ] @@ -228,39 +256,46 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bitstream-io" -version = "4.9.0" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" dependencies = [ - "core2", + "no_std_io2", ] [[package]] name = "borsh" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ + "bytes", "cfg_aliases", ] [[package]] name = "built" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" [[package]] name = "bumpalo" -version = "3.20.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" @@ -274,11 +309,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" -version = "1.2.56" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -334,9 +384,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "color_quant" @@ -346,9 +396,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -356,28 +406,41 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", ] [[package]] name = "console" -version = "0.16.2" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "once_cell", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] -name = "core2" -version = "0.4.0" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "memchr", + "unicode-segmentation", ] [[package]] @@ -391,9 +454,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -401,18 +464,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -422,9 +485,14 @@ checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags", "crossterm_winapi", + "derive_more", "document-features", + "filedescriptor", + "mio", "parking_lot", "rustix", + "signal-hook", + "signal-hook-mio", "winapi", ] @@ -443,6 +511,68 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "document-features" version = "0.2.12" @@ -454,9 +584,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encode_unicode" @@ -497,7 +627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -516,30 +646,22 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "2.3.0" +name = "fast-srgb8" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] -name = "fax" -version = "0.2.6" +name = "fastrand" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] -name = "fax_derive" -version = "0.2.0" +name = "fax" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -550,6 +672,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -568,9 +701,18 @@ dependencies = [ [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] [[package]] name = "futures-core" @@ -597,9 +739,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -622,28 +764,26 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasip2", - "wasip3", + "r-efi 6.0.0", ] [[package]] name = "gif" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" dependencies = [ "color_quant", "weezl", @@ -674,18 +814,25 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -694,10 +841,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "id-arena" -version = "2.3.0" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "image" @@ -735,20 +882,60 @@ dependencies = [ [[package]] name = "imgref" -version = "1.12.0" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", + "hashbrown 0.17.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inotify" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd854a95a4ac672fed8c054136039fd32c22cf039ff09ead7280afe920486483" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea94e891b3606826e9c998be69ddca42247dad8ad50b1649a5cb7e1c9ae06fd" +dependencies = [ + "libc", +] + +[[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", ] [[package]] @@ -794,25 +981,50 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "kasuari" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags", + "libc", +] [[package]] name = "lebe" @@ -822,25 +1034,40 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" dependencies = [ "arbitrary", "cc", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags", +] + [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" @@ -859,9 +1086,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "loop9" @@ -872,6 +1099,15 @@ dependencies = [ "imgref", ] +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "markdown" version = "1.0.0" @@ -893,9 +1129,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "miette" @@ -937,6 +1173,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -949,9 +1197,9 @@ dependencies = [ [[package]] name = "mq-markdown" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cb203323d6225628fc2dfbd1c208e914b85aabf96250e217fc4e883cef16e9" +checksum = "6d2cc321c5808b511b9b06ffb67b504ee6128a064cabfef16a2a0994ea8dce25" dependencies = [ "itertools 0.15.0", "markdown", @@ -964,12 +1212,16 @@ dependencies = [ name = "mq-view" version = "0.1.18" dependencies = [ + "ansi-to-tui", "clap", "colored", + "crossterm", "image", "itertools 0.15.0", "miette", "mq-markdown", + "notify", + "ratatui", "rstest", "terminal_size", "tree-sitter", @@ -1013,6 +1265,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "8.0.0" @@ -1028,16 +1289,49 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags", +] + [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -1078,6 +1372,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.37.3" @@ -1089,9 +1392,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1101,9 +1404,33 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "parking_lot" @@ -1142,9 +1469,9 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "png" @@ -1159,6 +1486,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1168,21 +1507,11 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -1198,18 +1527,18 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", "syn", @@ -1217,12 +1546,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "qoi" @@ -1241,9 +1567,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1254,11 +1580,17 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core", @@ -1283,6 +1615,72 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "ratatui" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags", + "compact_str", + "hashbrown 0.17.1", + "itertools 0.14.0", + "kasuari", + "lru", + "palette", + "serde", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.2", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width 0.2.2", +] + [[package]] name = "rav1e" version = "0.8.1" @@ -1313,7 +1711,7 @@ dependencies = [ "rand", "rand_chacha", "simd_helpers", - "thiserror", + "thiserror 2.0.18", "v_frame", "wasm-bindgen", ] @@ -1335,9 +1733,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1364,9 +1762,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -1387,9 +1785,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relative-path" @@ -1399,9 +1797,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rgb" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" dependencies = [ "bytemuck", ] @@ -1443,9 +1841,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -1458,22 +1856,37 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "scopeguard" @@ -1483,9 +1896,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1494,6 +1907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -1518,9 +1932,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap", "itoa", @@ -1532,15 +1946,46 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_helpers" @@ -1551,6 +1996,12 @@ dependencies = [ "quote", ] +[[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" @@ -1559,9 +2010,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smol_str" @@ -1579,6 +2030,12 @@ 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 = "streaming-iterator" version = "0.1.9" @@ -1591,6 +2048,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "supports-color" version = "3.0.2" @@ -1614,9 +2092,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -1625,15 +2103,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1652,7 +2130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1665,13 +2143,33 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1699,20 +2197,41 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap", "toml_datetime", @@ -1722,9 +2241,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] @@ -1852,7 +2371,7 @@ checksum = "adc5f880ad8d8f94e88cb81c3557024cf1a8b75e3b504c50481ed4f5a6006ff3" dependencies = [ "regex", "streaming-iterator", - "thiserror", + "thiserror 2.0.18", "tree-sitter", ] @@ -2070,6 +2589,23 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[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.2", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -2082,12 +2618,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "utf8parse" version = "0.2.2" @@ -2121,28 +2651,35 @@ dependencies = [ ] [[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "wit-bindgen", + "same-file", + "winapi-util", ] [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -2153,9 +2690,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2163,9 +2700,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -2176,47 +2713,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "weezl" version = "0.1.12" @@ -2245,7 +2748,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2260,6 +2763,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2270,102 +2782,85 @@ dependencies = [ ] [[package]] -name = "winnow" -version = "0.7.14" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "memchr", + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] -name = "wit-bindgen-core" -version = "0.51.0" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] -name = "wit-bindgen-rust" -version = "0.51.0" +name = "windows_i686_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] -name = "wit-component" -version = "0.244.0" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] -name = "wit-parser" -version = "0.244.0" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", + "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "y4m" version = "0.8.0" @@ -2374,18 +2869,18 @@ checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", @@ -2415,9 +2910,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.12" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index c10ed7a..d04ddb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,12 +84,16 @@ lang-typescript = ["dep:tree-sitter-typescript"] lang-yaml = ["dep:tree-sitter-yaml"] [dependencies] +ansi-to-tui = "8.0.1" clap = {version = "4.6.1", features = ["derive"]} colored = "3.1.1" +crossterm = {version = "0.29.0", features = ["use-dev-tty"]} image = "0.25" itertools = "0.15.0" miette = {version = "7.6.0", features = ["fancy"]} mq-markdown = "0.6.3" +notify = {version = "8.2.0", default-features = false, features = ["macos_fsevent"]} +ratatui = {version = "0.30.2", default-features = false, features = ["crossterm", "std"]} terminal_size = "0.4.4" tree-sitter = "0.25.10" tree-sitter-bash = {version = "0.25", optional = true} diff --git a/README.md b/README.md index 1a9fd25..aa10eb7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Built with [mq](https://github.com/harehare/mq) - jq-like command-line tool for - 🧜 **Mermaid Diagrams**: Best-effort ASCII-art rendering of simple `graph`/`flowchart` blocks - 🔔 **GitHub-style Callouts**: NOTE, TIP, IMPORTANT, WARNING, CAUTION, rendered as wrapped, bordered boxes - 🔗 **Clickable Links**: Terminal hyperlinks using OSC 8 +- 📖 **Pager Mode**: Interactive full-screen viewer with scrolling, a heading outline, search, and auto-reload on file changes ## Installation @@ -77,6 +78,35 @@ Pipe markdown content: echo "# Hello\n\n\`\`\`rust\nfn main() {}\n\`\`\`" | mq-view ``` +### Pager Mode + +Open an interactive, full-screen viewer with `--pager` (`-p`): + +```bash +mq-view --pager README.md +``` + +It also works with piped content, but without a file to watch there's +nothing to auto-reload: + +```bash +cat report.md | mq-view --pager +``` + +| Key | Action | +| --- | --- | +| `j` / `k`, `↓` / `↑` | Scroll down / up | +| `Space` / `PageDown`, `PageUp` | Scroll a page down / up | +| `Ctrl-d` / `Ctrl-u` | Scroll half a page down / up | +| `g` / `G` | Jump to top / bottom | +| `Tab` | Toggle the heading outline; `j`/`k` to move, `Enter` to jump | +| `/` | Search; `Enter` to confirm, `Esc` to cancel | +| `n` / `N` | Jump to the next / previous search match | +| `q` / `Esc` | Quit | + +When viewing a file (not piped input), the pager watches it and +automatically re-renders whenever it changes on disk. + ### Mermaid Diagrams Fenced code blocks tagged ` ```mermaid ` are rendered as ASCII art instead of diff --git a/src/lib.rs b/src/lib.rs index 0d77b5b..c233ea9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,9 +16,11 @@ mod highlighter; mod mermaid; +mod pager; mod renderer; pub use highlighter::SyntaxHighlighter; +pub use pager::run_pager; pub use renderer::{ RenderConfig, render_markdown, render_markdown_to_string, render_markdown_with_config, }; diff --git a/src/main.rs b/src/main.rs index 52bcebb..c5620aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use clap::Parser; use miette::{IntoDiagnostic, Result}; use mq_markdown::Markdown; -use mq_view::{RenderConfig, render_markdown_with_config}; +use mq_view::{RenderConfig, render_markdown_with_config, run_pager}; use std::fs; use std::io::{self, BufWriter, Write}; use std::io::{IsTerminal, Read}; @@ -17,6 +17,10 @@ pub struct Args { #[arg(short = 'H', long = "no-header-highlight")] no_header_highlight: bool, + /// Open an interactive pager (scroll, heading outline, search, auto-reload on file changes) + #[arg(short = 'p', long = "pager")] + pager: bool, + /// Markdown file to view file: Option, } @@ -24,8 +28,8 @@ pub struct Args { fn main() -> Result<()> { let args = Args::parse(); let content = if io::stdin().is_terminal() { - if let Some(file) = args.file { - fs::read_to_string(&file).into_diagnostic()? + if let Some(file) = &args.file { + fs::read_to_string(file).into_diagnostic()? } else { return Err(miette::miette!("No input file specified")); } @@ -34,12 +38,17 @@ fn main() -> Result<()> { io::stdin().read_to_string(&mut buffer).into_diagnostic()?; buffer }; - let markdown: Markdown = content.parse().map_err(|e| miette::miette!("{}", e))?; let config = RenderConfig { header_full_width_highlight: !args.no_header_highlight, + ..RenderConfig::default() }; + if args.pager { + return run_pager(&content, args.file, &config).into_diagnostic(); + } + + let markdown: Markdown = content.parse().map_err(|e| miette::miette!("{}", e))?; let stdout = io::stdout(); let mut writer = BufWriter::new(stdout.lock()); render_markdown_with_config(&markdown, &mut writer, &config).into_diagnostic()?; diff --git a/src/pager.rs b/src/pager.rs new file mode 100644 index 0000000..3113865 --- /dev/null +++ b/src/pager.rs @@ -0,0 +1,675 @@ +//! Interactive TUI viewer ("pager mode") for richer markdown browsing: +//! scrolling, a heading outline, incremental search, and auto-reload when +//! the source file changes on disk. + +use crate::renderer::{HeadingEntry, RenderConfig, render_markdown_with_outline}; +use ansi_to_tui::IntoText; +use mq_markdown::Markdown; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use ratatui::Frame; +use ratatui::Terminal; +use ratatui::backend::CrosstermBackend; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use ratatui::crossterm::execute; +use ratatui::crossterm::terminal::{ + self as crossterm_terminal, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, + enable_raw_mode, +}; +use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span, Text}; +use ratatui::widgets::{ + Block, BorderType, Clear, List, ListItem, ListState, Paragraph, Scrollbar, + ScrollbarOrientation, ScrollbarState, +}; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{Receiver, channel}; +use std::time::Duration; + +struct Document { + text: Text<'static>, + plain_lines: Vec, + headings: Vec, +} + +impl Document { + fn load(content: &str, config: &RenderConfig) -> io::Result { + let markdown: Markdown = content + .parse() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{e}")))?; + // Direct-to-terminal image drawing (viuer) bypasses our writer and + // would corrupt the alternate-screen buffer, so it's always off here. + let config = &RenderConfig { + inline_images: false, + ..config.clone() + }; + let (rendered, headings) = render_markdown_with_outline(&markdown, config)?; + let text = rendered + .into_text() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + let plain_lines = text.lines.iter().map(line_plain_text).collect(); + Ok(Self { + text, + plain_lines, + headings, + }) + } + + fn line_count(&self) -> usize { + self.text.lines.len() + } +} + +fn line_plain_text(line: &Line<'_>) -> String { + line.spans.iter().map(|s| s.content.as_ref()).collect() +} + +struct SearchMatch { + line: usize, + start: usize, + end: usize, +} + +fn find_matches(plain_lines: &[String], query: &str) -> Vec { + if query.is_empty() { + return Vec::new(); + } + let needle = query.to_lowercase(); + let mut matches = Vec::new(); + for (line_idx, line) in plain_lines.iter().enumerate() { + let haystack = line.to_lowercase(); + let mut cursor = 0; + while let Some(pos) = haystack[cursor..].find(&needle) { + let abs = cursor + pos; + matches.push(SearchMatch { + line: line_idx, + start: abs, + end: abs + needle.len(), + }); + cursor = abs + needle.len().max(1); + if cursor >= haystack.len() { + break; + } + } + } + matches +} + +/// Overlay search-match highlighting on top of a line's existing syntax +/// styling. `ranges` are `(start_byte, end_byte, is_current_match)`. +fn render_line_with_highlights( + line: &Line<'static>, + ranges: &[(usize, usize, bool)], +) -> Line<'static> { + if ranges.is_empty() { + return line.clone(); + } + + let mut spans = Vec::new(); + let mut pos = 0usize; + for span in &line.spans { + let content = span.content.as_ref(); + let span_start = pos; + let span_end = pos + content.len(); + + let mut cuts = vec![span_start, span_end]; + for &(s, e, _) in ranges { + if s > span_start && s < span_end { + cuts.push(s); + } + if e > span_start && e < span_end { + cuts.push(e); + } + } + cuts.sort_unstable(); + cuts.dedup(); + + for w in cuts.windows(2) { + let (a, b) = (w[0], w[1]); + if a == b { + continue; + } + let sub = &content[a - span_start..b - span_start]; + let mut style = span.style; + if let Some(&(_, _, current)) = ranges.iter().find(|&&(s, e, _)| a >= s && b <= e) { + style = style + .bg(if current { + Color::Rgb(255, 140, 0) + } else { + Color::Yellow + }) + .fg(Color::Black); + } + spans.push(Span::styled(sub.to_string(), style)); + } + pos = span_end; + } + Line::from(spans) +} + +enum Mode { + Normal, + Search, +} + +#[derive(Clone, Copy)] +enum StatusKind { + Info, + Success, + Warn, +} + +struct Status { + text: String, + kind: StatusKind, +} + +impl Status { + fn info(text: impl Into) -> Self { + Self { + text: text.into(), + kind: StatusKind::Info, + } + } + + fn success(text: impl Into) -> Self { + Self { + text: text.into(), + kind: StatusKind::Success, + } + } + + fn warn(text: impl Into) -> Self { + Self { + text: text.into(), + kind: StatusKind::Warn, + } + } +} + +struct App { + doc: Document, + scroll: usize, + show_outline: bool, + outline_state: ListState, + mode: Mode, + search_input: String, + matches: Vec, + current_match: Option, + status: Option, + path: Option, + title: String, +} + +fn max_scroll(doc: &Document, content_height: usize) -> usize { + doc.line_count().saturating_sub(content_height.max(1)) +} + +fn clamp_scroll(line: usize, total_lines: usize, content_height: usize) -> usize { + line.min(total_lines.saturating_sub(content_height.max(1))) +} + +/// Index of the heading the current scroll position is "inside" (the last +/// heading at or above the top visible line), falling back to the first +/// heading if we're above it. +fn nearest_heading_index(app: &App) -> Option { + app.doc + .headings + .iter() + .rposition(|h| h.line <= app.scroll) + .or(if app.doc.headings.is_empty() { + None + } else { + Some(0) + }) +} + +fn current_section_title(app: &App) -> Option<&str> { + nearest_heading_index(app).map(|i| app.doc.headings[i].title.as_str()) +} + +fn scroll_by(app: &mut App, delta: isize, content_height: usize) { + let max = max_scroll(&app.doc, content_height); + app.scroll = (app.scroll as isize + delta).clamp(0, max as isize) as usize; +} + +fn outline_move(app: &mut App, delta: isize) { + let len = app.doc.headings.len(); + if len == 0 { + return; + } + let current = app.outline_state.selected().unwrap_or(0) as isize; + let next = (current + delta).clamp(0, len as isize - 1); + app.outline_state.select(Some(next as usize)); +} + +fn jump_to_match(app: &mut App, content_height: usize) { + if let Some(m) = app.current_match.and_then(|i| app.matches.get(i)) { + app.scroll = clamp_scroll(m.line, app.doc.line_count(), content_height); + } +} + +fn cycle_match(app: &mut App, delta: isize, content_height: usize) { + if app.matches.is_empty() { + app.status = Some(Status::warn("No search pattern")); + return; + } + let len = app.matches.len() as isize; + let current = app.current_match.map_or(-1, |i| i as isize); + let next = (current + delta).rem_euclid(len); + app.current_match = Some(next as usize); + jump_to_match(app, content_height); + app.status = Some(Status::info(format!("match {}/{}", next + 1, len))); +} + +/// Returns `true` if the app should quit. +fn handle_key(app: &mut App, key: KeyEvent, content_height: usize) -> bool { + if let Mode::Search = app.mode { + match key.code { + KeyCode::Esc => { + app.mode = Mode::Normal; + app.search_input.clear(); + } + KeyCode::Enter => { + app.mode = Mode::Normal; + app.matches = find_matches(&app.doc.plain_lines, &app.search_input); + if app.matches.is_empty() { + app.current_match = None; + app.status = Some(Status::warn(format!( + "Pattern not found: {}", + app.search_input + ))); + } else { + let idx = app + .matches + .iter() + .position(|m| m.line >= app.scroll) + .unwrap_or(0); + app.current_match = Some(idx); + jump_to_match(app, content_height); + app.status = Some(Status::info(format!( + "match {}/{}", + idx + 1, + app.matches.len() + ))); + } + } + KeyCode::Backspace => { + app.search_input.pop(); + } + KeyCode::Char(c) => app.search_input.push(c), + _ => {} + } + return false; + } + + if app.show_outline { + match key.code { + KeyCode::Esc | KeyCode::Tab => app.show_outline = false, + KeyCode::Up | KeyCode::Char('k') => outline_move(app, -1), + KeyCode::Down | KeyCode::Char('j') => outline_move(app, 1), + KeyCode::Enter => { + if let Some(h) = app + .outline_state + .selected() + .and_then(|i| app.doc.headings.get(i)) + { + app.scroll = clamp_scroll(h.line, app.doc.line_count(), content_height); + } + app.show_outline = false; + } + KeyCode::Char('q') => return true, + _ => {} + } + return false; + } + + app.status = None; + match key.code { + KeyCode::Char('q') | KeyCode::Esc => return true, + KeyCode::Char('j') | KeyCode::Down => scroll_by(app, 1, content_height), + KeyCode::Char('k') | KeyCode::Up => scroll_by(app, -1, content_height), + KeyCode::Char(' ') | KeyCode::PageDown => { + scroll_by(app, content_height as isize, content_height) + } + KeyCode::PageUp => scroll_by(app, -(content_height as isize), content_height), + KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => { + scroll_by(app, (content_height / 2) as isize, content_height) + } + KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { + scroll_by(app, -((content_height as isize) / 2), content_height) + } + KeyCode::Char('g') => app.scroll = 0, + KeyCode::Char('G') => app.scroll = max_scroll(&app.doc, content_height), + KeyCode::Tab => { + app.show_outline = true; + app.outline_state.select(nearest_heading_index(app)); + } + KeyCode::Char('/') => { + app.mode = Mode::Search; + app.search_input.clear(); + } + KeyCode::Char('n') => cycle_match(app, 1, content_height), + KeyCode::Char('N') => cycle_match(app, -1, content_height), + _ => {} + } + false +} + +fn draw(frame: &mut Frame, app: &mut App) { + let area = frame.area(); + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Min(1), + Constraint::Length(1), + ]) + .split(area); + let title_area = rows[0]; + let body_area = rows[1]; + let footer_area = rows[2]; + + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(1), Constraint::Length(1)]) + .split(body_area); + let content_area = cols[0]; + let scrollbar_area = cols[1]; + + let height = content_area.height as usize; + let total = app.doc.line_count(); + let start = app.scroll.min(total); + let end = (start + height).min(total); + + let visible_lines: Vec = app.doc.text.lines[start..end] + .iter() + .enumerate() + .map(|(offset, line)| { + let line_idx = start + offset; + let ranges: Vec<(usize, usize, bool)> = app + .matches + .iter() + .enumerate() + .filter(|(_, m)| m.line == line_idx) + .map(|(i, m)| (m.start, m.end, Some(i) == app.current_match)) + .collect(); + render_line_with_highlights(line, &ranges) + }) + .collect(); + + frame.render_widget(Paragraph::new(Text::from(visible_lines)), content_area); + draw_title(frame, title_area, app); + draw_scrollbar(frame, scrollbar_area, total, start); + draw_footer(frame, footer_area, app, height); + + if app.show_outline { + draw_outline(frame, area, app); + } +} + +fn draw_title(frame: &mut Frame, area: Rect, app: &App) { + let style = Style::default() + .fg(Color::Black) + .bg(Color::Blue) + .add_modifier(Modifier::BOLD); + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(1), Constraint::Percentage(40)]) + .split(area); + frame.render_widget( + Paragraph::new(format!(" mq-view — {}", app.title)).style(style), + cols[0], + ); + let section = current_section_title(app).unwrap_or_default(); + frame.render_widget( + Paragraph::new(format!("{section} ")) + .style(style) + .alignment(Alignment::Right), + cols[1], + ); +} + +fn draw_scrollbar(frame: &mut Frame, area: Rect, total: usize, position: usize) { + let mut state = ScrollbarState::new(total).position(position); + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .end_symbol(None) + .track_style(Style::default().fg(Color::DarkGray)) + .thumb_style(Style::default().fg(Color::Cyan)); + frame.render_stateful_widget(scrollbar, area, &mut state); +} + +fn draw_footer(frame: &mut Frame, area: Rect, app: &App, content_height: usize) { + match &app.mode { + Mode::Search => { + let style = Style::default() + .fg(Color::Black) + .bg(Color::Cyan) + .add_modifier(Modifier::BOLD); + frame.render_widget( + Paragraph::new(format!(" /{}", app.search_input)).style(style), + area, + ); + } + Mode::Normal => { + if let Some(status) = &app.status { + let bg = match status.kind { + StatusKind::Info => Color::Cyan, + StatusKind::Success => Color::Green, + StatusKind::Warn => Color::Yellow, + }; + let style = Style::default() + .fg(Color::Black) + .bg(bg) + .add_modifier(Modifier::BOLD); + frame.render_widget( + Paragraph::new(format!(" {}", status.text)).style(style), + area, + ); + } else { + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(1), Constraint::Length(6)]) + .split(area); + frame.render_widget( + Paragraph::new( + " q:quit j/k:scroll Tab:outline /:search n/N:next/prev match", + ) + .style(Style::default().fg(Color::DarkGray)), + cols[0], + ); + let max = max_scroll(&app.doc, content_height); + let pct = (app.scroll * 100).checked_div(max).unwrap_or(100).min(100); + frame.render_widget( + Paragraph::new(format!("{pct}% ")) + .style(Style::default().fg(Color::Gray)) + .alignment(Alignment::Right), + cols[1], + ); + } + } + } +} + +fn draw_outline(frame: &mut Frame, area: Rect, app: &mut App) { + let popup = centered_rect(60, 70, area); + let items: Vec = app + .doc + .headings + .iter() + .map(|h| { + let indent = " ".repeat(h.depth.saturating_sub(1) as usize); + ListItem::new(format!("{indent}{}", h.title)) + }) + .collect(); + let title = format!(" Outline ({}) ", app.doc.headings.len()); + let list = List::new(items) + .block( + Block::bordered() + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::Blue)) + .title(title) + .title_style( + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ), + ) + .highlight_style( + Style::default() + .fg(Color::Black) + .bg(Color::Blue) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol("▸ "); + frame.render_widget(Clear, popup); + frame.render_stateful_widget(list, popup, &mut app.outline_state); +} + +fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect { + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(area); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(vertical[1])[1] +} + +fn spawn_watcher(path: &Path) -> notify::Result<(RecommendedWatcher, Receiver<()>)> { + let (tx, rx) = channel(); + let file_name = path.file_name().map(|n| n.to_os_string()); + let mut watcher = notify::recommended_watcher(move |res: notify::Result| { + if let Ok(event) = res { + let matches = file_name + .as_deref() + .is_none_or(|name| event.paths.iter().any(|p| p.file_name() == Some(name))); + if matches { + let _ = tx.send(()); + } + } + })?; + let watch_dir = path + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + watcher.watch(watch_dir, RecursiveMode::NonRecursive)?; + Ok((watcher, rx)) +} + +fn event_loop( + terminal: &mut Terminal>, + app: &mut App, + config: &RenderConfig, + watch_rx: Option<&Receiver<()>>, +) -> io::Result<()> { + loop { + let (_, term_height) = crossterm_terminal::size()?; + // Title bar (1 row) + footer (1 row) surround the scrollable body. + let content_height = term_height.saturating_sub(2).max(1) as usize; + + if let Some(rx) = watch_rx + && rx.try_iter().last().is_some() + { + std::thread::sleep(Duration::from_millis(80)); + while rx.try_recv().is_ok() {} + reload(app, config, content_height); + } + + terminal.draw(|frame| draw(frame, app))?; + + if event::poll(Duration::from_millis(200))? + && let Event::Key(key) = event::read()? + && key.kind == KeyEventKind::Press + && handle_key(app, key, content_height) + { + return Ok(()); + } + } +} + +fn reload(app: &mut App, config: &RenderConfig, content_height: usize) { + let Some(path) = app.path.clone() else { + return; + }; + match std::fs::read_to_string(&path).map(|content| Document::load(&content, config)) { + Ok(Ok(doc)) => { + app.doc = doc; + app.matches = find_matches(&app.doc.plain_lines, &app.search_input); + app.current_match = None; + app.scroll = app.scroll.min(max_scroll(&app.doc, content_height)); + app.status = Some(Status::success("Reloaded")); + } + Ok(Err(e)) | Err(e) => { + app.status = Some(Status::warn(format!("Reload failed: {e}"))); + } + } +} + +/// Run the interactive pager over `content`. When `path` is given, the file +/// is watched and the view auto-reloads on changes; without a path (e.g. +/// piped stdin) the document is static. +pub fn run_pager(content: &str, path: Option, config: &RenderConfig) -> io::Result<()> { + let doc = Document::load(content, config)?; + + // Make sure a panic mid-render doesn't leave the user's terminal stuck + // in raw mode / the alternate screen. + let original_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + let _ = disable_raw_mode(); + let _ = execute!(io::stdout(), LeaveAlternateScreen); + original_hook(info); + })); + + let watcher = path.as_deref().and_then(|p| spawn_watcher(p).ok()); + let title = path + .as_deref() + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "stdin".to_string()); + + enable_raw_mode()?; + execute!(io::stdout(), EnterAlternateScreen)?; + let backend = CrosstermBackend::new(io::stdout()); + let mut terminal = Terminal::new(backend)?; + + let mut app = App { + doc, + scroll: 0, + show_outline: false, + outline_state: ListState::default(), + mode: Mode::Normal, + search_input: String::new(), + matches: Vec::new(), + current_match: None, + status: None, + path, + title, + }; + + let result = event_loop( + &mut terminal, + &mut app, + config, + watcher.as_ref().map(|(_, rx)| rx), + ); + + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + // Drop back to the default hook now that the terminal is restored. + let _ = std::panic::take_hook(); + + result +} diff --git a/src/renderer.rs b/src/renderer.rs index 70a08f4..22866d9 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -12,12 +12,18 @@ use unicode_width::UnicodeWidthStr; pub struct RenderConfig { /// Enable full-width background highlighting for headers pub header_full_width_highlight: bool, + /// Draw local images directly to the terminal via `viuer`. This writes + /// straight to the real stdout, bypassing the writer passed to the + /// render functions, so it must be disabled by callers (like the pager) + /// that manage their own alternate-screen terminal buffer. + pub inline_images: bool, } impl Default for RenderConfig { fn default() -> Self { Self { header_full_width_highlight: true, + inline_images: true, } } } @@ -171,6 +177,68 @@ pub fn render_markdown_to_string(markdown: &Markdown) -> io::Result { String::from_utf8(output).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } +/// A heading found while rendering, with the rendered (post-wrap) line +/// number it starts on so a viewer can jump straight to it. +#[derive(Debug, Clone)] +pub(crate) struct HeadingEntry { + pub line: usize, + pub depth: u8, + pub title: String, +} + +/// Same as [`render_markdown_with_config`], but also returns the outline of +/// headings alongside the rendered line each one lands on. Used by the +/// pager to build a jump-to-heading sidebar. +pub(crate) fn render_markdown_with_outline( + markdown: &Markdown, + config: &RenderConfig, +) -> io::Result<(String, Vec)> { + let mut highlighter = SyntaxHighlighter::new(); + let mut output: Vec = Vec::new(); + let mut headings = Vec::new(); + let mut i = 0; + let len = markdown.nodes.len(); + + while i < len { + let node = &markdown.nodes[i]; + if let Node::Heading(heading) = node { + // Headings always start with a blank separator line (see + // render_node_inline), so the title itself lands one line later. + let line = bytecount_newlines(&output) + 1; + headings.push(HeadingEntry { + line, + depth: heading.depth, + title: render_inline_content(&heading.values), + }); + } + + if matches!(node, Node::TableCell(_)) { + let table_nodes: Vec<&Node> = markdown.nodes[i..] + .iter() + .take_while(|n| { + matches!( + n, + Node::TableCell(_) | Node::TableAlign(_) | Node::TableRow(_) + ) + }) + .collect(); + render_table(&table_nodes, &mut highlighter, &mut output)?; + i += table_nodes.len(); + } else { + render_node(node, 0, &mut highlighter, config, &mut output)?; + i += 1; + } + } + + let rendered = + String::from_utf8(output).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + Ok((rendered, headings)) +} + +fn bytecount_newlines(buf: &[u8]) -> usize { + buf.iter().filter(|&&b| b == b'\n').count() +} + /// Visible column width of a string, ignoring ANSI escape sequences /// (SGR color codes and OSC 8 hyperlinks) so that box borders and wrapping /// stay aligned even when the content contains colored or clickable text. @@ -533,7 +601,9 @@ fn render_node_inline( let alt = image.alt.as_str(); let url = image.url.as_str(); - let _ = render_image_to_terminal(url); + if config.inline_images { + let _ = render_image_to_terminal(url); + } // Always show the text description as well if alt.trim().is_empty() {