diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a12f1c0 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ + +[build] +rustflags = [ "-C", "link-arg=-fuse-ld=lld", ] diff --git a/.gitignore b/.gitignore index e873887..8e9b114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/target -.qtcreator -.idea +**/target/** +**/.qtcreator/** +**/.idea/** +**/*.autosave/** \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 10a9bbf..063f626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,65 +3,315 @@ version = 4 [[package]] -name = "anstyle" -version = "1.0.10" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[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.16" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", ] +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "clang-format" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "696283b40e1a39d208ee614b92e5f6521d16962edeb47c48372585ec92419943" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "clap" -version = "4.5.32" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ + "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] -name = "clap_lex" -version = "0.7.4" +name = "clap_derive" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "clide" version = "0.1.0" dependencies = [ + "anyhow", + "clap", "cxx", "cxx-qt", "cxx-qt-build", "cxx-qt-lib", + "dirs", + "edtui", + "log", + "ratatui", + "strum", + "syntect", + "tui-logger", + "tui-tree-widget", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", ] [[package]] @@ -71,7 +321,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", - "unicode-width", + "unicode-width 0.1.14", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width 0.2.2", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[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]] @@ -84,12 +365,99 @@ dependencies = [ ] [[package]] -name = "cxx" -version = "1.0.148" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "342b09ea23e087717542308a865984555782302855f29427540bbe02d5e8a28a" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.10.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "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 = "cxx" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" dependencies = [ "cc", + "cxx-build", "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", @@ -98,39 +466,55 @@ dependencies = [ ] [[package]] -name = "cxx-gen" -version = "0.7.148" +name = "cxx-build" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3438c8fed495501035e5db627f6d7defe4635f3e29824dafce97018dd71a1d" +checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" dependencies = [ - "codespan-reporting", + "cc", + "codespan-reporting 0.13.1", + "indexmap", "proc-macro2", "quote", - "syn", + "scratch", + "syn 2.0.114", +] + +[[package]] +name = "cxx-gen" +version = "0.7.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035b6c61a944483e8a4b2ad4fb8b13830d63491bd004943716ad16d85dcc64bc" +dependencies = [ + "codespan-reporting 0.13.1", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] name = "cxx-qt" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208ad6c4feac92f221fde00796f317b049ba1892b97be0d60ca177d0d3469fc5" +checksum = "ec7c6dea4b551221e1df4349af7ae6af2c906c16860bdab5cada5a957b43cbbc" dependencies = [ "cxx", "cxx-qt-build", "cxx-qt-macro", "qt-build-utils", "static_assertions", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "cxx-qt-build" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f80e109aa68795486c70c302f6c2d921f00028b3b62038a4601efb5c585c1c" +checksum = "c70cbc19fb351a0413632b326ad4862baea7f50d641d2d360b29ad0f772547bc" dependencies = [ "cc", - "codespan-reporting", + "codespan-reporting 0.11.1", "cxx-gen", "cxx-qt-gen", "proc-macro2", @@ -138,28 +522,28 @@ dependencies = [ "quote", "serde", "serde_json", - "version_check", ] [[package]] name = "cxx-qt-gen" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc17d95ca9cc60c2f91f804a4e0ba6a3e1b8ed338c207a1bd8d176133e2fd05d" +checksum = "33744d84f696836347071ad73da233f758f98e5c0b348e2855140173b36bffa2" dependencies = [ "clang-format", - "convert_case", + "convert_case 0.6.0", + "cxx-gen", "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] name = "cxx-qt-lib" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f116c5d982bbf3be707acf97f566802c30454d52ca319c745ed39a04834e8bc6" +checksum = "7eacfc219a287c422619b7704166bfd3b9b842367bd9124ad5557e6150a2d658" dependencies = [ "cxx", "cxx-qt", @@ -169,46 +553,192 @@ dependencies = [ [[package]] name = "cxx-qt-macro" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58a4fe02c0604eda28c605792f5ba0d0251b4947f8f0fc43e55b61c06b2b8ec6" +checksum = "3cb8ce32a983d56470101ff8e61c8a700ba37805b6e942186c6f3dd5d6ad44f6" dependencies = [ "cxx-qt-gen", "proc-macro2", - "syn", + "quote", + "syn 2.0.114", ] [[package]] name = "cxxbridge-cmd" -version = "1.0.148" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40399fddbf3977647bfff7453dacffc6b5701b19a282a283369a870115d0a049" +checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" dependencies = [ "clap", - "codespan-reporting", + "codespan-reporting 0.13.1", + "indexmap", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] name = "cxxbridge-flags" -version = "1.0.148" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9161673896b799047e79a245927e7921787ad016eed6770227f3f23de2746c7" +checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" [[package]] name = "cxxbridge-macro" -version = "1.0.148" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff513230582d396298cc00e8fb3d9a752822f85137c323fac4227ac5be6c268" +checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" dependencies = [ + "indexmap", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 2.0.114", ] +[[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.114", +] + +[[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.114", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "edtui" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49905ece098e793ca21a019598e9efc9a66459ad1d76bd7619e771a42dae2fc" +dependencies = [ + "arboard", + "crossterm", + "edtui-jagged", + "enum_dispatch", + "once_cell", + "ratatui-core", + "ratatui-widgets", + "syntect", + "unicode-width 0.2.2", +] + +[[package]] +name = "edtui-jagged" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6818b2d6b8b3da52f7491b6331e27d45ae34e5baaffeb1edfde43911fe63dd6" + [[package]] name = "either" version = "1.15.0" @@ -216,61 +746,471 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "foldhash" -version = "0.1.5" +name = "enum_dispatch" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] [[package]] -name = "libc" -version = "0.2.171" +name = "js-sys" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.10.0", +] [[package]] name = "link-cplusplus" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" dependencies = [ "cc", ] [[package]] -name = "memchr" -version = "2.7.4" +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown", +] + +[[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.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[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" @@ -278,6 +1218,51 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.3" @@ -289,76 +1274,653 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.94" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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.114", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +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 = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.10.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] -name = "qt-build-utils" -version = "0.7.1" +name = "pxfm" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb239fdd8c036fabb95364320041ef68197cd4ab971bb3b4ca3ea0b7b93d12c" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ + "num-traits", +] + +[[package]] +name = "qt-build-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cfd41d4f115dfc940ea3ea31b3aed77233ad09ab8859a227ed61323025590af" +dependencies = [ + "anyhow", "cc", - "thiserror", - "versions", + "semver", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] [[package]] -name = "rustversion" -version = "1.0.20" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "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.10.0", + "compact_str", + "hashbrown", + "indoc", + "itertools", + "kasuari", + "lru", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.2", +] + +[[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.10.0", + "hashbrown", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width 0.2.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -367,6 +1929,55 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "static_assertions" version = "1.1.0" @@ -380,16 +1991,69 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "syn" -version = "2.0.100" +name = "strum" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +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.114", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.18", + "walkdir", + "yaml-rust", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -399,13 +2063,85 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.10.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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "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 2.0.18", ] [[package]] @@ -416,14 +2152,110 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] -name = "unicode-ident" -version = "1.0.18" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tui-logger" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9384df20a5244a6ab204bc4b6959b41f37f0ee7b5e0f2feb7a8a78f58e684d06" +dependencies = [ + "chrono", + "env_filter", + "lazy_static", + "log", + "parking_lot", + "ratatui", + "unicode-segmentation", +] + +[[package]] +name = "tui-tree-widget" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deca119555009eee2e0cfb9c020f39f632444dc4579918d5fc009d51d75dff92" +dependencies = [ + "ratatui-core", + "ratatui-widgets", + "unicode-width 0.2.2", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -431,12 +2263,47 @@ 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 0.2.2", +] + [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "atomic", + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -444,39 +2311,277 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "versions" -version = "6.3.2" +name = "vtparse" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25d498b63d1fdb376b4250f39ab3a5ee8d103957346abacd911e2d8b612c139" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" dependencies = [ - "itertools", - "nom", + "utf8parse", ] [[package]] -name = "winapi-util" -version = "0.1.9" +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "windows-sys", + "same-file", + "winapi-util", +] + +[[package]] +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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", @@ -489,48 +2594,121 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index a10ab35..c9b44f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,19 @@ edition = "2024" [dependencies] cxx = "1.0.95" -cxx-qt = "0.7" -cxx-qt-lib = { version="0.7", features = ["qt_full"] } +cxx-qt = "0.8.0" +cxx-qt-lib = { version = "0.8.0", features = ["qt_full", "qt_gui", "qt_qml"] } +log = { version = "0.4.27", features = [] } +dirs = "6.0.0" +syntect = "5.2.0" +clap = { version = "4.5.54", features = ["derive"] } +ratatui = "0.30.0" +anyhow = "1.0.100" +tui-tree-widget = "0.24.0" +tui-logger = "0.18.1" +edtui = "0.11.1" +strum = "0.27.2" [build-dependencies] # The link_qt_object_files feature is required for statically linking Qt 6. -cxx-qt-build = { version = "0.7", features = [ "link_qt_object_files" ] } \ No newline at end of file +cxx-qt-build = { version = "0.8.0", features = ["link_qt_object_files"] } diff --git a/README.md b/README.md index dcf168c..dc4dbd7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # CLIDE -CLIDE is an IDE written in Rust that supports both full and headless Linux environments. +CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments. +The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate. + +It's up to you to build your own development environment for your tools. Plugins are planned to be supported in the future for bringing your own language-specific tools or features. +Once you've created your plugin, you can submit a pull request to add a link to the git repository for your plugin to the final section in this README if you'd like to contribute. The following packages must be installed before the application will build. +In the future, we may provide a minimal installation option that only includes dependencies for the headless TUI. ```bash -sudo apt install qt6-base-dev qt6-declarative-dev qt6-tools-dev qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick +sudo apt install qt6-base-dev qt6-declarative-dev qt6-tools-dev qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick qml6-module-qtquick-dialogs qt6-svg-dev ``` And of course, [Rust](https://www.rust-lang.org/tools/install). @@ -14,6 +19,68 @@ And of course, [Rust](https://www.rust-lang.org/tools/install). curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` +## Usage + +To install and run clide + +```bash +git clone https://git.shaunreed.com/shaunrd0/clide +cd clide +cargo install --path . +``` + +After installation `clide` can be used directly + +```bash +clide --help + +Extendable command-line driven development environment written in Rust using the Qt UI framework. +If no flags are provided, the GUI editor is launched in a separate process. +If no path is provided, the current directory is used. + +Usage: clide [OPTIONS] [PATH] + +Arguments: + [PATH] The root directory for the project to open with the clide editor + +Options: + -t, --tui Run clide in headless mode + -g, --gui Run the clide GUI in the current process, blocking the terminal and showing all output streams + -h, --help Print help +``` + +### TUI + +The TUI is implemented using the ratatui crate and has the typical features you would expect from a text editor. +You can browse your project tree, open / close new editor tabs, and save / reload files. +Controls for the TUI are listed at the bottom of the window, and update depending on which widget you have focused. +For now, there are no language-specific features or plugins available for the TUI – it is only a text editor. + +To run the TUI, pass the `-t` or `--tui` flags. + +```bash +# With cargo from the project root +cargo run -- -t +# Or via clide directly after installation +clide -t +``` + +![image](./resources/tui.png) + +### GUI + +The GUI is still in development. It is at this point a text viewer, instead of a text editor. +There are many placeholder buttons and features in the GUI that do nothing when used. + +The GUI is run by default when executing the `clide` application. + +```bash +# With cargo from the project root +cargo run +# Or via clide directly after installation +clide +``` + ## Development It's recommended to use RustRover or Qt Creator for development. @@ -22,10 +89,44 @@ The [Qt Installer](https://www.qt.io/download-qt-installer) will provide the lat If using RustRover be sure to set your QML binaries path in the settings menu. If Qt was installed to its default directory this will be `$HOME/Qt/6.8.3/gcc_64/bin/`. +Viewing documentation in the web browser is possible, but using Qt Assistant is recommended. +It comes with Qt6 when installed. Run the following command to start it. + +```bash +nohup $HOME/Qt/6.8.3/gcc_64/bin/assistant > /dev/null 2>&1 & +``` + +If you are looking for an include path from Qt + +```bash +find /usr/include/x86_64-linux-gnu/qt6/ -name QFile* + +/usr/include/x86_64-linux-gnu/qt6/QtWidgets/QFileIconProvider +/usr/include/x86_64-linux-gnu/qt6/QtWidgets/QFileDialog +/usr/include/x86_64-linux-gnu/qt6/QtGui/QFileSystemModel +/usr/include/x86_64-linux-gnu/qt6/QtGui/QFileOpenEvent +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFile +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileDevice +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileSystemWatcher +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileInfoList +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileInfo +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileSelector +``` + +This helped find that QFileSystemModel is in QtGui and not QtCore. + ### Resources Some helpful links for reading up on QML if you're just getting started. +* [Rust Crates - cxx-qt](https://docs.rs/releases/search?query=cxx_qt) * [QML Reference](https://doc.qt.io/qt-6/qmlreference.html) * [QML Coding Conventions](https://doc.qt.io/qt-6/qml-codingconventions.html) * [All QML Controls Types](https://doc.qt.io/qt-6/qtquick-controls-qmlmodule.html) +* [KDAB CXX-Qt Book](https://kdab.github.io/cxx-qt/book/) +* [github.com/KDAB/cxx-qt](https://github.com/KDAB/cxx-qt) + + +### Plugins + +TODO: Add a list of plugins here. The first example will be C++ with CMake functionality. diff --git a/build.rs b/build.rs index e14579a..1d20876 100644 --- a/build.rs +++ b/build.rs @@ -1,21 +1,23 @@ use cxx_qt_build::{CxxQtBuilder, QmlModule}; fn main() { - CxxQtBuilder::new() - // Link Qt's Network library - // - Qt Core is always linked - // - Qt Gui is linked by enabling the qt_gui Cargo feature of cxx-qt-lib. - // - Qt Qml is linked by enabling the qt_qml Cargo feature of cxx-qt-lib. - // - Qt Qml requires linking Qt Network on macOS - .qt_module("Network") - .qml_module(QmlModule { - uri: "test", - rust_files: &["src/main.rs"], - qml_files: &["qml/main.qml", - "qml/Menu/ClideMenu.qml", - "qml/Menu/ClideMenuBar.qml", - "qml/Menu/ClideMenuBarItem.qml"], - ..Default::default() - }) - .build(); + CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files(&[ + "qml/main.qml", + "qml/ClideAboutWindow.qml", + "qml/ClideTreeView.qml", + "qml/ClideProjectView.qml", + "qml/ClideEditor.qml", + "qml/ClideMenuBar.qml", + ])) + // Link Qt's Network library + // - Qt Core is always linked + // - Qt Gui is linked by enabling the qt_gui Cargo feature of cxx-qt-lib. + // - Qt Qml is linked by enabling the qt_qml Cargo feature of cxx-qt-lib. + // - Qt Qml requires linking Qt Network on macOS + .qt_module("Network") + .qt_module("Gui") + .qt_module("Svg") + .qt_module("Xml") + .files(["src/gui/colors.rs", "src/gui/filesystem.rs"]) + .build(); } diff --git a/icons/kilroy-256.png b/icons/kilroy-256.png new file mode 100644 index 0000000..ef5590a Binary files /dev/null and b/icons/kilroy-256.png differ diff --git a/qml/ClideAboutWindow.qml b/qml/ClideAboutWindow.qml new file mode 100644 index 0000000..2922764 --- /dev/null +++ b/qml/ClideAboutWindow.qml @@ -0,0 +1,73 @@ +// TODO: Header + +import QtQuick +import QtQuick.Controls.Basic + +import clide.module 1.0 + +ApplicationWindow { + id: root + width: 450 + height: 350 + // Create the window with no frame and keep it on top. + flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint + color: RustColors.gutter + + // Hide the window when it loses focus. + onActiveChanged: { + if (!active) { + root.visible = false; + } + } + + // Kilroy logo. + Image { + id: logo + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 20 + + source: "../icons/kilroy-256.png" + sourceSize.width: 80 + sourceSize.height: 80 + fillMode: Image.PreserveAspectFit + + smooth: true + antialiasing: true + asynchronous: true + } + + ScrollView { + anchors.top: logo.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 20 + + TextArea { + selectedTextColor: RustColors.editor_highlighted_text + selectionColor: RustColors.editor_highlight + horizontalAlignment: Text.AlignHCenter + textFormat: Text.RichText + + text: qsTr("

About CLIDE

" + + "

A simple text editor written in Rust and QML using CXX-Qt.

" + + "

Personal website shaunreed.com

" + + "

Project notes knoats.com

" + + "

This project is developed at git.shaunreed.com

" + + "

KDAB CXX-Qt repository

" + + "

Copyright (C) 2025 Shaun Reed, all rights reserved.

") + color: RustColors.editor_text + wrapMode: Text.WordWrap + readOnly: true + antialiasing: true + background: null + + onLinkActivated: function (link) { + Qt.openUrlExternally(link) + } + } + } +} diff --git a/qml/ClideEditor.qml b/qml/ClideEditor.qml new file mode 100644 index 0000000..b16d9b5 --- /dev/null +++ b/qml/ClideEditor.qml @@ -0,0 +1,206 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import clide.module 1.0 + +SplitView { + id: root + Layout.fillHeight: true + Layout.fillWidth: true + orientation: Qt.Vertical + + // The path to the file to show in the text editor. + // This is updated by a signal caught within ClideProjectView. + // Initialized by the Default trait for the Rust QML singleton FileSystem. + required property string filePath; + + // Customized handle to drag between the Editor and the Console. + handle: Rectangle { + border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter + color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter + implicitHeight: 8 + radius: 2.5 + + // Execute these behaviors when the color is changed. + Behavior on color { + ColorAnimation { + duration: 400 + } + } + } + RowLayout { + // We use a flickable to synchronize the position of the editor and + // the line numbers. This is necessary because the line numbers can + // extend the available height. + Flickable { + id: lineNumbers + Layout.fillHeight: true + Layout.fillWidth: false + // Calculating the width correctly is important as the number grows. + // We need to ensure space required to show N line number digits. + // We use log10 to find how many digits are needed in a line number. + // log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc + // We +1 to ensure space for at least 1 digit, as floor(1.95) = 1. + // The +10 is additional spacing and can be adjusted. + Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10 + contentY: editorFlickable.contentY + interactive: false + + Column { + anchors.fill: parent + topPadding: textArea.topPadding + + Repeater { + id: repeatedLineNumbers + // TODO: Bug where text wrapping shows as new line number. + model: textArea.lineCount + + // This Item is used for each line number in the gutter. + delegate: Item { + // Calculates the height of each line in the text area. + height: textArea.contentHeight / textArea.lineCount + width: parent.width + + required property int index + + // Show the line number. + Label { + id: numbers + color: RustColors.linenumber + font: textArea.font + height: parent.height + horizontalAlignment: Text.AlignLeft + text: parent.index + 1 + verticalAlignment: Text.AlignVCenter + width: parent.width - indicator.width + } + // Draw edge along the right side of the line number. + Rectangle { + id: indicator + anchors.left: numbers.right + color: RustColors.linenumber + height: parent.height + width: 1 + } + } + } + } + } + Flickable { + id: editorFlickable + Layout.fillHeight: true + Layout.fillWidth: true + boundsBehavior: Flickable.StopAtBounds + height: 650 + + ScrollBar.horizontal: MyScrollBar { + } + ScrollBar.vertical: MyScrollBar { + } + + TextArea.flickable: TextArea { + id: textArea + focus: true + persistentSelection: true + antialiasing: true + selectByMouse: true + selectionColor: RustColors.editor_highlight + selectedTextColor: RustColors.editor_highlighted_text + textFormat: Qt.AutoText + wrapMode: TextArea.Wrap + text: FileSystem.readFile(root.filePath) + + onLinkActivated: function (link) { + Qt.openUrlExternally(link); + } + + // TODO: Handle saving + // Component.onCompleted: { + // if (Qt.application.arguments.length === 2) + // textDocument.source = "file:" + Qt.application.arguments[1] + // else + // textDocument.source = "qrc:/texteditor.html" + // } + // textDocument.onStatusChanged: { + // // a message lookup table using computed properties: + // // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer + // const statusMessages = { + // [ TextDocument.ReadError ]: qsTr("Failed to load “%1”"), + // [ TextDocument.WriteError ]: qsTr("Failed to save “%1”"), + // [ TextDocument.NonLocalFileError ]: qsTr("Not a local file: “%1”"), + // } + // const err = statusMessages[textDocument.status] + // if (err) { + // errorDialog.text = err.arg(textDocument.source) + // errorDialog.open() + // } + // } + } + + FontMetrics { + id: fontMetrics + + font: textArea.font + } + } + } + TextArea { + id: areaConsole + + height: 100 + placeholderText: qsTr("Placeholder for bash terminal.") + placeholderTextColor: "white" + readOnly: true + wrapMode: TextArea.Wrap + background: Rectangle { + color: RustColors.editor_background + implicitHeight: 100 + // border.color: control.enabled ? RustColors.active : RustColors.inactive + } + } + + // We use an inline component to customize the horizontal and vertical + // scroll-bars. This is convenient when the component is only used in one file. + component MyScrollBar: ScrollBar { + id: scrollBar + + // Scroll bar gutter + background: Rectangle { + implicitHeight: scrollBar.interactive ? 8 : 4 + implicitWidth: scrollBar.interactive ? 8 : 4 + color: RustColors.scrollbar_gutter + + // Fade the scrollbar gutter when inactive. + opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.2 + Behavior on opacity { + OpacityAnimator { + duration: 500 + } + } + } + + // Scroll bar + contentItem: Rectangle { + implicitHeight: scrollBar.interactive ? 8 : 4 + implicitWidth: scrollBar.interactive ? 8 : 4 + + // If we don't need a scrollbar, fallback to the gutter color. + // If the scrollbar is required change it's color based on activity. + color: scrollBar.size < 1.0 ? scrollBar.active ? RustColors.scrollbar_active : RustColors.scrollbar : RustColors.scrollbar_gutter + // Smooth transition between color changes based on the state above. + Behavior on color { + ColorAnimation { + duration: 1000 + } + } + // Fade the scrollbar when inactive. + opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.35 + Behavior on opacity { + OpacityAnimator { + duration: 500 + } + } + } + } +} diff --git a/qml/Menu/ClideMenuBar.qml b/qml/ClideMenuBar.qml similarity index 58% rename from qml/Menu/ClideMenuBar.qml rename to qml/ClideMenuBar.qml index 66356bd..73a6655 100644 --- a/qml/Menu/ClideMenuBar.qml +++ b/qml/ClideMenuBar.qml @@ -1,11 +1,42 @@ import QtQuick import QtQuick.Controls +import clide.module 1.0 + MenuBar { - background: Rectangle { - color: "#3b3e40" // Dark background like CLion + // Base settings for each Menu. + component ClideMenu : Menu { + background: Rectangle { + color: RustColors.menubar + implicitWidth: 100 + radius: 2 + } } + // Base settings for each MenuItem. + component ClideMenuItem : MenuItem { + id: root + + background: Rectangle { + color: root.hovered ? RustColors.hovered : RustColors.unhovered + radius: 2.5 + } + contentItem: IconLabel { + color: "black" + font.family: "Helvetica" + text: root.text + } + } + + // Background for this MenuBar. + background: Rectangle { + color: RustColors.menubar + border.color: RustColors.menubar_border + } + + + // + // File Menu Action { id: actionNewProject @@ -25,32 +56,37 @@ MenuBar { id: actionExit text: qsTr("&Exit") + onTriggered: Qt.quit() } ClideMenu { title: qsTr("&File") - ClideMenuBarItem { + ClideMenuItem { action: actionNewProject } - ClideMenuBarItem { + ClideMenuItem { action: actionOpen + onTriggered: FileSystem.setDirectory(FileSystem.filePath) } - ClideMenuBarItem { + ClideMenuItem { action: actionSave } MenuSeparator { background: Rectangle { border.color: color - color: "#3c3f41" + color: RustColors.menubar_border implicitHeight: 3 implicitWidth: 200 } } - ClideMenuBarItem { + ClideMenuItem { action: actionExit } } + + // + // Edit Menu Action { id: actionUndo @@ -79,22 +115,25 @@ MenuBar { ClideMenu { title: qsTr("&Edit") - ClideMenuBarItem { + ClideMenuItem { action: actionUndo } - ClideMenuBarItem { + ClideMenuItem { action: actionRedo } - ClideMenuBarItem { + ClideMenuItem { action: actionCut } - ClideMenuBarItem { + ClideMenuItem { action: actionCopy } - ClideMenuBarItem { + ClideMenuItem { action: actionPaste } } + + // + // View Menu Action { id: actionToolWindows @@ -108,13 +147,20 @@ MenuBar { ClideMenu { title: qsTr("&View") - ClideMenuBarItem { + ClideMenuItem { action: actionToolWindows } - ClideMenuBarItem { + ClideMenuItem { action: actionAppearance } } + + // + // Help Menu + ClideAboutWindow { + id: clideAbout + } + Action { id: actionDocumentation @@ -122,16 +168,18 @@ MenuBar { } Action { id: actionAbout + // Toggle the about window with the menu item is clicked. + onTriggered: clideAbout.visible = !clideAbout.visible text: qsTr("&About") } ClideMenu { title: qsTr("&Help") - ClideMenuBarItem { + ClideMenuItem { action: actionDocumentation } - ClideMenuBarItem { + ClideMenuItem { action: actionAbout } } diff --git a/qml/ClideProjectView.qml b/qml/ClideProjectView.qml new file mode 100644 index 0000000..1f97333 --- /dev/null +++ b/qml/ClideProjectView.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import clide.module 1.0 + +SplitView { + id: root + + // Path to the file selected in the tree view. + default property string selectedFilePath: FileSystem.filePath; + + Layout.fillHeight: true + Layout.fillWidth: true + anchors.fill: parent + + // Customized handle to drag between the Navigation and the Editor. + handle: Rectangle { + id: verticalSplitHandle + border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter + color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter + implicitWidth: 8 + radius: 2.5 + + // Execute these behaviors when the color is changed. + Behavior on color { + ColorAnimation { + duration: 400 + } + } + } + + Rectangle { + id: navigationView + color: RustColors.explorer_background + + SplitView.fillHeight: true + SplitView.minimumWidth: 0 + SplitView.preferredWidth: 200 + SplitView.maximumWidth: 250 + + StackLayout { + anchors.fill: parent + ClideTreeView { + id: clideTreeView + onFileClicked: path => root.selectedFilePath = path + } + } + } + ClideEditor { + SplitView.fillWidth: true + // Initialize using the Default trait in Rust QML singleton FileSystem. + filePath: root.selectedFilePath + } +} diff --git a/qml/ClideTreeView.qml b/qml/ClideTreeView.qml new file mode 100644 index 0000000..fa4b483 --- /dev/null +++ b/qml/ClideTreeView.qml @@ -0,0 +1,150 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import clide.module 1.0 + +Rectangle { + id: root + color: RustColors.explorer_background + + signal fileClicked(string filePath) + + TreeView { + id: fileSystemTreeView + anchors.margins: 15 + + // rootIndex: FileSystem.rootIndex + property int lastIndex: -1 + + model: FileSystem + anchors.fill: parent + boundsBehavior: Flickable.StopAtBounds + boundsMovement: Flickable.StopAtBounds + clip: true + + Component.onCompleted: { + FileSystem.setDirectory(FileSystem.filePath) + fileSystemTreeView.expandRecursively(0, 4) + } + + // The delegate represents a single entry in the filesystem. + delegate: TreeViewDelegate { + id: treeDelegate + indentation: 8 + implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250 + implicitHeight: 25 + + required property int index + required property url filePath + required property string fileName + + indicator: Image { + id: directoryIcon + + x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation) + anchors.verticalCenter: parent.verticalCenter + source: { + let folderOpen = "data:image/svg+xml;utf8,"; + let folderClosed = "data:image/svg+xml;utf8,"; + let file = "data:image/svg+xml;utf8,"; + // If the item has children, it's a directory. + if (treeDelegate.hasChildren) { + return treeDelegate.expanded ? + folderOpen : folderClosed; + } else { + return file + } + } + sourceSize.width: 15 + sourceSize.height: 15 + fillMode: Image.PreserveAspectFit + + smooth: true + antialiasing: true + asynchronous: true + } + + contentItem: Text { + text: treeDelegate.fileName + color: RustColors.explorer_text + } + + background: Rectangle { + // TODO: Fix flickering from color transition on states here. + color: (treeDelegate.index === fileSystemTreeView.lastIndex) + ? RustColors.explorer_text_selected + : (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent") + radius: 2.5 + opacity: hoverHandler.hovered ? 0.75 : 1.0 + + Behavior on color { + ColorAnimation { + duration: 300 + } + } + } + + HoverHandler { + id: hoverHandler + } + + TapHandler { + acceptedButtons: Qt.LeftButton | Qt.RightButton + onSingleTapped: (eventPoint, button) => { + switch (button) { + case Qt.LeftButton: + fileSystemTreeView.toggleExpanded(treeDelegate.row) + fileSystemTreeView.lastIndex = treeDelegate.index + // If this model item doesn't have children, it means it's + // representing a file. + if (!treeDelegate.hasChildren) + root.fileClicked(treeDelegate.filePath) + break; + case Qt.RightButton: + if (treeDelegate.hasChildren) + contextMenu.popup(); + break; + } + } + } + + Menu { + id: contextMenu + Action { + text: qsTr("Set as root index") + onTriggered: { + console.log("Setting directory: " + treeDelegate.filePath) + FileSystem.setDirectory(treeDelegate.filePath) + } + } + Action { + text: qsTr("Reset root index") + onTriggered: { + FileSystem.setDirectory("") + } + } + } + } + + // Provide our own custom ScrollIndicator for the TreeView. + ScrollIndicator.vertical: ScrollIndicator { + active: true + implicitWidth: 15 + + contentItem: Rectangle { + implicitWidth: 6 + implicitHeight: 6 + + color: RustColors.scrollbar + opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0 + + Behavior on opacity { + OpacityAnimator { + duration: 500 + } + } + } + } + } +} diff --git a/qml/Menu/ClideMenu.qml b/qml/Menu/ClideMenu.qml deleted file mode 100644 index 6616882..0000000 --- a/qml/Menu/ClideMenu.qml +++ /dev/null @@ -1,10 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Menu { - background: Rectangle { - color: "#3c3f41" - implicitWidth: 200 - radius: 2 - } -} diff --git a/qml/Menu/ClideMenuBarItem.qml b/qml/Menu/ClideMenuBarItem.qml deleted file mode 100644 index f740727..0000000 --- a/qml/Menu/ClideMenuBarItem.qml +++ /dev/null @@ -1,16 +0,0 @@ -import QtQuick -import QtQuick.Controls - -MenuItem { - id: root - - background: Rectangle { - color: root.hovered ? "#4b4f51" : "#3c3f41" // Hover effect - radius: 2.5 - } - contentItem: IconLabel { - color: "white" - font.family: "Helvetica" - text: root.text - } -} diff --git a/qml/main.qml b/qml/main.qml index 9345f21..cba1e07 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -1,10 +1,13 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtQuick.Dialogs -import "Menu" +import clide.module 1.0 ApplicationWindow { + id: appWindow + height: 800 title: "CLIDE" visible: true @@ -15,6 +18,15 @@ ApplicationWindow { Rectangle { anchors.fill: parent - color: "#1e1f22" // Dark background + color: RustColors.gutter + } + + MessageDialog { + id: errorDialog + + title: qsTr("Error") + } + ClideProjectView { } } + diff --git a/resources/tui.png b/resources/tui.png new file mode 100644 index 0000000..639cf3d Binary files /dev/null and b/resources/tui.png differ diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..6aefef4 --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use cxx_qt_lib::QString; +use log::trace; + +pub mod colors; +pub mod filesystem; + +pub fn run(root_path: std::path::PathBuf) -> Result<()> { + trace!(target:"gui::run()", "Starting the GUI editor at {root_path:?}"); + + use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; + + let mut app = QGuiApplication::new(); + let mut engine = QQmlApplicationEngine::new(); + + if let Some(engine) = engine.as_mut() { + engine.add_import_path(&QString::from("qml/")); + } + if let Some(engine) = engine.as_mut() { + engine.load(&QUrl::from("qml/main.qml")); + } + + if let Some(app) = app.as_mut() { + app.exec(); + } + + Ok(()) +} diff --git a/src/gui/colors.rs b/src/gui/colors.rs new file mode 100644 index 0000000..d113109 --- /dev/null +++ b/src/gui/colors.rs @@ -0,0 +1,92 @@ +#[cxx_qt::bridge] +pub mod qobject { + unsafe extern "C++" { + include!("cxx-qt-lib/qcolor.h"); + type QColor = cxx_qt_lib::QColor; + } + + unsafe extern "RustQt" { + #[qobject] + #[qml_element] + #[qml_singleton] + #[qproperty(QColor, hovered)] + #[qproperty(QColor, unhovered)] + #[qproperty(QColor, pressed)] + #[qproperty(QColor, menubar)] + #[qproperty(QColor, menubar_border)] + #[qproperty(QColor, scrollbar)] + #[qproperty(QColor, scrollbar_active)] + #[qproperty(QColor, scrollbar_gutter)] + #[qproperty(QColor, linenumber)] + #[qproperty(QColor, active)] + #[qproperty(QColor, inactive)] + #[qproperty(QColor, editor_background)] + #[qproperty(QColor, editor_text)] + #[qproperty(QColor, editor_highlighted_text)] + #[qproperty(QColor, editor_highlight)] + #[qproperty(QColor, gutter)] + #[qproperty(QColor, explorer_hovered)] + #[qproperty(QColor, explorer_text)] + #[qproperty(QColor, explorer_text_selected)] + #[qproperty(QColor, explorer_background)] + #[qproperty(QColor, explorer_folder)] + #[qproperty(QColor, explorer_folder_open)] + type RustColors = super::RustColorsImpl; + } +} + +use cxx_qt_lib::QColor; + +pub struct RustColorsImpl { + hovered: QColor, + unhovered: QColor, + pressed: QColor, + menubar: QColor, + menubar_border: QColor, + scrollbar: QColor, + scrollbar_active: QColor, + scrollbar_gutter: QColor, + linenumber: QColor, + active: QColor, + inactive: QColor, + editor_background: QColor, + editor_text: QColor, + editor_highlighted_text: QColor, + editor_highlight: QColor, + gutter: QColor, + explorer_hovered: QColor, + explorer_text: QColor, + explorer_text_selected: QColor, + explorer_background: QColor, + explorer_folder: QColor, + explorer_folder_open: QColor, +} + +impl Default for RustColorsImpl { + fn default() -> Self { + Self { + hovered: QColor::try_from("#303234").unwrap(), + unhovered: QColor::try_from("#3c3f41").unwrap(), + pressed: QColor::try_from("#4b4f51").unwrap(), + menubar: QColor::try_from("#3c3f41").unwrap(), + menubar_border: QColor::try_from("#575757").unwrap(), + scrollbar: QColor::try_from("#4b4f51").unwrap(), + scrollbar_active: QColor::try_from("#4b4f51").unwrap(), + scrollbar_gutter: QColor::try_from("#3b3b3b").unwrap(), + linenumber: QColor::try_from("#94989b").unwrap(), + active: QColor::try_from("#a9acb0").unwrap(), + inactive: QColor::try_from("#FFF").unwrap(), + editor_background: QColor::try_from("#2b2b2b").unwrap(), + editor_text: QColor::try_from("#acaea3").unwrap(), + editor_highlighted_text: QColor::try_from("#ccced3").unwrap(), + editor_highlight: QColor::try_from("#ccced3").unwrap(), + gutter: QColor::try_from("#1e1f22").unwrap(), + explorer_hovered: QColor::try_from("#4c5053").unwrap(), + explorer_text: QColor::try_from("#3b3b3b").unwrap(), + explorer_text_selected: QColor::try_from("#8b8b8b").unwrap(), + explorer_background: QColor::try_from("#676c70").unwrap(), + explorer_folder: QColor::try_from("#54585b").unwrap(), + explorer_folder_open: QColor::try_from("#FFF").unwrap(), + } + } +} diff --git a/src/gui/filesystem.rs b/src/gui/filesystem.rs new file mode 100644 index 0000000..557203b --- /dev/null +++ b/src/gui/filesystem.rs @@ -0,0 +1,137 @@ +#[cxx_qt::bridge] +pub mod qobject { + unsafe extern "C++" { + // Import Qt Types from C++ + include!("cxx-qt-lib/qstring.h"); + type QString = cxx_qt_lib::QString; + include!("cxx-qt-lib/qmodelindex.h"); + type QModelIndex = cxx_qt_lib::QModelIndex; + include!(); + type QFileSystemModel; + } + + unsafe extern "RustQt" { + // Export QML Types from Rust + #[qobject] + #[base = QFileSystemModel] + #[qml_element] + #[qml_singleton] + #[qproperty(QString, file_path, cxx_name = "filePath")] + #[qproperty(QModelIndex, root_index, cxx_name = "rootIndex")] + type FileSystem = super::FileSystemImpl; + + #[inherit] + #[cxx_name = "setRootPath"] + fn set_root_path(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex; + + #[qinvokable] + #[cxx_override] + #[cxx_name = "columnCount"] + fn column_count(self: &FileSystem, _index: &QModelIndex) -> i32; + + #[qinvokable] + #[cxx_name = "readFile"] + fn read_file(self: &FileSystem, path: &QString) -> QString; + + #[qinvokable] + #[cxx_name = "setDirectory"] + fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex; + } +} + +use cxx_qt_lib::{QModelIndex, QString}; +use dirs; +use log::warn; +use std::fs; +use std::io::BufRead; +use syntect::easy::HighlightFile; +use syntect::highlighting::ThemeSet; +use syntect::html::{ + IncludeBackground, append_highlighted_html_for_styled_line, start_highlighted_html_snippet, +}; +use syntect::parsing::SyntaxSet; + +// TODO: Impleent a provider for QFileSystemModel::setIconProvider for icons. +pub struct FileSystemImpl { + file_path: QString, + root_index: QModelIndex, +} + +// Default is explicit to make the editor open this source file initially. +impl Default for FileSystemImpl { + fn default() -> Self { + Self { + file_path: QString::from(file!()), + root_index: Default::default(), + } + } +} + +impl qobject::FileSystem { + fn read_file(&self, path: &QString) -> QString { + if path.is_empty() { + return QString::default(); + } + if !fs::metadata(path.to_string()) + .expect(format!("Failed to get file metadata {path:?}").as_str()) + .is_file() + { + warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file"); + return QString::default(); + } + let ss = SyntaxSet::load_defaults_nonewlines(); + let ts = ThemeSet::load_defaults(); + let theme = &ts.themes["base16-ocean.dark"]; + + let mut highlighter = + HighlightFile::new(path.to_string(), &ss, theme).expect("Failed to create highlighter"); + let (mut output, _bg) = start_highlighted_html_snippet(theme); + let mut line = String::new(); + while highlighter + .reader + .read_line(&mut line) + .expect("Failed to read file.") + > 0 + { + let regions = highlighter + .highlight_lines + .highlight_line(&line, &ss) + .expect("Failed to highlight"); + + append_highlighted_html_for_styled_line( + ®ions[..], + IncludeBackground::Yes, + &mut output, + ) + .expect("Failed to insert highlighted html"); + line.clear(); + } + output.push_str("\n"); + QString::from(output) + } + + // There will never be more than one column. + fn column_count(&self, _index: &QModelIndex) -> i32 { + 1 + } + + fn set_directory(self: std::pin::Pin<&mut Self>, path: &QString) -> QModelIndex { + if !path.is_empty() + && fs::metadata(path.to_string()) + .expect(format!("Failed to get metadata for path {path:?}").as_str()) + .is_dir() + { + self.set_root_path(path) + } else { + // If the initial directory can't be opened, attempt to find the home directory. + self.set_root_path(&QString::from( + dirs::home_dir() + .expect("Failed to get home directory") + .as_path() + .to_str() + .unwrap() + .to_string(), + )) + } + } +} diff --git a/src/main.rs b/src/main.rs index 50edc97..f5cf2eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,90 +1,65 @@ -// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company -// SPDX-FileContributor: Leon Matthes -// -// SPDX-License-Identifier: MIT OR Apache-2.0 +use crate::tui::Tui; +use anyhow::{Context, Result}; +use clap::Parser; +use log::{info, trace}; +use std::process::{Command, Stdio}; -#[cxx_qt::bridge] -mod qobject { - unsafe extern "C++" { - include!("cxx-qt-lib/qstring.h"); - type QString = cxx_qt_lib::QString; - } +pub mod gui; +pub mod tui; +/// Extendable command-line driven development environment written in Rust using the Qt UI framework. +/// If no flags are provided, the GUI editor is launched in a separate process. +/// If no path is provided, the current directory is used. +#[derive(Parser, Debug)] +#[structopt(name = "clide", verbatim_doc_comment)] +struct Cli { + /// The root directory for the project to open with the clide editor. + #[arg(value_parser = clap::value_parser!(std::path::PathBuf))] + pub path: Option, - #[qenum(Greeter)] - pub enum Language { - English, - German, - French, - } + /// Run clide in headless mode. + #[arg(value_name = "tui", short, long)] + pub tui: bool, - #[qenum(Greeter)] - pub enum Greeting { - Hello, - Bye, - } - - unsafe extern "RustQt" { - #[qobject] - #[qml_element] - #[qproperty(Greeting, greeting)] - #[qproperty(Language, language)] - type Greeter = super::GreeterRust; - - #[qinvokable] - fn greet(self: &Greeter) -> QString; - } + /// Run the clide GUI in the current process, blocking the terminal and showing all output streams. + #[arg(value_name = "gui", short, long)] + pub gui: bool, } -use qobject::*; +fn main() -> Result<()> { + let args = Cli::parse(); -impl Greeting { - fn translate(&self, language: Language) -> String { - match (self, language) { - (&Greeting::Hello, Language::English) => "Hello, World!", - (&Greeting::Hello, Language::German) => "Hallo, Welt!", - (&Greeting::Hello, Language::French) => "Bonjour, le monde!", - (&Greeting::Bye, Language::English) => "Bye!", - (&Greeting::Bye, Language::German) => "Auf Wiedersehen!", - (&Greeting::Bye, Language::French) => "Au revoir!", - _ => "🤯", + let root_path = match args.path { + // If the CLI was provided a directory, convert it to absolute. + Some(path) => std::path::absolute(path)?, + // If no path was provided, use the current directory. + None => std::env::current_dir().unwrap_or( + // If we can't find the CWD, attempt to open the home directory. + dirs::home_dir().context("Failed to obtain home directory")?, + ), + }; + info!(target:"main()", "Root path detected: {root_path:?}"); + + match args.gui { + true => { + trace!(target:"main()", "Starting GUI"); + gui::run(root_path) } - .to_string() - } -} - -pub struct GreeterRust { - greeting: Greeting, - language: Language, -} - -impl Default for GreeterRust { - fn default() -> Self { - Self { - greeting: Greeting::Hello, - language: Language::English, - } - } -} - -use cxx_qt_lib::QString; - -impl qobject::Greeter { - fn greet(&self) -> QString { - QString::from(self.greeting.translate(self.language)) - } -} - -fn main() { - use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; - - let mut app = QGuiApplication::new(); - let mut engine = QQmlApplicationEngine::new(); - - if let Some(engine) = engine.as_mut() { - engine.load(&QUrl::from("qml/main.qml")); - } - - if let Some(app) = app.as_mut() { - app.exec(); + false => match args.tui { + // Open the TUI editor if requested, otherwise use the QML GUI by default. + true => { + trace!(target:"main()", "Starting TUI"); + Ok(Tui::new(root_path)?.start()?) + } + false => { + trace!(target:"main()", "Starting GUI in a new process"); + Command::new(std::env::current_exe()?) + .args(&["--gui", root_path.to_str().unwrap()]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .stdin(Stdio::null()) + .spawn()?; + Ok(()) + } + }, } } diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..3a0127d --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,88 @@ +mod about; +mod app; +mod component; +mod editor; +mod editor_tab; +mod explorer; +mod logger; +mod menu_bar; + +use anyhow::{Context, Result}; +use log::{LevelFilter, debug, info, trace}; +use ratatui::Terminal; +use ratatui::backend::CrosstermBackend; +use ratatui::crossterm::event::{ + DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, +}; +use ratatui::crossterm::terminal::{ + EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, +}; +use std::env; +use std::io::{Stdout, stdout}; +use tui_logger::{ + TuiLoggerFile, TuiLoggerLevelOutput, init_logger, set_default_level, set_log_file, +}; + +pub struct Tui { + terminal: Terminal>, + root_path: std::path::PathBuf, +} + +impl Tui { + pub fn id() -> &'static str { + "Tui" + } + + pub fn new(root_path: std::path::PathBuf) -> Result { + trace!(target:Self::id(), "Building {}", Self::id()); + init_logger(LevelFilter::Trace)?; + set_default_level(LevelFilter::Trace); + debug!(target:Self::id(), "Logging initialized"); + + let mut dir = env::temp_dir(); + dir.push("clide.log"); + let file_options = TuiLoggerFile::new( + dir.to_str() + .context("Failed to set temp directory for file logging")?, + ) + .output_level(Some(TuiLoggerLevelOutput::Abbreviated)) + .output_file(false) + .output_separator(':'); + set_log_file(file_options); + debug!(target:Self::id(), "Logging to file: {dir:?}"); + + Ok(Self { + terminal: Terminal::new(CrosstermBackend::new(stdout()))?, + root_path, + }) + } + + pub fn start(self) -> Result<()> { + info!(target:Self::id(), "Starting the TUI editor at {:?}", self.root_path); + ratatui::crossterm::execute!( + stdout(), + EnterAlternateScreen, + EnableMouseCapture, + EnableBracketedPaste + )?; + enable_raw_mode()?; + + let app_result = app::App::new(self.root_path)? + .run(self.terminal) + .context("Failed to start the TUI editor."); + Self::stop()?; + app_result + } + + fn stop() -> Result<()> { + info!(target:Self::id(), "Stopping the TUI editor"); + disable_raw_mode()?; + ratatui::crossterm::execute!( + stdout(), + LeaveAlternateScreen, + DisableMouseCapture, + DisableBracketedPaste + )?; + Ok(()) + } +} diff --git a/src/tui/about.rs b/src/tui/about.rs new file mode 100644 index 0000000..c5f222a --- /dev/null +++ b/src/tui/about.rs @@ -0,0 +1,138 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap}; + +pub struct About {} + +impl About { + #[allow(unused)] + pub fn id() -> &'static str { + "About" + } + + pub fn new() -> Self { + // trace!(target:Self::id(), "Building {}", Self::id()); + Self {} + } +} + +impl Widget for About { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + Clear::default().render(area, buf); + // Split main area + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(2), // image column + Constraint::Fill(1), // image column + Constraint::Fill(2), // text column + ]) + .split(area); + + let top_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Fill(3), + Constraint::Fill(1), + ]) + .split(chunks[1]); + + let bottom_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Fill(3), + Constraint::Fill(1), + ]) + .split(chunks[2]); + + // ---------- IMAGE ---------- + let kilroy_art = [ + " * ", + " |.===. ", + " {}o o{} ", + "-----------------------ooO--(_)--Ooo---------------------------", + "# #", + "# CLIDE WAS HERE #", + "# #", + "# https://git.shaunreed.com/shaunred/clide #", + "# https://shaunreed.com/shaunred/clide #", + "# #", + ]; + + let kilroy_lines: Vec = kilroy_art + .iter() + .map(|l| Line::from(Span::raw(*l))) + .collect(); + + Paragraph::new(kilroy_lines) + .block( + Block::default() + .borders(Borders::NONE) + .padding(Padding::bottom(0)), + ) + .wrap(Wrap { trim: false }) + .centered() + .render(top_chunks[1], buf); + + // ---------- TEXT ---------- + let about_text = vec![ + Line::from(vec![Span::styled( + "clide\n", + Style::default().add_modifier(Modifier::BOLD), + )]) + .centered(), + Line::from(""), + Line::from(vec![ + Span::styled("Author: ", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("Shaun Reed"), + ]) + .left_aligned(), + Line::from(vec![ + Span::styled("Email: ", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("shaunrd0@gmail.com"), + ]) + .left_aligned(), + Line::from(vec![ + Span::styled("URL: ", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("https://git.shaunreed.com/shaunrd0/clide"), + ]) + .left_aligned(), + Line::from(vec![ + Span::styled("Blog: ", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("https://shaunreed.com"), + ]) + .left_aligned(), + Line::from(""), + Line::from(vec![Span::styled( + "Description\n", + Style::default().add_modifier(Modifier::BOLD), + )]) + .left_aligned(), + Line::from(concat!( + "CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments. ", + "The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate. ", + )) + .style(Style::default()) + .left_aligned(), + ]; + Block::bordered().render(area, buf); + + let paragraph = Paragraph::new(about_text) + .block( + Block::default() + .title("About") + .borders(Borders::ALL) + .padding(Padding::top(0)), + ) + .wrap(Wrap { trim: true }); + + paragraph.render(bottom_chunks[1], buf); + } +} diff --git a/src/tui/app.rs b/src/tui/app.rs new file mode 100644 index 0000000..6537437 --- /dev/null +++ b/src/tui/app.rs @@ -0,0 +1,415 @@ +use crate::tui::about::About; +use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger}; +use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState}; +use crate::tui::editor_tab::EditorTab; +use crate::tui::explorer::Explorer; +use crate::tui::logger::Logger; +use crate::tui::menu_bar::MenuBar; +use AppComponent::AppMenuBar; +use anyhow::{Context, Result}; +use log::{error, info, trace}; +use ratatui::DefaultTerminal; +use ratatui::buffer::Buffer; +use ratatui::crossterm::event; +use ratatui::crossterm::event::{ + Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind, +}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::prelude::{Color, Widget}; +use ratatui::widgets::{Paragraph, Wrap}; +use std::path::PathBuf; +use std::time::Duration; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum AppComponent { + AppEditor, + AppExplorer, + AppLogger, + AppMenuBar, +} + +pub struct App<'a> { + editor_tabs: EditorTab, + explorer: Explorer<'a>, + logger: Logger, + menu_bar: MenuBar, + last_active: AppComponent, + about: bool, +} + +impl<'a> App<'a> { + pub fn id() -> &'static str { + "App" + } + + pub fn new(root_path: PathBuf) -> Result { + trace!(target:Self::id(), "Building {}", Self::id()); + let app = Self { + editor_tabs: EditorTab::new(None), + explorer: Explorer::new(&root_path)?, + logger: Logger::new(), + menu_bar: MenuBar::new(), + last_active: AppEditor, + about: false, + }; + Ok(app) + } + + /// Logic that should be executed once on application startup. + pub fn start(&mut self) -> Result<()> { + trace!(target:Self::id(), "Starting App"); + Ok(()) + } + + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + self.start()?; + trace!(target:Self::id(), "Entering App run loop"); + loop { + terminal.draw(|f| { + f.render_widget(&mut self, f.area()); + })?; + + if event::poll(Duration::from_millis(250)).context("event poll failed")? { + match self.handle_event(event::read()?)? { + Action::Quit => break, + Action::Handled => {} + _ => { + // bail!("Unhandled event: {:?}", event); + } + } + } + } + Ok(()) + } + + fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) { + // Determine help text from the most recently focused component. + let help = match self.last_active { + AppEditor => match self.editor_tabs.current_editor() { + Some(editor) => editor.component_state.help_text.clone(), + None => { + if !self.editor_tabs.is_empty() { + error!(target:Self::id(), "Failed to get Editor while drawing bottom status bar"); + } + "Failed to get current Editor while getting widget help text".to_string() + } + }, + AppExplorer => self.explorer.component_state.help_text.clone(), + AppLogger => self.logger.component_state.help_text.clone(), + AppMenuBar => self.menu_bar.component_state.help_text.clone(), + }; + Paragraph::new( + concat!( + "ALT+Q: Focus project explorer | ALT+W: Focus editor | ALT+E: Focus logger |", + " ALT+R: Focus menu bar | CTRL+C: Quit\n" + ) + .to_string() + + help.as_str(), + ) + .style(Color::Gray) + .wrap(Wrap { trim: false }) + .centered() + .render(area, buf); + } + + fn clear_focus(&mut self) { + info!(target:Self::id(), "Clearing all widget focus"); + self.explorer.component_state.set_focus(Focus::Inactive); + self.explorer.component_state.set_focus(Focus::Inactive); + self.logger.component_state.set_focus(Focus::Inactive); + self.menu_bar.component_state.set_focus(Focus::Inactive); + match self.editor_tabs.current_editor_mut() { + None => { + error!(target:Self::id(), "Failed to get current Editor while clearing focus") + } + Some(editor) => editor.component_state.set_focus(Focus::Inactive), + } + } + + fn change_focus(&mut self, focus: AppComponent) { + info!(target:Self::id(), "Changing widget focus to {:?}", focus); + self.clear_focus(); + match focus { + AppEditor => match self.editor_tabs.current_editor_mut() { + None => { + error!(target:Self::id(), "Failed to get current Editor while changing focus") + } + Some(editor) => editor.component_state.set_focus(Focus::Active), + }, + AppExplorer => self.explorer.component_state.set_focus(Focus::Active), + AppLogger => self.logger.component_state.set_focus(Focus::Active), + AppMenuBar => self.menu_bar.component_state.set_focus(Focus::Active), + } + self.last_active = focus; + } + + /// Refresh the contents of the editor to match the selected TreeItem in the file Explorer. + /// If the selected item is not a file, this does nothing. + #[allow(unused)] + fn refresh_editor_contents(&mut self) -> Result<()> { + // TODO: This may be useful for a preview mode of the selected file prior to opening a tab. + // Use the currently selected TreeItem or get an absolute path to this source file. + // let selected_pathbuf = match self.explorer.selected() { + // Ok(path) => PathBuf::from(path), + // Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()), + // }; + // match self.editor_tabs.current_editor_mut() { + // None => bail!("Failed to get current Editor while refreshing editor contents"), + // Some(editor) => { + // let current_file_path = editor + // .file_path + // .clone() + // .context("Failed to get Editor current file_path")?; + // if selected_pathbuf == current_file_path || !selected_pathbuf.is_file() { + // return Ok(()); + // } + // editor.set_contents(&selected_pathbuf) + // } + // } + Ok(()) + } +} + +impl<'a> Widget for &mut App<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let vertical_constraints = match self.logger.component_state.vis { + Visibility::Visible => { + vec![ + Constraint::Length(3), // top status bar + Constraint::Percentage(70), // horizontal layout + Constraint::Fill(1), // terminal + Constraint::Length(3), // bottom status bar + ] + } + Visibility::Hidden => { + vec![ + Constraint::Length(3), // top status bar + Constraint::Fill(1), // horizontal layout + Constraint::Length(3), // bottom status bar + ] + } + }; + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints(vertical_constraints) + .split(area); + + let horizontal_constraints = match self.explorer.component_state.vis { + Visibility::Visible => { + vec![ + Constraint::Max(30), // File explorer with a max width of 30 characters. + Constraint::Fill(1), // Editor fills the remaining space. + ] + } + Visibility::Hidden => { + vec![ + Constraint::Fill(1), // Editor fills the remaining space. + ] + } + }; + + // The index used for vertical here does not care if the Logger is Visible or not. + let horizontal = Layout::default() + .direction(Direction::Horizontal) + .constraints(horizontal_constraints) + .split(vertical[1]); + match self.explorer.component_state.vis { + Visibility::Visible => { + let editor_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), // Editor tabs. + Constraint::Fill(1), // Editor contents. + ]) + .split(horizontal[1]); + self.editor_tabs + .render(editor_layout[0], editor_layout[1], buf); + self.explorer.render(horizontal[0], buf); + } + Visibility::Hidden => { + let editor_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), // Editor tabs. + Constraint::Fill(1), // Editor contents. + ]) + .split(horizontal[0]); + self.editor_tabs + .render(editor_layout[0], editor_layout[1], buf); + } + } + + match self.logger.component_state.vis { + // Index 1 of vertical is rendered with the horizontal layout above. + Visibility::Visible => { + self.logger.render(vertical[2], buf); + self.draw_bottom_status(vertical[3], buf); + // The title bar is rendered last to overlay any popups created for drop-down menus. + self.menu_bar.render(vertical[0], buf); + } + Visibility::Hidden => { + self.draw_bottom_status(vertical[2], buf); + // The title bar is rendered last to overlay any popups created for drop-down menus. + self.menu_bar.render(vertical[0], buf); + } + } + + if self.about { + let about_area = area.centered(Constraint::Percentage(50), Constraint::Percentage(45)); + About::new().render(about_area, buf); + } + } +} + +impl<'a> Component for App<'a> { + /// Handles events for the App and delegates to attached Components. + fn handle_event(&mut self, event: Event) -> Result { + // Handle events in the primary application. + if let Some(key_event) = event.as_key_event() { + let res = self + .handle_key_events(key_event) + .context("Failed to handle key events for primary App Component."); + match res { + Ok(Action::Quit) | Ok(Action::Handled) => return res, + _ => {} + } + } + // Handle events for all components. + let action = match self.last_active { + AppEditor => self.editor_tabs.handle_event(event.clone())?, + AppExplorer => self.explorer.handle_event(event.clone())?, + AppLogger => self.logger.handle_event(event.clone())?, + AppMenuBar => self.menu_bar.handle_event(event.clone())?, + }; + + // Components should always handle mouse events for click interaction. + if let Some(mouse) = event.as_mouse_event() { + if mouse.kind == MouseEventKind::Down(MouseButton::Left) { + if let Some(editor) = self.editor_tabs.current_editor_mut() { + editor.handle_mouse_events(mouse)?; + } + self.explorer.handle_mouse_events(mouse)?; + self.logger.handle_mouse_events(mouse)?; + } + } + + // Handle actions returned from widgets that may need context on other widgets or app state. + match action { + Action::Quit | Action::Handled => Ok(action), + Action::Save => match self.editor_tabs.current_editor_mut() { + None => { + error!(target:Self::id(), "Failed to get current editor while handling App Action::Save"); + Ok(Action::Noop) + } + Some(editor) => match editor.save() { + Ok(_) => Ok(Action::Handled), + Err(e) => { + error!(target:Self::id(), "Failed to save editor contents: {e}"); + Ok(Action::Noop) + } + }, + }, + Action::OpenTab => { + if let Ok(path) = self.explorer.selected() { + let path_buf = PathBuf::from(path); + self.editor_tabs.open_tab(&path_buf)?; + Ok(Action::Handled) + } else { + Ok(Action::Noop) + } + } + Action::CloseTab => match self.editor_tabs.close_current_tab() { + Ok(_) => Ok(Action::Handled), + Err(_) => Ok(Action::Noop), + }, + Action::ReloadFile => { + trace!(target:Self::id(), "Reloading file for current editor"); + if let Some(editor) = self.editor_tabs.current_editor_mut() { + editor + .reload_contents() + .map(|_| Action::Handled) + .context("Failed to handle Action::ReloadFile") + } else { + error!(target:Self::id(), "Failed to get current editor while handling App Action::ReloadFile"); + Ok(Action::Noop) + } + } + Action::ShowHideLogger => { + self.logger.component_state.togget_visible(); + Ok(Action::Handled) + } + Action::ShowHideExplorer => { + self.explorer.component_state.togget_visible(); + Ok(Action::Handled) + } + Action::ShowHideAbout => { + self.about = !self.about; + Ok(Action::Handled) + } + _ => Ok(Action::Noop), + } + } + + /// Handles key events for the App Component only. + fn handle_key_events(&mut self, key: KeyEvent) -> Result { + match key.code { + // If the ESC key is pressed with the About page open, hide it. + KeyCode::Esc | KeyCode::Char('q') => { + if self.about { + self.about = false; + return Ok(Action::Handled); + } + } + _ => {} + } + + match key { + KeyEvent { + code: KeyCode::Char('q'), + modifiers: KeyModifiers::ALT, + kind: KeyEventKind::Press, + state: _state, + } => { + self.change_focus(AppExplorer); + Ok(Action::Handled) + } + KeyEvent { + code: KeyCode::Char('w'), + modifiers: KeyModifiers::ALT, + kind: KeyEventKind::Press, + state: _state, + } => { + self.change_focus(AppEditor); + Ok(Action::Handled) + } + KeyEvent { + code: KeyCode::Char('e'), + modifiers: KeyModifiers::ALT, + kind: KeyEventKind::Press, + state: _state, + } => { + self.change_focus(AppLogger); + Ok(Action::Handled) + } + KeyEvent { + code: KeyCode::Char('r'), + modifiers: KeyModifiers::ALT, + kind: KeyEventKind::Press, + state: _state, + } => { + self.change_focus(AppMenuBar); + Ok(Action::Handled) + } + KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: _state, + } => Ok(Action::Quit), + _ => Ok(Action::Noop), + } + } +} diff --git a/src/tui/component.rs b/src/tui/component.rs new file mode 100644 index 0000000..5a119fe --- /dev/null +++ b/src/tui/component.rs @@ -0,0 +1,167 @@ +#![allow(dead_code, unused_variables)] + +use crate::tui::component::Focus::Inactive; +use Focus::Active; +use anyhow::Result; +use log::trace; +use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent}; +use ratatui::style::Color; + +pub enum Action { + /// Exit the application. + Quit, + + /// The input was checked by the Component and had no effect. + Noop, + + /// Pass input to another component or external handler. + /// Similar to Noop with the added context that externally handled input may have had an impact. + Pass, + + /// Save the current file. + Save, + + /// The input was handled by a Component and should not be passed to the next component. + Handled, + OpenTab, + ReloadFile, + ShowHideExplorer, + ShowHideLogger, + ShowHideAbout, + CloseTab, +} + +pub trait Component { + fn handle_event(&mut self, event: Event) -> Result { + match event { + Event::Key(key_event) => self.handle_key_events(key_event), + _ => Ok(Action::Noop), + } + } + + fn handle_key_events(&mut self, key: KeyEvent) -> Result { + Ok(Action::Noop) + } + + fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result { + Ok(Action::Noop) + } + + fn update(&mut self, action: Action) -> Result { + Ok(Action::Noop) + } + + /// Override this method for creating components that conditionally handle input. + fn is_active(&self) -> bool { + true + } +} + +#[derive(Debug, Clone, Default)] +pub struct ComponentState { + pub(crate) focus: Focus, + pub(crate) vis: Visibility, + pub(crate) help_text: String, +} + +impl ComponentState { + pub fn id() -> &'static str { + "ComponentState" + } + + fn new() -> Self { + trace!(target:Self::id(), "Building {}", Self::id()); + Self { + focus: Active, + vis: Visibility::Visible, + help_text: String::new(), + } + } + + pub(crate) fn with_help_text(mut self, help_text: &str) -> Self { + self.help_text = help_text.into(); + self + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub enum Focus { + Active, + #[default] + Inactive, +} + +impl Focus { + pub(crate) fn get_active_color(&self) -> Color { + match self { + Active => Color::LightYellow, + Inactive => Color::White, + } + } +} + +pub trait FocusState { + fn with_focus(self, focus: Focus) -> Self; + fn set_focus(&mut self, focus: Focus); + fn toggle_focus(&mut self); + fn get_active_color(&self) -> Color; +} + +impl FocusState for ComponentState { + fn with_focus(self, focus: Focus) -> Self { + Self { + focus, + vis: Visibility::Visible, + help_text: self.help_text, + } + } + + fn set_focus(&mut self, focus: Focus) { + self.focus = focus; + } + + fn toggle_focus(&mut self) { + match self.focus { + Active => self.set_focus(Inactive), + Inactive => self.set_focus(Active), + } + } + + fn get_active_color(&self) -> Color { + self.focus.get_active_color() + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +pub enum Visibility { + #[default] + Visible, + Hidden, +} + +pub trait VisibleState { + fn with_visible(self, vis: Visibility) -> Self; + fn set_visible(&mut self, vis: Visibility); + fn togget_visible(&mut self); +} + +impl VisibleState for ComponentState { + fn with_visible(self, vis: Visibility) -> Self { + Self { + focus: self.focus, + vis, + help_text: self.help_text, + } + } + + fn set_visible(&mut self, vis: Visibility) { + self.vis = vis; + } + + fn togget_visible(&mut self) { + match self.vis { + Visibility::Visible => self.set_visible(Visibility::Hidden), + Visibility::Hidden => self.set_visible(Visibility::Visible), + } + } +} diff --git a/src/tui/editor.rs b/src/tui/editor.rs new file mode 100644 index 0000000..5070d90 --- /dev/null +++ b/src/tui/editor.rs @@ -0,0 +1,144 @@ +use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; +use anyhow::{Context, Result, bail}; +use edtui::{ + EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, +}; +use log::{error, trace}; +use ratatui::buffer::Buffer; +use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use ratatui::layout::{Alignment, Rect}; +use ratatui::prelude::{Color, Style}; +use ratatui::widgets::{Block, Borders, Padding, Widget}; +use syntect::parsing::SyntaxSet; + +pub struct Editor { + pub state: EditorState, + pub event_handler: EditorEventHandler, + pub file_path: Option, + syntax_set: SyntaxSet, + pub(crate) component_state: ComponentState, +} + +impl Editor { + pub fn id() -> &'static str { + "Editor" + } + + pub fn new(path: &std::path::PathBuf) -> Self { + trace!(target:Self::id(), "Building {}", Self::id()); + Editor { + state: EditorState::default(), + event_handler: EditorEventHandler::default(), + file_path: Some(path.to_owned()), + syntax_set: SyntaxSet::load_defaults_nonewlines(), + component_state: ComponentState::default().with_help_text(concat!( + "CTRL+S: Save file | ALT+(←/h): Previous tab | ALT+(l/→): Next tab |", + " All other input is handled by vim" + )), + } + } + + pub fn reload_contents(&mut self) -> Result<()> { + trace!(target:Self::id(), "Reloading editor file contents {:?}", self.file_path); + match self.file_path.clone() { + None => { + error!(target:Self::id(), "Failed to reload editor contents with None file_path"); + bail!("Failed to reload editor contents with None file_path") + } + Some(path) => self.set_contents(&path), + } + } + + pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> { + trace!(target:Self::id(), "Setting Editor contents from path {:?}", path); + if let Ok(contents) = std::fs::read_to_string(path) { + let lines: Vec<_> = contents + .lines() + .map(|line| line.chars().collect::>()) + .collect(); + self.file_path = Some(path.clone()); + self.state.lines = Lines::new(lines); + self.state.cursor.row = 0; + self.state.cursor.col = 0; + } + Ok(()) + } + + pub fn save(&self) -> Result<()> { + if let Some(path) = &self.file_path { + trace!(target:Self::id(), "Saving Editor contents {:?}", path); + return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into()); + }; + error!(target:Self::id(), "Failed saving Editor contents; file_path was None"); + bail!("File not saved. No file path set.") + } +} + +impl Widget for &mut Editor { + fn render(self, area: Rect, buf: &mut Buffer) { + let lang = self + .file_path + .as_ref() + .and_then(|p| p.extension()) + .map(|e| e.to_str().unwrap_or("md")) + .unwrap_or("md"); + let lang_name = self + .syntax_set + .find_syntax_by_extension(lang) + .map(|s| s.name.to_string()) + .unwrap_or("Unknown".to_string()); + + EditorView::new(&mut self.state) + .wrap(true) + .theme( + EditorTheme::default().block( + Block::default() + .title(lang_name.to_owned()) + .title_style(Style::default().fg(Color::Yellow)) + .title_alignment(Alignment::Right) + .borders(Borders::ALL) + .padding(Padding::new(0, 0, 0, 1)) + .style(Style::default().fg(self.component_state.get_active_color())), + ), + ) + .syntax_highlighter(SyntaxHighlighter::new("dracula", lang).ok()) + .tab_width(2) + .line_numbers(LineNumbers::Absolute) + .render(area, buf); + } +} + +impl Component for Editor { + fn handle_event(&mut self, event: Event) -> Result { + if let Some(key_event) = event.as_key_event() { + // Handle events here that should not be passed on to the vim emulation handler. + match self.handle_key_events(key_event)? { + Action::Handled => return Ok(Action::Handled), + _ => {} + } + } + self.event_handler.on_event(event, &mut self.state); + Ok(Action::Pass) + } + + /// The events for the vim emulation should be handled by EditorEventHandler::on_event. + /// These events are custom to the clide application. + fn handle_key_events(&mut self, key: KeyEvent) -> Result { + match key { + KeyEvent { + code: KeyCode::Char('s'), + modifiers: KeyModifiers::CONTROL, + .. + } => { + self.save().context("Failed to save file.")?; + Ok(Action::Handled) + } + // For other events not handled here, pass to the vim emulation handler. + _ => Ok(Action::Noop), + } + } + + fn is_active(&self) -> bool { + self.component_state.focus == Focus::Active + } +} diff --git a/src/tui/editor_tab.rs b/src/tui/editor_tab.rs new file mode 100644 index 0000000..1a5fb01 --- /dev/null +++ b/src/tui/editor_tab.rs @@ -0,0 +1,250 @@ +use crate::tui::component::{Action, Component, Focus, FocusState}; +use crate::tui::editor::Editor; +use anyhow::{Context, Result, anyhow}; +use log::{error, info, trace, warn}; +use ratatui::buffer::Buffer; +use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use ratatui::layout::Rect; +use ratatui::prelude::{Color, Style}; +use ratatui::widgets::{Block, Borders, Padding, Tabs, Widget}; +use std::collections::HashMap; + +// Render the tabs with keys as titles +// Tab keys can be file names. +// Render the editor using the key as a reference for lookup +pub struct EditorTab { + pub(crate) editors: HashMap, + tab_order: Vec, + current_editor: usize, +} + +impl EditorTab { + fn id() -> &'static str { + "EditorTab" + } + + pub fn new(path: Option<&std::path::PathBuf>) -> Self { + trace!(target:Self::id(), "Building EditorTab with path {path:?}"); + match path { + None => Self { + editors: HashMap::new(), + tab_order: Vec::new(), + current_editor: 0, + }, + Some(path) => { + let tab_order = vec![path.to_string_lossy().to_string()]; + Self { + editors: HashMap::from([( + tab_order.first().unwrap().to_owned(), + Editor::new(path), + )]), + tab_order, + current_editor: 0, + } + } + } + } + + pub fn next_editor(&mut self) { + let next = (self.current_editor + 1) % self.tab_order.len(); + trace!(target:Self::id(), "Moving from {} to next editor tab at {}", self.current_editor, next); + self.set_tab_focus(Focus::Active, next); + self.current_editor = next; + } + + pub fn prev_editor(&mut self) { + let prev = self + .current_editor + .checked_sub(1) + .unwrap_or(self.tab_order.len() - 1); + trace!(target:Self::id(), "Moving from {} to previous editor tab at {}", self.current_editor, prev); + self.set_tab_focus(Focus::Active, prev); + self.current_editor = prev; + } + + pub fn get_editor_key(&self, index: usize) -> Option { + match self.tab_order.get(index) { + None => { + if !self.tab_order.is_empty() { + error!(target:Self::id(), "Failed to get editor tab key with invalid index {index}"); + } + None + } + Some(key) => Some(key.to_owned()), + } + } + + pub fn current_editor(&self) -> Option<&Editor> { + self.editors.get(&self.get_editor_key(self.current_editor)?) + } + + pub fn current_editor_mut(&mut self) -> Option<&mut Editor> { + self.editors + .get_mut(&self.get_editor_key(self.current_editor)?) + } + + pub fn set_current_tab_focus(&mut self, focus: Focus) { + trace!(target:Self::id(), "Setting current tab {} focus to {:?}", self.current_editor, focus); + self.set_tab_focus(focus, self.current_editor) + } + + pub fn set_tab_focus(&mut self, focus: Focus, index: usize) { + trace!(target:Self::id(), "Setting tab {} focus to {:?}", index, focus); + if focus == Focus::Active && index != self.current_editor { + // If we are setting another tab to active, disable the current one. + trace!( + target:Self::id(), + "New tab {} focus set to Active; Setting current tab {} to Inactive", + index, + self.current_editor + ); + self.set_current_tab_focus(Focus::Inactive); + } + match self.get_editor_key(index) { + None => { + error!(target:Self::id(), "Failed setting tab focus for invalid key {index}"); + } + Some(key) => match self.editors.get_mut(&key) { + None => { + error!( + target:Self::id(), + "Failed to update tab focus at index {} with invalid key: {}", + self.current_editor, + self.tab_order[self.current_editor] + ) + } + Some(editor) => editor.component_state.set_focus(focus), + }, + } + } + + pub fn open_tab(&mut self, path: &std::path::PathBuf) -> Result<()> { + trace!(target:Self::id(), "Opening new EditorTab with path {:?}", path); + if self + .editors + .contains_key(&path.to_string_lossy().to_string()) + { + warn!(target:Self::id(), "EditorTab already opened with this file"); + return Ok(()); + } + + let path_str = path.to_string_lossy().to_string(); + self.tab_order.push(path_str.clone()); + let mut editor = Editor::new(path); + editor.set_contents(path).context("Failed to open tab")?; + self.editors.insert(path_str, editor); + self.current_editor = self.tab_order.len() - 1; + Ok(()) + } + + pub fn close_current_tab(&mut self) -> Result<()> { + self.close_tab(self.current_editor) + } + + pub fn close_tab(&mut self, index: usize) -> Result<()> { + let key = self + .tab_order + .get(index) + .ok_or(anyhow!( + "Failed to get tab order with invalid index {index}" + ))? + .to_owned(); + match self.editors.remove(&key) { + None => { + error!(target:Self::id(), "Failed to remove editor tab {key} with invalid index {index}") + } + Some(_) => { + self.prev_editor(); + self.tab_order.remove(index); + info!(target:Self::id(), "Closed editor tab {key} at index {index}") + } + } + Ok(()) + } + + pub fn is_empty(&self) -> bool { + self.editors.is_empty() + } + + pub fn render(&mut self, tabs_area: Rect, editor_area: Rect, buf: &mut Buffer) { + // TODO: Only file name is displayed in tab title, so files with the same name in different + // directories will appear confusing. + let tab_titles = self.tab_order.iter().map(|t| { + std::path::PathBuf::from(t) + .file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or("Unknown".to_string()) + }); + // Don't set border color based on ComponentState::focus, the Editor renders the border. + Tabs::new(tab_titles) + .select(self.current_editor) + .divider("|") + .block( + Block::default() + .borders(Borders::NONE) + .padding(Padding::new(0, 0, 0, 0)), + ) + .highlight_style(Style::default().fg(Color::LightRed)) + .render(tabs_area, buf); + Widget::render(self, editor_area, buf); + } +} + +impl Widget for &mut EditorTab { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + if let Some(editor) = self.current_editor_mut() { + editor.render(area, buf); + } + } +} + +impl Component for EditorTab { + fn handle_event(&mut self, event: Event) -> Result { + if let Some(key) = event.as_key_event() { + let action = self.handle_key_events(key)?; + match action { + Action::Quit | Action::Handled => return Ok(action), + _ => {} + } + } + if let Some(editor) = self.current_editor_mut() { + return editor.handle_event(event); + } + Ok(Action::Noop) + } + + fn handle_key_events(&mut self, key: KeyEvent) -> Result { + match key { + KeyEvent { + code: KeyCode::Char('h'), + modifiers: KeyModifiers::ALT, + .. + } + | KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::ALT, + .. + } => { + self.prev_editor(); + Ok(Action::Handled) + } + KeyEvent { + code: KeyCode::Char('l'), + modifiers: KeyModifiers::ALT, + .. + } + | KeyEvent { + code: KeyCode::Right, + modifiers: KeyModifiers::ALT, + .. + } => { + self.next_editor(); + Ok(Action::Handled) + } + _ => Ok(Action::Noop), + } + } +} diff --git a/src/tui/explorer.rs b/src/tui/explorer.rs new file mode 100644 index 0000000..14a5585 --- /dev/null +++ b/src/tui/explorer.rs @@ -0,0 +1,193 @@ +use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; +use anyhow::{Context, Result, bail}; +use log::trace; +use ratatui::buffer::Buffer; +use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; +use ratatui::layout::{Alignment, Position, Rect}; +use ratatui::prelude::Style; +use ratatui::style::{Color, Modifier}; +use ratatui::widgets::{Block, Borders, StatefulWidget, Widget}; +use std::fs; +use std::path::PathBuf; +use tui_tree_widget::{Tree, TreeItem, TreeState}; + +#[derive(Debug)] +pub struct Explorer<'a> { + pub(crate) root_path: PathBuf, + tree_items: TreeItem<'a, String>, + tree_state: TreeState, + pub(crate) component_state: ComponentState, +} + +impl<'a> Explorer<'a> { + pub fn id() -> &'static str { + "Explorer" + } + + pub fn new(path: &PathBuf) -> Result { + trace!(target:Self::id(), "Building {}", Self::id()); + let explorer = Explorer { + root_path: path.to_owned(), + tree_items: Self::build_tree_from_path(path.to_owned())?, + tree_state: TreeState::default(), + component_state: ComponentState::default().with_help_text(concat!( + "(↑/k)/(↓/j): Select item | ←/h: Close folder | →/l: Open folder |", + " Space: Open / close folder | Enter: Open file in new editor tab" + )), + }; + Ok(explorer) + } + + fn build_tree_from_path(path: PathBuf) -> Result> { + let mut children = vec![]; + if let Ok(entries) = fs::read_dir(&path) { + let mut paths = entries + .map(|res| res.map(|e| e.path())) + .collect::, std::io::Error>>() + .context(format!( + "Failed to build vector of paths under directory: {:?}", + path + ))?; + paths.sort(); + for path in paths { + if path.is_dir() { + children.push(Self::build_tree_from_path(path)?); + } else { + if let Ok(path) = std::path::absolute(&path) { + let path_str = path.to_string_lossy().to_string(); + children.push(TreeItem::new_leaf( + path_str, + path.file_name() + .context("Failed to get file name from path.")? + .to_string_lossy() + .to_string(), + )); + } + } + } + } + + let abs = std::path::absolute(&path) + .context(format!( + "Failed to find absolute path for TreeItem: {:?}", + path + ))? + .to_string_lossy() + .to_string(); + TreeItem::new( + abs, + path.file_name() + .expect("Failed to get file name from path.") + .to_string_lossy() + .to_string(), + children, + ) + .context("Failed to build tree from path.") + } + + pub fn selected(&self) -> Result { + if let Some(path) = self.tree_state.selected().last() { + return Ok(std::path::absolute(path)? + .to_str() + .context("Failed to get absolute path to selected TreeItem")? + .to_string()); + } + bail!("Failed to get selected TreeItem") + } +} + +impl<'a> Widget for &mut Explorer<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + if let Ok(tree) = Tree::new(&self.tree_items.children()) { + let file_name = self.root_path.file_name().unwrap_or("Unknown".as_ref()); + StatefulWidget::render( + tree.block( + Block::default() + .borders(Borders::ALL) + .title(file_name.to_string_lossy()) + .border_style(Style::default().fg(self.component_state.get_active_color())) + .title_style(Style::default().fg(Color::Green)) + .title_alignment(Alignment::Center), + ) + .highlight_style( + Style::new() + .fg(Color::Black) + .bg(Color::Rgb(57, 59, 64)) + .add_modifier(Modifier::BOLD), + ), + area, + buf, + &mut self.tree_state, + ); + } + } +} + +impl<'a> Component for Explorer<'a> { + fn handle_event(&mut self, event: Event) -> Result { + if let Some(key_event) = event.as_key_event() { + // Handle events here that should not be passed on to the vim emulation handler. + match self.handle_key_events(key_event)? { + Action::Handled => return Ok(Action::Handled), + Action::OpenTab => return Ok(Action::OpenTab), + _ => {} + } + } + if let Some(mouse_event) = event.as_mouse_event() { + match self.handle_mouse_events(mouse_event)? { + Action::Handled => return Ok(Action::Handled), + _ => {} + } + } + Ok(Action::Pass) + } + + fn handle_key_events(&mut self, key: KeyEvent) -> Result { + if key.code == KeyCode::Enter { + if let Ok(selected) = self.selected() { + if PathBuf::from(&selected).is_file() { + return Ok(Action::OpenTab); + } + } + // Otherwise fall through and handle Enter in the next match case. + } + + let changed = match key.code { + KeyCode::Up | KeyCode::Char('k') => self.tree_state.key_up(), + KeyCode::Down | KeyCode::Char('j') => self.tree_state.key_down(), + KeyCode::Left | KeyCode::Char('h') => { + // Do not call key_left(); Calling it on a closed folder clears the selection. + let key = self.tree_state.selected().to_owned(); + self.tree_state.close(key.as_ref()) + } + KeyCode::Char(' ') | KeyCode::Enter => self + .tree_state + .toggle(self.tree_state.selected().to_owned()), + KeyCode::Right | KeyCode::Char('l') => self.tree_state.key_right(), + _ => false, + }; + if changed { + return Ok(Action::Handled); + } + Ok(Action::Noop) + } + + fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result { + let changed = match mouse.kind { + MouseEventKind::ScrollDown => self.tree_state.scroll_down(1), + MouseEventKind::ScrollUp => self.tree_state.scroll_up(1), + MouseEventKind::Down(_button) => self + .tree_state + .click_at(Position::new(mouse.column, mouse.row)), + _ => false, + }; + if changed { + return Ok(Action::Handled); + } + Ok(Action::Noop) + } + + fn is_active(&self) -> bool { + self.component_state.focus == Focus::Active + } +} diff --git a/src/tui/logger.rs b/src/tui/logger.rs new file mode 100644 index 0000000..a0d2403 --- /dev/null +++ b/src/tui/logger.rs @@ -0,0 +1,92 @@ +use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; +use log::{LevelFilter, trace}; +use ratatui::buffer::Buffer; +use ratatui::crossterm::event::{Event, KeyCode, KeyEvent}; +use ratatui::layout::Rect; +use ratatui::style::{Color, Style}; +use ratatui::widgets::Widget; +use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetEvent, TuiWidgetState}; + +/// Any log written as info!(target:self.id(), "message") will work with this logger. +/// The logger is bound to info!, debug!, error!, trace! macros within Tui::new(). +pub struct Logger { + state: TuiWidgetState, + pub(crate) component_state: ComponentState, +} + +impl Logger { + pub fn id() -> &'static str { + "Logger" + } + + pub fn new() -> Self { + trace!(target:Self::id(), "Building {}", Self::id()); + let state = TuiWidgetState::new(); + state.transition(TuiWidgetEvent::HideKey); + Self { + state: state + .set_level_for_target("arboard::platform::linux::x11", LevelFilter::Off) + .set_level_for_target("mio::poll", LevelFilter::Off), + component_state: ComponentState::default().with_help_text(concat!( + "Space: Hide/show logging target selector panel | (↑/k)/(↓/j): Select target |", + " (←/h)/(→/l): Display level | f: Focus target | +/-: Filter level |", + " v: Toggle filtered targets visibility | PageUp/Down: Scroll | Esc: Cancel scroll" + )), + } + } +} + +impl Widget for &Logger { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + TuiLoggerSmartWidget::default() + .border_style(Style::default().fg(self.component_state.get_active_color())) + .style_error(Style::default().fg(Color::Red)) + .style_debug(Style::default().fg(Color::Green)) + .style_warn(Style::default().fg(Color::Yellow)) + .style_trace(Style::default().fg(Color::Magenta)) + .style_info(Style::default().fg(Color::Cyan)) + .output_separator(':') + .output_timestamp(Some("%H:%M:%S".to_string())) + .output_level(Some(TuiLoggerLevelOutput::Abbreviated)) + .output_target(true) + .output_file(true) + .output_line(true) + .state(&self.state) + .render(area, buf); + } +} + +impl Component for Logger { + fn handle_event(&mut self, event: Event) -> anyhow::Result { + if let Some(key_event) = event.as_key_event() { + return self.handle_key_events(key_event); + } + Ok(Action::Noop) + } + + fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result { + match key.code { + KeyCode::Char('v') => self.state.transition(TuiWidgetEvent::SpaceKey), + KeyCode::Esc => self.state.transition(TuiWidgetEvent::EscapeKey), + KeyCode::PageUp => self.state.transition(TuiWidgetEvent::PrevPageKey), + KeyCode::PageDown => self.state.transition(TuiWidgetEvent::NextPageKey), + KeyCode::Up | KeyCode::Char('k') => self.state.transition(TuiWidgetEvent::UpKey), + KeyCode::Down | KeyCode::Char('j') => self.state.transition(TuiWidgetEvent::DownKey), + KeyCode::Left | KeyCode::Char('h') => self.state.transition(TuiWidgetEvent::LeftKey), + KeyCode::Right | KeyCode::Char('l') => self.state.transition(TuiWidgetEvent::RightKey), + KeyCode::Char('+') => self.state.transition(TuiWidgetEvent::PlusKey), + KeyCode::Char('-') => self.state.transition(TuiWidgetEvent::MinusKey), + KeyCode::Char(' ') => self.state.transition(TuiWidgetEvent::HideKey), + KeyCode::Char('f') => self.state.transition(TuiWidgetEvent::FocusKey), + _ => (), + } + Ok(Action::Pass) + } + + fn is_active(&self) -> bool { + self.component_state.focus == Focus::Active + } +} diff --git a/src/tui/menu_bar.rs b/src/tui/menu_bar.rs new file mode 100644 index 0000000..bc11bb0 --- /dev/null +++ b/src/tui/menu_bar.rs @@ -0,0 +1,240 @@ +use crate::tui::component::{Action, Component, ComponentState, FocusState}; +use crate::tui::menu_bar::MenuBarItemOption::{ + About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger, +}; +use log::trace; +use ratatui::buffer::Buffer; +use ratatui::crossterm::event::{KeyCode, KeyEvent}; +use ratatui::layout::Rect; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::Line; +use ratatui::widgets::{ + Block, Borders, Clear, List, ListItem, ListState, StatefulWidget, Tabs, Widget, +}; +use strum::{EnumIter, FromRepr, IntoEnumIterator}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)] +enum MenuBarItem { + File, + View, + Help, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)] +enum MenuBarItemOption { + Save, + CloseTab, + Reload, + Exit, + ShowHideExplorer, + ShowHideLogger, + About, +} + +impl MenuBarItemOption { + fn id(&self) -> &str { + match self { + Save => "Save", + Reload => "Reload", + Exit => "Exit", + ShowHideExplorer => "Show / hide explorer", + ShowHideLogger => "Show / hide logger", + About => "About", + CloseTab => "Close tab", + } + } +} + +impl MenuBarItem { + pub fn next(self) -> Self { + let cur = self as usize; + let next = cur.saturating_add(1); + Self::from_repr(next).unwrap_or(self) + } + + pub fn prev(self) -> Self { + let cur = self as usize; + let prev = cur.saturating_sub(1); + Self::from_repr(prev).unwrap_or(self) + } + + pub fn id(&self) -> &str { + match self { + MenuBarItem::File => "File", + MenuBarItem::View => "View", + MenuBarItem::Help => "Help", + } + } + + pub fn options(&self) -> &[MenuBarItemOption] { + match self { + MenuBarItem::File => &[Save, CloseTab, Reload, Exit], + MenuBarItem::View => &[ShowHideExplorer, ShowHideLogger], + MenuBarItem::Help => &[About], + } + } +} + +pub struct MenuBar { + selected: MenuBarItem, + opened: Option, + pub(crate) component_state: ComponentState, + list_state: ListState, +} + +impl MenuBar { + pub fn id() -> &'static str { + "MenuBar" + } + + const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection"; + pub fn new() -> Self { + trace!(target:Self::id(), "Building {}", Self::id()); + Self { + selected: MenuBarItem::File, + opened: None, + component_state: ComponentState::default().with_help_text(Self::DEFAULT_HELP), + list_state: ListState::default().with_selected(Some(0)), + } + } + + fn render_title_bar(&self, area: Rect, buf: &mut Buffer) { + let titles: Vec = MenuBarItem::iter() + .map(|item| Line::from(item.id().to_owned())) + .collect(); + let tabs_style = Style::default(); + let highlight_style = if self.opened.is_some() { + Style::default().bg(Color::Blue).fg(Color::White) + } else { + Style::default().bg(Color::Cyan).fg(Color::Black) + }; + Tabs::new(titles) + .style(tabs_style) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(self.component_state.get_active_color())), + ) + .highlight_style(highlight_style) + .select(self.selected as usize) + .render(area, buf); + } + + fn render_drop_down( + &mut self, + title_bar_anchor: Rect, + area: Rect, + buf: &mut Buffer, + opened: MenuBarItem, + ) { + let popup_area = Self::rect_under_option(title_bar_anchor, area, 27, 10); + Clear::default().render(popup_area, buf); + let options = opened.options().iter().map(|i| ListItem::new(i.id())); + StatefulWidget::render( + List::new(options) + .block(Block::bordered().title(self.selected.id())) + .highlight_style( + Style::default() + .bg(Color::Blue) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "), + popup_area, + buf, + &mut self.list_state, + ); + } + + fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect { + let rect = Rect { + x: anchor.x, + y: anchor.y + anchor.height, + width: width.min(area.width), + height, + }; + // TODO: X offset for item option? It's fine as-is, but it might look nicer. + // trace!(target:Self::id(), "Building Rect under MenuBar popup {}", rect); + rect + } +} + +impl Widget for &mut MenuBar { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let title_bar_area = Rect { + x: area.x, + y: area.y, + width: area.width, + height: 3, + }; + self.render_title_bar(title_bar_area, buf); + if let Some(opened) = self.opened { + self.render_drop_down(title_bar_area, area, buf, opened); + } + } +} + +impl Component for MenuBar { + fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result { + if self.opened.is_some() { + // Keybinds for popup menu. + match key.code { + KeyCode::Up | KeyCode::Char('k') => { + self.list_state.select_previous(); + Ok(Action::Handled) + } + KeyCode::Down | KeyCode::Char('j') => { + self.list_state.select_next(); + Ok(Action::Handled) + } + KeyCode::Enter => { + if let Some(selected) = self.list_state.selected() { + let selection = self.selected.options()[selected]; + return match selection { + Save => Ok(Action::Save), + Exit => Ok(Action::Quit), + Reload => Ok(Action::ReloadFile), + ShowHideExplorer => Ok(Action::ShowHideExplorer), + ShowHideLogger => Ok(Action::ShowHideLogger), + About => Ok(Action::ShowHideAbout), + CloseTab => Ok(Action::CloseTab), + }; + } + Ok(Action::Noop) + } + KeyCode::Esc | KeyCode::Char('q') => { + self.opened = None; + self.component_state.help_text = Self::DEFAULT_HELP.to_string(); + self.list_state.select_first(); + Ok(Action::Handled) + } + _ => Ok(Action::Noop), + } + } else { + // Keybinds for title bar. + match key.code { + KeyCode::Left | KeyCode::Char('h') => { + self.selected = self.selected.prev(); + Ok(Action::Handled) + } + KeyCode::Right | KeyCode::Char('l') => { + self.selected = self.selected.next(); + Ok(Action::Handled) + } + KeyCode::Enter => { + self.opened = Some(self.selected); + self.component_state.help_text = concat!( + "(↑/k)/(↓/j): Select option | Enter: Choose selection |", + " ESC/Q: Close drop-down menu" + ) + .to_string(); + Ok(Action::Handled) + } + _ => Ok(Action::Noop), + } + } + } +}