From 296b987e3b4bdd680d9eb1df667bf845688bbc82 Mon Sep 17 00:00:00 2001 From: Marumarsudev Date: Thu, 15 Oct 2026 16:22:00 +0300 Subject: [PATCH] Initial commit --- .gitignore | 5 + Cargo.lock | 2298 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 + assets/ratbin.desktop | 11 + packaging/arch/PKGBUILD | 35 + src/appimage.rs | 150 +++ src/config.rs | 69 ++ src/desktop.rs | 232 ++++ src/detect.rs | 76 ++ src/fastfetch.rs | 56 + src/main.rs | 174 +++ src/store.rs | 322 ++++++ src/thunar.rs | 131 +++ src/tui.rs | 463 ++++++++ 14 files changed, 4035 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 assets/ratbin.desktop create mode 100644 packaging/arch/PKGBUILD create mode 100644 src/appimage.rs create mode 100644 src/config.rs create mode 100644 src/desktop.rs create mode 100644 src/detect.rs create mode 100644 src/fastfetch.rs create mode 100644 src/main.rs create mode 100644 src/store.rs create mode 100644 src/thunar.rs create mode 100644 src/tui.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c033b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target + +/packaging/arch/src/ +/packaging/arch/pkg/ +/packaging/arch/*.pkg.tar* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d5472c0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2298 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +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", +] + +[[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", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backhand" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb8799e91f66c730e3ae50e8a260d63401e9479f236fb74f793d7656f143f0c" +dependencies = [ + "deku", + "flate2", + "liblzma", + "liblzma-sys", + "lz4_flex", + "no_std_io2", + "rayon", + "solana-nohash-hasher", + "thiserror 2.0.21", + "tracing", + "xxhash-rust", + "zstd", + "zstd-safe", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[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.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[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.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.2", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +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 = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core 0.24.1", + "darling_macro 0.24.1", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.9", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core 0.24.1", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "deku" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf55291257a2a5c90cf50ae17b6bbaabc3fd13642cf3895a71c412513c19630" +dependencies = [ + "bitvec", + "deku_derive", + "no_std_io2", + "rustversion", +] + +[[package]] +name = "deku_derive" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec2a42b511fc5efd9183f4f71c17885d627b17e7fd9a61a92089406caa4397e" +dependencies = [ + "darling 0.21.3", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[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 = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +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", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "finl_unicode" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb028c8b4148c9ee0cca68fcd9add6044e81d3619f48577ddf13a263d047a2" + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "zlib-rs", +] + +[[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 = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[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 = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[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 = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +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 = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "indoc" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20" +dependencies = [ + "darling 0.24.1", + "indoc", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.21", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "liblzma" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fe0a34ca854fd4f20c07f696fc8675aec78f87d88d29f5e10257a7490a1b2e1" +dependencies = [ + "liblzma-sys", + "num_cpus", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e3581f367a78d7b7e7ae948d023310556f0cfc13156c2e4e00e25616492b9" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "line-clipping" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "lz4_flex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226" + +[[package]] +name = "mac_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0581a75c45969c63afd2bb87b1d8b25cfcc556c7a918cf9bb13b0fe8d2381" +dependencies = [ + "nix 0.31.3", + "windows-sys", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[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 = "palette" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "libm", + "palette_derive", + "palette_math", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + +[[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 = "pest" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa" +dependencies = [ + "memchr", + "psm", + "stacker", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "pest_meta" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1" +dependencies = [ + "pest", +] + +[[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.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags 2.13.2", + "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru", + "palette", + "serde", + "strum", + "thiserror 2.0.21", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags 2.13.2", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "ratbin" +version = "0.1.0" +dependencies = [ + "anyhow", + "backhand", + "clap", + "dirs", + "ratatui", + "serde", + "toml", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[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]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "solana-nohash-hasher" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.2", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys", +] + +[[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.13.2", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "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 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[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.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "atomic", + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550a2b930b62486a393c52d5c3b84bff264b28aa437ed64694d31e93b1757af7" + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.1+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeec9eaf2dffbbd09201e23bd0ffcbaa33bb8e9266a10734fd7ed90a85eca078" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f3ad250 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ratbin" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.104" +backhand = "0.25.5" +clap = { version = "4.6.7", features = ["derive"] } +dirs = "7.0.0" +ratatui = "0.30.2" +serde = { version = "1.0.229", features = ["derive"] } +toml = "1.1.8" diff --git a/assets/ratbin.desktop b/assets/ratbin.desktop new file mode 100644 index 0000000..058cb3b --- /dev/null +++ b/assets/ratbin.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Name=Ratbin +GenericName=Application Integration Manager +Comment=Manage menu entries for AppImages, binaries and scripts +Exec=kitty --class ratbin --title Ratbin ratbin +Icon=application-x-executable +Terminal=false +StartupWMClass=ratbin +Categories=System; +Keywords=appimage;desktop;launcher;integration; diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..325b17e --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,35 @@ +pkgname=ratbin +pkgver=0.1.0 +pkgrel=1 +pkgdesc="Integrate AppImages, binaries and scripts into the application menu via the Thunar context menu" +arch=('x86_64') +license=('unknown') +depends=('gcc-libs' 'glibc') +makedepends=('cargo') +optdepends=( + 'kitty: terminal used by the Ratbin menu entry' + 'thunar: context menu integration' + 'desktop-file-utils: refresh the menu cache after changes' + 'libnotify: notifications from context menu actions' +) +options=('!lto') + +_root="$startdir/../.." + +prepare() { + cd "$_root" + export RUSTUP_TOOLCHAIN=stable + cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" +} + +build() { + cd "$_root" + export RUSTUP_TOOLCHAIN=stable + export CARGO_TARGET_DIR="$srcdir/target" + cargo build --frozen --release +} + +package() { + install -Dm755 "$srcdir/target/release/ratbin" "$pkgdir/usr/bin/ratbin" + install -Dm644 "$_root/assets/ratbin.desktop" "$pkgdir/usr/share/applications/ratbin.desktop" +} diff --git a/src/appimage.rs b/src/appimage.rs new file mode 100644 index 0000000..d63848a --- /dev/null +++ b/src/appimage.rs @@ -0,0 +1,150 @@ +use std::{ + collections::HashMap, + ffi::OsStr, + fs::File, + io::{BufReader, Read}, + path::{Component, Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use backhand::{FilesystemReader, InnerNode, SquashfsFileReader}; + +use crate::desktop::DesktopEntry; + +pub struct Icon { + pub data: Vec, + pub ext: &'static str, +} + +pub struct Embedded { + pub desktop: Option, + pub icon: Option, +} + +pub fn read(path: &Path, offset: u64) -> Result { + let file = BufReader::new(File::open(path)?); + let fs = FilesystemReader::from_reader_with_offset(file, offset).with_context(|| { + format!( + "cannot read the squashfs image embedded in {}", + path.display() + ) + })?; + let image = Image::new(&fs); + let desktop = image.root_desktop(); + let icon_name = desktop.as_ref().and_then(|d| d.get("Icon")); + let icon = image.icon(icon_name.as_deref()); + Ok(Embedded { desktop, icon }) +} + +struct Image<'a, 'b> { + fs: &'a FilesystemReader<'b>, + nodes: HashMap>, +} + +impl<'a, 'b> Image<'a, 'b> { + fn new(fs: &'a FilesystemReader<'b>) -> Self { + let nodes = fs + .files() + .map(|node| (normalize(&node.fullpath), &node.inner)) + .collect(); + Self { fs, nodes } + } + + fn read(&self, path: &Path) -> Option> { + let mut path = normalize(path); + for _ in 0..16 { + match self.nodes.get(&path)? { + InnerNode::File(file) => { + let mut data = Vec::new(); + self.fs.file(file).reader().read_to_end(&mut data).ok()?; + return Some(data); + } + InnerNode::Symlink(symlink) => { + let parent = path.parent().unwrap_or(Path::new("")); + path = normalize(&parent.join(&symlink.link)); + } + _ => return None, + } + } + None + } + + fn root_desktop(&self) -> Option { + let mut candidates: Vec<&PathBuf> = self + .nodes + .keys() + .filter(|p| p.components().count() == 1 && p.extension() == Some(OsStr::new("desktop"))) + .collect(); + candidates.sort(); + let data = self.read(candidates.first()?)?; + Some(DesktopEntry::parse(&String::from_utf8_lossy(&data))) + } + + fn icon(&self, name: Option<&str>) -> Option { + let mut candidates = Vec::new(); + if let Some(name) = name.filter(|n| !n.is_empty()) { + let name = Path::new(name) + .file_name() + .and_then(OsStr::to_str) + .unwrap_or(name); + candidates.push(PathBuf::from(name)); + for ext in ["svg", "png", "xpm"] { + candidates.push(PathBuf::from(format!("{name}.{ext}"))); + } + candidates.extend(self.hicolor(name)); + } + candidates.push(PathBuf::from(".DirIcon")); + candidates + .iter() + .find_map(|path| self.read(path).and_then(icon_from)) + } + + fn hicolor(&self, name: &str) -> Vec { + let mut found: Vec<(u32, PathBuf)> = self + .nodes + .keys() + .filter_map(|path| { + let rest = path.strip_prefix("usr/share/icons/hicolor").ok()?; + if path.file_stem()? != OsStr::new(name) { + return None; + } + let size = rest.components().next()?.as_os_str().to_str()?; + let rank = if size == "scalable" { + u32::MAX + } else { + size.split('x').next()?.parse().ok()? + }; + Some((rank, path.clone())) + }) + .collect(); + found.sort_by_key(|(rank, _)| std::cmp::Reverse(*rank)); + found.into_iter().map(|(_, path)| path).collect() + } +} + +fn icon_from(data: Vec) -> Option { + let ext = if data.starts_with(b"\x89PNG") { + "png" + } else if data.starts_with(b"/* XPM */") { + "xpm" + } else if String::from_utf8_lossy(&data[..data.len().min(4096)]).contains(" PathBuf { + let mut out = PathBuf::new(); + for component in path.components() { + match component { + Component::Normal(part) => out.push(part), + Component::ParentDir => { + out.pop(); + } + _ => {} + } + } + out +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..45c0e23 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,69 @@ +use std::{fs, path::PathBuf}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(default)] +pub struct Config { + pub apps_dir: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + apps_dir: "~/Applications".into(), + } + } +} + +impl Config { + pub fn path() -> Result { + Ok(dirs::config_dir() + .context("cannot determine the config directory")? + .join("ratbin/config.toml")) + } + + pub fn load() -> Result { + let path = Self::path()?; + if !path.exists() { + let config = Self::default(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&path, toml::to_string_pretty(&config)?) + .with_context(|| format!("cannot write {}", path.display()))?; + return Ok(config); + } + let text = + fs::read_to_string(&path).with_context(|| format!("cannot read {}", path.display()))?; + toml::from_str(&text).with_context(|| format!("invalid config in {}", path.display())) + } + + pub fn apps_dir(&self) -> Result { + expand_home(&self.apps_dir) + } +} + +pub fn applications_dir() -> Result { + Ok(data_dir()?.join("applications")) +} + +pub fn icons_dir() -> Result { + Ok(data_dir()?.join("ratbin/icons")) +} + +fn data_dir() -> Result { + dirs::data_dir().context("cannot determine the data directory") +} + +fn expand_home(path: &str) -> Result { + let home = || dirs::home_dir().context("cannot determine the home directory"); + if path == "~" { + return home(); + } + if let Some(rest) = path.strip_prefix("~/") { + return Ok(home()?.join(rest)); + } + Ok(PathBuf::from(path)) +} diff --git a/src/desktop.rs b/src/desktop.rs new file mode 100644 index 0000000..09f78a6 --- /dev/null +++ b/src/desktop.rs @@ -0,0 +1,232 @@ +use std::{fmt, path::Path}; + +const MAIN: &str = "Desktop Entry"; + +#[derive(Clone, Debug)] +pub struct DesktopEntry { + groups: Vec, +} + +#[derive(Clone, Debug)] +struct Group { + name: Option, + lines: Vec, +} + +#[derive(Clone, Debug)] +enum Line { + Pair(String, String), + Raw(String), +} + +impl DesktopEntry { + pub fn new() -> Self { + Self { + groups: vec![Group { + name: Some(MAIN.into()), + lines: Vec::new(), + }], + } + } + + pub fn parse(text: &str) -> Self { + let mut groups = vec![Group { + name: None, + lines: Vec::new(), + }]; + for line in text.lines() { + let trimmed = line.trim(); + let current = groups.last_mut().expect("groups is never empty"); + if trimmed.starts_with('[') && trimmed.ends_with(']') { + groups.push(Group { + name: Some(trimmed[1..trimmed.len() - 1].to_string()), + lines: Vec::new(), + }); + } else if !trimmed.starts_with('#') + && let Some((key, value)) = line.split_once('=') + { + current + .lines + .push(Line::Pair(key.trim().into(), value.trim_start().into())); + } else { + current.lines.push(Line::Raw(line.into())); + } + } + if groups[0].lines.is_empty() { + groups.remove(0); + } + Self { groups } + } + + pub fn get(&self, key: &str) -> Option { + self.group(MAIN)?.lines.iter().find_map(|line| match line { + Line::Pair(k, v) if k == key => Some(unescape(v)), + _ => None, + }) + } + + pub fn set(&mut self, key: &str, value: &str) { + let value = escape(value); + let group = self.main_mut(); + for line in &mut group.lines { + if let Line::Pair(k, v) = line + && k == key + { + *v = value; + return; + } + } + group.lines.push(Line::Pair(key.into(), value)); + } + + pub fn remove(&mut self, key: &str) { + self.main_mut() + .lines + .retain(|line| !matches!(line, Line::Pair(k, _) if k == key)); + } + + pub fn remove_localized(&mut self, key: &str) { + self.main_mut().lines.retain(|line| match line { + Line::Pair(k, _) => !(k.starts_with(key) && k[key.len()..].starts_with('[')), + Line::Raw(_) => true, + }); + } + + pub fn map_values(&mut self, key: &str, f: impl Fn(&str) -> String) { + for group in &mut self.groups { + for line in &mut group.lines { + if let Line::Pair(k, v) = line + && k == key + { + *v = escape(&f(&unescape(v))); + } + } + } + } + + fn group(&self, name: &str) -> Option<&Group> { + self.groups.iter().find(|g| g.name.as_deref() == Some(name)) + } + + fn main_mut(&mut self) -> &mut Group { + let index = match self + .groups + .iter() + .position(|g| g.name.as_deref() == Some(MAIN)) + { + Some(index) => index, + None => { + let at = usize::from(self.groups.first().is_some_and(|g| g.name.is_none())); + self.groups.insert( + at, + Group { + name: Some(MAIN.into()), + lines: Vec::new(), + }, + ); + at + } + }; + &mut self.groups[index] + } +} + +impl fmt::Display for DesktopEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for group in &self.groups { + if let Some(name) = &group.name { + writeln!(f, "[{name}]")?; + } + for line in &group.lines { + match line { + Line::Pair(k, v) => writeln!(f, "{k}={v}")?, + Line::Raw(raw) => writeln!(f, "{raw}")?, + } + } + } + Ok(()) + } +} + +fn unescape(value: &str) -> String { + let mut out = String::with_capacity(value.len()); + let mut chars = value.chars(); + while let Some(c) = chars.next() { + if c != '\\' { + out.push(c); + continue; + } + match chars.next() { + Some('s') => out.push(' '), + Some('n') => out.push('\n'), + Some('t') => out.push('\t'), + Some('r') => out.push('\r'), + Some('\\') => out.push('\\'), + Some(other) => { + out.push('\\'); + out.push(other); + } + None => out.push('\\'), + } + } + out +} + +fn escape(value: &str) -> String { + value + .replace('\\', "\\\\") + .replace('\n', "\\n") + .replace('\t', "\\t") + .replace('\r', "\\r") +} + +pub fn quote_exec_arg(arg: &str) -> String { + const RESERVED: &[char] = &[ + ' ', '\t', '\n', '"', '\'', '\\', '>', '<', '~', '|', '&', ';', '$', '*', '?', '#', '(', + ')', '`', + ]; + let arg = arg.replace('%', "%%"); + if !arg.is_empty() && !arg.contains(RESERVED) { + return arg; + } + let mut out = String::from('"'); + for c in arg.chars() { + if matches!(c, '"' | '`' | '$' | '\\') { + out.push('\\'); + } + out.push(c); + } + out.push('"'); + out +} + +pub fn exec_args(exec: &str) -> &str { + let exec = exec.trim_start(); + let end = match exec.strip_prefix('"') { + Some(rest) => { + let mut escaped = false; + rest.char_indices() + .find_map(|(i, c)| { + match c { + _ if escaped => escaped = false, + '\\' => escaped = true, + '"' => return Some(i + 2), + _ => {} + } + None + }) + .unwrap_or(exec.len()) + } + None => exec.find(char::is_whitespace).unwrap_or(exec.len()), + }; + exec[end..].trim() +} + +pub fn build_exec(program: &Path, args: &str) -> String { + let program = quote_exec_arg(&program.to_string_lossy()); + if args.is_empty() { + program + } else { + format!("{program} {args}") + } +} diff --git a/src/detect.rs b/src/detect.rs new file mode 100644 index 0000000..60650ab --- /dev/null +++ b/src/detect.rs @@ -0,0 +1,76 @@ +use std::{fs::File, io::Read, path::Path}; + +use anyhow::{Context, Result, bail}; + +pub enum Kind { + AppImage(Option), + Elf, + Script, +} + +impl Kind { + pub fn label(&self) -> &'static str { + match self { + Kind::AppImage(_) => "AppImage", + Kind::Elf => "ELF", + Kind::Script => "Script", + } + } + + pub fn fallback_icon(&self) -> &'static str { + match self { + Kind::AppImage(_) | Kind::Elf => "application-x-executable", + Kind::Script => "text-x-script", + } + } +} + +pub fn detect(path: &Path) -> Result { + let mut header = Vec::with_capacity(64); + File::open(path) + .with_context(|| format!("cannot open {}", path.display()))? + .take(64) + .read_to_end(&mut header)?; + + if header.starts_with(b"#!") { + return Ok(Kind::Script); + } + if !header.starts_with(b"\x7fELF") { + bail!( + "{} is not an AppImage, ELF binary or script", + path.display() + ); + } + let named_appimage = path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("appimage")); + Ok(match header.get(8..11) { + Some(b"AI\x02") => Kind::AppImage(elf_end(&header)), + Some(b"AI\x01") => Kind::AppImage(None), + _ if named_appimage => Kind::AppImage(elf_end(&header)), + _ => Kind::Elf, + }) +} + +fn elf_end(header: &[u8]) -> Option { + let little_endian = match header.get(5)? { + 1 => true, + 2 => false, + _ => return None, + }; + let read = |offset: usize, len: usize| -> Option { + let bytes = header.get(offset..offset + len)?; + let fold = |acc: u64, &b: &u8| (acc << 8) | u64::from(b); + Some(if little_endian { + bytes.iter().rev().fold(0, fold) + } else { + bytes.iter().fold(0, fold) + }) + }; + let (shoff, shentsize, shnum) = match header.get(4)? { + 1 => (read(0x20, 4)?, read(0x2E, 2)?, read(0x30, 2)?), + 2 => (read(0x28, 8)?, read(0x3A, 2)?, read(0x3C, 2)?), + _ => return None, + }; + Some(shoff + shentsize * shnum) +} diff --git a/src/fastfetch.rs b/src/fastfetch.rs new file mode 100644 index 0000000..d7a0b7f --- /dev/null +++ b/src/fastfetch.rs @@ -0,0 +1,56 @@ +use std::{path::Path, process::Command}; + +use anyhow::Result; + +use crate::store; + +const SCANNED_DIRS: [&str; 2] = ["AppImages", "Applications"]; + +pub fn packages_line() -> Result { + let integrations = store::list()?; + let home = dirs::home_dir().unwrap_or_default(); + let counted_twice = integrations + .iter() + .filter(|i| counted_by_fastfetch(&i.source(), &home)) + .count(); + + let mut parts: Vec = fastfetch_packages() + .split(", ") + .filter(|part| !part.is_empty()) + .filter_map(|part| match part.strip_suffix(" (appimage)") { + Some(count) => { + let count = count.parse::().ok()?.saturating_sub(counted_twice); + (count > 0).then(|| format!("{count} (appimage)")) + } + None => Some(part.to_string()), + }) + .collect(); + parts.push(format!("{} (ratbin)", integrations.len())); + Ok(parts.join(", ")) +} + +fn fastfetch_packages() -> String { + let Ok(output) = Command::new("fastfetch") + .args(["--config", "none", "--logo", "none", "--pipe", "true"]) + .args(["--structure", "packages"]) + .output() + else { + return String::new(); + }; + let text = String::from_utf8_lossy(&output.stdout); + let line = text.lines().next().unwrap_or_default(); + line.split_once(": ") + .map_or(line, |(_, counts)| counts) + .trim() + .to_string() +} + +fn counted_by_fastfetch(source: &Path, home: &Path) -> bool { + source.is_file() + && source + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("appimage")) + && source + .parent() + .is_some_and(|dir| SCANNED_DIRS.iter().any(|d| dir == home.join(d))) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5118fd6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,174 @@ +mod appimage; +mod config; +mod desktop; +mod detect; +mod fastfetch; +mod store; +mod thunar; +mod tui; + +use std::{ + path::{Path, PathBuf}, + process::{Command as Process, ExitCode}, +}; + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +use crate::config::Config; + +#[derive(Parser)] +#[command( + version, + about = "Integrate AppImages, binaries and scripts into the application menu" +)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Command { + /// Move files into the applications folder and create menu entries + Integrate { + /// Report the result as a desktop notification + #[arg(long)] + notify: bool, + #[arg(required = true)] + files: Vec, + }, + /// Remove the menu entries of integrated files + Remove { + #[arg(long)] + notify: bool, + /// Also delete the files themselves + #[arg(long)] + delete: bool, + #[arg(required = true)] + files: Vec, + }, + /// List integrated files + List, + /// Open the interactive manager (default) + Tui, + /// Print fastfetch's package counts with ratbin integrations added + Fastfetch, + /// Manage the Thunar context menu actions + Thunar { + #[command(subcommand)] + action: ThunarAction, + }, +} + +#[derive(Subcommand)] +enum ThunarAction { + /// Add the ratbin actions to Thunar's context menu + Install, + /// Remove the ratbin actions from Thunar's context menu + Uninstall, +} + +fn main() -> ExitCode { + match run(Cli::parse()) { + Ok(code) => code, + Err(e) => { + eprintln!("error: {e:#}"); + ExitCode::FAILURE + } + } +} + +fn run(cli: Cli) -> Result { + match cli.command.unwrap_or(Command::Tui) { + Command::Integrate { notify, files } => Ok(batch("Integrated", notify, &files, |file| { + let apps_dir = Config::load()?.apps_dir()?; + store::integrate(&apps_dir, file).map(|i| i.name()) + })), + Command::Remove { + notify, + delete, + files, + } => Ok(batch("Removed", notify, &files, |file| { + store::remove_path(file, delete) + })), + Command::List => { + for integration in store::list()? { + println!( + "{}\t{}\t{}", + integration.name(), + integration.kind(), + integration.source().display() + ); + } + Ok(ExitCode::SUCCESS) + } + Command::Fastfetch => { + println!("{}", fastfetch::packages_line()?); + Ok(ExitCode::SUCCESS) + } + Command::Tui => { + tui::run()?; + Ok(ExitCode::SUCCESS) + } + Command::Thunar { action } => { + match action { + ThunarAction::Install => { + let path = thunar::install()?; + println!("Added the ratbin actions to {}", path.display()); + } + ThunarAction::Uninstall => { + let path = thunar::uninstall()?; + println!("Removed the ratbin actions from {}", path.display()); + } + } + println!("Restart Thunar (thunar -q) to apply."); + Ok(ExitCode::SUCCESS) + } + } +} + +fn batch( + verb: &str, + notify: bool, + files: &[PathBuf], + op: impl Fn(&Path) -> Result, +) -> ExitCode { + let mut done = Vec::new(); + let mut failed = Vec::new(); + for file in files { + match op(file) { + Ok(name) => { + println!("{verb}: {name}"); + done.push(name); + } + Err(e) => { + eprintln!("error: {e:#}"); + failed.push(format!("{e:#}")); + } + } + } + if notify { + if !done.is_empty() { + send_notification(&format!("{verb} {}", done.join(", ")), "", false); + } + if !failed.is_empty() { + send_notification("ratbin", &failed.join("\n"), true); + } + } + if failed.is_empty() { + ExitCode::SUCCESS + } else { + ExitCode::FAILURE + } +} + +fn send_notification(summary: &str, body: &str, error: bool) { + let icon = if error { + "dialog-error" + } else { + "application-x-executable" + }; + let _ = Process::new("notify-send") + .args(["--app-name=ratbin", "--icon", icon, summary, body]) + .status(); +} diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..8f08dcd --- /dev/null +++ b/src/store.rs @@ -0,0 +1,322 @@ +use std::{ + fs, io, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use anyhow::{Context, Result, bail}; + +use crate::{ + appimage, config, + desktop::{self, DesktopEntry}, + detect::{self, Kind}, +}; + +const KEY_SOURCE: &str = "X-Ratbin-Source"; +const KEY_ARGS: &str = "X-Ratbin-Args"; +const KEY_KIND: &str = "X-Ratbin-Kind"; +const PREFIX: &str = "ratbin-"; + +pub struct Integration { + pub desktop_path: PathBuf, + pub entry: DesktopEntry, +} + +pub struct Fields { + pub name: String, + pub comment: String, + pub categories: String, + pub icon: String, + pub args: String, + pub terminal: bool, +} + +impl Integration { + pub fn source(&self) -> PathBuf { + self.entry + .get(KEY_SOURCE) + .map(PathBuf::from) + .unwrap_or_default() + } + + pub fn name(&self) -> String { + self.entry + .get("Name") + .unwrap_or_else(|| file_stem(&self.source())) + } + + pub fn kind(&self) -> String { + self.entry.get(KEY_KIND).unwrap_or_default() + } + + pub fn fields(&self) -> Fields { + let get = |key: &str| self.entry.get(key).unwrap_or_default(); + Fields { + name: self.name(), + comment: get("Comment"), + categories: get("Categories"), + icon: get("Icon"), + args: get(KEY_ARGS), + terminal: get("Terminal") == "true", + } + } + + pub fn apply(&mut self, fields: &Fields) { + let source = self.source(); + let entry = &mut self.entry; + + let name = fields.name.trim(); + entry.remove_localized("Name"); + if name.is_empty() { + entry.set("Name", &file_stem(&source)); + } else { + entry.set("Name", name); + } + + entry.remove_localized("Comment"); + set_or_remove(entry, "Comment", fields.comment.trim()); + + let categories = fields.categories.trim(); + if categories.is_empty() || categories.ends_with(';') { + set_or_remove(entry, "Categories", categories); + } else { + entry.set("Categories", &format!("{categories};")); + } + + set_or_remove(entry, "Icon", fields.icon.trim()); + + let args = fields.args.trim(); + entry.set(KEY_ARGS, args); + entry.set("Exec", &desktop::build_exec(&source, args)); + entry.set("Terminal", if fields.terminal { "true" } else { "false" }); + } +} + +pub fn list() -> Result> { + let mut out = Vec::new(); + let Ok(entries) = fs::read_dir(config::applications_dir()?) else { + return Ok(out); + }; + for dir_entry in entries.flatten() { + let file_name = dir_entry.file_name(); + let file_name = file_name.to_string_lossy(); + if !file_name.starts_with(PREFIX) || !file_name.ends_with(".desktop") { + continue; + } + let Ok(text) = fs::read_to_string(dir_entry.path()) else { + continue; + }; + let entry = DesktopEntry::parse(&text); + if entry.get(KEY_SOURCE).is_some() { + out.push(Integration { + desktop_path: dir_entry.path(), + entry, + }); + } + } + out.sort_by_cached_key(|i| i.name().to_lowercase()); + Ok(out) +} + +fn find(source: &Path) -> Result> { + Ok(list()?.into_iter().find(|i| i.source() == source)) +} + +pub fn integrate(apps_dir: &Path, path: &Path) -> Result { + let src = path + .canonicalize() + .with_context(|| format!("cannot access {}", path.display()))?; + if !src.is_file() { + bail!("{} is not a file", src.display()); + } + if let Some(existing) = find(&src)? { + bail!( + "{} is already integrated as \"{}\"", + src.display(), + existing.name() + ); + } + let kind = detect::detect(&src)?; + let target = place(apps_dir, &src)?; + make_executable(&target)?; + let id = unique_id(&target)?; + let entry = build_entry(&kind, &target, &id)?; + let integration = Integration { + desktop_path: config::applications_dir()?.join(format!("{id}.desktop")), + entry, + }; + save(&integration)?; + Ok(integration) +} + +pub fn remove_path(path: &Path, delete_file: bool) -> Result { + let src = path + .canonicalize() + .with_context(|| format!("cannot access {}", path.display()))?; + let integration = + find(&src)?.with_context(|| format!("{} is not integrated", src.display()))?; + remove(&integration, delete_file)?; + Ok(integration.name()) +} + +pub fn remove(integration: &Integration, delete_file: bool) -> Result<()> { + remove_if_exists(&integration.desktop_path)?; + if let Some(icon) = integration.entry.get("Icon").map(PathBuf::from) + && icon.starts_with(config::icons_dir()?) + { + remove_if_exists(&icon)?; + } + if delete_file { + remove_if_exists(&integration.source())?; + } + refresh_menu(); + Ok(()) +} + +pub fn save(integration: &Integration) -> Result<()> { + let path = &integration.desktop_path; + if let Some(dir) = path.parent() { + fs::create_dir_all(dir)?; + } + let tmp = path.with_extension("desktop.tmp"); + fs::write(&tmp, integration.entry.to_string()) + .with_context(|| format!("cannot write {}", tmp.display()))?; + fs::rename(&tmp, path).with_context(|| format!("cannot write {}", path.display()))?; + refresh_menu(); + Ok(()) +} + +fn build_entry(kind: &Kind, target: &Path, id: &str) -> Result { + let embedded = match kind { + Kind::AppImage(Some(offset)) => appimage::read(target, *offset) + .inspect_err(|e| eprintln!("warning: {e:#}")) + .ok(), + _ => None, + }; + let (desktop, icon) = embedded.map(|e| (e.desktop, e.icon)).unwrap_or_default(); + + let mut entry = desktop.unwrap_or_else(DesktopEntry::new); + let args = entry + .get("Exec") + .map(|exec| desktop::exec_args(&exec).to_string()) + .unwrap_or_default(); + entry.map_values("Exec", |exec| { + desktop::build_exec(target, desktop::exec_args(exec)) + }); + entry.remove("TryExec"); + entry.remove("DBusActivatable"); + + entry.set("Type", "Application"); + if entry.get("Name").is_none() { + entry.set("Name", &file_stem(target)); + } + if entry.get("Terminal").is_none() { + let terminal = matches!(kind, Kind::Script); + entry.set("Terminal", if terminal { "true" } else { "false" }); + } + let icon = match icon { + Some(icon) => store_icon(&icon, id)?.to_string_lossy().into_owned(), + None => kind.fallback_icon().into(), + }; + entry.set("Icon", &icon); + entry.set(KEY_SOURCE, &target.to_string_lossy()); + entry.set(KEY_KIND, kind.label()); + entry.set(KEY_ARGS, &args); + entry.set("Exec", &desktop::build_exec(target, &args)); + Ok(entry) +} + +fn place(apps_dir: &Path, src: &Path) -> Result { + fs::create_dir_all(apps_dir) + .with_context(|| format!("cannot create {}", apps_dir.display()))?; + let apps_dir = apps_dir.canonicalize()?; + if src.starts_with(&apps_dir) { + return Ok(src.to_path_buf()); + } + let dest = apps_dir.join(src.file_name().context("path has no file name")?); + if dest.exists() { + bail!("{} already exists", dest.display()); + } + if fs::rename(src, &dest).is_err() { + fs::copy(src, &dest).with_context(|| format!("cannot copy to {}", dest.display()))?; + fs::remove_file(src).with_context(|| format!("cannot remove {}", src.display()))?; + } + Ok(dest) +} + +fn make_executable(path: &Path) -> Result<()> { + let mut permissions = fs::metadata(path)?.permissions(); + let mode = permissions.mode(); + permissions.set_mode(mode | 0o100 | ((mode & 0o044) >> 2)); + fs::set_permissions(path, permissions) + .with_context(|| format!("cannot make {} executable", path.display())) +} + +fn unique_id(target: &Path) -> Result { + let slug: String = file_stem(target) + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() { + c.to_ascii_lowercase() + } else { + '-' + } + }) + .collect(); + let slug = slug + .split('-') + .filter(|part| !part.is_empty()) + .collect::>() + .join("-"); + let slug = if slug.is_empty() { "app".into() } else { slug }; + let apps = config::applications_dir()?; + let mut id = format!("{PREFIX}{slug}"); + let mut n = 2; + while apps.join(format!("{id}.desktop")).exists() { + id = format!("{PREFIX}{slug}-{n}"); + n += 1; + } + Ok(id) +} + +fn store_icon(icon: &appimage::Icon, id: &str) -> Result { + let dir = config::icons_dir()?; + fs::create_dir_all(&dir)?; + let path = dir.join(format!("{id}.{}", icon.ext)); + fs::write(&path, &icon.data).with_context(|| format!("cannot write {}", path.display()))?; + Ok(path) +} + +fn set_or_remove(entry: &mut DesktopEntry, key: &str, value: &str) { + if value.is_empty() { + entry.remove(key); + } else { + entry.set(key, value); + } +} + +fn file_stem(path: &Path) -> String { + path.file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +fn remove_if_exists(path: &Path) -> Result<()> { + match fs::remove_file(path) { + Err(e) if e.kind() != io::ErrorKind::NotFound => { + Err(e).with_context(|| format!("cannot remove {}", path.display())) + } + _ => Ok(()), + } +} + +fn refresh_menu() { + if let Ok(dir) = config::applications_dir() { + let _ = Command::new("update-desktop-database") + .arg(dir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} diff --git a/src/thunar.rs b/src/thunar.rs new file mode 100644 index 0000000..7e41109 --- /dev/null +++ b/src/thunar.rs @@ -0,0 +1,131 @@ +use std::{fs, path::PathBuf}; + +use anyhow::{Context, Result}; + +const SKELETON: &str = "\n\n\n"; +const IDS: [&str; 2] = ["ratbin-integrate", "ratbin-remove"]; + +pub fn install() -> Result { + let exe = std::env::current_exe()?.canonicalize()?; + let exe = shell_quote(&exe.to_string_lossy()); + let actions = [ + action( + IDS[0], + "Integrate into menu", + "application-x-executable", + &format!("{exe} integrate --notify %F"), + "Move to the applications folder and add a menu entry", + ), + action( + IDS[1], + "Remove menu integration", + "edit-delete", + &format!("{exe} remove --notify %F"), + "Remove the menu entry created by ratbin", + ), + ]; + let mut doc = stripped()?; + let pos = doc + .rfind("") + .context("uca.xml has no closing tag")?; + doc.insert_str(pos, &actions.concat()); + write(doc) +} + +pub fn uninstall() -> Result { + write(stripped()?) +} + +fn uca_path() -> Result { + Ok(dirs::config_dir() + .context("cannot determine the config directory")? + .join("Thunar/uca.xml")) +} + +fn stripped() -> Result { + let path = uca_path()?; + let doc = if path.exists() { + fs::read_to_string(&path).with_context(|| format!("cannot read {}", path.display()))? + } else { + SKELETON.into() + }; + Ok(strip(&doc)) +} + +fn write(doc: String) -> Result { + let path = uca_path()?; + if path.exists() { + fs::copy(&path, path.with_extension("xml.bak"))?; + } else if let Some(dir) = path.parent() { + fs::create_dir_all(dir)?; + } + fs::write(&path, doc).with_context(|| format!("cannot write {}", path.display()))?; + Ok(path) +} + +fn strip(doc: &str) -> String { + const CLOSE: &str = ""; + let mut out = String::with_capacity(doc.len()); + let mut rest = doc; + while let Some(start) = rest.find("") { + let Some(len) = rest[start..].find(CLOSE) else { + break; + }; + let mut end = start + len + CLOSE.len(); + let block = &rest[start..end]; + out.push_str(&rest[..start]); + if IDS + .iter() + .any(|id| block.contains(&format!("{id}"))) + { + if rest[end..].starts_with('\n') { + end += 1; + } + } else { + out.push_str(block); + } + rest = &rest[end..]; + } + out.push_str(rest); + out +} + +fn action(id: &str, name: &str, icon: &str, command: &str, description: &str) -> String { + format!( + "\n\ + \t{icon}\n\ + \t{name}\n\ + \t\n\ + \t{id}\n\ + \t{command}\n\ + \t{description}\n\ + \t\n\ + \t*\n\ + \t\n\ + \t\n\ + \n", + icon = xml_escape(icon), + name = xml_escape(name), + id = xml_escape(id), + command = xml_escape(command), + description = xml_escape(description), + ) +} + +fn xml_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +fn shell_quote(s: &str) -> String { + if s.chars() + .all(|c| c.is_ascii_alphanumeric() || "/._-+".contains(c)) + { + s.into() + } else { + format!("'{}'", s.replace('\'', "'\\''")) + } +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..2c0b2aa --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,463 @@ +use std::{io, path::PathBuf}; + +use anyhow::Result; +use ratatui::{ + DefaultTerminal, Frame, + crossterm::{ + event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, + execute, + terminal::SetTitle, + }, + layout::{Constraint, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Clear, List, ListItem, ListState, Paragraph, Wrap}, +}; + +use crate::store::{self, Fields, Integration}; + +const LABELS: [&str; 6] = [ + "Name", + "Comment", + "Categories", + "Icon", + "Arguments", + "Terminal", +]; +const TERMINAL_FIELD: usize = 5; +const LABEL_WIDTH: u16 = 12; + +pub fn run() -> Result<()> { + let mut app = App::new()?; + let mut terminal = ratatui::init(); + let _ = execute!(io::stdout(), SetTitle("Ratbin")); + let result = app.run(&mut terminal); + ratatui::restore(); + result +} + +enum Mode { + Browse, + Edit(Form), + Confirm, +} + +struct App { + items: Vec, + list: ListState, + mode: Mode, + status: Option<(String, bool)>, + quit: bool, +} + +impl App { + fn new() -> Result { + let mut app = Self { + items: Vec::new(), + list: ListState::default(), + mode: Mode::Browse, + status: None, + quit: false, + }; + app.reload(None)?; + Ok(app) + } + + fn reload(&mut self, keep: Option) -> Result<()> { + self.items = store::list()?; + let index = keep + .and_then(|path| self.items.iter().position(|i| i.desktop_path == path)) + .or(self.list.selected()) + .unwrap_or(0) + .min(self.items.len().saturating_sub(1)); + self.list.select((!self.items.is_empty()).then_some(index)); + Ok(()) + } + + fn selected(&self) -> Option<&Integration> { + self.items.get(self.list.selected()?) + } + + fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> { + while !self.quit { + terminal.draw(|frame| self.draw(frame))?; + if let Event::Key(key) = event::read()? + && key.kind == KeyEventKind::Press + { + self.status = None; + self.on_key(key); + } + } + Ok(()) + } + + fn on_key(&mut self, key: KeyEvent) { + let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + match &mut self.mode { + Mode::Browse => self.on_browse(key), + Mode::Confirm => self.on_confirm(key), + Mode::Edit(form) => match key.code { + KeyCode::Esc => self.mode = Mode::Browse, + KeyCode::Enter => self.save(), + KeyCode::Char('s') if ctrl => self.save(), + _ => form.on_key(key), + }, + } + } + + fn on_browse(&mut self, key: KeyEvent) { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => self.quit = true, + KeyCode::Down | KeyCode::Char('j') => self.list.select_next(), + KeyCode::Up | KeyCode::Char('k') => self.list.select_previous(), + KeyCode::Home | KeyCode::Char('g') => self.list.select_first(), + KeyCode::End | KeyCode::Char('G') => self.list.select_last(), + KeyCode::Enter | KeyCode::Char('e') => { + if let Some(item) = self.selected() { + self.mode = Mode::Edit(Form::new(item.fields())); + } + } + KeyCode::Delete | KeyCode::Char('d') => { + if self.selected().is_some() { + self.mode = Mode::Confirm; + } + } + KeyCode::Char('r') => { + let result = self.reload(None); + self.report(result.map(|()| "Reloaded".into())); + } + _ => {} + } + } + + fn on_confirm(&mut self, key: KeyEvent) { + self.mode = Mode::Browse; + let delete_file = match key.code { + KeyCode::Char('e') => false, + KeyCode::Char('d') => true, + _ => return, + }; + let Some(item) = self.selected() else { + return; + }; + let name = item.name(); + let result = store::remove(item, delete_file).and_then(|()| self.reload(None)); + self.report(result.map(|()| format!("Removed {name}"))); + } + + fn save(&mut self) { + let Mode::Edit(form) = std::mem::replace(&mut self.mode, Mode::Browse) else { + return; + }; + let Some(item) = self + .list + .selected() + .and_then(|index| self.items.get_mut(index)) + else { + return; + }; + item.apply(&form.fields()); + let path = item.desktop_path.clone(); + let name = item.name(); + match store::save(item) { + Ok(()) => { + let result = self.reload(Some(path)); + self.report(result.map(|()| format!("Saved {name}"))); + } + Err(e) => { + self.status = Some((format!("{e:#}"), true)); + self.mode = Mode::Edit(form); + } + } + } + + fn report(&mut self, result: Result) { + self.status = Some(match result { + Ok(message) => (message, false), + Err(e) => (format!("{e:#}"), true), + }); + } + + fn draw(&mut self, frame: &mut Frame) { + let [main, footer] = + Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).areas(frame.area()); + let [left, right] = + Layout::horizontal([Constraint::Percentage(40), Constraint::Percentage(60)]) + .areas(main); + + let items: Vec = self + .items + .iter() + .map(|item| { + let name_style = if item.source().exists() { + Style::default() + } else { + Style::default().fg(Color::Red) + }; + ListItem::new(Line::from(vec![ + Span::styled(item.name(), name_style), + Span::styled( + format!(" {}", item.kind()), + Style::default().fg(Color::DarkGray), + ), + ])) + }) + .collect(); + let list = List::new(items) + .block(Block::bordered().title(" ratbin ")) + .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) + .highlight_symbol("› "); + frame.render_stateful_widget(list, left, &mut self.list); + frame.render_widget(self.details(), right); + frame.render_widget(self.footer(), footer); + + match &self.mode { + Mode::Browse => {} + Mode::Edit(form) => form.draw(frame, main), + Mode::Confirm => self.draw_confirm(frame, main), + } + } + + fn details(&self) -> Paragraph<'static> { + let block = Block::bordered().title(" Details "); + let Some(item) = self.selected() else { + return Paragraph::new( + "Nothing integrated yet.\n\nRight-click an AppImage, binary or script in Thunar \ + and choose \"Integrate into menu\".", + ) + .block(block) + .wrap(Wrap { trim: false }); + }; + let source = item.source(); + let get = |key: &str| item.entry.get(key).unwrap_or_default(); + let row = |label: &str, value: String| { + Line::from(vec![ + Span::styled( + format!("{label: Line<'static> { + if let Some((message, error)) = &self.status { + let color = if *error { Color::Red } else { Color::Green }; + return Line::styled(message.clone(), Style::default().fg(color)); + } + let hints = match self.mode { + Mode::Browse => "↑↓ select enter edit d remove r reload q quit", + Mode::Edit(_) => "tab/↑↓ field space toggle ctrl+u clear enter save esc cancel", + Mode::Confirm => "e entry only d entry + file esc cancel", + }; + Line::styled(hints, Style::default().fg(Color::DarkGray)) + } + + fn draw_confirm(&self, frame: &mut Frame, area: Rect) { + let Some(item) = self.selected() else { + return; + }; + let file_name = item + .source() + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); + let rect = popup(area, 64, 6); + frame.render_widget(Clear, rect); + frame.render_widget( + Paragraph::new(vec![ + Line::from(format!("Remove \"{}\"?", item.name())), + Line::default(), + Line::from("e remove the menu entry only"), + Line::from(format!("d also delete {file_name}")), + ]) + .block( + Block::bordered() + .title(" Remove ") + .border_style(Style::default().fg(Color::Red)), + ), + rect, + ); + } +} + +struct Form { + inputs: [Input; 5], + terminal: bool, + focus: usize, +} + +impl Form { + fn new(fields: Fields) -> Self { + Self { + inputs: [ + fields.name, + fields.comment, + fields.categories, + fields.icon, + fields.args, + ] + .map(Input::new), + terminal: fields.terminal, + focus: 0, + } + } + + fn fields(&self) -> Fields { + let [name, comment, categories, icon, args] = + self.inputs.each_ref().map(|input| input.value.clone()); + Fields { + name, + comment, + categories, + icon, + args, + terminal: self.terminal, + } + } + + fn on_key(&mut self, key: KeyEvent) { + match key.code { + KeyCode::Tab | KeyCode::Down => self.focus = (self.focus + 1) % LABELS.len(), + KeyCode::BackTab | KeyCode::Up => { + self.focus = (self.focus + LABELS.len() - 1) % LABELS.len(); + } + KeyCode::Char(' ') | KeyCode::Left | KeyCode::Right if self.focus == TERMINAL_FIELD => { + self.terminal = !self.terminal; + } + _ if self.focus == TERMINAL_FIELD => {} + _ => self.inputs[self.focus].on_key(key), + } + } + + fn draw(&self, frame: &mut Frame, area: Rect) { + let rect = popup(area, 80, LABELS.len() as u16 + 2); + frame.render_widget(Clear, rect); + let block = Block::bordered().title(" Edit entry "); + let inner = block.inner(rect); + frame.render_widget(block, rect); + + let value_width = inner.width.saturating_sub(LABEL_WIDTH) as usize; + for (row, label) in LABELS.iter().enumerate() { + let y = inner.y + row as u16; + if y >= inner.bottom() { + break; + } + let focused = row == self.focus; + let label_style = if focused { + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::DarkGray) + }; + let (text, cursor) = if row == TERMINAL_FIELD { + let mark = if self.terminal { "[x]" } else { "[ ]" }; + (format!("{mark} run in a terminal"), None) + } else { + let (text, cursor) = self.inputs[row].view(value_width); + (text, Some(cursor)) + }; + let line = Line::from(vec![ + Span::styled( + format!("{label: Self { + let cursor = value.chars().count(); + Self { value, cursor } + } + + fn byte_index(&self, char_index: usize) -> usize { + self.value + .char_indices() + .nth(char_index) + .map_or(self.value.len(), |(i, _)| i) + } + + fn on_key(&mut self, key: KeyEvent) { + let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + let len = self.value.chars().count(); + match key.code { + KeyCode::Char('u') if ctrl => { + self.value.clear(); + self.cursor = 0; + } + KeyCode::Char(c) if !ctrl => { + let at = self.byte_index(self.cursor); + self.value.insert(at, c); + self.cursor += 1; + } + KeyCode::Backspace if self.cursor > 0 => { + self.cursor -= 1; + let at = self.byte_index(self.cursor); + self.value.remove(at); + } + KeyCode::Delete if self.cursor < len => { + let at = self.byte_index(self.cursor); + self.value.remove(at); + } + KeyCode::Left => self.cursor = self.cursor.saturating_sub(1), + KeyCode::Right => self.cursor = (self.cursor + 1).min(len), + KeyCode::Home => self.cursor = 0, + KeyCode::End => self.cursor = len, + _ => {} + } + } + + fn view(&self, width: usize) -> (String, usize) { + let width = width.max(1); + let start = (self.cursor + 1).saturating_sub(width); + let text = self.value.chars().skip(start).take(width).collect(); + (text, self.cursor - start) + } +} + +fn popup(area: Rect, width: u16, height: u16) -> Rect { + let width = width.min(area.width); + let height = height.min(area.height); + Rect::new( + area.x + (area.width - width) / 2, + area.y + (area.height - height) / 2, + width, + height, + ) +}