From e7dedf3e02e22a7e2b8f89a2f260c65ce395e970 Mon Sep 17 00:00:00 2001 From: Russell Date: Mon, 28 Jul 2025 16:47:52 +0000 Subject: [PATCH] This works so far --- .gitignore | 1 + Cargo.lock | 769 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 + justfile | 16 + src/cli/mod.rs | 59 +++ src/cli/tree.rs | 100 +++++ src/database/format.rs | 85 ++++ src/main.rs | 198 ++++++++++ src/package_managers/dnf.rs | 159 ++++++++ src/package_managers/mod.rs | 223 +++++++++++ 10 files changed, 1623 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 justfile create mode 100644 src/cli/mod.rs create mode 100644 src/cli/tree.rs create mode 100644 src/database/format.rs create mode 100644 src/main.rs create mode 100644 src/package_managers/dnf.rs create mode 100644 src/package_managers/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..53a59ef --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,769 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "futures-core", + "inotify-sys", + "libc", + "tokio", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +dependencies = [ + "backtrace", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "where-cmd" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "inotify", + "memmap2", + "regex", + "serde", + "walkdir", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..627d402 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "where-cmd" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.0", features = ["derive"] } +memmap2 = "0.9" +inotify = "0.10" +serde = { version = "1.0", features = ["derive"] } +walkdir = "2.0" +regex = "1.0" +chrono = "0.4" \ No newline at end of file diff --git a/justfile b/justfile new file mode 100644 index 0000000..754876d --- /dev/null +++ b/justfile @@ -0,0 +1,16 @@ +default: + just -l + +run mode="quiet": + #! /bin/bash + case "{{ mode }}" in + quiet) + RUSTFLAGS="-A warnings" cargo run --quiet + ;; + *) + cargo run + ;; + esac + +build: + cargo build --release diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..59d201f --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,59 @@ +pub mod tree; + +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "where", about = "Universal system file and package mapper")] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Scan system and build/update database + Scan { + #[arg(short, long, help = "Force full rescan")] + force: bool, + }, + + /// Show file information + File { + path: String, + }, + + /// List files in tree format for packages + Tree { + #[arg(long, help = "Show tree for specific package")] + package: Option, + + #[arg(short, long, help = "Maximum depth")] + depth: Option, + + #[arg(short, long, help = "Filter by source type")] + source: Option, + }, + + /// Search for files + Find { + #[arg(short, long, help = "File name pattern")] + name: Option, + + #[arg(short, long, help = "Package name")] + package: Option, + + #[arg(short, long, help = "Files larger than size")] + size: Option, + }, + + /// Show package information + Package { + name: String, + }, + + /// List all packages + Packages { + #[arg(short, long, help = "Filter by source")] + source: Option, + }, +} \ No newline at end of file diff --git a/src/cli/tree.rs b/src/cli/tree.rs new file mode 100644 index 0000000..1eb8738 --- /dev/null +++ b/src/cli/tree.rs @@ -0,0 +1,100 @@ +use crate::{PackageManager, PackageSource, WhereError}; +use std::collections::HashMap; +use std::path::Path; + +pub fn print_package_tree(manager: &dyn PackageManager, package_name: &str) -> Result<(), WhereError> { + // Get package info first + let package_info = match manager.get_package_info(package_name)? { + Some(info) => info, + None => { + println!("Package '{}' not found", package_name); + return Ok(()); + } + }; + + // Print package header + println!("{}{} {}\x1b[0m", + package_info.source.color_code(), + package_info.name, + package_info.version); + + // Get all files for this package + let files = manager.get_package_files(package_name)?; + + if files.is_empty() { + println!(" (no files found)"); + return Ok(()); + } + + // Build directory tree structure + let tree = build_tree_structure(&files); + + // Print the tree + print_tree_node(&tree, "", true); + + Ok(()) +} + +#[derive(Debug)] +struct TreeNode { + name: String, + is_file: bool, + children: HashMap, +} + +impl TreeNode { + fn new(name: String, is_file: bool) -> Self { + TreeNode { + name, + is_file, + children: HashMap::new(), + } + } +} + +fn build_tree_structure(files: &[std::path::PathBuf]) -> TreeNode { + let mut root = TreeNode::new("".to_string(), false); + + for file_path in files { + let path_str = file_path.to_string_lossy(); + let components: Vec<&str> = path_str.split('/').filter(|s| !s.is_empty()).collect(); + + let mut current = &mut root; + + for (i, component) in components.iter().enumerate() { + let is_last = i == components.len() - 1; + let is_file = is_last && file_path.is_file(); + + current.children + .entry(component.to_string()) + .or_insert_with(|| TreeNode::new(component.to_string(), is_file)); + + current = current.children.get_mut(*component).unwrap(); + } + } + + root +} + +fn print_tree_node(node: &TreeNode, prefix: &str, is_last: bool) { + if !node.name.is_empty() { + let connector = if is_last { "└── " } else { "├── " }; + let file_indicator = if node.is_file { "" } else { "/" }; + + println!("{}{}{}{}", prefix, connector, node.name, file_indicator); + } + + let mut children: Vec<_> = node.children.iter().collect(); + children.sort_by_key(|(name, child)| (!child.is_file, name.to_lowercase())); + + for (i, (_, child)) in children.iter().enumerate() { + let is_child_last = i == children.len() - 1; + let child_prefix = if node.name.is_empty() { + prefix.to_string() + } else { + format!("{}{} ", prefix, if is_last { " " } else { "│" }) + }; + + print_tree_node(child, &child_prefix, is_child_last); + } +} \ No newline at end of file diff --git a/src/database/format.rs b/src/database/format.rs new file mode 100644 index 0000000..0963e8f --- /dev/null +++ b/src/database/format.rs @@ -0,0 +1,85 @@ +use memmap2::Mmap; +use std::fs::File; + +#[repr(C, packed)] +pub struct DatabaseHeader { + magic: [u8; 8], // "WHEREDB\0" + version: u32, + created: u64, // Unix timestamp + file_count: u64, + package_count: u32, + files_offset: u64, + packages_offset: u64, + strings_offset: u64, + index_offset: u64, +} + +#[repr(C, packed)] +pub struct FileRecord { + path_hash: u64, // FNV-1a hash for quick lookups + package_id: u32, + source: u8, + permissions: u16, + size: u64, + mtime: u64, + path_offset: u32, // offset into string pool +} + +#[repr(C, packed)] +pub struct PackageRecord { + id: u32, + source: u8, + install_time: u64, + name_offset: u32, + version_offset: u32, + file_count: u32, + first_file_idx: u32, +} + +pub struct WhereDatabase { + _file: File, + mmap: Mmap, + header: &'static DatabaseHeader, +} + +impl WhereDatabase { + pub fn open>(path: P) -> Result { + let file = File::open(path)?; + let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? }; + + let header = unsafe { + &*(mmap.as_ptr() as *const DatabaseHeader) + }; + + // Validate magic number + if &header.magic != b"WHEREDB\0" { + return Err(WhereError::InvalidDatabase); + } + + Ok(WhereDatabase { + _file: file, + mmap, + header, + }) + } + + pub fn find_file(&self, path: &str) -> Option { + let hash = fnv1a_hash(path.as_bytes()); + + // Binary search through sorted file records + let files = self.get_file_records(); + match files.binary_search_by_key(&hash, |record| record.path_hash) { + Ok(idx) => { + let record = &files[idx]; + Some(FileInfo { + path: self.get_string(record.path_offset), + package: self.get_package_name(record.package_id), + source: PackageSource::from_u8(record.source), + size: record.size, + modified: record.mtime, + }) + } + Err(_) => None, + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..efcbbca --- /dev/null +++ b/src/main.rs @@ -0,0 +1,198 @@ +#![allow(dead_code)] +#![allow(unused_imports)] + +pub mod package_managers; +pub mod cli; + +use std::path::PathBuf; +use package_managers::{detect_available_managers, PackageManager, PackageSource}; +use cli::{Cli, Commands}; +use clap::Parser; + +// Define the types that your modules need +#[derive(Debug, Clone)] +pub struct PackageInfo { + pub name: String, + pub version: String, + pub source: PackageSource, + pub install_time: u64, + pub size: u64, +} + +#[derive(Debug)] +pub enum WhereError { + Io(std::io::Error), + Utf8(std::string::FromUtf8Error), + CommandFailed(String), + PackageNotFound(String), + ManagerNotAvailable(String), + InvalidDatabase, +} + +// Implement From traits for error conversion +impl From for WhereError { + fn from(err: std::io::Error) -> Self { + WhereError::Io(err) + } +} + +impl From for WhereError { + fn from(err: std::string::FromUtf8Error) -> Self { + WhereError::Utf8(err) + } +} + +impl std::fmt::Display for WhereError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WhereError::Io(e) => write!(f, "IO error: {}", e), + WhereError::Utf8(e) => write!(f, "UTF-8 error: {}", e), + WhereError::CommandFailed(s) => write!(f, "Command failed: {}", s), + WhereError::PackageNotFound(s) => write!(f, "Package not found: {}", s), + WhereError::ManagerNotAvailable(s) => write!(f, "Manager not available: {}", s), + WhereError::InvalidDatabase => write!(f, "Invalid database format"), + } + } +} + +impl std::error::Error for WhereError {} + +fn main() -> Result<(), Box> { + let cli = Cli::parse(); + let managers = detect_available_managers(); + + match cli.command { + Commands::Tree { depth, package, source } => { + handle_tree_command(&managers, depth, package, source)?; + } + Commands::Package { name } => { + handle_package_command(&managers, &name)?; + } + Commands::Packages { source } => { + handle_packages_command(&managers, source)?; + } + Commands::File { path } => { + handle_file_command(&managers, &path)?; + } + Commands::Find { name, package, size } => { + handle_find_command(&managers, name, package, size)?; + } + Commands::Scan { force } => { + handle_scan_command(&managers, force)?; + } + } + + Ok(()) +} + +fn handle_tree_command( + managers: &[Box], + _depth: Option, + package_filter: Option, + _source_filter: Option +) -> Result<(), Box> { + if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") { + if let Some(package_name) = package_filter { + // Show tree for specific package + cli::tree::print_package_tree(dnf.as_ref(), &package_name)?; + } else { + // Show tree for first few packages as demo + println!("Package file trees (first 5 packages):"); + let packages = dnf.get_installed_packages()?; + for package in packages.iter().take(5) { + cli::tree::print_package_tree(dnf.as_ref(), &package.name)?; + println!(); // Empty line between packages + } + } + } else { + println!("No DNF package manager found"); + } + Ok(()) +} + +fn handle_package_command( + managers: &[Box], + name: &str +) -> Result<(), Box> { + if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") { + match dnf.get_package_info(name) { + Ok(Some(info)) => { + println!("Package: {}", info.name); + println!("Version: {}", info.version); + println!("Source: {}", info.source.name()); + println!("Install time: {}", info.install_time); + println!("Size: {} bytes", info.size); + + // Show file tree for this package + println!("\nFiles:"); + cli::tree::print_package_tree(dnf.as_ref(), name)?; + } + Ok(None) => println!("Package '{}' not found", name), + Err(e) => println!("Error: {}", e), + } + } + Ok(()) +} + +fn handle_packages_command( + managers: &[Box], + _source_filter: Option +) -> Result<(), Box> { + if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") { + let packages = dnf.get_installed_packages()?; + println!("Installed packages ({} total):", packages.len()); + for package in packages.iter() { + println!(" {} {} ({})", + package.name, + package.version, + package.source.name()); + } + } + Ok(()) +} + +fn handle_file_command( + managers: &[Box], + path: &str +) -> Result<(), Box> { + if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") { + match dnf.get_file_owner(std::path::Path::new(path)) { + Ok(Some(owner)) => println!("{} is owned by package: {}", path, owner), + Ok(None) => println!("{} is not owned by any package", path), + Err(e) => println!("Error checking {}: {}", path, e), + } + } + Ok(()) +} + +fn handle_find_command( + managers: &[Box], + _name: Option, + package: Option, + _size: Option +) -> Result<(), Box> { + if let Some(package_name) = package { + if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") { + match dnf.get_package_files(&package_name) { + Ok(files) => { + println!("Files in package '{}':", package_name); + for file in files { + println!(" {}", file.display()); + } + } + Err(e) => println!("Error: {}", e), + } + } + } else { + println!("Find command needs more specific criteria"); + } + Ok(()) +} + +fn handle_scan_command( + _managers: &[Box], + _force: bool +) -> Result<(), Box> { + println!("Scan functionality not yet implemented"); + Ok(()) +} \ No newline at end of file diff --git a/src/package_managers/dnf.rs b/src/package_managers/dnf.rs new file mode 100644 index 0000000..f4874ce --- /dev/null +++ b/src/package_managers/dnf.rs @@ -0,0 +1,159 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use crate::{PackageInfo, WhereError}; +use super::{PackageManager, PackageSource}; + +pub struct DnfManager; + +impl PackageManager for DnfManager { + fn name(&self) -> &'static str { "dnf" } + + fn source_type(&self) -> PackageSource { + PackageSource::Dnf + } + + fn is_available(&self) -> bool { + Path::new("/usr/bin/dnf").exists() || Path::new("/usr/bin/rpm").exists() + } + + fn get_installed_packages(&self) -> Result, WhereError> { + // Use rpm directly for better performance and parsing + let output = Command::new("rpm") + .args(["-qa", "--queryformat", "%{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{SIZE}\n"]) + .output()?; + + if !output.status.success() { + return Err(WhereError::CommandFailed("rpm query failed".to_string())); + } + + let stdout = String::from_utf8(output.stdout)?; + Ok(stdout.lines() + .filter_map(|line| { + let parts: Vec<&str> = line.split('\t').collect(); + if parts.len() >= 4 { + Some(PackageInfo { + name: parts[0].to_string(), + version: parts[1].to_string(), + source: PackageSource::Dnf, + install_time: parts[2].parse().unwrap_or(0), + size: parts[3].parse().unwrap_or(0), + }) + } else { + None + } + }) + .collect()) + } + + fn get_package_files(&self, package: &str) -> Result, WhereError> { + let output = Command::new("rpm") + .args(["-ql", package]) + .output()?; + + if !output.status.success() { + return Err(WhereError::PackageNotFound(package.to_string())); + } + + let stdout = String::from_utf8(output.stdout)?; + Ok(stdout.lines() + .filter(|line| !line.is_empty()) + .map(|line| PathBuf::from(line.trim())) + .filter(|path| path.exists()) // Only include files that actually exist + .collect()) + } + + fn get_file_owner(&self, path: &Path) -> Result, WhereError> { + let output = Command::new("rpm") + .args(["-qf", &path.to_string_lossy()]) + .output()?; + + if output.status.success() { + let stdout = String::from_utf8(output.stdout)?; + let package_name = stdout.trim(); + + // RPM returns full package name with version, extract just the name + if let Some(base_name) = package_name.split('-').next() { + return Ok(Some(base_name.to_string())); + } + + Ok(Some(package_name.to_string())) + } else { + // Check if it's a "file not owned by any package" error vs other errors + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains("is not owned by any package") { + Ok(None) + } else { + Err(WhereError::CommandFailed(format!("rpm query failed: {}", stderr))) + } + } + } + + fn get_package_info(&self, package: &str) -> Result, WhereError> { + let output = Command::new("rpm") + .args(["-qi", package]) + .output()?; + + if !output.status.success() { + return Ok(None); + } + + let stdout = String::from_utf8(output.stdout)?; + let mut name = String::new(); + let mut version = String::new(); + let mut install_time = 0u64; + let mut size = 0u64; + + for line in stdout.lines() { + if line.starts_with("Name : ") { + name = line.strip_prefix("Name : ").unwrap_or("").to_string(); + } else if line.starts_with("Version : ") { + let version_part = line.strip_prefix("Version : ").unwrap_or(""); + // Get release info too + if let Some(release_line) = stdout.lines() + .find(|l| l.starts_with("Release : ")) { + let release = release_line.strip_prefix("Release : ").unwrap_or(""); + version = format!("{}-{}", version_part, release); + } else { + version = version_part.to_string(); + } + } else if line.starts_with("Install Date: ") { + let date_str = line.strip_prefix("Install Date: ").unwrap_or(""); + install_time = self.parse_rpm_date(date_str).unwrap_or(0); + } else if line.starts_with("Size : ") { + let size_str = line.strip_prefix("Size : ").unwrap_or(""); + size = size_str.split_whitespace().next() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + } + } + + if !name.is_empty() { + Ok(Some(PackageInfo { + name, + version, + source: PackageSource::Dnf, + install_time, + size, + })) + } else { + Ok(None) + } + } +} + +impl DnfManager { + fn parse_rpm_date(&self, date_str: &str) -> Option { + // RPM date format: "Wed 28 Jul 2025 10:30:00 AM UTC" + // Convert to Unix timestamp + use chrono::{DateTime, Utc}; + + // Try parsing the RPM date format + if let Ok(dt) = DateTime::parse_from_str(date_str, "%a %d %b %Y %I:%M:%S %p %Z") { + Some(dt.timestamp() as u64) + } else if let Ok(dt) = DateTime::parse_from_str(date_str, "%a %b %d %H:%M:%S %Y") { + Some(dt.timestamp() as u64) + } else { + None + } + } +} \ No newline at end of file diff --git a/src/package_managers/mod.rs b/src/package_managers/mod.rs new file mode 100644 index 0000000..b85e240 --- /dev/null +++ b/src/package_managers/mod.rs @@ -0,0 +1,223 @@ +use std::path::{Path, PathBuf}; + +pub mod dnf; +// pub mod flatpak; +// pub mod snap; + +use self::dnf::DnfManager; +// Uncomment these as we implement the managers +// use self::flatpak::FlatpakManager; +// use self::snap::SnapManager; + +use crate::{PackageInfo, WhereError}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PackageSource { + Apt = 1, + Dnf = 2, + Pacman = 3, + Flatpak = 4, + Snap = 5, + Homebrew = 6, + Manual = 7, +} + +impl PackageSource { + pub fn from_u8(value: u8) -> Self { + match value { + 1 => PackageSource::Apt, + 2 => PackageSource::Dnf, + 3 => PackageSource::Pacman, + 4 => PackageSource::Flatpak, + 5 => PackageSource::Snap, + 6 => PackageSource::Homebrew, + _ => PackageSource::Manual, + } + } + + pub fn name(&self) -> &'static str { + match self { + PackageSource::Apt => "apt", + PackageSource::Dnf => "dnf", + PackageSource::Pacman => "pacman", + PackageSource::Flatpak => "flatpak", + PackageSource::Snap => "snap", + PackageSource::Homebrew => "homebrew", + PackageSource::Manual => "manual", + } + } + + pub fn color_code(&self) -> &'static str { + match self { + PackageSource::Apt => "\x1b[32m", // Green + PackageSource::Dnf => "\x1b[31m", // Red + PackageSource::Pacman => "\x1b[36m", // Cyan + PackageSource::Flatpak => "\x1b[34m", // Blue + PackageSource::Snap => "\x1b[35m", // Magenta + PackageSource::Homebrew => "\x1b[33m", // Yellow + PackageSource::Manual => "\x1b[37m", // White + } + } +} + +pub trait PackageManager { + fn name(&self) -> &'static str; + fn source_type(&self) -> PackageSource; + fn is_available(&self) -> bool; + fn get_installed_packages(&self) -> Result, WhereError>; + fn get_package_files(&self, package: &str) -> Result, WhereError>; + fn get_file_owner(&self, path: &Path) -> Result, WhereError>; + fn get_package_info(&self, package: &str) -> Result, WhereError>; + + /// Priority for file ownership resolution (higher = preferred) + fn priority(&self) -> u8 { + match self.source_type() { + PackageSource::Dnf => 10, // System package manager gets highest priority + PackageSource::Apt => 10, + PackageSource::Pacman => 10, + PackageSource::Flatpak => 5, // User applications + PackageSource::Snap => 5, + PackageSource::Homebrew => 3, // Usually supplementary + PackageSource::Manual => 1, // Lowest priority + } + } +} + +/// Detect all available package managers on the system +pub fn detect_available_managers() -> Vec> { + let mut managers: Vec> = Vec::new(); + + // Check system package managers first + let dnf = DnfManager; + if dnf.is_available() { + managers.push(Box::new(dnf)); + } + + // Add other managers as we implement them + // let flatpak = FlatpakManager; + // if flatpak.is_available() { + // managers.push(Box::new(flatpak)); + // } + + // let snap = SnapManager; + // if snap.is_available() { + // managers.push(Box::new(snap)); + // } + + // Sort by priority (highest first) for consistent ordering + managers.sort_by(|a, b| b.priority().cmp(&a.priority())); + + managers +} + +/// Find which package owns a file, checking all available managers +pub fn find_file_owner(path: &Path) -> Result, WhereError> { + let managers = detect_available_managers(); + + for manager in managers { + if let Ok(Some(package)) = manager.get_file_owner(path) { + return Ok(Some((package, manager.source_type()))); + } + } + + Ok(None) +} + +/// Get all installed packages from all available managers +pub fn get_all_packages() -> Result, WhereError> { + let managers = detect_available_managers(); + let mut all_packages = Vec::new(); + + for manager in managers { + match manager.get_installed_packages() { + Ok(mut packages) => all_packages.append(&mut packages), + Err(e) => { + eprintln!("Warning: Failed to get packages from {}: {}", manager.name(), e); + // Continue with other managers + } + } + } + + // Sort by name for consistent output + all_packages.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(all_packages) +} + +/// Get package information by name, searching all managers +pub fn find_package(name: &str) -> Result, WhereError> { + let managers = detect_available_managers(); + + for manager in managers { + if let Ok(Some(package)) = manager.get_package_info(name) { + return Ok(Some(package)); + } + } + + Ok(None) +} + +/// Get packages from a specific source type +pub fn get_packages_by_source(source: PackageSource) -> Result, WhereError> { + let managers = detect_available_managers(); + + for manager in managers { + if manager.source_type() == source { + return manager.get_installed_packages(); + } + } + + Err(WhereError::ManagerNotAvailable(source.name().to_string())) +} + +/// Summary of available package managers +#[derive(Debug)] +pub struct ManagerSummary { + pub name: String, + pub source_type: PackageSource, + pub available: bool, + pub package_count: Option, +} + +pub fn get_manager_summary() -> Vec { + let available_managers = detect_available_managers(); + let mut summaries = Vec::new(); + + for manager in available_managers { + let package_count = manager.get_installed_packages() + .map(|packages| packages.len()) + .ok(); + + summaries.push(ManagerSummary { + name: manager.name().to_string(), + source_type: manager.source_type(), + available: true, + package_count, + }); + } + + summaries +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_package_source_conversion() { + assert_eq!(PackageSource::from_u8(2), PackageSource::Dnf); + assert_eq!(PackageSource::Dnf.name(), "dnf"); + } + + #[test] + fn test_manager_detection() { + let managers = detect_available_managers(); + assert!(!managers.is_empty(), "Should detect at least one package manager"); + + // On Fedora, should detect DNF + let has_dnf = managers.iter().any(|m| m.name() == "dnf"); + if Path::new("/usr/bin/dnf").exists() { + assert!(has_dnf, "Should detect DNF on systems where it's installed"); + } + } +} \ No newline at end of file