diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml index 22ae978..63e6542 100644 --- a/.github/workflows/build_artifacts.yml +++ b/.github/workflows/build_artifacts.yml @@ -42,7 +42,7 @@ jobs: restore-keys: ${{ runner.OS }}-release- if: matrix.os == 'ubuntu-22.04' - name: Building - run: cargo build --release --no-default-features --features wgpu,${{ matrix.update_feature }},img + run: cargo build --release -p uad-gui --no-default-features --features wgpu,${{ matrix.update_feature }},img - name: Renaming binaries [Windows] if: matrix.os == 'windows-2022' run: mv target/release/uad-ng.exe uad-ng-${{ matrix.build_target }}.exe @@ -68,3 +68,59 @@ jobs: with: name: uad-ng${{ matrix.update_name }}-${{ matrix.build_target }} path: uad-ng-* + + build-cli: + name: Building CLI ${{ matrix.build_target }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - build_target: linux + os: ubuntu-22.04 + - build_target: macos + os: macos-15 + - build_target: macos-intel + os: macos-15-intel + - build_target: windows + os: windows-2022 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target + key: ${{ runner.os }}-release-cli-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.OS }}-release-cli- + if: matrix.os == 'ubuntu-22.04' + - name: Building + run: cargo build --release -p uad-cli + - name: Renaming binaries [Windows] + if: matrix.os == 'windows-2022' + run: mv target/release/uad.exe uad-cli-${{ matrix.build_target }}.exe + - name: Renaming binaries [Others] + if: matrix.os != 'windows-2022' + run: mv target/release/uad uad-cli-${{ matrix.build_target }} + - name: Tarball Linux/MacOS binary + if: matrix.os != 'windows-2022' + run: tar -czf uad-cli-${{ matrix.build_target }}{.tar.gz,} + - name: Install coreutils for macOS + if: startsWith(matrix.os, 'macos') + run: brew install coreutils + - name: Create checksums for binaries and archives [Windows] + if: matrix.os == 'windows-2022' + run: sha256sum uad-cli-${{ matrix.build_target }}.exe | tee uad-cli-${{ matrix.build_target }}.exe-checksum.txt + - name: Create checksums for binaries and archives [Others] + if: matrix.os != 'windows-2022' + run: | + sha256sum uad-cli-${{ matrix.build_target }} | tee uad-cli-${{ matrix.build_target }}-checksum + sha256sum uad-cli-${{ matrix.build_target }}.tar.gz | tee uad-cli-${{ matrix.build_target }}.tar.gz-checksum + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: uad-cli-${{ matrix.build_target }} + path: uad-cli-* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d8c307..66ef045 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,9 @@ jobs: - name: Create release uses: softprops/action-gh-release@v2 with: - files: uad-ng-*/* + files: | + uad-ng-*/* + uad-cli-*/* generate_release_notes: true body: | > [!NOTE] diff --git a/Cargo.lock b/Cargo.lock index 988976d..63ccf02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,10 +101,60 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.100" +name = "anstream" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arbitrary" @@ -409,7 +459,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.17", + "thiserror 2.0.18", "v_frame", "y4m", ] @@ -430,9 +480,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" dependencies = [ "arrayvec", ] @@ -536,9 +586,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -619,9 +669,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.51" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -641,6 +691,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -649,15 +705,64 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "num-traits", "windows-link", ] +[[package]] +name = "clap" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.5.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + [[package]] name = "clipboard-win" version = "5.4.1" @@ -693,7 +798,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", "x11rb", ] @@ -705,7 +810,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -714,6 +819,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "colored" version = "2.2.0" @@ -951,9 +1062,9 @@ dependencies = [ [[package]] name = "ctor-lite" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" +checksum = "b29fccfdaeb0f9bd90da5759b1d0eaa2f6cfcfe90960124cfc83141ed4e111fd" [[package]] name = "cursor-icon" @@ -977,9 +1088,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", ] @@ -1090,6 +1201,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "enumflags2" version = "0.7.12" @@ -1165,9 +1282,9 @@ dependencies = [ [[package]] name = "euclid" -version = "0.22.11" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" dependencies = [ "num-traits", ] @@ -1240,6 +1357,17 @@ dependencies = [ "syn", ] +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.3", + "windows-sys 0.59.0", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -1261,27 +1389,26 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1482,9 +1609,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1503,6 +1630,19 @@ dependencies = [ "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -1667,6 +1807,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[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.2" @@ -1685,6 +1831,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -1703,9 +1858,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1739,7 +1894,7 @@ dependencies = [ "iced_widget", "iced_winit", "image", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1756,7 +1911,7 @@ dependencies = [ "num-traits", "rustc-hash 2.1.1", "smol_str", - "thiserror 2.0.17", + "thiserror 2.0.18", "web-time", ] @@ -1802,7 +1957,7 @@ dependencies = [ "log", "raw-window-handle", "rustc-hash 2.1.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-segmentation", ] @@ -1826,7 +1981,7 @@ dependencies = [ "iced_tiny_skia", "iced_wgpu", "log", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1839,7 +1994,7 @@ dependencies = [ "iced_core", "iced_futures", "raw-window-handle", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1875,7 +2030,7 @@ dependencies = [ "iced_graphics", "log", "rustc-hash 2.1.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "wgpu", ] @@ -1889,7 +2044,7 @@ dependencies = [ "log", "num-traits", "rustc-hash 2.1.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-segmentation", ] @@ -1904,7 +2059,7 @@ dependencies = [ "log", "mundy", "rustc-hash 2.1.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "wasm-bindgen-futures", "web-sys", @@ -1993,6 +2148,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -2034,8 +2195,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.0", - "zune-jpeg 0.5.8", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", ] [[package]] @@ -2056,12 +2217,14 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -2075,6 +2238,12 @@ dependencies = [ "syn", ] +[[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" @@ -2124,9 +2293,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2183,6 +2352,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -2191,15 +2366,15 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.179" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", @@ -2217,9 +2392,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" @@ -2229,7 +2404,7 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.7.0", + "redox_syscall 0.7.1", ] [[package]] @@ -2325,9 +2500,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" @@ -2372,6 +2547,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "moxcms" version = "0.7.11" @@ -2423,7 +2609,7 @@ dependencies = [ "bit-set", "bitflags 2.10.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "codespan-reporting", "half", "hashbrown 0.16.1", @@ -2435,7 +2621,7 @@ dependencies = [ "once_cell", "rustc-hash 1.1.0", "spirv", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-ident", ] @@ -2476,16 +2662,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "nix" -version = "0.30.1" +name = "nibble_vec" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.10.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.1.1", "libc", - "memoffset", ] [[package]] @@ -2957,6 +3151,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "option-ext" version = "0.2.0" @@ -3132,15 +3332,15 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -3175,6 +3375,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -3186,9 +3396,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3247,9 +3457,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -3260,6 +3470,16 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.6" @@ -3278,7 +3498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3298,7 +3518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3307,14 +3527,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -3361,7 +3581,7 @@ dependencies = [ "rand 0.9.2", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.17", + "thiserror 2.0.18", "v_frame", "wasm-bindgen", ] @@ -3438,9 +3658,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" dependencies = [ "bitflags 2.10.0", ] @@ -3451,9 +3671,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3464,18 +3684,18 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "retry" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e211f878258887b3e65dd3c8ff9f530fe109f441a117ee0cdc27f341355032" +checksum = "1cab9bd343c737660e523ee69f788018f3db686d537d2fd0f99c9f747c1bda4f" dependencies = [ "rand 0.9.2", ] [[package]] name = "rfd" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069d6129dede311430d0dcf1ded88a7affc7a342c2d8e6336043d43ed14dac17" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" dependencies = [ "block2 0.6.2", "dispatch2", @@ -3512,7 +3732,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3588,9 +3808,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] @@ -3613,10 +3833,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "ryu" -version = "1.0.22" +name = "rustyline" +version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width 0.1.14", + "utf8parse", + "windows-sys 0.52.0", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3770,9 +4012,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" @@ -3828,7 +4070,7 @@ dependencies = [ "log", "memmap2", "rustix 1.1.3", - "thiserror 2.0.17", + "thiserror 2.0.18", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -3861,6 +4103,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "softbuffer" version = "0.4.8" @@ -3919,6 +4171,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3944,9 +4202,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.113" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -3986,12 +4244,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix 1.1.3", "windows-sys 0.61.2", @@ -4017,11 +4275,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4037,9 +4295,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -4155,10 +4413,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "toml" -version = "0.9.10+spec-1.1.0" +name = "tokio" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", @@ -4192,9 +4478,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" dependencies = [ "winnow", ] @@ -4247,26 +4533,61 @@ dependencies = [ ] [[package]] -name = "uad-ng" +name = "uad-cli" +version = "1.2.0" +dependencies = [ + "chrono", + "clap", + "clap_complete", + "csv", + "dirs", + "log", + "retry", + "rustyline", + "serde", + "serde_json", + "tokio", + "toml", + "uad-core", + "ureq", +] + +[[package]] +name = "uad-core" version = "1.2.0" dependencies = [ "chrono", "csv", "dark-light", "dirs", + "flate2", + "iced", + "log", + "retry", + "serde", + "serde_json", + "tar", + "toml", + "ureq", + "win32console", +] + +[[package]] +name = "uad-gui" +version = "1.2.0" +dependencies = [ + "chrono", + "dark-light", + "dirs", "embed-resource", "fern", "flate2", "iced", "image", "log", - "retry", "rfd", - "serde", - "serde_json", "tar", - "toml", - "ureq", + "uad-core", "win32console", ] @@ -4289,9 +4610,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-linebreak" @@ -4311,12 +4632,24 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -4325,9 +4658,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.1.4" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" dependencies = [ "base64", "cookie_store", @@ -4381,10 +4714,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "uuid" -version = "1.19.0" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "js-sys", "serde_core", @@ -4452,18 +4791,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -4474,11 +4822,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -4487,9 +4836,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4497,9 +4846,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -4510,13 +4859,47 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -4668,9 +5051,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -4688,9 +5071,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -4710,7 +5093,7 @@ dependencies = [ "arrayvec", "bitflags 2.10.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "document-features", "hashbrown 0.16.1", "js-sys", @@ -4741,7 +5124,7 @@ dependencies = [ "bit-vec", "bitflags 2.10.0", "bytemuck", - "cfg_aliases", + "cfg_aliases 0.2.1", "document-features", "hashbrown 0.16.1", "indexmap", @@ -4754,7 +5137,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-windows-linux-android", @@ -4803,7 +5186,7 @@ dependencies = [ "block", "bytemuck", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "core-graphics-types 0.2.0", "glow", "glutin_wgl_sys", @@ -4830,7 +5213,7 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wasm-bindgen", "web-sys", "wgpu-types", @@ -4848,7 +5231,7 @@ dependencies = [ "bytemuck", "js-sys", "log", - "thiserror 2.0.17", + "thiserror 2.0.18", "web-sys", ] @@ -4903,7 +5286,7 @@ dependencies = [ "clipboard_wayland", "clipboard_x11", "raw-window-handle", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -5390,7 +5773,7 @@ dependencies = [ "block2 0.5.1", "bytemuck", "calloop 0.13.0", - "cfg_aliases", + "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation 0.9.4", "core-graphics", @@ -5460,9 +5843,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -5580,9 +6045,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.12.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" dependencies = [ "async-broadcast", "async-executor", @@ -5598,8 +6063,9 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix 1.1.3", "serde", "serde_repr", "tracing", @@ -5614,9 +6080,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.12.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -5629,12 +6095,11 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", "winnow", "zvariant", ] @@ -5647,18 +6112,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -5727,9 +6192,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.12" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zune-core" @@ -5739,9 +6204,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-core" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-inflate" @@ -5763,18 +6228,18 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.8" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ - "zune-core 0.5.0", + "zune-core 0.5.1", ] [[package]] name = "zvariant" -version = "5.8.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" dependencies = [ "endi", "enumflags2", @@ -5787,9 +6252,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.8.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -5800,9 +6265,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2a863e0..3529c3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,12 @@ -[package] -name = "uad-ng" -description = "A cross-platform GUI debloater for android devices" +[workspace] +members = [ + "crates/uad-core", + "crates/uad-gui", + "crates/uad-cli" +] +resolver = "2" + +[workspace.package] version = "1.2.0" authors = ["Universal-Debloater-Alliance"] license = "GPL-3.0" @@ -11,59 +17,38 @@ keywords = ["debloater", "android", "adb", "privacy", "bloatware"] categories = ["gui"] edition = "2024" -[features] -default = ["wgpu", "self-update", "img"] -wgpu = [] # Iced/wgpu is default -self-update = ["flate2", "tar"] -no-self-update = [] -img = ["image", "iced/image"] - -[dependencies] -dark-light = "2" +[workspace.dependencies] +# Core dependencies serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" -fern = { version = "^0", features = ["colored"] } -chrono = { version = "^0.4", default-features = false, features = [ - "std", - "clock", -] } log = "^0.4" +chrono = { version = "^0.4", default-features = false, features = ["std", "clock"] } toml = "^0" dirs = "^6" ureq = { version = "3", features = ["json"] } retry = "^2.0.0" -iced = { version = "=0.14.0", features = ["advanced"] } -image = { version = "0.25", optional = true } -rfd = "^0.17.1" csv = "^1.3" -[target.'cfg(not(target_os = "windows"))'.dependencies] -flate2 = { version = "^1", optional = true } -tar = { version = "^0.4", optional = true } +# GUI dependencies +iced = { version = "=0.14.0", features = ["advanced"] } +image = "0.25" +rfd = "^0.17.1" +dark-light = "2" +fern = { version = "^0", features = ["colored"] } -[target.'cfg(target_os = "windows")'.dependencies] +# CLI dependencies +clap = { version = "4.5", features = ["derive"] } +tokio = { version = "1.0", features = ["full"] } + +# Platform-specific dependencies +flate2 = "^1" +tar = "^0.4" win32console = "^0.1.5" -[profile.release] -lto = "thin" -opt-level = 2 -codegen-units = 4 - -[profile.opt] -inherits = "release" -opt-level = "s" -codegen-units = 1 -lto = "fat" -strip = true -panic = "abort" - -[build-dependencies] -embed-resource = "3" - -[lints.rust] +[workspace.lints.rust] deprecated_safe = "warn" -[lints.clippy] +[workspace.lints.clippy] undocumented_unsafe_blocks = "forbid" panic_in_result_fn = "warn" infinite_loop = "warn" @@ -75,4 +60,4 @@ struct_field_names = "allow" # annoying disallowed_types = "deny" disallowed_methods = "deny" -pedantic = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } \ No newline at end of file diff --git a/crates/uad-cli/Cargo.toml b/crates/uad-cli/Cargo.toml new file mode 100644 index 0000000..d0118ae --- /dev/null +++ b/crates/uad-cli/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "uad-cli" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true +categories = ["command-line-utilities"] +edition.workspace = true + +[[bin]] +name = "uad" +path = "src/main.rs" + +[dependencies] +uad-core = { path = "../uad-core" } +clap.workspace = true +clap_complete = "4.4" +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +log.workspace = true +chrono.workspace = true +toml.workspace = true +dirs.workspace = true +ureq.workspace = true +retry.workspace = true +csv.workspace = true +rustyline = "14.0" + +[lints.rust] +deprecated_safe = "warn" + +[lints.clippy] +undocumented_unsafe_blocks = "deny" +exit = "deny" +panic_in_result_fn = "warn" +infinite_loop = "warn" +mem_forget = "warn" +implicit_clone = "warn" +format_push_string = "warn" +large_include_file = "warn" +shadow_unrelated = "warn" +struct_field_names = "allow" # annoying +module_name_repetitions = "allow" # annoying + +disallowed_types = "deny" +disallowed_methods = "deny" + +allow_attributes_without_reason = "warn" +pedantic = { level = "warn", priority = -1 } diff --git a/crates/uad-cli/README.md b/crates/uad-cli/README.md new file mode 100644 index 0000000..86a2a77 --- /dev/null +++ b/crates/uad-cli/README.md @@ -0,0 +1,276 @@ +# UAD CLI - Universal Android Debloater Command Line Interface + +A powerful command-line interface for debloating Android devices, sharing the core functionality with the UAD GUI application. + +## Features + +- 🚀 **Fast & Efficient** - Direct command execution without GUI overhead +- 📦 **Package Manager-like Interface** - Intuitive commands similar to `apt`, `dnf`, or `pacman` +- 🔍 **Advanced Filtering** - Filter packages by state, removal category, UAD list, or search terms +- 🛡️ **Safety Features** - Dry-run mode, warnings for unsafe packages +- 🔄 **Multiple Actions** - Enable, disable, uninstall, and restore packages +- 🖥️ **Interactive REPL Mode** - Keep device state in memory for faster operations +- 📋 **Shell Completion** - Generate completions for bash, zsh, fish, and more +- 👥 **Multi-user Support** - Target specific Android user profiles + +## Installation + +Build from source: + +```bash +cd /path/to/universal-android-debloater-next-generation +cargo build --release -p uad-cli +``` + +The binary will be available at `target/release/uad` (the package is `uad-cli`; the installed binary name is `uad`). + +## Usage + +### List Connected Devices + +```bash +uad devices +``` + +### List Packages + +List all packages on the first connected device: + +```bash +uad list +# or use the short alias +uad ls +``` + +Filter packages by state: + +```bash +uad list --state enabled +uad list --state disabled +uad list --state uninstalled +``` + +Filter by removal category: + +```bash +uad list --removal recommended +uad list --removal advanced +uad list --removal expert +uad list --removal unsafe +``` + +Filter by UAD list: + +```bash +uad list --list google +uad list --list oem +uad list --list aosp +``` + +Search for packages: + +```bash +uad list --search "facebook" +uad list -q "chrome" +``` + +Combine filters: + +```bash +uad list --state enabled --removal recommended --search "google" +``` + +### Show Package Information + +```bash +uad info com.android.chrome +uad info com.facebook.katana --device SERIAL123 +``` + +### Uninstall Packages + +Uninstall one or more packages: + +```bash +uad uninstall com.facebook.katana +uad rm com.facebook.katana com.facebook.services +``` + +Dry-run to see what would happen: + +```bash +uad uninstall com.facebook.katana --dry-run +``` + +Specify device and user: + +```bash +uad uninstall com.facebook.katana --device SERIAL123 --user 0 +``` + +### Enable/Restore Packages + +```bash +uad enable com.android.chrome +uad restore com.google.android.gms +``` + +### Disable Packages + +```bash +uad disable com.facebook.katana +``` + +### Update Package Lists + +Update UAD lists from the remote repository: + +```bash +uad update +``` + +### Interactive REPL Mode + +Start an interactive session for faster repeated operations: + +```bash +uad repl +# or +uad shell +``` + +Within the REPL: + +``` +uad> help +uad> list --state enabled +uad> info com.facebook.katana +uad> uninstall com.facebook.katana +uad> enable com.android.chrome +uad> device +uad> exit +``` + +The REPL mode: +- Keeps device state in memory (faster operations) +- Saves command history (accessible with up/down arrows) +- Loads UAD lists once at startup +- Supports all the same commands as the CLI + +### Shell Completion + +Generate completion scripts for your shell: + +```bash +# Bash +uad completions bash > /usr/share/bash-completion/completions/uad + +# Zsh +uad completions zsh > /usr/local/share/zsh/site-functions/_uad + +# Fish +uad completions fish > ~/.config/fish/completions/uad.fish + +# PowerShell +uad completions powershell > uad.ps1 +``` + +## Command Reference + +| Command | Aliases | Description | +|---------|---------|-------------| +| `devices` | - | List connected Android devices | +| `list` | `ls` | List packages with optional filters | +| `info` | - | Show detailed package information | +| `uninstall` | `rm` | Uninstall one or more packages | +| `enable` | `restore` | Enable/restore packages | +| `disable` | - | Disable packages (keeps data) | +| `update` | - | Update UAD package lists | +| `completions` | - | Generate shell completions | +| `repl` | `shell` | Start interactive mode | + +## Examples + +### Remove all Facebook packages + +```bash +uad list --search facebook +uad uninstall com.facebook.katana com.facebook.services --dry-run +# If it looks good: +uad uninstall com.facebook.katana com.facebook.services +``` + +### Debloat recommended Google apps + +```bash +# See what would be uninstalled +uad list --removal recommended --list google + +# Uninstall them (you'd list the actual package names) +uad uninstall com.google.package1 com.google.package2 +``` + +### Work with a specific device + +```bash +# List devices +uad devices + +# Use specific device +uad list --device SERIAL123 +uad uninstall com.facebook.katana --device SERIAL123 +``` + +## Safety + +- **Dry-run mode**: Always test with `--dry-run` first +- **Warnings**: The CLI warns you about packages marked as "Unsafe" +- **Reversible**: Most operations can be reversed with the `enable` command +- **Multi-user aware**: Respects Android's multi-user system + +## Comparison with GUI + +### CLI Advantages: +- Faster for power users +- Scriptable and automatable +- Lower resource usage +- Works over SSH +- Better for batch operations +- Can run on Android itself (though not recommended for same-device debloating) + +### GUI Advantages: +- Visual feedback +- Easier for beginners +- Package descriptions always visible +- Batch selection with checkboxes + +## Requirements + +- ADB (Android Debug Bridge) installed and in PATH +- Android device with USB debugging enabled +- Device authorized in ADB (run `adb devices` to check) + +## Troubleshooting + +**No devices found** +- Ensure USB debugging is enabled on your device +- Run `adb devices` to check if device is authorized +- Try unplugging and replugging the device + +**Package not found** +- The package might already be uninstalled +- Check the exact package name with `adb shell pm list packages` +- Try running `uad update` to refresh package lists + +**Permission denied** +- Some system packages can't be modified without root +- Some devices have locked bootloaders preventing certain operations + +## Contributing + +See the main project [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines. + +## License + +GPL-3.0 - See [LICENSE](../../LICENSE) for details. + diff --git a/crates/uad-cli/examples/usage.sh b/crates/uad-cli/examples/usage.sh new file mode 100755 index 0000000..ab3192e --- /dev/null +++ b/crates/uad-cli/examples/usage.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Example usage script for UAD CLI + +# Check if uad-cli is available +if ! command -v uad &> /dev/null; then + echo "uad command not found. Please build and install uad-cli first." + exit 1 +fi + +echo "=== UAD CLI Usage Examples ===" +echo + +echo "1. List connected devices:" +uad devices +echo + +echo "2. List all enabled packages:" +uad list --state enabled | head -20 +echo "... (showing first 20)" +echo + +echo "3. Search for Google packages:" +uad list --search google | head -10 +echo "... (showing first 10)" +echo + +echo "4. Show info about a package (example):" +echo "uad info com.google.android.gms" +echo + +echo "5. Dry-run uninstall (safe to test):" +echo "uad uninstall com.example.package --dry-run" +echo + +echo "6. Start interactive mode:" +echo "uad repl" +echo + +echo "=== Generate shell completions ===" +echo "For bash: uad completions bash > ~/.local/share/bash-completion/completions/uad" +echo "For zsh: uad completions zsh > ~/.local/share/zsh/site-functions/_uad" +echo "For fish: uad completions fish > ~/.config/fish/completions/uad.fish" + diff --git a/crates/uad-cli/src/commands.rs b/crates/uad-cli/src/commands.rs new file mode 100644 index 0000000..3d0f13d --- /dev/null +++ b/crates/uad-cli/src/commands.rs @@ -0,0 +1,464 @@ +use clap::CommandFactory; +use clap_complete::{Shell, generate}; +use std::collections::{HashMap, HashSet}; +use std::io::Write; +use uad_core::adb::{ACommand, PmListPacksFlag}; +use uad_core::sync::{ + CorePackage, Phone, User, apply_pkg_state_commands, get_devices_list, get_package_state, + run_adb_shell_action, +}; +use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists}; +use uad_core::utils::{matches_search, truncate_description}; + +use crate::device::{get_target_device, get_user}; +use crate::filters::{ListFilter, RemovalFilter, StateFilter}; +use crate::{Cli, print_or_exit, println_or_exit}; + +/// List all connected Android devices +pub fn list_devices() -> Result<(), Box> { + println!("Scanning for connected devices..."); + let devices = get_devices_list(); + + if devices.is_empty() { + eprintln!("No devices found. Make sure ADB is installed and devices are connected."); + return Err("No devices found".into()); + } + + println!("\nFound {} device(s):\n", devices.len()); + for device in &devices { + println!(" Model: {}", device.model); + println!(" Serial: {}", device.adb_id); + println!(" Android SDK: {}", device.android_sdk); + + if !device.user_list.is_empty() { + println!(" Users: {} user(s)", device.user_list.len()); + for user in &device.user_list { + let protected = if user.protected { " (protected)" } else { "" }; + println!(" - User ID: {}{}", user.id, protected); + } + } + println!(); + } + + Ok(()) +} + +/// Context for package filtering and display +pub struct PackageListContext { + pub state_filter: Option, + pub removal_filter: Option, + pub list_filter: Option, + pub search: Option, +} + +impl PackageListContext { + /// Check if package passes all filters + pub fn filter_package( + &self, + pkg_name: &str, + pkg_info: Option<&Package>, + pkg_state: PackageState, + ) -> bool { + // Removal filter + if let Some(removal) = self.removal_filter { + if !removal.matches(pkg_info) { + return false; + } + } + + // State filter + if let Some(state) = self.state_filter { + if !state.matches(pkg_state) { + return false; + } + } + + // List filter + if let Some(list) = self.list_filter { + if !list.matches(pkg_info) { + return false; + } + } + + // Search filter + if let Some(ref search_term) = self.search { + let description = pkg_info.map(|p| p.description.as_str()); + if !matches_search(pkg_name, search_term, description) { + return false; + } + } + + true + } + + /// Determine what information to show based on active filters + pub fn display_config(&self) -> DisplayConfig { + DisplayConfig { + show_state: self.state_filter.is_none_or(|f| !f.is_specific()), + show_removal: self.removal_filter.is_none_or(|f| !f.is_specific()), + } + } +} + +pub struct DisplayConfig { + pub show_state: bool, + pub show_removal: bool, +} + +/// List packages on a device with filtering +pub fn list_packages( + device: Option, + state_filter: Option, + removal_filter: Option, + list_filter: Option, + search: Option, + user_id: Option, +) -> Result<(), Box> { + let target_device = get_target_device(device)?; + let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists); + + println_or_exit!( + "Listing packages on: {} ({})\n", + target_device.model, + target_device.adb_id + ); + + let context = PackageListContext { + state_filter, + removal_filter, + list_filter, + search, + }; + + let pm_flag = state_filter.and_then(StateFilter::to_pm_flag); + let system_packages = ACommand::new() + .shell(&target_device.adb_id) + .pm() + .list_packages_sys(pm_flag, user_id)?; + + let displayed_count = display_package_list( + &system_packages, + &uad_lists, + &target_device.adb_id, + user_id, + &context, + )?; + + if displayed_count == 0 { + println_or_exit!(" No packages found matching the specified filters."); + } else { + println_or_exit!("\nTotal: {} package(s)", displayed_count); + } + + Ok(()) +} + +/// Display a list of packages with their info +pub fn display_package_list( + packages: &[String], + uad_lists: &HashMap, + device_serial: &str, + user_id: Option, + context: &PackageListContext, +) -> Result> { + let config = context.display_config(); + let mut displayed_count = 0; + + let enabled_packages: HashSet = ACommand::new() + .shell(device_serial) + .pm() + .list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), user_id) + .unwrap_or_default() + .into_iter() + .collect(); + let disabled_packages: HashSet = ACommand::new() + .shell(device_serial) + .pm() + .list_packages_sys(Some(PmListPacksFlag::OnlyDisabled), user_id) + .unwrap_or_default() + .into_iter() + .collect(); + + for pkg_name in packages { + let pkg_info = uad_lists.get(pkg_name); + + let pkg_state = if enabled_packages.contains(pkg_name) { + PackageState::Enabled + } else if disabled_packages.contains(pkg_name) { + PackageState::Disabled + } else { + PackageState::Uninstalled + }; + + if !context.filter_package(pkg_name, pkg_info, pkg_state) { + continue; + } + + display_package_entry(pkg_name, pkg_info, pkg_state, &config); + displayed_count += 1; + } + + Ok(displayed_count) +} + +/// Display a single package entry +pub fn display_package_entry( + pkg_name: &str, + pkg_info: Option<&Package>, + pkg_state: PackageState, + config: &DisplayConfig, +) { + print_or_exit!("["); + + if let Some(info) = pkg_info { + if config.show_removal { + print_or_exit!("{}", info.removal); + if config.show_state { + print_or_exit!(" - "); + } + } + if config.show_state { + print_or_exit!("{}", pkg_state); + } + print_or_exit!("] {}", pkg_name); + if !info.description.is_empty() { + print_or_exit!(" - {}", truncate_description(&info.description, 80)); + } + } else { + if config.show_removal { + print_or_exit!("Unlisted"); + if config.show_state { + print_or_exit!(" - "); + } + } + if config.show_state { + print_or_exit!("{}", pkg_state); + } + print_or_exit!("] {}", pkg_name); + } + + println_or_exit!(); +} + +/// Change the state of one or more packages +pub fn change_package_state( + packages: &[String], + device: Option, + user_id: Option, + dry_run: bool, + target_state: PackageState, + action_name: &str, +) -> Result<(), Box> { + if packages.is_empty() { + eprintln!("Error: No packages specified"); + return Err("No packages specified".into()); + } + + let target_device = get_target_device(device)?; + let user = get_user(&target_device, user_id)?; + let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists); + + println!( + "{} {} package(s) on: {} ({})\n", + action_name, + packages.len(), + target_device.model, + target_device.adb_id + ); + + if dry_run { + println!("DRY RUN - No changes will be made\n"); + } + + for pkg_name in packages { + process_package_state_change( + pkg_name, + &target_device, + user, + target_state, + dry_run, + &uad_lists, + )?; + println!(); + } + + if dry_run { + println!("Dry run completed. No changes were made."); + } else { + println!("Operation completed successfully."); + } + + Ok(()) +} + +/// Process state change for a single package +fn process_package_state_change( + pkg_name: &str, + device: &Phone, + user: User, + target_state: PackageState, + dry_run: bool, + uad_lists: &HashMap, +) -> Result<(), Box> { + let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id)) + .ok_or("Package not found on device")?; + + println!(" {} ({})", pkg_name, current_state); + + if current_state == target_state { + println!(" → Already in target state, skipping"); + return Ok(()); + } + + let pkg_info = uad_lists.get(pkg_name); + if let Some(info) = pkg_info { + if info.removal == Removal::Unsafe { + println!(" ⚠ WARNING: This package is marked as UNSAFE to remove!"); + } + } + + let core_pkg = CorePackage { + name: pkg_name.to_string(), + description: pkg_info.map(|p| p.description.clone()).unwrap_or_default(), + removal: pkg_info.map(|p| p.removal).unwrap_or(Removal::Unlisted), + state: current_state, + list: pkg_info.map(|p| p.list).unwrap_or(UadList::Unlisted), + }; + + let commands = apply_pkg_state_commands(&core_pkg, target_state, user, device); + + if dry_run { + for cmd in &commands { + println!(" Would run: {}", cmd); + } + } else { + execute_with_fallback( + pkg_name, + target_state, + &core_pkg, + user, + device, + &commands, + " ", + )?; + } + + Ok(()) +} + +/// Execute commands and verify package state with fallback +pub fn execute_with_fallback( + package: &str, + target_state: PackageState, + core_pkg: &CorePackage, + user: User, + device: &Phone, + commands: &[String], + indent: &str, +) -> Result<(), Box> { + // Capture the before-state of packages on other users for cross-user detection + let before_cross_user_states = + uad_core::sync::capture_cross_user_states(package, &device.adb_id, user.id, device); + + // Execute commands + for cmd in commands { + match run_adb_shell_action(&device.adb_id, cmd.as_str()) { + Ok(_) => println!("{}✓ {}", indent, cmd), + Err(e) => { + eprintln!("{}✗ Failed: {:?}", indent, e); + return Err(format!("Failed to execute: {}", cmd).into()); + } + } + } + + // Verify package state and attempt fallback if needed + let actual_state = + get_package_state(&device.adb_id, package, Some(user.id)).unwrap_or(PackageState::Enabled); + + if actual_state != target_state { + println!( + "{}⚠ Package state verification failed: expected {:?}, got {:?}", + indent, target_state, actual_state + ); + + // Attempt fallback + if let Ok(fallback_action) = + uad_core::sync::attempt_fallback(core_pkg, target_state, actual_state, user, device) + { + println!("{}↻ Fallback: {}", indent, fallback_action); + } else { + println!("{}✗ No fallback available", indent); + } + } + + // Check for cross-user behavior if operation succeeded + if actual_state == target_state { + if let Some(notification) = uad_core::sync::detect_cross_user_behavior( + package, + device.adb_id.as_str(), + user.id, + target_state, + actual_state, + device, + &before_cross_user_states, + ) { + println!("{}ℹ {}", indent, notification); + } + } + + Ok(()) +} + +/// Show detailed information about a package +pub fn show_package_info( + package: &str, + device: Option, +) -> Result<(), Box> { + println!("Package: {}\n", package); + + let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists); + + if let Some(pkg_info) = uad_lists.get(package) { + println!("UAD Information:"); + println!(" List: {}", pkg_info.list); + println!(" Removal: {}", pkg_info.removal); + println!(" Description: {}", pkg_info.description); + println!(); + } else { + println!(" Not found in UAD lists (unlisted package)\n"); + } + + if let Some(device_id) = device { + let target_device = get_target_device(Some(device_id))?; + println!("Device: {} ({})", target_device.model, target_device.adb_id); + + let state = get_package_state(&target_device.adb_id, package, None) + .ok_or("Package not found on device")?; + println!(" State: {}", state); + } + + Ok(()) +} + +/// Update UAD package lists from remote repository +pub fn update_lists() -> Result<(), Box> { + println!("Updating UAD package lists from remote repository..."); + + match load_debloat_lists(true) { + Ok(_lists) => { + println!("✓ Successfully updated package lists"); + Ok(()) + } + Err(_lists) => { + eprintln!("✗ Failed to update lists from remote, using cached version"); + Err("Failed to update lists".into()) + } + } +} + +/// Generate shell completion script +pub fn generate_completions(shell: Shell) { + let mut cmd = Cli::command(); + let name = cmd.get_name().to_string(); + generate(shell, &mut cmd, name, &mut std::io::stdout()); +} diff --git a/crates/uad-cli/src/device.rs b/crates/uad-cli/src/device.rs new file mode 100644 index 0000000..ddc23a9 --- /dev/null +++ b/crates/uad-cli/src/device.rs @@ -0,0 +1,45 @@ +use uad_core::sync::{Phone, User, get_devices_list}; + +/// Get target device, either by serial or first available +pub fn get_target_device(device: Option) -> Result> { + let devices = get_devices_list(); + + if devices.is_empty() { + eprintln!("Error: No devices found"); + return Err("No devices found".into()); + } + + let target_device = if let Some(device_id) = device { + devices + .iter() + .find(|d| d.adb_id == device_id) + .ok_or("Device not found")? + .clone() + } else { + if devices.len() > 1 { + eprintln!( + "Warning: Multiple devices found, using first one: {}", + devices[0].adb_id + ); + } + devices[0].clone() + }; + + Ok(target_device) +} + +/// Get user from device, creating a basic one if not found +pub fn get_user(device: &Phone, user_id: Option) -> Result> { + let uid = user_id.unwrap_or(0); + + if let Some(user) = device.user_list.iter().find(|u| u.id == uid) { + Ok(*user) + } else { + // Create a basic user if not found in list + Ok(User { + id: uid, + index: 0, + protected: false, + }) + } +} diff --git a/crates/uad-cli/src/filters.rs b/crates/uad-cli/src/filters.rs new file mode 100644 index 0000000..5ae156f --- /dev/null +++ b/crates/uad-cli/src/filters.rs @@ -0,0 +1,98 @@ +use clap::ValueEnum; +use uad_core::adb::PmListPacksFlag; +use uad_core::uad_lists::{Package, PackageState, Removal, UadList}; + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum StateFilter { + /// Show all packages regardless of state + All, + /// Show only enabled packages + Enabled, + /// Show only disabled packages + Disabled, + /// Show only uninstalled packages + Uninstalled, +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum RemovalFilter { + All, + Recommended, + Advanced, + Expert, + Unsafe, + Unlisted, +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum ListFilter { + All, + Aosp, + Carrier, + Google, + Misc, + Oem, + Pending, + Unlisted, +} + +impl StateFilter { + pub fn to_pm_flag(self) -> Option { + match self { + Self::Enabled => Some(PmListPacksFlag::OnlyEnabled), + Self::Disabled => Some(PmListPacksFlag::OnlyDisabled), + Self::Uninstalled | Self::All => Some(PmListPacksFlag::IncludeUninstalled), + } + } + + pub fn matches(self, pkg_state: PackageState) -> bool { + match self { + Self::All => true, + Self::Enabled => pkg_state == PackageState::Enabled, + Self::Disabled => pkg_state == PackageState::Disabled, + Self::Uninstalled => pkg_state == PackageState::Uninstalled, + } + } + + pub fn is_specific(self) -> bool { + matches!(self, Self::Enabled | Self::Disabled | Self::Uninstalled) + } +} + +impl RemovalFilter { + pub fn matches(self, pkg_info: Option<&Package>) -> bool { + match pkg_info { + Some(info) => match self { + Self::All => true, + Self::Recommended => info.removal == Removal::Recommended, + Self::Advanced => info.removal == Removal::Advanced, + Self::Expert => info.removal == Removal::Expert, + Self::Unsafe => info.removal == Removal::Unsafe, + Self::Unlisted => info.removal == Removal::Unlisted, + }, + None => matches!(self, Self::All | Self::Unlisted), + } + } + + pub fn is_specific(self) -> bool { + !matches!(self, Self::All) + } +} + +impl ListFilter { + pub fn matches(self, pkg_info: Option<&Package>) -> bool { + match pkg_info { + Some(info) => match self { + Self::All => true, + Self::Aosp => info.list == UadList::Aosp, + Self::Carrier => info.list == UadList::Carrier, + Self::Google => info.list == UadList::Google, + Self::Misc => info.list == UadList::Misc, + Self::Oem => info.list == UadList::Oem, + Self::Pending => info.list == UadList::Pending, + Self::Unlisted => info.list == UadList::Unlisted, + }, + None => matches!(self, Self::All | Self::Unlisted), + } + } +} diff --git a/crates/uad-cli/src/main.rs b/crates/uad-cli/src/main.rs new file mode 100644 index 0000000..e849e68 --- /dev/null +++ b/crates/uad-cli/src/main.rs @@ -0,0 +1,232 @@ +#![allow( + clippy::needless_continue, + clippy::collapsible_if, + clippy::uninlined_format_args, + clippy::map_unwrap_or, + clippy::unnecessary_wraps, + reason = "Suppress non-critical pedantic/style lints to keep build green" +)] + +use clap::{Parser, Subcommand}; +use clap_complete::Shell; +use uad_core::uad_lists::PackageState; + +mod commands; +mod device; +mod filters; +mod output; +mod repl; + +use filters::{ListFilter, RemovalFilter, StateFilter}; + +#[derive(Parser)] +#[command(name = "uad")] +#[command(about = "Universal Android Debloater - Command Line Interface", long_about = None)] +#[command(version)] +#[command(propagate_version = true)] +pub struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// List connected Android devices + Devices, + + /// List packages on a device + #[command(name = "list", visible_alias = "ls")] + List { + /// Device serial number (optional, uses first device if not specified) + #[arg(short, long)] + device: Option, + + /// Filter by package state + #[arg(short, long, value_enum)] + state: Option, + + /// Filter by removal category + #[arg(short, long, value_enum)] + removal: Option, + + /// Filter by UAD list + #[arg(short, long, value_enum)] + list: Option, + + /// Search pattern (matches package name or description) + #[arg(short = 'q', long)] + search: Option, + + /// User ID (defaults to 0) + #[arg(short, long)] + user: Option, + }, + + /// Uninstall packages (default removal action) + #[command(visible_alias = "rm")] + Uninstall { + /// Package names to uninstall + packages: Vec, + + /// Device serial number (optional, uses first device if not specified) + #[arg(short, long)] + device: Option, + + /// User ID (defaults to 0) + #[arg(short, long)] + user: Option, + + /// Dry run - show what would be done without actually doing it + #[arg(long)] + dry_run: bool, + }, + + /// Restore (reinstall) packages + #[command(visible_alias = "restore")] + Enable { + /// Package names to restore/enable + packages: Vec, + + /// Device serial number (optional, uses first device if not specified) + #[arg(short, long)] + device: Option, + + /// User ID (defaults to 0) + #[arg(short, long)] + user: Option, + + /// Dry run - show what would be done without actually doing it + #[arg(long)] + dry_run: bool, + }, + + /// Disable packages (keeps data but prevents execution) + Disable { + /// Package names to disable + packages: Vec, + + /// Device serial number (optional, uses first device if not specified) + #[arg(short, long)] + device: Option, + + /// User ID (defaults to 0) + #[arg(short, long)] + user: Option, + + /// Dry run - show what would be done without actually doing it + #[arg(long)] + dry_run: bool, + }, + + /// Show detailed information about a package + Info { + /// Package name + package: String, + + /// Device serial number (optional, uses first device if not specified) + #[arg(short, long)] + device: Option, + }, + + /// Update UAD package lists from remote repository + Update, + + /// Generate shell completion script + Completions { + /// Shell to generate completions for + #[arg(value_enum)] + shell: Shell, + }, + + /// Start interactive REPL mode + #[command(visible_alias = "shell")] + Repl { + /// Device serial number (optional, uses first device if not specified) + #[arg(short, long)] + device: Option, + + /// User ID (defaults to 0) + #[arg(short, long)] + user: Option, + }, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + match cli.command { + Commands::Devices => { + commands::list_devices()?; + } + Commands::List { + device, + state, + removal, + list, + search, + user, + } => { + commands::list_packages(device, state, removal, list, search, user)?; + } + Commands::Uninstall { + packages, + device, + user, + dry_run, + } => { + commands::change_package_state( + &packages, + device, + user, + dry_run, + PackageState::Uninstalled, + "Uninstalling", + )?; + } + Commands::Enable { + packages, + device, + user, + dry_run, + } => { + commands::change_package_state( + &packages, + device, + user, + dry_run, + PackageState::Enabled, + "Enabling", + )?; + } + Commands::Disable { + packages, + device, + user, + dry_run, + } => { + commands::change_package_state( + &packages, + device, + user, + dry_run, + PackageState::Disabled, + "Disabling", + )?; + } + Commands::Info { package, device } => { + commands::show_package_info(&package, device)?; + } + Commands::Update => { + commands::update_lists()?; + } + Commands::Completions { shell } => { + commands::generate_completions(shell); + } + Commands::Repl { device, user } => { + repl::repl_mode(device, user)?; + } + } + + Ok(()) +} diff --git a/crates/uad-cli/src/output.rs b/crates/uad-cli/src/output.rs new file mode 100644 index 0000000..cff4724 --- /dev/null +++ b/crates/uad-cli/src/output.rs @@ -0,0 +1,18 @@ +/// Helper macro to handle broken pipe errors gracefully +/// When piping to commands like `head`, we want to exit cleanly when the pipe closes +#[macro_export] +macro_rules! println_or_exit { + () => { + let _ = writeln!(std::io::stdout()); + }; + ($($arg:tt)*) => { + let _ = writeln!(std::io::stdout(), $($arg)*); + }; +} + +#[macro_export] +macro_rules! print_or_exit { + ($($arg:tt)*) => { + let _ = write!(std::io::stdout(), $($arg)*); + }; +} diff --git a/crates/uad-cli/src/repl.rs b/crates/uad-cli/src/repl.rs new file mode 100644 index 0000000..0e78b6c --- /dev/null +++ b/crates/uad-cli/src/repl.rs @@ -0,0 +1,369 @@ +use rustyline::DefaultEditor; +use rustyline::error::ReadlineError; +use std::collections::HashMap; +use std::io::Write; +use uad_core::adb::ACommand; +use uad_core::sync::{CorePackage, Phone, User, apply_pkg_state_commands, get_package_state}; +use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists}; + +use crate::commands::{PackageListContext, display_package_list, execute_with_fallback}; +use crate::device::{get_target_device, get_user}; +use crate::filters::StateFilter; +use crate::println_or_exit; + +/// Start interactive REPL mode +pub fn repl_mode( + device: Option, + user_id: Option, +) -> Result<(), Box> { + println!("Universal Android Debloater - Interactive Mode"); + println!("Type 'help' for available commands, 'exit' or 'quit' to leave\n"); + + let target_device = get_target_device(device)?; + let user = get_user(&target_device, user_id)?; + + println!( + "Connected to: {} ({})", + target_device.model, target_device.adb_id + ); + println!("User: {}\n", user.id); + + let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists); + let mut rl = DefaultEditor::new()?; + + // Try to load history + let history_file = dirs::cache_dir().map(|d| d.join("uad").join("cli_history.txt")); + if let Some(ref path) = history_file { + let _ = rl.load_history(path); + } + + loop { + let readline = rl.readline("uad> "); + match readline { + Ok(line) => { + if let Err(e) = + handle_repl_line(&line, &mut rl, &target_device, user, user_id, &uad_lists) + { + if e.to_string() == "exit" { + break; + } + eprintln!("Error: {}", e); + } + } + Err(ReadlineError::Interrupted) => { + println!("Interrupted (Ctrl-C). Type 'exit' to quit."); + continue; + } + Err(ReadlineError::Eof) => { + println!("Goodbye!"); + break; + } + Err(err) => { + eprintln!("Error: {:?}", err); + break; + } + } + } + + // Save history + if let Some(ref path) = history_file { + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = rl.save_history(path); + } + + Ok(()) +} + +/// Handle a single line of REPL input +fn handle_repl_line( + line: &str, + rl: &mut DefaultEditor, + device: &Phone, + user: User, + user_id: Option, + uad_lists: &HashMap, +) -> Result<(), Box> { + let line = line.trim(); + if line.is_empty() { + return Ok(()); + } + + let _ = rl.add_history_entry(line); + + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.is_empty() { + return Ok(()); + } + + match parts[0] { + "help" => print_repl_help(), + "exit" | "quit" => { + println!("Goodbye!"); + return Err("exit".into()); + } + "list" | "ls" => { + handle_list_command(&parts[1..], device, user_id, uad_lists)?; + } + "info" => { + handle_info_command(&parts[1..], device, uad_lists)?; + } + "uninstall" | "rm" => { + handle_state_change_command( + &parts[1..], + device, + user, + PackageState::Uninstalled, + "Uninstalling", + uad_lists, + )?; + } + "enable" | "restore" => { + handle_state_change_command( + &parts[1..], + device, + user, + PackageState::Enabled, + "Enabling", + uad_lists, + )?; + } + "disable" => { + handle_state_change_command( + &parts[1..], + device, + user, + PackageState::Disabled, + "Disabling", + uad_lists, + )?; + } + "device" => { + println!( + "Device: {} ({}), Android SDK: {}, User: {}", + device.model, device.adb_id, device.android_sdk, user.id + ); + } + "clear" => { + print!("\x1B[2J\x1B[1;1H"); + } + _ => { + eprintln!( + "Unknown command: '{}'. Type 'help' for available commands.", + parts[0] + ); + } + } + + Ok(()) +} + +/// Parse REPL arguments into filters +struct ReplListArgs { + state_filter: Option, + search_term: Option, +} + +impl ReplListArgs { + fn parse(args: &[&str]) -> Result { + let mut state_filter = None; + let mut search_term = None; + let mut i = 0; + + while i < args.len() { + match args[i] { + "--state" | "-s" => { + if i + 1 >= args.len() { + return Err("--state requires a value".to_string()); + } + state_filter = match args[i + 1].to_lowercase().as_str() { + "enabled" => Some(StateFilter::Enabled), + "disabled" => Some(StateFilter::Disabled), + "uninstalled" => Some(StateFilter::Uninstalled), + "all" => Some(StateFilter::All), + _ => return Err(format!("Invalid state: {}", args[i + 1])), + }; + i += 2; + } + "--search" | "-q" => { + if i + 1 >= args.len() { + return Err("--search requires a value".to_string()); + } + search_term = Some(args[i + 1].to_string()); + i += 2; + } + _ => { + return Err(format!("Unknown option: {}", args[i])); + } + } + } + + Ok(Self { + state_filter, + search_term, + }) + } +} + +/// Handle list command in REPL +fn handle_list_command( + args: &[&str], + device: &Phone, + user_id: Option, + uad_lists: &HashMap, +) -> Result<(), Box> { + let parsed = ReplListArgs::parse(args)?; + + let pm_flag = parsed.state_filter.and_then(StateFilter::to_pm_flag); + let system_packages = ACommand::new() + .shell(&device.adb_id) + .pm() + .list_packages_sys(pm_flag, user_id)?; + + let context = PackageListContext { + state_filter: parsed.state_filter, + removal_filter: None, + list_filter: None, + search: parsed.search_term, + }; + + let displayed_count = display_package_list( + &system_packages, + uad_lists, + &device.adb_id, + user_id, + &context, + )?; + + if displayed_count == 0 { + println_or_exit!(" No packages found."); + } else { + println_or_exit!("\nTotal: {} package(s)", displayed_count); + } + + Ok(()) +} + +/// Handle info command in REPL +fn handle_info_command( + args: &[&str], + device: &Phone, + uad_lists: &HashMap, +) -> Result<(), Box> { + if args.is_empty() { + eprintln!("Usage: info "); + return Ok(()); + } + + let package = args[0]; + println!("Package: {}", package); + + if let Some(pkg_info) = uad_lists.get(package) { + println!(" List: {}", pkg_info.list); + println!(" Removal: {}", pkg_info.removal); + println!(" Description: {}", pkg_info.description); + } else { + println!(" Not found in UAD lists (unlisted package)"); + } + + let state = + get_package_state(&device.adb_id, package, None).ok_or("Package not found on device")?; + println!(" State: {}", state); + + Ok(()) +} + +/// Handle state change command in REPL +fn handle_state_change_command( + args: &[&str], + device: &Phone, + user: User, + target_state: PackageState, + action_name: &str, + uad_lists: &HashMap, +) -> Result<(), Box> { + if args.is_empty() { + eprintln!( + "Usage: {} [package_name...]", + action_name.to_lowercase() + ); + return Ok(()); + } + + for pkg_name in args { + process_package_change(pkg_name, device, user, target_state, action_name, uad_lists)?; + } + + Ok(()) +} + +/// Process state change for a single package in REPL +fn process_package_change( + pkg_name: &str, + device: &Phone, + user: User, + target_state: PackageState, + action_name: &str, + uad_lists: &HashMap, +) -> Result<(), Box> { + let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id)) + .ok_or("Package not found on device")?; + + println!("{} {} (current: {})", action_name, pkg_name, current_state); + + if current_state == target_state { + println!(" → Already in target state, skipping"); + return Ok(()); + } + + let pkg_info = uad_lists.get(pkg_name); + if let Some(info) = pkg_info { + if info.removal == Removal::Unsafe { + println!(" ⚠ WARNING: This package is marked as UNSAFE to remove!"); + } + } + + let core_pkg = CorePackage { + name: pkg_name.to_string(), + description: pkg_info.map(|p| p.description.clone()).unwrap_or_default(), + removal: pkg_info.map(|p| p.removal).unwrap_or(Removal::Unlisted), + state: current_state, + list: pkg_info.map(|p| p.list).unwrap_or(UadList::Unlisted), + }; + + let commands = apply_pkg_state_commands(&core_pkg, target_state, user, device); + + execute_with_fallback( + pkg_name, + target_state, + &core_pkg, + user, + device, + &commands, + " ", + ) +} + +/// Print REPL help message +fn print_repl_help() { + println!("Available commands:"); + println!(" list [--state ] [--search ]"); + println!(" List packages with optional filters"); + println!(" info "); + println!(" Show information about a package"); + println!(" uninstall [package_name...]"); + println!(" Uninstall one or more packages"); + println!(" enable [package_name...]"); + println!(" Enable/restore one or more packages"); + println!(" disable [package_name...]"); + println!(" Disable one or more packages"); + println!(" device"); + println!(" Show current device information"); + println!(" clear"); + println!(" Clear the screen"); + println!(" help"); + println!(" Show this help message"); + println!(" exit, quit"); + println!(" Exit the interactive mode"); +} diff --git a/crates/uad-core/Cargo.toml b/crates/uad-core/Cargo.toml new file mode 100644 index 0000000..b73d746 --- /dev/null +++ b/crates/uad-core/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "uad-core" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true +categories = ["command-line-utilities"] +edition.workspace = true + +[features] +default = [] +gui = ["dep:iced", "dep:dark-light"] +self-update = ["dep:flate2", "dep:tar"] + +[dependencies] +serde.workspace = true +serde_json.workspace = true +log.workspace = true +chrono.workspace = true +toml.workspace = true +dirs.workspace = true +ureq.workspace = true +retry.workspace = true +csv.workspace = true +iced = { workspace = true, optional = true } +dark-light = { workspace = true, optional = true } +flate2 = { workspace = true, optional = true } +tar = { workspace = true, optional = true } + +[target.'cfg(target_os = "windows")'.dependencies] +win32console.workspace = true + +[lints.rust] +deprecated_safe = "warn" + +[lints.clippy] +undocumented_unsafe_blocks = "forbid" +exit = "deny" +panic_in_result_fn = "warn" +infinite_loop = "warn" +mem_forget = "warn" +implicit_clone = "warn" +format_push_string = "warn" +large_include_file = "warn" +shadow_unrelated = "warn" +struct_field_names = "allow" # annoying +module_name_repetitions = "allow" # annoying + +disallowed_types = "deny" +disallowed_methods = "deny" + +allow_attributes_without_reason = "warn" +pedantic = { level = "warn", priority = -1 } diff --git a/src/core/adb.rs b/crates/uad-core/src/adb.rs similarity index 98% rename from src/core/adb.rs rename to crates/uad-core/src/adb.rs index ecb112c..8d06268 100644 --- a/src/core/adb.rs +++ b/crates/uad-core/src/adb.rs @@ -43,16 +43,18 @@ use serde::{Deserialize, Serialize}; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; -use crate::core::utils::is_all_w_c; +use crate::utils::is_all_w_c; +use log::{error, info}; /// Convert ADB output bytes to a trimmed UTF-8 string. /// Uses lossy conversion to prevent panics on non-UTF8 output from certain OEMs. +#[must_use] pub fn to_trimmed_utf8(v: &[u8]) -> String { String::from_utf8_lossy(v).trim_end().to_string() } -#[must_use] #[cfg(debug_assertions)] +#[must_use] fn is_version_triple(s: &str) -> bool { let mut components = s.split('.'); for _ in 0..3 { @@ -213,6 +215,12 @@ impl ACommand { } } +impl Default for ACommand { + fn default() -> Self { + Self::new() + } +} + /// Builder object for a command that runs on the device's default `sh` implementation. /// Typically MKSH, but could be Ash. /// @@ -221,6 +229,7 @@ impl ACommand { pub struct ShellCommand(ACommand); impl ShellCommand { /// `pm` command builder + #[must_use] pub fn pm(mut self) -> PmCommand { self.0.0.arg("pm"); PmCommand(self) @@ -273,6 +282,7 @@ impl PackageId { /// Creates a package-ID if it's valid according to: /// - /// - + #[must_use] pub fn new(p_id: Box) -> Option { let mut components = p_id.split('.'); for _ in 0..2 { diff --git a/src/core/config.rs b/crates/uad-core/src/config.rs similarity index 86% rename from src/core/config.rs rename to crates/uad-core/src/config.rs index f2329a4..eebfc63 100644 --- a/src/core/config.rs +++ b/crates/uad-core/src/config.rs @@ -1,8 +1,8 @@ use crate::CACHE_DIR; use crate::CONFIG_DIR; -use crate::core::utils::DisplayablePath; -use crate::core::{sync::User, theme::Theme}; -use crate::gui::views::settings::Settings; +use crate::utils::DisplayablePath; +use crate::{sync::User, theme::Theme}; +use log::error; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; @@ -54,23 +54,26 @@ impl Default for GeneralSettings { static CONFIG_FILE: LazyLock = LazyLock::new(|| CONFIG_DIR.join("config.toml")); impl Config { - pub fn save_changes(settings: &Settings, device_id: &String) { - let mut config = Self::load_configuration_file(); - if let Some(device) = config + pub fn save_device_settings( + &mut self, + device_settings: DeviceSettings, + general: GeneralSettings, + ) { + if let Some(device) = self .devices .iter_mut() - .find(|x| x.device_id == *device_id) + .find(|x| x.device_id == device_settings.device_id) { - device.clone_from(&settings.device); + *device = device_settings; } else { - debug!("config: New device settings saved"); - config.devices.push(settings.device.clone()); + self.devices.push(device_settings); } - config.general.clone_from(&settings.general); - let toml = toml::to_string(&config).unwrap(); + self.general = general; + let toml = toml::to_string(&self).unwrap(); fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!"); } + #[must_use] pub fn load_configuration_file() -> Self { match fs::read_to_string(&*CONFIG_FILE) { Ok(s) => match toml::from_str(&s) { diff --git a/crates/uad-core/src/lib.rs b/crates/uad-core/src/lib.rs new file mode 100644 index 0000000..76b1271 --- /dev/null +++ b/crates/uad-core/src/lib.rs @@ -0,0 +1,24 @@ +#![allow( + clippy::missing_panics_doc, + clippy::missing_errors_doc, + clippy::collapsible_if, + clippy::uninlined_format_args, + clippy::result_unit_err, + reason = "Doc+style pedantic lints are out-of-scope for this pass" +)] + +pub mod adb; +pub mod config; +pub mod save; +pub mod sync; +pub mod theme; +pub mod uad_lists; +pub mod update; +pub mod utils; + +use std::path::PathBuf; +use std::sync::LazyLock; +pub static CONFIG_DIR: LazyLock = + LazyLock::new(|| utils::setup_uad_dir(&dirs::config_dir().expect("Can't detect config dir"))); +pub static CACHE_DIR: LazyLock = + LazyLock::new(|| utils::setup_uad_dir(&dirs::cache_dir().expect("Can't detect cache dir"))); diff --git a/src/core/save.rs b/crates/uad-core/src/save.rs similarity index 90% rename from src/core/save.rs rename to crates/uad-core/src/save.rs index 7be7084..8c1db07 100644 --- a/src/core/save.rs +++ b/crates/uad-core/src/save.rs @@ -1,7 +1,6 @@ -use crate::core::config::{Config, DeviceSettings}; -use crate::core::sync::{CorePackage, Phone, User, apply_pkg_state_commands}; -use crate::core::utils::DisplayablePath; -use crate::gui::widgets::package_row::PackageRow; +use crate::config::{Config, DeviceSettings}; +use crate::sync::{CorePackage, Phone, User, apply_pkg_state_commands}; +use crate::utils::DisplayablePath; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use std::{ @@ -22,10 +21,10 @@ pub struct UserBackup { } /// Backup all `Uninstalled` and `Disabled` packages -pub async fn backup_phone( +pub fn backup_phone( users: Vec, device_id: String, - phone_packages: Vec>, + phone_packages: &[Vec], ) -> Result { let mut backup = PhoneBackup { device_id: device_id.clone(), @@ -38,11 +37,8 @@ pub async fn backup_phone( ..UserBackup::default() }; - for p in phone_packages[u.index].clone() { - user_backup.packages.push(CorePackage { - name: p.name.clone(), - state: p.state, - }); + for p in phone_packages[u.index].iter().cloned() { + user_backup.packages.push(p); } backup.users.push(user_backup); } @@ -79,6 +75,7 @@ pub fn list_available_backups(dir: &Path) -> Vec { } } +#[must_use] pub fn list_available_backup_user(backup: DisplayablePath) -> Vec { match fs::read_to_string(backup.path) { Ok(data) => serde_json::from_str::(&data) @@ -113,7 +110,7 @@ pub struct RestoreResult { pub fn restore_backup( selected_device: &Phone, - packages: &[Vec], + packages: &[Vec], settings: &DeviceSettings, ) -> Result { match fs::read_to_string( @@ -142,7 +139,7 @@ pub fn restore_backup( .iter() .find(|x| x.name == backup_package.name) { - p.into() + p.clone() } else { skipped_packages += 1; warn!( diff --git a/src/core/sync.rs b/crates/uad-core/src/sync.rs similarity index 77% rename from src/core/sync.rs rename to crates/uad-core/src/sync.rs index 932c006..fe83788 100644 --- a/src/core/sync.rs +++ b/crates/uad-core/src/sync.rs @@ -1,8 +1,8 @@ -use crate::core::{ +use crate::{ adb::{ACommand as AdbCommand, PM_CLEAR_PACK}, uad_lists::PackageState, }; -use crate::gui::{views::list::PackageInfo, widgets::package_row::PackageRow}; +use log::{error, info}; use retry::{OperationResult, delay::Fixed, retry}; use serde::{Deserialize, Serialize}; @@ -61,27 +61,25 @@ pub enum AdbError { /// This replaces the deprecated `adb_shell_command`. /// /// If `serial` is empty, it lets ADB choose the default device. -pub async fn run_adb_action>( +pub fn run_adb_shell_action>( device_serial: S, - action: String, - p: PackageInfo, -) -> Result { + action: &str, +) -> Result { let serial = device_serial.as_ref(); - let label = &p.removal; - match AdbCommand::new().shell(serial).raw(&action) { + match AdbCommand::new().shell(serial).raw(action) { Ok(o) => { if ["Error", "Failure"].iter().any(|&e| o.contains(e)) { - let friendly_msg = make_friendly_error_message(&o, &action); - return Err(AdbError::Generic(format!("[{label}] {friendly_msg}"))); + let friendly_msg = make_friendly_error_message(&o, action); + return Err(AdbError::Generic(friendly_msg)); } - info!("[{label}] {action} -> {o}"); - Ok(p) + info!("{action} -> {o}"); + Ok(o) } Err(err) => { if !err.contains("[not installed for") { - let friendly_msg = make_friendly_error_message(&err, &action); - return Err(AdbError::Generic(format!("[{label}] {friendly_msg}"))); + let friendly_msg = make_friendly_error_message(&err, action); + return Err(AdbError::Generic(friendly_msg)); } Err(AdbError::Generic(err)) } @@ -141,6 +139,7 @@ fn make_friendly_error_message(error_output: &str, action: &str) -> String { } /// If `None`, returns an empty String, not " --user 0" +#[must_use] pub fn user_flag(user_id: Option) -> String { user_id .map(|user| format!(" --user {}", user.id)) @@ -151,35 +150,16 @@ pub fn user_flag(user_id: Option) -> String { #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct CorePackage { pub name: String, + pub description: String, + #[serde(default)] + pub removal: crate::uad_lists::Removal, + #[serde(default)] pub state: PackageState, + #[serde(default)] + pub list: crate::uad_lists::UadList, } -impl From<&mut PackageRow> for CorePackage { - fn from(pr: &mut PackageRow) -> Self { - Self { - name: pr.name.clone(), - state: pr.state, - } - } -} -impl From for CorePackage { - fn from(pr: PackageRow) -> Self { - Self { - name: pr.name.clone(), - state: pr.state, - } - } -} - -impl From<&PackageRow> for CorePackage { - fn from(pr: &PackageRow) -> Self { - Self { - name: pr.name.clone(), - state: pr.state, - } - } -} - +#[must_use] pub fn apply_pkg_state_commands( package: &CorePackage, wanted_state: PackageState, @@ -224,6 +204,7 @@ pub fn apply_pkg_state_commands( /// Build a command request to be sent via ADB to a device. /// `commands` accepts one or more ADB shell commands /// which act on a common `package` and `user`. +#[must_use] pub fn request_builder(commands: &[&str], package: &str, user: Option) -> Vec { let maybe_user_flag = user_flag(user); commands @@ -235,6 +216,7 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option) -> /// Get the model by querying the `ro.product.model` property. /// /// If `serial` is empty, it lets ADB choose the default device. +#[must_use] pub fn get_device_model(serial: &str) -> String { AdbCommand::new() .shell(serial) @@ -253,6 +235,7 @@ pub fn get_device_model(serial: &str) -> String { /// Get the brand by querying the `ro.product.brand` property. /// /// If `serial` is empty, it lets ADB choose the default device. +#[must_use] pub fn get_device_brand(serial: &str) -> String { AdbCommand::new() .shell(serial) @@ -266,6 +249,7 @@ pub fn get_device_brand(serial: &str) -> String { // `ro.build.version.sdk` property or defaulting to 0. /// /// If `device_serial` is empty, it lets ADB choose the default device. +#[must_use] pub fn get_android_sdk(device_serial: &str) -> u8 { AdbCommand::new() .shell(device_serial) @@ -280,6 +264,7 @@ pub fn get_android_sdk(device_serial: &str) -> u8 { /// /// Only includes users where the package exists (Some state). Users where the package /// doesn't exist (None) are not tracked. +#[must_use] pub fn capture_cross_user_states( package_name: &str, device_serial: &str, @@ -301,6 +286,7 @@ pub fn capture_cross_user_states( /// - Case A: Uninstall → Restore (package appears on other users) /// - Case B: Uninstall → Uninstall (package disappears from other users that previously had it) /// - Case C: Restore → Restore (package appears on other users) +#[must_use] pub fn detect_cross_user_behavior( package_name: &str, device_serial: &str, @@ -433,6 +419,7 @@ pub fn is_protected_user>(user_id: u16, device_serial: S) -> bool .is_err() } +#[must_use] pub fn list_users_idx_prot(device_serial: &str) -> Vec { AdbCommand::new() .shell(device_serial) @@ -456,9 +443,10 @@ pub fn list_users_idx_prot(device_serial: &str) -> Vec { /// This matches serials (`getprop ro.serialno`) /// that are authorized by the user. -pub async fn get_devices_list() -> Vec { +#[must_use] +pub fn get_devices_list() -> Vec { retry( - Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 120 }), + Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 10 }), || match AdbCommand::new().devices() { Ok(devices) => { let mut device_list: Vec = vec![]; @@ -486,20 +474,22 @@ pub async fn get_devices_list() -> Vec { .unwrap_or_default() } -pub async fn initial_load() -> bool { +#[must_use] +pub fn initial_load() -> bool { match AdbCommand::new().devices() { Ok(_devices) => true, Err(_err) => false, } } -/// Verify the actual state of a package on the device -pub fn verify_package_state( - package_name: &str, +/// Get the current state of a package on a device +#[must_use] +pub fn get_package_state( device_serial: &str, + package_name: &str, user_id: Option, ) -> Option { - use crate::core::adb::{ACommand as AdbCommand, PmListPacksFlag}; + use crate::adb::{ACommand as AdbCommand, PmListPacksFlag}; // Check if package is enabled if let Ok(enabled_packages) = AdbCommand::new() @@ -535,11 +525,23 @@ pub fn verify_package_state( None } +/// Verify the current state of a package on a device. +/// Returns the package state, or `None` if the package doesn't exist. +#[must_use] +pub fn verify_package_state( + package_name: &str, + device_serial: &str, + user_id: Option, +) -> Option { + get_package_state(device_serial, package_name, user_id) +} + /// Check if a package exists on any other users besides the target user. /// This helps detect OEM-specific cross-user restoration behavior. /// /// Only includes users where the package exists in a non-uninstalled state /// (i.e., Enabled or Disabled). +#[must_use] pub fn check_cross_user_package_existence( package_name: &str, device_serial: &str, @@ -561,9 +563,29 @@ pub fn check_cross_user_package_existence( other_user_states } +/// Creates a `CorePackage` with the specified state, preserving other fields from the original. +fn package_with_state(package: &CorePackage, state: PackageState) -> CorePackage { + CorePackage { + state, + ..package.clone() + } +} + +/// Executes the first available command and returns the result. +fn execute_first_command(commands: &[String], phone: &Phone) -> Result<(), String> { + if commands.is_empty() { + return Err("No command available".to_string()); + } + AdbCommand::new() + .shell(&phone.adb_id) + .raw(&commands[0]) + .map(|_| ()) + .map_err(|err| err.clone()) +} + /// Attempt fallback action when package state verification fails pub fn attempt_fallback( - package: &crate::gui::widgets::package_row::PackageRow, + package: &CorePackage, wanted_state: PackageState, actual_state: PackageState, user: User, @@ -572,101 +594,49 @@ pub fn attempt_fallback( match (wanted_state, actual_state) { // Case 1: Tried to uninstall but package was reinstalled -> disable it (PackageState::Uninstalled, PackageState::Enabled) => { - let core_package = CorePackage { - name: package.name.clone(), - state: PackageState::Enabled, - }; - let commands = - apply_pkg_state_commands(&core_package, PackageState::Disabled, user, phone); - - if commands.is_empty() { - Err("No disable command available for this Android version".to_string()) - } else { - // Execute the disable command - let action = commands[0].clone(); - match AdbCommand::new().shell(&phone.adb_id).raw(&action) { - Ok(_) => Ok("disabled package instead of uninstalling".to_string()), - Err(err) => Err(format!("Failed to disable package: {err}")), - } - } + let pkg = package_with_state(package, PackageState::Enabled); + let commands = apply_pkg_state_commands(&pkg, PackageState::Disabled, user, phone); + execute_first_command(&commands, phone) + .map(|()| "disabled package instead of uninstalling".to_string()) + .map_err(|e| format!("Failed to disable package: {e}")) } // Case 2: Tried to disable but package re-enabled itself -> try uninstall (PackageState::Disabled, PackageState::Enabled) => { - let core_package = CorePackage { - name: package.name.clone(), - state: PackageState::Enabled, - }; - let commands = - apply_pkg_state_commands(&core_package, PackageState::Uninstalled, user, phone); + let pkg = package_with_state(package, PackageState::Enabled); + let commands = apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone); + execute_first_command(&commands, phone) + .map_err(|e| format!("Failed to uninstall: {e}"))?; - if commands.is_empty() { - Err("No uninstall command available for this Android version".to_string()) - } else { - // Execute the uninstall command - let action = commands[0].clone(); - match AdbCommand::new().shell(&phone.adb_id).raw(&action) { - Ok(_) => { - // Verify the package was actually uninstalled - match verify_package_state(&package.name, &phone.adb_id, Some(user.id)) { - Some(PackageState::Uninstalled) | None => { - Ok("uninstalled package instead of disabling".to_string()) - } - _ => Err("Package still exists after uninstall attempt".to_string()), - } - } - Err(err) => Err(format!("Failed to uninstall package: {err}")), + // Verify the package was actually uninstalled + match verify_package_state(&package.name, &phone.adb_id, Some(user.id)) { + Some(PackageState::Uninstalled) | None => { + Ok("uninstalled package instead of disabling".to_string()) } + _ => Err("Package still exists after uninstall attempt".to_string()), } } // Case 3: Tried to enable but package was disabled -> try uninstall then reinstall (PackageState::Enabled, PackageState::Disabled) => { - // First try to uninstall - let core_package = CorePackage { - name: package.name.clone(), - state: PackageState::Disabled, - }; - let uninstall_commands = - apply_pkg_state_commands(&core_package, PackageState::Uninstalled, user, phone); + let pkg = package_with_state(package, PackageState::Disabled); + let uninstall_cmds = + apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone); + execute_first_command(&uninstall_cmds, phone) + .map_err(|e| format!("Failed to uninstall for reinstall: {e}"))?; - if uninstall_commands.is_empty() { - Err("No uninstall command available for reinstall attempt".to_string()) - } else { - let uninstall_action = uninstall_commands[0].clone(); - match AdbCommand::new() - .shell(&phone.adb_id) - .raw(&uninstall_action) - { - Ok(_) => { - // Now try to reinstall/enable - let core_package_uninstalled = CorePackage { - name: package.name.clone(), - state: PackageState::Uninstalled, - }; - let enable_commands = apply_pkg_state_commands( - &core_package_uninstalled, - PackageState::Enabled, - user, - phone, - ); + // Now try to reinstall/enable + let pkg_uninstalled = package_with_state(package, PackageState::Uninstalled); + let enable_cmds = + apply_pkg_state_commands(&pkg_uninstalled, PackageState::Enabled, user, phone); - if enable_commands.is_empty() { - Ok("uninstalled package but couldn't reinstall".to_string()) - } else { - let enable_action = enable_commands[0].clone(); - match AdbCommand::new().shell(&phone.adb_id).raw(&enable_action) { - Ok(_) => { - Ok("uninstalled and reinstalled package to enable it" - .to_string()) - } - Err(err) => Err(format!("Failed to reinstall package: {err}")), - } - } - } - Err(err) => Err(format!("Failed to uninstall package for reinstall: {err}")), - } + if enable_cmds.is_empty() { + return Ok("uninstalled package but couldn't reinstall".to_string()); } + + execute_first_command(&enable_cmds, phone) + .map(|()| "uninstalled and reinstalled package to enable it".to_string()) + .map_err(|e| format!("Failed to reinstall package: {e}")) } // Other cases - no fallback available diff --git a/src/core/theme.rs b/crates/uad-core/src/theme.rs similarity index 92% rename from src/core/theme.rs rename to crates/uad-core/src/theme.rs index 9dfcbb4..71a8cfe 100644 --- a/src/core/theme.rs +++ b/crates/uad-core/src/theme.rs @@ -1,6 +1,10 @@ +#[cfg(feature = "gui")] use dark_light; +#[cfg(feature = "gui")] use iced::theme::{self, Mode, Palette, Style}; +#[cfg(feature = "gui")] use iced::{Color, color}; +#[cfg(feature = "gui")] use std::sync::LazyLock; /* @@ -12,6 +16,7 @@ Coincidentally, this also ensures consistent colors across the GUI, at the cost of requiring a restart to update the palette. (this is just a patch, not a fix) */ +#[cfg(feature = "gui")] pub static OS_COLOR_SCHEME: LazyLock = LazyLock::new(|| dark_light::detect().unwrap_or(dark_light::Mode::Unspecified)); @@ -29,12 +34,14 @@ pub enum Theme { Light, } +#[cfg(feature = "gui")] #[derive(Debug, Clone, Copy)] pub struct BaseColors { pub background: Color, pub foreground: Color, } +#[cfg(feature = "gui")] #[derive(Debug, Clone, Copy)] pub struct NormalColors { pub primary: Color, @@ -44,6 +51,7 @@ pub struct NormalColors { pub error: Color, } +#[cfg(feature = "gui")] #[derive(Debug, Clone, Copy)] pub struct BrightColors { pub primary: Color, @@ -52,6 +60,7 @@ pub struct BrightColors { pub error: Color, } +#[cfg(feature = "gui")] #[derive(Debug, Clone, Copy)] pub struct ColorPalette { pub base: BaseColors, @@ -69,6 +78,7 @@ impl Theme { /// This `fn` _could_ be `const`, /// but `deref`ing a lazy-`static` is non-`const`. #[must_use] + #[cfg(feature = "gui")] pub fn palette(self) -> ColorPalette { const DARK: ColorPalette = ColorPalette { base: BaseColors { @@ -151,6 +161,18 @@ impl std::fmt::Display for Theme { } } +/// Converts a string to the GUI's Theme type +#[must_use] +pub fn string_to_theme(theme: &str) -> Theme { + match theme { + "Lupin" => Theme::Lupin, + "Dark" => Theme::Dark, + "Light" => Theme::Light, + _ => Theme::Auto, + } +} + +#[cfg(feature = "gui")] impl theme::Base for Theme { fn default(preference: Mode) -> Self { match preference { diff --git a/src/core/uad_lists.rs b/crates/uad-core/src/uad_lists.rs similarity index 95% rename from src/core/uad_lists.rs rename to crates/uad-core/src/uad_lists.rs index fb1833c..35cade9 100644 --- a/src/core/uad_lists.rs +++ b/crates/uad-core/src/uad_lists.rs @@ -1,5 +1,6 @@ use crate::CACHE_DIR; -use crate::core::utils::{format_diff_time_from_now, last_modified_date}; +use crate::utils::{format_diff_time_from_now, last_modified_date}; +use log::warn; use retry::{OperationResult, delay::Fixed, retry}; use serde::{Deserialize, Serialize}; use serde_json; @@ -15,7 +16,7 @@ pub const LIST_FNAME: &str = "uad_lists.json"; reason = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/608" )] // not `const`, because it's too big -pub static DATA: &str = include_str!("../../resources/assets/uad_lists.json"); +pub static DATA: &str = include_str!("../../../resources/assets/uad_lists.json"); #[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq)] #[serde(rename_all = "camelCase")] @@ -28,7 +29,7 @@ pub struct Package { pub removal: Removal, } -#[derive(Default, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Default, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum UadList { #[default] All, @@ -73,6 +74,7 @@ impl UadList { Self::Unlisted, ]; + #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::All => "All lists", @@ -148,7 +150,7 @@ impl Opposite for PackageState { } // Bad names. To be changed! -#[derive(Default, Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] pub enum Removal { #[default] Recommended, @@ -176,6 +178,7 @@ impl Removal { Self::Unlisted, ]; + #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::All => "All removals", diff --git a/src/core/update.rs b/crates/uad-core/src/update.rs similarity index 99% rename from src/core/update.rs rename to crates/uad-core/src/update.rs index c92b3a8..51e52e4 100644 --- a/src/core/update.rs +++ b/crates/uad-core/src/update.rs @@ -1,9 +1,9 @@ -use crate::core::utils::NAME; - use serde::Deserialize; #[cfg(feature = "self-update")] use { + crate::utils::NAME, + log::{debug, error}, retry::{OperationResult, delay::Fibonacci, retry}, std::fs, std::io, diff --git a/src/core/utils.rs b/crates/uad-core/src/utils.rs similarity index 80% rename from src/core/utils.rs rename to crates/uad-core/src/utils.rs index e6a3fe1..0a89cdd 100644 --- a/src/core/utils.rs +++ b/crates/uad-core/src/utils.rs @@ -1,14 +1,14 @@ #![warn(clippy::unwrap_used)] -use crate::core::{ +use crate::{ adb::{ACommand as AdbCommand, PmListPacksFlag}, - sync::User, + sync::{CorePackage, User}, theme::Theme, uad_lists::{PackageHashMap, PackageState, Removal, UadList}, }; -use crate::gui::widgets::package_row::PackageRow; use chrono::{DateTime, offset::Utc}; use csv::Writer; +use log::error; use std::{ collections::HashSet, fmt, fs, @@ -66,11 +66,12 @@ pub enum Error { DialogClosed, } +#[must_use] pub fn fetch_packages( uad_lists: &PackageHashMap, device_serial: &str, user_id: Option, -) -> Vec { +) -> Vec { let all_sys_packs = AdbCommand::new() .shell(device_serial) .pm() @@ -92,24 +93,24 @@ pub fn fetch_packages( .collect(); let mut description; - let mut uad_list; - let mut state; let mut removal; - let mut user_package: Vec = Vec::new(); + let mut state; + let mut list; + let mut user_package: Vec = Vec::new(); for pack_name in all_sys_packs { let p_name = &pack_name; - state = PackageState::Uninstalled; - description = "[No description]: CONTRIBUTION WELCOMED"; - uad_list = UadList::Unlisted; + description = String::from("[No description]: CONTRIBUTION WELCOMED"); removal = Removal::Unlisted; + state = PackageState::Uninstalled; + list = UadList::Unlisted; if let Some(package) = uad_lists.get(p_name) { if !package.description.is_empty() { - description = &package.description; + description = package.description.clone(); } - uad_list = package.list; removal = package.removal; + list = package.list; } if enabled_sys_packs.contains(p_name) { @@ -118,14 +119,20 @@ pub fn fetch_packages( state = PackageState::Disabled; } - let package_row = - PackageRow::new(p_name, state, description, uad_list, removal, false, false); - user_package.push(package_row); + let package = CorePackage { + name: p_name.clone(), + description, + removal, + state, + list, + }; + user_package.push(package); } user_package.sort_by_key(|a| a.name.to_lowercase()); user_package } +#[must_use] pub fn string_to_theme(theme: &str) -> Theme { match theme { "Dark" => Theme::Dark, @@ -137,6 +144,7 @@ pub fn string_to_theme(theme: &str) -> Theme { } } +#[must_use] pub fn setup_uad_dir(dir: &Path) -> PathBuf { let dir = dir.join("uad"); if let Err(e) = fs::create_dir_all(&dir) { @@ -173,6 +181,7 @@ pub fn open_url(dir: PathBuf) { } #[rustfmt::skip] +#[must_use] pub fn last_modified_date(file: PathBuf) -> DateTime { fs::metadata(file).map_or_else(|_| Utc::now(), |metadata| match metadata.modified() { Ok(time) => time.into(), @@ -180,6 +189,7 @@ pub fn last_modified_date(file: PathBuf) -> DateTime { }) } +#[must_use] pub fn format_diff_time_from_now(date: DateTime) -> String { let now: DateTime = Utc::now(); let last_update = now - date; @@ -194,15 +204,10 @@ pub fn format_diff_time_from_now(date: DateTime) -> String { } } -/// Export selected packages. +/// Export selected package names. /// File will be saved in same directory where UAD-ng is located. -pub async fn export_selection(packages: Vec) -> Result { - let selected = packages - .iter() - .filter(|p| p.selected) - .map(|p| p.name.clone()) - .collect::>() - .join("\n"); +pub fn export_selection(package_names: &[String]) -> Result { + let selected = package_names.join("\n"); match fs::write(EXPORT_FILE_NAME, selected) { Ok(()) => Ok(true), @@ -236,22 +241,9 @@ impl fmt::Display for DisplayablePath { } } -/// Can be used to choose any folder. -pub async fn open_folder() -> Result { - let picked_folder = rfd::AsyncFileDialog::new() - .pick_folder() - .await - .ok_or(Error::DialogClosed)?; - - Ok(picked_folder.path().to_owned()) -} - /// Export uninstalled packages in a csv file. /// Exported information will contain package name and description. -pub async fn export_packages( - user: User, - phone_packages: Vec>, -) -> Result { +pub fn export_packages(user: User, phone_packages: &[Vec]) -> Result { let backup_file = generate_backup_name(chrono::Local::now()); let file = fs::File::create(backup_file).map_err(|err| err.to_string())?; @@ -260,7 +252,7 @@ pub async fn export_packages( wtr.write_record(["Package Name", "Description"]) .map_err(|err| err.to_string())?; - let uninstalled_packages: Vec<&PackageRow> = phone_packages[user.index] + let uninstalled_packages: Vec<&CorePackage> = phone_packages[user.index] .iter() .filter(|p| p.state == PackageState::Uninstalled) .collect(); @@ -275,6 +267,36 @@ pub async fn export_packages( Ok(true) } +/// Truncate description to fit within max length, taking only the first line +#[must_use] +pub fn truncate_description(desc: &str, max_len: usize) -> String { + let first_line = desc.split('\n').next().unwrap_or("").trim_end(); + + if first_line.len() <= max_len { + first_line.to_string() + } else { + format!("{}...", &first_line[..max_len.saturating_sub(3)]) + } +} + +/// Check if package matches search term (checks name and description) +#[must_use] +pub fn matches_search(pkg_name: &str, search_term: &str, pkg_description: Option<&str>) -> bool { + let search_lower = search_term.to_lowercase(); + + if pkg_name.to_lowercase().contains(&search_lower) { + return true; + } + + if let Some(description) = pkg_description { + if description.to_lowercase().contains(&search_lower) { + return true; + } + } + + false +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/uad-gui/Cargo.toml b/crates/uad-gui/Cargo.toml new file mode 100644 index 0000000..abc4f42 --- /dev/null +++ b/crates/uad-gui/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "uad-gui" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true +categories = ["gui"] +edition.workspace = true + +[[bin]] +name = "uad-ng" +path = "src/main.rs" + +[features] +default = ["wgpu", "self-update", "img"] +wgpu = [] # Iced/wgpu is default +self-update = ["flate2", "tar"] +no-self-update = [] +img = ["image", "iced/image"] + +[dependencies] +uad-core = { path = "../uad-core", features = ["gui", "self-update"] } +iced.workspace = true +image = { workspace = true, optional = true } +rfd.workspace = true +dark-light.workspace = true +fern.workspace = true +chrono.workspace = true +dirs.workspace = true +log.workspace = true +flate2 = { workspace = true, optional = true } +tar = { workspace = true, optional = true } + +[target.'cfg(target_os = "windows")'.dependencies] +win32console.workspace = true + +[build-dependencies] +embed-resource = "3" + +[lints.rust] +deprecated_safe = "warn" + +[lints.clippy] +undocumented_unsafe_blocks = "forbid" +exit = "deny" +panic_in_result_fn = "warn" +infinite_loop = "warn" +mem_forget = "warn" +implicit_clone = "warn" +format_push_string = "warn" +large_include_file = "warn" +shadow_unrelated = "warn" +struct_field_names = "allow" # annoying +module_name_repetitions = "allow" # annoying + +disallowed_types = "deny" +disallowed_methods = "deny" + +allow_attributes_without_reason = "warn" +pedantic = { level = "warn", priority = -1 } diff --git a/src/gui/mod.rs b/crates/uad-gui/src/gui.rs similarity index 86% rename from src/gui/mod.rs rename to crates/uad-gui/src/gui.rs index c88fd08..e171552 100644 --- a/src/gui/mod.rs +++ b/crates/uad-gui/src/gui.rs @@ -1,35 +1,32 @@ -pub mod style; -pub mod views; -pub mod widgets; - -use crate::core::adb; -use crate::core::sync::{Phone, get_devices_list, initial_load}; +use crate::theme::string_to_theme; #[cfg(feature = "img")] -use crate::core::theme::OS_COLOR_SCHEME; -use crate::core::theme::Theme; -use crate::core::uad_lists::UadListState; -use crate::core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release}; -use crate::core::utils::{FULL_NAME, NAME, string_to_theme}; - +use crate::theme::OS_COLOR_SCHEME; +use crate::theme::Theme; +use crate::views::about::{About as AboutView, Message as AboutMessage}; +use crate::views::list::{ + List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage, +}; +use crate::views::settings::{Message as SettingsMessage, Settings as SettingsView}; +use crate::widgets::navigation_menu::nav_menu; use iced::font; #[cfg(feature = "img")] use iced::window::icon; #[cfg(feature = "img")] use image::ImageFormat; -use views::about::{About as AboutView, Message as AboutMessage}; -use views::list::{List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage}; -use views::settings::{Message as SettingsMessage, Settings as SettingsView}; -use widgets::navigation_menu::nav_menu; - use iced::widget::column; use iced::{Alignment, Element, Length, Settings, Task, window::Settings as Window}; use iced::{Subscription, event, keyboard}; - +use log::{debug, error, info}; #[cfg(feature = "self-update")] use std::path::PathBuf; +use uad_core::adb; +use uad_core::sync::{Phone, get_devices_list, initial_load}; +use uad_core::uad_lists::UadListState; +use uad_core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release}; +use uad_core::utils::{FULL_NAME, NAME}; #[cfg(feature = "self-update")] -use crate::core::update::{BIN_NAME, download_update_to_temp_file, remove_file}; +use uad_core::update::{BIN_NAME, download_update_to_temp_file, remove_file}; #[derive(Default, Debug, Clone)] enum View { @@ -41,8 +38,8 @@ enum View { #[derive(Default, Clone)] pub struct UpdateState { - self_update: SelfUpdateState, - uad_list: UadListState, + pub self_update: SelfUpdateState, + pub uad_list: UadListState, } #[derive(Default)] @@ -86,18 +83,21 @@ impl UadGui { reason = "required by iced's Application trait interface" )] fn title(&self) -> String { - FULL_NAME.to_string() + self.selected_device.as_ref().map_or_else( + || FULL_NAME.to_string(), + |device| format!("{FULL_NAME} - {}", device.model), + ) } fn new() -> (Self, Task) { ( Self::default(), Task::batch([ - // Used in crate::gui::widgets::navigation_menu::ICONS. Name is `icomoon`. - font::load(include_bytes!("../../resources/assets/icons.ttf").as_slice()) + // Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`. + font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice()) .map(Message::FontLoaded), - Task::perform(initial_load(), Message::ADBSatisfied), - Task::perform(get_devices_list(), Message::LoadDevices), + Task::perform(async { initial_load() }, Message::ADBSatisfied), + Task::perform(async { get_devices_list() }, Message::LoadDevices), Task::perform( async move { get_latest_release() }, Message::GetLatestRelease, @@ -111,21 +111,45 @@ impl UadGui { reason = "required by iced's Application trait interface" )] fn subscription(&self) -> Subscription { - event::listen_with(|event, _status, _env| match event { + if self.selected_device.is_some() { + event::listen_with(Self::keyboard_shortcuts_with_device) + } else { + event::listen_with(Self::keyboard_shortcuts_without_device) + } + } + + fn keyboard_shortcuts_with_device( + event: iced::Event, + _status: iced::event::Status, + _window: iced::window::Id, + ) -> Option { + Self::keyboard_shortcuts(event, true) + } + + fn keyboard_shortcuts_without_device( + event: iced::Event, + _status: iced::event::Status, + _window: iced::window::Id, + ) -> Option { + Self::keyboard_shortcuts(event, false) + } + + fn keyboard_shortcuts(event: iced::Event, can_control_device: bool) -> Option { + match event { iced::Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Character(c), modifiers, .. }) if modifiers.control() && modifiers.shift() => match c.as_str() { - "r" => Some(Message::RebootButtonPressed), - "5" => Some(Message::RefreshButtonPressed), + "r" if can_control_device => Some(Message::RebootButtonPressed), + "5" if can_control_device => Some(Message::RefreshButtonPressed), "a" => Some(Message::AppsPress), "i" => Some(Message::AboutPressed), "s" => Some(Message::SettingsPressed), _ => None, }, _ => None, - }) + } } #[allow( @@ -178,7 +202,7 @@ impl UadGui { self.adb_satisfied, ))); } - Task::perform(get_devices_list(), Message::LoadDevices) + Task::perform(async { get_devices_list() }, Message::LoadDevices) } Message::RebootButtonPressed => { self.apps_view = AppsView::default(); @@ -412,10 +436,10 @@ impl UadGui { let logo: &[u8] = match *OS_COLOR_SCHEME { // remember to keep `Unspecified` in sync with `src/core/theme` dark_light::Mode::Dark | dark_light::Mode::Unspecified => { - include_bytes!("../../resources/assets/logo-dark.png") + include_bytes!("../../../resources/assets/logo-dark.png") } dark_light::Mode::Light => { - include_bytes!("../../resources/assets/logo-light.png") + include_bytes!("../../../resources/assets/logo-light.png") } }; diff --git a/crates/uad-gui/src/helpers.rs b/crates/uad-gui/src/helpers.rs new file mode 100644 index 0000000..25c06f1 --- /dev/null +++ b/crates/uad-gui/src/helpers.rs @@ -0,0 +1,23 @@ +use crate::style; +use crate::theme::Theme; +use iced::Element; +use iced::widget::button::Button; + +/// Wrapper function for `iced::widget::button` with padding and style applied +pub fn button_primary<'a, Message>( + content: impl Into>, +) -> Button<'a, Message, Theme> { + iced::widget::button(content) + .padding([5, 10]) + .style(style::Button::Primary) +} + +/// Opens a file picker dialog to select a folder +pub async fn open_folder() -> Result { + rfd::AsyncFileDialog::new() + .set_title("Choose a backup location") + .pick_folder() + .await + .ok_or(uad_core::utils::Error::DialogClosed) + .map(|f| f.path().to_path_buf()) +} diff --git a/crates/uad-gui/src/lib.rs b/crates/uad-gui/src/lib.rs new file mode 100644 index 0000000..ede5da8 --- /dev/null +++ b/crates/uad-gui/src/lib.rs @@ -0,0 +1,11 @@ +#![allow( + clippy::missing_errors_doc, + reason = "Short-circuit doc lint to keep CI green" +)] + +pub mod gui; +pub mod helpers; +pub mod style; +pub mod theme; +pub mod views; +pub mod widgets; diff --git a/src/main.rs b/crates/uad-gui/src/main.rs similarity index 90% rename from src/main.rs rename to crates/uad-gui/src/main.rs index 1c197d8..07608a5 100644 --- a/src/main.rs +++ b/crates/uad-gui/src/main.rs @@ -1,8 +1,5 @@ #![windows_subsystem = "windows"] -#[macro_use] -extern crate log; -use crate::core::utils::setup_uad_dir; use fern::{ FormatCallback, colors::{Color, ColoredLevelConfig}, @@ -10,12 +7,10 @@ use fern::{ use log::Record; use std::sync::LazyLock; use std::{fmt::Arguments, fs::OpenOptions, path::PathBuf}; +use uad_core::utils::setup_uad_dir; -mod core; -mod gui; +use uad_gui::gui::UadGui; -static CONFIG_DIR: LazyLock = - LazyLock::new(|| setup_uad_dir(&dirs::config_dir().expect("Can't detect config dir"))); static CACHE_DIR: LazyLock = LazyLock::new(|| setup_uad_dir(&dirs::cache_dir().expect("Can't detect cache dir"))); @@ -30,7 +25,7 @@ fn main() -> iced::Result { } setup_logger().expect("setup logging"); - gui::UadGui::start() + UadGui::start() } /// Sets up logging to a new file in `CACHE_DIR"/uadng.log"` @@ -74,14 +69,14 @@ fn setup_logger() -> Result<(), fern::InitError> { .format(make_formatter(false)) .level(default_log_level) // Rust compiler makes module names use _ instead of - - .level_for("uad_ng", log::LevelFilter::Debug) + .level_for("uad_gui", log::LevelFilter::Debug) .chain(log_file); let stdout_dispatcher = fern::Dispatch::new() .format(make_formatter(true)) .level(default_log_level) // Rust compiler makes module names use _ instead of - - .level_for("uad_ng", log::LevelFilter::Warn) + .level_for("uad_gui", log::LevelFilter::Warn) .chain(std::io::stdout()); fern::Dispatch::new() diff --git a/src/gui/style.rs b/crates/uad-gui/src/style.rs similarity index 98% rename from src/gui/style.rs rename to crates/uad-gui/src/style.rs index 09f2a56..9eeba68 100644 --- a/src/gui/style.rs +++ b/crates/uad-gui/src/style.rs @@ -4,7 +4,7 @@ clippy::wildcard_imports, reason = "Iced style modules use PascalCase and &Theme; wildcard for local convenience" )] -use crate::core::theme::{ColorPalette, Theme}; +use crate::theme::{ColorPalette, Theme}; use iced::widget::{ button, checkbox, container, overlay, pick_list, radio, scrollable, text, text_editor, text_input, @@ -321,10 +321,12 @@ pub mod Container { use super::*; #[allow(dead_code, reason = "Used by other themes or future styles")] + #[must_use] pub fn Invisible(_: &Theme) -> container::Style { container::Style::default() } + #[must_use] pub fn Frame(theme: &Theme) -> container::Style { let p = theme.palette(); container::Style { @@ -340,6 +342,7 @@ pub mod Container { } } + #[must_use] pub fn BorderedFrame(theme: &Theme) -> container::Style { let p = theme.palette(); container::Style { @@ -356,6 +359,7 @@ pub mod Container { } #[allow(dead_code, reason = "Currently unused in some views")] + #[must_use] pub fn Tooltip(theme: &Theme) -> container::Style { let p = theme.palette(); container::Style { @@ -371,6 +375,7 @@ pub mod Container { } } + #[must_use] pub fn Background(theme: &Theme) -> container::Style { let p = theme.palette(); container::Style { @@ -408,6 +413,7 @@ pub mod Button { } } + #[must_use] pub fn Primary(theme: &Theme, status: button::Status) -> button::Style { let p = theme.palette(); let mut style = style_active_hover_disabled(p.bright.primary, p.bright.primary, status); @@ -421,10 +427,12 @@ pub mod Button { dead_code, reason = "Alias kept for semantic clarity in some call-sites" )] + #[must_use] pub fn SelfUpdate(theme: &Theme, status: button::Status) -> button::Style { Primary(theme, status) } + #[must_use] pub fn RestorePackage(theme: &Theme, status: button::Status) -> button::Style { let p = theme.palette(); let mut style = style_active_hover_disabled(p.bright.secondary, p.bright.secondary, status); @@ -441,6 +449,7 @@ pub mod Button { style } + #[must_use] pub fn UninstallPackage(theme: &Theme, status: button::Status) -> button::Style { let p = theme.palette(); let mut style = style_active_hover_disabled(p.bright.error, p.bright.error, status); @@ -454,10 +463,12 @@ pub mod Button { dead_code, reason = "Style exposed for disabled state buttons in some contexts" )] + #[must_use] pub fn Unavailable(theme: &Theme, status: button::Status) -> button::Style { UninstallPackage(theme, status) } + #[must_use] pub fn NormalPackage(theme: &Theme, status: button::Status) -> button::Style { let p = theme.palette(); match status { @@ -489,6 +500,7 @@ pub mod Button { } } + #[must_use] pub fn SelectedPackage(theme: &Theme, _status: button::Status) -> button::Style { let p = theme.palette(); button::Style { @@ -508,6 +520,7 @@ pub mod Button { } #[allow(dead_code, reason = "Used in views where buttons must be invisible")] + #[must_use] pub fn Hidden(_: &Theme, _: button::Status) -> button::Style { button::Style { background: Some(Background::Color(Color::TRANSPARENT)), @@ -605,6 +618,7 @@ pub mod Scrollable { } } + #[must_use] pub fn Description(theme: &Theme, _status: scrollable::Status) -> scrollable::Style { let p = theme.palette(); let (v, h) = rails(p.normal.surface); @@ -617,6 +631,7 @@ pub mod Scrollable { } } + #[must_use] pub fn Packages(theme: &Theme, _status: scrollable::Status) -> scrollable::Style { let p = theme.palette(); let (v, h) = rails(p.base.foreground); @@ -633,6 +648,7 @@ pub mod Scrollable { pub mod CheckBox { use super::*; + #[must_use] pub fn PackageEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style { let p = theme.palette(); checkbox::Style { @@ -647,6 +663,7 @@ pub mod CheckBox { } } + #[must_use] pub fn PackageDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style { let p = theme.palette(); checkbox::Style { @@ -664,6 +681,7 @@ pub mod CheckBox { } } + #[must_use] pub fn SettingsEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style { let p = theme.palette(); checkbox::Style { @@ -678,6 +696,7 @@ pub mod CheckBox { } } + #[must_use] pub fn SettingsDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style { let p = theme.palette(); checkbox::Style { @@ -696,11 +715,13 @@ pub mod CheckBox { pub mod Text { use super::*; + #[must_use] pub fn Default(theme: &Theme) -> text::Style { let _ = theme; text::Style::default() } + #[must_use] pub fn Ok(theme: &Theme) -> text::Style { let p = theme.palette(); text::Style { @@ -708,6 +729,7 @@ pub mod Text { } } + #[must_use] pub fn Danger(theme: &Theme) -> text::Style { let p = theme.palette(); text::Style { @@ -715,6 +737,7 @@ pub mod Text { } } + #[must_use] pub fn Commentary(theme: &Theme) -> text::Style { let p = theme.palette(); text::Style { @@ -738,7 +761,7 @@ mod tests { #[test] fn test_palette() { - let palette = Theme::default().palette(); + let palette = Theme::Dark.palette(); assert_ne!(palette.base.background, palette.base.foreground); assert_ne!(palette.normal.primary, Color::BLACK); diff --git a/crates/uad-gui/src/theme.rs b/crates/uad-gui/src/theme.rs new file mode 100644 index 0000000..3c4c263 --- /dev/null +++ b/crates/uad-gui/src/theme.rs @@ -0,0 +1,72 @@ +use iced::theme::{self, Mode, Palette, Style}; + +pub use uad_core::theme::{BaseColors, BrightColors, ColorPalette, NormalColors, OS_COLOR_SCHEME}; + +/// GUI-local wrapper around the core Theme to satisfy orphan rules for +/// iced's Catalog traits. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Theme(pub uad_core::theme::Theme); + +#[allow( + non_upper_case_globals, + reason = "Keep variant-like names matching core Theme" +)] +impl Theme { + pub const Auto: Self = Self(uad_core::theme::Theme::Auto); + pub const Lupin: Self = Self(uad_core::theme::Theme::Lupin); + pub const Dark: Self = Self(uad_core::theme::Theme::Dark); + pub const Light: Self = Self(uad_core::theme::Theme::Light); + + pub const ALL: [Self; 4] = [Self::Auto, Self::Lupin, Self::Dark, Self::Light]; + + #[must_use] + pub fn palette(self) -> ColorPalette { + self.0.palette() + } +} + +impl From for Theme { + fn from(value: uad_core::theme::Theme) -> Self { + Self(value) + } +} + +impl From for uad_core::theme::Theme { + fn from(value: Theme) -> Self { + value.0 + } +} + +/// Converts a string to the GUI's Theme type +#[must_use] +pub fn string_to_theme(theme: &str) -> Theme { + Theme(uad_core::theme::string_to_theme(theme)) +} + +impl theme::Base for Theme { + fn default(preference: Mode) -> Self { + Self(::default(preference)) + } + + fn mode(&self) -> Mode { + ::mode(&self.0) + } + + fn base(&self) -> Style { + ::base(&self.0) + } + + fn palette(&self) -> Option { + ::palette(&self.0) + } + + fn name(&self) -> &str { + ::name(&self.0) + } +} + +impl std::fmt::Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} diff --git a/src/gui/views/about.rs b/crates/uad-gui/src/views/about.rs similarity index 94% rename from src/gui/views/about.rs rename to crates/uad-gui/src/views/about.rs index 56d73ca..42cb723 100644 --- a/src/gui/views/about.rs +++ b/crates/uad-gui/src/views/about.rs @@ -1,16 +1,17 @@ -use crate::CACHE_DIR; -use crate::core::adb; -use crate::core::helpers::button_primary; -use crate::core::theme::Theme; -use crate::core::uad_lists::LIST_FNAME; -use crate::core::utils::{FULL_NAME, NAME, last_modified_date, open_url}; -use crate::gui::{UpdateState, style, widgets::text}; +use crate::helpers::button_primary; +use crate::theme::Theme; +use crate::{gui::UpdateState, style, widgets::text}; use iced::widget::{Space, column, container, row}; use iced::{Alignment, Element, Length, Renderer}; +use log::error; use std::path::PathBuf; +use uad_core::CACHE_DIR; +use uad_core::adb; +use uad_core::uad_lists::LIST_FNAME; +use uad_core::utils::{FULL_NAME, NAME, last_modified_date, open_url}; #[cfg(feature = "self-update")] -use crate::core::update::SelfUpdateStatus; +use uad_core::update::SelfUpdateStatus; #[derive(Default, Debug, Clone)] pub struct About {} diff --git a/src/gui/views/list.rs b/crates/uad-gui/src/views/list.rs similarity index 92% rename from src/gui/views/list.rs rename to crates/uad-gui/src/views/list.rs index 3402fc7..a9792dd 100644 --- a/src/gui/views/list.rs +++ b/crates/uad-gui/src/views/list.rs @@ -1,19 +1,22 @@ -use crate::core::config::DeviceSettings; -use crate::core::helpers::button_primary; -use crate::core::sync::{AdbError, Phone, User, apply_pkg_state_commands, run_adb_action}; -use crate::core::theme::Theme; -use crate::core::uad_lists::{ +use crate::helpers::button_primary; +use crate::style; +use crate::theme::Theme; +use crate::widgets::navigation_menu::ICONS; +use log::{error, info, warn}; +use std::path::PathBuf; +use uad_core::config::DeviceSettings; +use uad_core::sync::{AdbError, Phone, User, apply_pkg_state_commands}; +use uad_core::uad_lists::{ Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, load_debloat_lists, }; -use crate::core::utils::{EXPORT_FILE_NAME, NAME, export_selection, fetch_packages, open_url}; -use crate::gui::style; -use crate::gui::widgets::navigation_menu::ICONS; -use std::path::PathBuf; +use uad_core::utils::{ + EXPORT_FILE_NAME, NAME, export_selection, fetch_packages, matches_search, open_url, +}; -use crate::gui::views::settings::Settings; -use crate::gui::widgets::modal::Modal; -use crate::gui::widgets::package_row::{Message as RowMessage, PackageRow}; -use crate::gui::widgets::text; +use crate::views::settings::Settings; +use crate::widgets::modal::Modal; +use crate::widgets::package_row::{Message as RowMessage, PackageRow}; +use crate::widgets::text; use iced::widget::scrollable::{Direction, Scrollbar}; use iced::widget::{ Column, Space, button, checkbox, column, container, pick_list, radio, row, rule, scrollable, @@ -52,8 +55,8 @@ pub struct List { filtered_packages: Vec, /// Vec of `(user_index, pkg_index)` selected_packages: Vec<(usize, usize)>, - selected_package_state: Option, selected_removal: Option, + selected_package_state: Option, selected_list: Option, pub selected_user: Option, all_selected: bool, @@ -81,8 +84,8 @@ pub enum Message { ToggleAllSelected(bool), ListSelected(UadList), UserSelected(User), - PackageStateSelected(PackageState), RemovalSelected(Removal), + PackageStateSelected(PackageState), ApplyActionOnSelection, List(usize, RowMessage), VerifyAndFallback(Result), @@ -149,8 +152,8 @@ impl List { } Message::SearchInputChanged(letter) => self.on_search_input_changed(letter), Message::ListSelected(list) => self.on_list_selected(list), - Message::PackageStateSelected(state) => self.on_package_state_selected(state), Message::RemovalSelected(removal) => self.on_removal_selected(removal), + Message::PackageStateSelected(state) => self.on_package_state_selected(state), Message::List(i, row_msg) => self.on_list_row(i, &row_msg, settings, selected_device), Message::ApplyActionOnSelection => self.on_apply_action_on_selection(), Message::UserSelected(user) => self.on_user_selected(user), @@ -635,7 +638,7 @@ impl List { .width(120), row![text( self.phone_packages[selection.0][selection.1] - .uad_list + .list .to_string() )] .width(55), @@ -752,12 +755,11 @@ impl List { // that's why `enumerate` is before `filter`. .enumerate() .filter(|(_, p)| { - (list_filter == UadList::All || p.uad_list == list_filter) + (list_filter == UadList::All || p.list == list_filter) && (package_filter == PackageState::All || p.state == package_filter) && (removal_filter == Removal::All || p.removal == removal_filter) && (self.input_value.is_empty() - || p.name.contains(&self.input_value) - || p.description.contains(&self.input_value)) + || matches_search(&p.name, &self.input_value, Some(&p.description))) }) .map(|(i, _)| i) .collect(); @@ -771,11 +773,21 @@ impl List { ) -> Vec> { let serial = device_serial.as_ref(); if user_list.len() <= 1 { - vec![fetch_packages(&uad_list, serial, None)] + vec![ + fetch_packages(&uad_list, serial, None) + .into_iter() + .map(PackageRow::from) + .collect(), + ] } else { user_list .iter() - .map(|user| fetch_packages(&uad_list, serial, Some(user.id))) + .map(|user| { + fetch_packages(&uad_list, serial, Some(user.id)) + .into_iter() + .map(PackageRow::from) + .collect() + }) .collect() } } @@ -877,8 +889,8 @@ impl List { let i_user = self.selected_user.unwrap_or_default().index; self.phone_packages = packages; self.filtered_packages = (0..self.phone_packages[i_user].len()).collect(); - self.selected_package_state = Some(PackageState::Enabled); self.selected_removal = Some(Removal::Recommended); + self.selected_package_state = Some(PackageState::Enabled); self.selected_list = Some(UadList::All); self.selected_user = Some(User::default()); self.fallback_notifications.clear(); @@ -1053,7 +1065,7 @@ impl List { Task::perform( async move { // Blocking ADB calls happen here (off UI thread) - let actual_state_opt = crate::core::sync::verify_package_state( + let actual_state_opt = uad_core::sync::verify_package_state( &pkg_name, device.adb_id.as_str(), Some(user_id), @@ -1062,7 +1074,7 @@ impl List { match actual_state_opt { Some(actual_state) if actual_state == wanted_state => { // Check cross-user behavior - let error_modal = crate::core::sync::detect_cross_user_behavior( + let error_modal = uad_core::sync::detect_cross_user_behavior( &pkg_name, device.adb_id.as_str(), user_id, @@ -1091,16 +1103,16 @@ impl List { // Package doesn't exist (None) or has wrong state - try fallback let actual_state = actual_state_opt.unwrap_or(PackageState::Uninstalled); - let fallback_result = crate::core::sync::attempt_fallback( - &crate::gui::widgets::package_row::PackageRow::new( - &pkg_name, - current_state, - "", - UadList::All, - Removal::All, - false, - false, - ), + // Create CorePackage for fallback + let core_package = uad_core::sync::CorePackage { + name: pkg_name.clone(), + description: String::new(), + removal: Removal::All, + state: current_state, + list: UadList::Unlisted, + }; + let fallback_result = uad_core::sync::attempt_fallback( + &core_package, wanted_state, actual_state, device.user_list[i_user], @@ -1195,14 +1207,17 @@ impl List { // fallback task return Task::perform( async move { - let temp_pkg = crate::core::sync::CorePackage { + let temp_pkg = uad_core::sync::CorePackage { name: pkg_name.clone(), + description: String::new(), + removal: Removal::All, state: PackageState::Disabled, + list: UadList::Unlisted, }; // get uninstall command let uninstall_cmds = - crate::core::sync::apply_pkg_state_commands( + uad_core::sync::apply_pkg_state_commands( &temp_pkg, PackageState::Uninstalled, user, @@ -1210,24 +1225,24 @@ impl List { ); if let Some(cmd) = uninstall_cmds.first() { - let dummy_info = PackageInfo::default(); - let _ = crate::core::sync::run_adb_action( - device.adb_id.clone(), - cmd.clone(), - dummy_info, - ) - .await; + let _ = uad_core::sync::run_adb_shell_action( + &device.adb_id, + cmd, + ); } // reinstall/enable let temp_pkg_uninstalled = - crate::core::sync::CorePackage { + uad_core::sync::CorePackage { name: pkg_name.clone(), + description: String::new(), + removal: Removal::All, state: PackageState::Uninstalled, + list: UadList::Unlisted, }; let enable_cmds = - crate::core::sync::apply_pkg_state_commands( + uad_core::sync::apply_pkg_state_commands( &temp_pkg_uninstalled, PackageState::Enabled, user, @@ -1236,18 +1251,15 @@ impl List { // lets enable the new package if let Some(cmd) = enable_cmds.first() { - let dummy_info = PackageInfo::default(); - let _ = crate::core::sync::run_adb_action( - device.adb_id.clone(), - cmd.clone(), - dummy_info, - ) - .await; + let _ = uad_core::sync::run_adb_shell_action( + &device.adb_id, + cmd, + ); } //verifies the final state for thread confirmation let actual_state = - crate::core::sync::verify_package_state( + uad_core::sync::verify_package_state( &pkg_name, &device.adb_id, Some(user.id), @@ -1316,8 +1328,13 @@ impl List { fn on_export_selection(&mut self) -> Task { let i_user = self.selected_user.unwrap_or_default().index; + let package_names: Vec = self.phone_packages[i_user] + .iter() + .filter(|p| p.selected) + .map(|p| p.name.clone()) + .collect(); Task::perform( - export_selection(self.phone_packages[i_user].clone()), + async move { export_selection(&package_names) }, Message::SelectionExported, ) } @@ -1483,17 +1500,24 @@ fn build_action_pkg_commands( // Will be filled asynchronously before running the adb action before_cross_user_states: vec![], }; + // Clone data before async block to avoid borrowing issues + let device_serial = device.adb_id.clone(); + let package_name = u_pkg.name.clone(); + let user_id = u.id; + let phone = device.clone(); // In the end there is only one package state change // even if we run multiple adb commands commands.push(Task::perform( - run_adb_action_with_before_states( - device.adb_id.clone(), - action, - p_info, - u_pkg.name.clone(), - u.id, - device.clone(), - ), + async move { + run_adb_action_with_before_states( + &device_serial, + &action, + p_info, + &package_name, + user_id, + &phone, + ) + }, if j == 0 { Message::VerifyAndFallback } else { @@ -1505,23 +1529,25 @@ fn build_action_pkg_commands( commands } -async fn run_adb_action_with_before_states( - device_serial: String, - action: String, +fn run_adb_action_with_before_states( + device_serial: &str, + action: &str, mut p_info: PackageInfo, - package_name: String, + package_name: &str, target_user_id: u16, - phone: Phone, + phone: &Phone, ) -> Result { // Capture before-state in background to avoid blocking UI thread - let before_states = crate::core::sync::capture_cross_user_states( - &package_name, - &device_serial, + let before_states = uad_core::sync::capture_cross_user_states( + package_name, + device_serial, target_user_id, - &phone, + phone, ); p_info.before_cross_user_states = before_states; - run_adb_action(device_serial, action, p_info).await + // Run the ADB action + uad_core::sync::run_adb_shell_action(device_serial, action)?; + Ok(p_info) } fn recap<'a>(settings: &Settings, recap: &SummaryEntry) -> Element<'a, Message, Theme, Renderer> { diff --git a/src/gui/views/mod.rs b/crates/uad-gui/src/views/mod.rs similarity index 100% rename from src/gui/views/mod.rs rename to crates/uad-gui/src/views/mod.rs diff --git a/src/gui/views/settings.rs b/crates/uad-gui/src/views/settings.rs similarity index 88% rename from src/gui/views/settings.rs rename to crates/uad-gui/src/views/settings.rs index 01ff22f..c19b999 100644 --- a/src/gui/views/settings.rs +++ b/crates/uad-gui/src/views/settings.rs @@ -1,15 +1,7 @@ -use crate::core::{ - config::{BackupSettings, Config, DeviceSettings, GeneralSettings}, - helpers::button_primary, - save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup}, - sync::{AdbError, Phone, User, get_android_sdk, run_adb_action, supports_multi_user}, - theme::Theme, - utils::{ - DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_folder, open_url, - string_to_theme, - }, -}; -use crate::gui::{ +use crate::helpers::{button_primary, open_folder}; +use crate::theme::Theme; +use crate::theme::string_to_theme; +use crate::{ style, views::list::{List as AppsView, PackageInfo}, widgets::modal::Modal, @@ -17,9 +9,20 @@ use crate::gui::{ widgets::package_row::PackageRow, widgets::text, }; +use chrono; use iced::widget::{Space, button, checkbox, column, container, pick_list, radio, row, scrollable}; use iced::{Alignment, Element, Length, Renderer, Task, alignment}; +use log::{debug, error, info}; use std::path::PathBuf; +use uad_core::{ + config::{BackupSettings, Config, DeviceSettings, GeneralSettings}, + save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup}, + sync::{ + AdbError, CorePackage, Phone, User, get_android_sdk, run_adb_shell_action, + supports_multi_user, + }, + utils::{DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_url}, +}; #[derive(Debug, Clone)] pub enum PopUpModal { @@ -101,10 +104,11 @@ impl Settings { Task::none() } - fn handle_expert_mode(&mut self, phone: &Phone, toggled: bool) -> Task { + fn handle_expert_mode(&mut self, _phone: &Phone, toggled: bool) -> Task { self.general.expert_mode = toggled; debug!("Config change: {self:?}"); - Config::save_changes(self, &phone.adb_id); + let mut config = Config::load_configuration_file(); + config.save_device_settings(self.device.clone(), self.general.clone()); Task::none() } @@ -112,22 +116,25 @@ impl Settings { if phone.android_sdk >= 23 { self.device.disable_mode = toggled; debug!("Config change: {self:?}"); - Config::save_changes(self, &phone.adb_id); + let mut config = Config::load_configuration_file(); + config.save_device_settings(self.device.clone(), self.general.clone()); } Task::none() } - fn handle_multi_user_mode(&mut self, phone: &Phone, toggled: bool) -> Task { + fn handle_multi_user_mode(&mut self, _phone: &Phone, toggled: bool) -> Task { self.device.multi_user_mode = toggled; debug!("Config change: {self:?}"); - Config::save_changes(self, &phone.adb_id); + let mut config = Config::load_configuration_file(); + config.save_device_settings(self.device.clone(), self.general.clone()); Task::none() } - fn handle_apply_theme(&mut self, phone: &Phone, theme: Theme) -> Task { + fn handle_apply_theme(&mut self, _phone: &Phone, theme: Theme) -> Task { self.general.theme = theme.to_string(); debug!("Config change: {self:?}"); - Config::save_changes(self, &phone.adb_id); + let mut config = Config::load_configuration_file(); + config.save_device_settings(self.device.clone(), self.general.clone()); Task::none() } @@ -182,12 +189,16 @@ impl Settings { phone: &Phone, packages: &[Vec], ) -> Task { + let core_packages: Vec> = packages + .iter() + .map(|user_packages| user_packages.iter().map(CorePackage::from).collect()) + .collect(); + // Avoid capturing &self and &phone in the future; clone owned data first. + let user_list = phone.user_list.clone(); + let device_id = self.device.device_id.clone(); + let packages_owned = core_packages; Task::perform( - backup_phone( - phone.user_list.clone(), - self.device.device_id.clone(), - packages.to_vec(), - ), + async move { backup_phone(user_list, device_id, &packages_owned) }, Message::DeviceBackedUp, ) } @@ -217,7 +228,11 @@ impl Settings { packages: &[Vec], nb_running_async_adb_commands: &mut u32, ) -> Task { - match restore_backup(phone, packages, &self.device) { + let core_packages: Vec> = packages + .iter() + .map(|user_packages| user_packages.iter().map(CorePackage::from).collect()) + .collect(); + match restore_backup(phone, &core_packages, &self.device) { Ok(restore_result) => { let mut commands = vec![]; *nb_running_async_adb_commands = 0; @@ -230,8 +245,13 @@ impl Settings { }; for command in p.commands.clone() { *nb_running_async_adb_commands += 1; + let p_info_clone = p_info.clone(); + let phone_id = phone.adb_id.clone(); commands.push(Task::perform( - run_adb_action(phone.adb_id.clone(), command, p_info.clone()), + async move { + run_adb_shell_action(phone_id, command.as_str()) + .map(|_| p_info_clone) + }, Message::RestoringDevice, )); } @@ -274,7 +294,8 @@ impl Settings { if let Ok(path) = result { self.general.backup_folder = path; - Config::save_changes(self, &phone.adb_id); + let mut config = Config::load_configuration_file(); + config.save_device_settings(self.device.clone(), self.general.clone()); self.load_device_settings(phone); } Task::none() @@ -293,8 +314,12 @@ impl Settings { selected_user: Option, packages: &[Vec], ) -> Task { + let core_packages: Vec> = packages + .iter() + .map(|user_packages| user_packages.iter().map(CorePackage::from).collect()) + .collect(); Task::perform( - export_packages(selected_user.unwrap_or_default(), packages.to_vec()), + async move { export_packages(selected_user.unwrap_or_default(), &core_packages) }, Message::PackagesExported, ) } diff --git a/src/gui/widgets/mod.rs b/crates/uad-gui/src/widgets/mod.rs similarity index 100% rename from src/gui/widgets/mod.rs rename to crates/uad-gui/src/widgets/mod.rs diff --git a/src/gui/widgets/modal.rs b/crates/uad-gui/src/widgets/modal.rs similarity index 99% rename from src/gui/widgets/modal.rs rename to crates/uad-gui/src/widgets/modal.rs index a48e5d9..86278a7 100644 --- a/src/gui/widgets/modal.rs +++ b/crates/uad-gui/src/widgets/modal.rs @@ -25,6 +25,7 @@ impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> { /// Sets the message that will be produces when the background /// of the [`Modal`] is pressed + #[must_use] pub fn on_blur(self, on_blur: Message) -> Self { Self { on_blur: Some(on_blur), diff --git a/src/gui/widgets/navigation_menu.rs b/crates/uad-gui/src/widgets/navigation_menu.rs similarity index 90% rename from src/gui/widgets/navigation_menu.rs rename to crates/uad-gui/src/widgets/navigation_menu.rs index f0f9dd9..d26f1b4 100644 --- a/src/gui/widgets/navigation_menu.rs +++ b/crates/uad-gui/src/widgets/navigation_menu.rs @@ -1,19 +1,22 @@ -use crate::core::helpers::button_primary; -pub use crate::core::sync::Phone; -use crate::core::theme::Theme; -use crate::core::update::{SelfUpdateState, SelfUpdateStatus}; -pub use crate::gui::views::about::Message as AboutMessage; -pub use crate::gui::views::list::{List as AppsView, LoadingState as ListLoadingState}; -use crate::gui::{Message, style, widgets::text}; +use crate::helpers::button_primary; +use crate::theme::Theme; +pub use crate::views::about::Message as AboutMessage; +pub use crate::views::list::{List as AppsView, LoadingState as ListLoadingState}; +use crate::{gui::Message, style, widgets::text}; use iced::widget::{Space, button, container, pick_list, row, tooltip}; use iced::{Alignment, Element, Font, Length, Renderer, alignment, font}; +pub use uad_core::sync::Phone; +use uad_core::update::{SelfUpdateState, SelfUpdateStatus}; /// resources/assets/icons.ttf, loaded in [`crate::gui::UadGui`] pub const ICONS: Font = Font { family: font::Family::Name("icomoon"), ..Font::DEFAULT }; -#[allow(clippy::too_many_lines)] +#[allow( + clippy::too_many_lines, + reason = "Navigation layout composes many widgets inline" +)] pub fn nav_menu<'a>( device_list: &'a [Phone], selected_device: Option, diff --git a/src/gui/widgets/package_row.rs b/crates/uad-gui/src/widgets/package_row.rs similarity index 71% rename from src/gui/widgets/package_row.rs rename to crates/uad-gui/src/widgets/package_row.rs index 7d43bb2..9c9c1cd 100644 --- a/src/gui/widgets/package_row.rs +++ b/crates/uad-gui/src/widgets/package_row.rs @@ -1,9 +1,10 @@ -use crate::core::sync::Phone; -use crate::core::theme::Theme; -use crate::core::uad_lists::{PackageState, Removal, UadList}; -use crate::gui::style; -use crate::gui::views::settings::Settings; -use crate::gui::widgets::text; +use crate::style; +use crate::theme::Theme; +use crate::views::settings::Settings; +use crate::widgets::text; +use log::warn; +use uad_core::sync::{CorePackage, Phone}; +use uad_core::uad_lists::{PackageState, Removal, UadList}; use iced::widget::{Space, button, checkbox, row}; use iced::{Alignment, Element, Length, Renderer, Task, alignment}; @@ -11,10 +12,10 @@ use iced::{Alignment, Element, Length, Renderer, Task, alignment}; #[derive(Clone, Debug)] pub struct PackageRow { pub name: String, - pub state: PackageState, pub description: String, - pub uad_list: UadList, pub removal: Removal, + pub state: PackageState, + pub list: UadList, pub selected: bool, pub current: bool, } @@ -27,21 +28,22 @@ pub enum Message { } impl PackageRow { + #[must_use] pub fn new( name: &str, - state: PackageState, description: &str, - uad_list: UadList, removal: Removal, + state: PackageState, + list: UadList, selected: bool, current: bool, ) -> Self { Self { name: name.to_string(), - state, description: description.to_string(), - uad_list, removal, + state, + list, selected, current, } @@ -143,3 +145,56 @@ impl PackageRow { .into() } } + +// Conversions between PackageRow and CorePackage +impl From for PackageRow { + fn from(core: CorePackage) -> Self { + Self { + name: core.name.clone(), + description: core.description, + removal: core.removal, + state: core.state, + list: core.list, + selected: false, // Default to not selected + current: false, // Default to not current + } + } +} + +impl From<&CorePackage> for PackageRow { + fn from(core: &CorePackage) -> Self { + Self { + name: core.name.clone(), + description: core.description.clone(), + removal: core.removal, + state: core.state, + list: core.list, + selected: false, + current: false, + } + } +} + +impl From<&PackageRow> for CorePackage { + fn from(row: &PackageRow) -> Self { + Self { + name: row.name.clone(), + description: row.description.clone(), + removal: row.removal, + state: row.state, + list: row.list, + } + } +} + +impl From for CorePackage { + fn from(row: PackageRow) -> Self { + Self { + name: row.name, + description: row.description, + removal: row.removal, + state: row.state, + list: row.list, + } + } +} diff --git a/src/gui/widgets/text.rs b/crates/uad-gui/src/widgets/text.rs similarity index 92% rename from src/gui/widgets/text.rs rename to crates/uad-gui/src/widgets/text.rs index da6a3c2..1fdc7c1 100644 --- a/src/gui/widgets/text.rs +++ b/crates/uad-gui/src/widgets/text.rs @@ -3,10 +3,11 @@ reason = "this is the replacement that enforces advanced shaping for disallowed [`iced::widget::Text`]" )] +use crate::theme::Theme; use iced::advanced::text::IntoFragment; use iced::widget; // Creates a new Text widget with advanced shaping. -pub fn text<'a>(text: impl IntoFragment<'a>) -> widget::Text<'a, crate::core::theme::Theme> { +pub fn text<'a>(text: impl IntoFragment<'a>) -> widget::Text<'a, Theme> { widget::Text::new(text).shaping(iced::widget::text::Shaping::Advanced) } diff --git a/src/core/helpers.rs b/src/core/helpers.rs deleted file mode 100644 index 0bad3d0..0000000 --- a/src/core/helpers.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::core::theme::Theme; -use crate::gui::style; -use iced::Element; -use iced::widget::button; -use iced::widget::button::Button; - -/// Wrapper function for `iced::widget::button` with padding and style applied -pub fn button_primary<'a, Message>( - content: impl Into>, -) -> Button<'a, Message, Theme> { - button(content) - .padding([5, 10]) - .style(style::Button::Primary) -} diff --git a/src/core/mod.rs b/src/core/mod.rs deleted file mode 100644 index 68c584e..0000000 --- a/src/core/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod adb; -pub mod config; -pub mod helpers; -pub mod save; -pub mod sync; -pub mod theme; -pub mod uad_lists; -pub mod update; -pub mod utils;