Compare commits
11 Commits
v0.1.0-rc-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33748c85b8 | ||
|
|
554657d6ef | ||
|
|
d14699846d | ||
|
|
b8cc6e7500 | ||
|
|
9308276f6f | ||
|
|
ef2c0d2f2b | ||
|
|
711b7ed77e | ||
|
|
e7b00f4532 | ||
|
|
53ce50de1f | ||
|
|
37d358a479 | ||
|
|
bb92466cfa |
141
Cargo.lock
generated
141
Cargo.lock
generated
@@ -110,8 +110,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "adb_client"
|
||||
version = "2.1.18"
|
||||
source = "git+https://github.com/lavafroth/adb_client?branch=with-read-timeout#f69a3d8a1aa58713e899a8d07ba13588027390e9"
|
||||
version = "2.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "517ba09db77302f5326492b7f0b9cb99fdf1b4c58f1b749a00f081195cf52949"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bincode",
|
||||
@@ -297,6 +298,28 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
|
||||
dependencies = [
|
||||
"async-fs",
|
||||
"async-net",
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.2",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.1"
|
||||
@@ -374,6 +397,17 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
@@ -403,6 +437,17 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-net"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
@@ -634,6 +679,15 @@ dependencies = [
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
@@ -1021,6 +1075,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"libc",
|
||||
"objc2 0.6.3",
|
||||
]
|
||||
|
||||
@@ -1476,6 +1532,15 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
@@ -1525,8 +1590,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -2417,7 +2484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-data",
|
||||
@@ -2433,6 +2500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"objc2 0.6.3",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
@@ -2446,7 +2514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -2458,7 +2526,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -2470,7 +2538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -2505,7 +2573,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
@@ -2517,7 +2585,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-contacts",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -2536,7 +2604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"dispatch",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
@@ -2570,7 +2638,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -2583,7 +2651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -2595,7 +2663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
@@ -2618,7 +2686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
@@ -2638,7 +2706,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -2650,7 +2718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -2958,6 +3026,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.12.0"
|
||||
@@ -3232,6 +3306,30 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2 0.6.2",
|
||||
"dispatch2",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.6.3",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"urlencoding",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -3960,6 +4058,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -4865,7 +4969,7 @@ dependencies = [
|
||||
"android-activity",
|
||||
"atomic-waker",
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"bytemuck",
|
||||
"calloop 0.13.0",
|
||||
"cfg_aliases",
|
||||
@@ -5221,7 +5325,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zilch"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"eframe",
|
||||
@@ -5229,7 +5333,9 @@ dependencies = [
|
||||
"egui_alignments",
|
||||
"egui_extras",
|
||||
"env_logger",
|
||||
"log",
|
||||
"phf 0.13.1",
|
||||
"rfd",
|
||||
"rusb",
|
||||
]
|
||||
|
||||
@@ -5257,6 +5363,7 @@ dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"url",
|
||||
"winnow",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -1,14 +1,20 @@
|
||||
[package]
|
||||
name = "zilch"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
[dependencies]
|
||||
adb_client = {version = "2.1.18", git = "https://github.com/lavafroth/adb_client", branch = "with-read-timeout"}
|
||||
eframe = "0.33.2"
|
||||
adb_client = { version = "2.1.18" }
|
||||
# TODO: drop x11 support when wayland adoption increases. Will decrease binary size
|
||||
eframe = {version = "0.33.2", features = ["accesskit", "default_fonts", "glow", "wayland", "x11"] }
|
||||
egui = "0.33.2"
|
||||
egui_alignments = "0.3.6"
|
||||
egui_extras = "0.33.2"
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.29"
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
rfd = { version = "0.16.0", features = ["ashpd", "pollster", "urlencoding", "wayland", "xdg-portal"] }
|
||||
rusb = "0.9.4"
|
||||
|
||||
13
README.md
13
README.md
@@ -4,9 +4,6 @@ Purge Android bloat with confidence.
|
||||
|
||||

|
||||
|
||||
> [!WARNING]
|
||||
> This app is not production ready, some features are missing.
|
||||
|
||||
## Features
|
||||
|
||||
- Click on app entries to select them
|
||||
@@ -17,11 +14,17 @@ Purge Android bloat with confidence.
|
||||
- Accidentally removed apps can be restored via the revert button
|
||||
- Recommendation categories (borrowed from UAD)
|
||||
- Press `S` or `/` or `Ctrl` `F` to search apps
|
||||
- Save the current state of packages on the phone with `Ctrl` `S`
|
||||
|
||||
### Not yet implemented
|
||||
|
||||
- Save button + shortcut `Ctrl` `S`
|
||||
- Make uninstall and disable options depend on Android SDK
|
||||
- Make uninstall and disable options depend on Android SDK version
|
||||
|
||||
## Installation
|
||||
|
||||
### From binary releases
|
||||
|
||||
Precompiled binaries are available under the releases tab.
|
||||
|
||||
### Build from source
|
||||
|
||||
|
||||
62
src/action.rs
Normal file
62
src/action.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::adb_shell_text::ShellCommandText;
|
||||
use crate::{Package, PackageIdentifier, ShellRunError};
|
||||
use adb_client::ADBUSBDevice;
|
||||
|
||||
pub enum Action {
|
||||
Uninstall(Package),
|
||||
Revert(PackageIdentifier, crate::listview::State),
|
||||
Disable(PackageIdentifier),
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn apply_on_device(self, device: &mut ADBUSBDevice) -> Result<(), ShellRunError> {
|
||||
match self {
|
||||
Action::Uninstall(pkg) => {
|
||||
if pkg.path.is_empty() {
|
||||
return Err(ShellRunError::BackupNotPossible(pkg.id));
|
||||
}
|
||||
|
||||
let _copy_command_no_output = device.shell_command_text(&format!(
|
||||
"cp {} /data/local/tmp/{}.apk",
|
||||
pkg.path, pkg.id
|
||||
))?;
|
||||
|
||||
let output =
|
||||
device.shell_command_text(&format!("pm uninstall --user 0 -k {}", pkg.id))?;
|
||||
|
||||
if !output.contains("Success") {
|
||||
return Err(ShellRunError::UninstallFailed(pkg.id));
|
||||
}
|
||||
}
|
||||
Action::Revert(id, crate::listview::State::Disabled) => {
|
||||
let revert_command = format!("pm enable {id}");
|
||||
let output = device.shell_command_text(&revert_command)?;
|
||||
if !output.contains("new state: enabled") {
|
||||
return Err(ShellRunError::RevertFailed(id));
|
||||
}
|
||||
}
|
||||
Action::Revert(id, _uninstalled) => {
|
||||
let revert_command = format!("pm install-existing {id}");
|
||||
let output = device.shell_command_text(&revert_command)?;
|
||||
|
||||
if !output.contains("inaccessible or not found") {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let revert_command = format!("pm install -r --user 0 /data/local/tmp/{id}.apk");
|
||||
let output = device.shell_command_text(&revert_command)?;
|
||||
if !output.contains("Success") {
|
||||
return Err(ShellRunError::RevertFailed(id));
|
||||
}
|
||||
}
|
||||
Action::Disable(id) => {
|
||||
let disable_command = format!("pm disable-user {id}");
|
||||
let output = device.shell_command_text(&disable_command)?;
|
||||
if !output.contains("new state: disabled-user") {
|
||||
return Err(ShellRunError::DisableFailed(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
98
src/action_bar.rs
Normal file
98
src/action_bar.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use crate::Action;
|
||||
use crate::categories;
|
||||
use crate::listview;
|
||||
use crate::listview::State;
|
||||
|
||||
use egui::Vec2;
|
||||
use egui::{Button, RichText, Spinner};
|
||||
|
||||
impl crate::App {
|
||||
pub fn action_bar(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.style_mut().spacing.button_padding = [6.0, 6.0].into();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (category, bits) in categories::NAMES.into_iter().zip(categories::VALUES) {
|
||||
let selected = self.categories & bits == bits;
|
||||
if ui
|
||||
.add(
|
||||
Button::selectable(selected, RichText::new(category).size(12.0))
|
||||
.corner_radius(10.0),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.categories ^= bits;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let button = if self.disable_mode {
|
||||
Button::new("disable")
|
||||
} else {
|
||||
Button::new("uninstall")
|
||||
};
|
||||
|
||||
let mut selected: Vec<&listview::Entry> = vec![];
|
||||
let mut selected_app_state = 0b111;
|
||||
for entry in self.entries.values().filter(|entry| entry.selected) {
|
||||
selected.push(entry);
|
||||
selected_app_state &= entry.state as u8;
|
||||
}
|
||||
|
||||
// eprintln!("{0:08b}", self.selected_app_state);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let button_size = [80.0, 30.0].into();
|
||||
if self.busy {
|
||||
ui.add_sized(button_size, Spinner::new());
|
||||
} else if selected_app_state == State::Enabled as u8 {
|
||||
if add_enabled_button(true, ui, button_size, button) {
|
||||
if self.disable_mode {
|
||||
for entry in selected.iter() {
|
||||
self.action_tx
|
||||
.send(Action::Disable(entry.package.id.clone()))
|
||||
.expect("failed to send message to backend");
|
||||
}
|
||||
} else {
|
||||
for entry in selected.iter() {
|
||||
self.action_tx
|
||||
.send(Action::Uninstall(entry.package.clone()))
|
||||
.expect("failed to send message to backend");
|
||||
}
|
||||
}
|
||||
self.busy = true;
|
||||
}
|
||||
} else if selected_app_state == State::Uninstalled as u8 {
|
||||
if add_enabled_button(true, ui, button_size, Button::new("revert")) {
|
||||
for entry in selected.iter() {
|
||||
self.action_tx
|
||||
.send(Action::Revert(entry.package.id.clone(), entry.state))
|
||||
.expect("failed to send message to backend");
|
||||
}
|
||||
|
||||
self.busy = true;
|
||||
}
|
||||
} else {
|
||||
// the selection is a mix of enabled and disabled apps:
|
||||
// gray out the button
|
||||
add_enabled_button(false, ui, button_size, button);
|
||||
}
|
||||
|
||||
ui.checkbox(&mut self.disable_mode, "disable mode")
|
||||
.on_hover_text("prefer disabling apps to uninstalling");
|
||||
|
||||
ui.separator();
|
||||
ui.label(format!("{} selected", selected.len()));
|
||||
ui.separator();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_enabled_button(enabled: bool, ui: &mut egui::Ui, size: Vec2, button: Button) -> bool {
|
||||
ui.add_enabled_ui(enabled, |ui| ui.add_sized(size, button))
|
||||
.inner
|
||||
.clicked()
|
||||
}
|
||||
18
src/adb_shell_text.rs
Normal file
18
src/adb_shell_text.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use crate::ShellRunError;
|
||||
use adb_client::{ADBDeviceExt, ADBUSBDevice};
|
||||
|
||||
pub trait ShellCommandText {
|
||||
fn shell_command_text(&mut self, command: &str) -> Result<String, ShellRunError>;
|
||||
}
|
||||
|
||||
impl ShellCommandText for ADBUSBDevice {
|
||||
fn shell_command_text(&mut self, command: &str) -> Result<String, ShellRunError> {
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
self.shell_command(&[command], &mut buf)
|
||||
.map_err(|e| match e {
|
||||
adb_client::RustADBError::UsbError(rusb::Error::Timeout) => ShellRunError::Timeout,
|
||||
_ => ShellRunError::Unrecoverable,
|
||||
})?;
|
||||
String::from_utf8(buf).map_err(|_| ShellRunError::ParseError)
|
||||
}
|
||||
}
|
||||
24
src/categories.rs
Normal file
24
src/categories.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
pub const RECOMMENDED: u8 = 0b10000;
|
||||
pub const ADVANCED: u8 = 0b01000;
|
||||
pub const EXPERT: u8 = 0b00100;
|
||||
pub const UNSAFE: u8 = 0b00010;
|
||||
pub const UNIDENTIFIED: u8 = 0b00001;
|
||||
|
||||
pub const VALUES: [u8; 5] = [RECOMMENDED, ADVANCED, EXPERT, UNSAFE, UNIDENTIFIED];
|
||||
pub const NAMES: [&str; 5] = [
|
||||
"Recommended",
|
||||
"Advanced",
|
||||
"Expert",
|
||||
"Unsafe",
|
||||
"Unidentified",
|
||||
];
|
||||
|
||||
pub fn value_to_name(value: u8) -> &'static str {
|
||||
match value {
|
||||
RECOMMENDED => "Recommended",
|
||||
ADVANCED => "Advanced",
|
||||
EXPERT => "Expert",
|
||||
UNSAFE => "Unsafe",
|
||||
_ => "Unidentified",
|
||||
}
|
||||
}
|
||||
114
src/listview.rs
Normal file
114
src/listview.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use crate::Metadata;
|
||||
use crate::Package;
|
||||
use crate::categories;
|
||||
use egui::{Align, Button, Color32, Layout, RichText, Sense, Stroke, Style, text::LayoutJob};
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum State {
|
||||
Enabled = 0b001,
|
||||
Uninstalled = 0b010,
|
||||
Disabled = 0b110,
|
||||
}
|
||||
|
||||
pub struct Entry {
|
||||
pub package: Package,
|
||||
pub expand_triggered: bool,
|
||||
pub state: State,
|
||||
pub selected: bool,
|
||||
pub metadata: Option<&'static Metadata>,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn render(&mut self, ui: &mut egui::Ui) {
|
||||
let id = ui.make_persistent_id(format!("{}_state", self.package.id));
|
||||
let mut state =
|
||||
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, false);
|
||||
|
||||
if self.expand_triggered {
|
||||
state.toggle(ui);
|
||||
self.expand_triggered = false;
|
||||
}
|
||||
|
||||
let header = ui.horizontal(|ui| {
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(20.0, 10.0);
|
||||
ui.with_layout(Layout::top_down_justified(egui::Align::LEFT), |ui| {
|
||||
// supply faint versions of the colors for disabled apps
|
||||
let faint_bg = ui.style().visuals.faint_bg_color;
|
||||
let selection_bg = ui.style().visuals.selection.bg_fill;
|
||||
let fg = ui.style().visuals.selection.stroke.color;
|
||||
let faint_selection_bg = selection_bg.lerp_to_gamma(faint_bg, 0.6);
|
||||
let faint_fg = fg.lerp_to_gamma(faint_bg, 0.6);
|
||||
|
||||
let response = ui.add(self.button(
|
||||
faint_selection_bg,
|
||||
selection_bg,
|
||||
faint_fg,
|
||||
categories::value_to_name(self.metadata.map(|m| m.removal).unwrap_or_default()),
|
||||
));
|
||||
let id = ui.make_persistent_id(format!("{}_interact", self.package.id));
|
||||
if ui
|
||||
.interact(response.rect, id, Sense::click())
|
||||
.double_clicked()
|
||||
{
|
||||
self.expand_triggered = true;
|
||||
self.selected ^= true;
|
||||
} else if ui.interact(response.rect, id, Sense::click()).clicked() {
|
||||
self.selected ^= true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
state.show_body_indented(&header.response, ui, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
self.metadata
|
||||
.map(|m| m.description)
|
||||
.unwrap_or("Description unavailable."),
|
||||
)
|
||||
.size(12.0),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
}
|
||||
|
||||
fn button<'a>(
|
||||
&self,
|
||||
faint_bg: Color32,
|
||||
selection_bg: Color32,
|
||||
faint_fg: Color32,
|
||||
right_text: &'a str,
|
||||
) -> Button<'a> {
|
||||
let mut job = LayoutJob::default();
|
||||
let mut label = RichText::new(format!("{}\n", self.package.label)).size(12.0);
|
||||
let mut package_id = RichText::new(&self.package.id).monospace().size(10.0);
|
||||
let mut right_text = RichText::new(right_text);
|
||||
|
||||
if self.state != State::Enabled {
|
||||
label = label.strikethrough().color(faint_fg);
|
||||
package_id = package_id.strikethrough().color(faint_fg);
|
||||
right_text = right_text.color(faint_fg);
|
||||
}
|
||||
|
||||
label.append_to(
|
||||
&mut job,
|
||||
&Style::default(),
|
||||
egui::FontSelection::Default,
|
||||
Align::Min,
|
||||
);
|
||||
package_id.append_to(
|
||||
&mut job,
|
||||
&Style::default(),
|
||||
egui::FontSelection::Default,
|
||||
Align::Min,
|
||||
);
|
||||
let button = Button::selectable(self.selected, job);
|
||||
if self.state != State::Enabled {
|
||||
button.stroke(Stroke::new(1.0, selection_bg)).fill(faint_bg)
|
||||
} else {
|
||||
button
|
||||
}
|
||||
.right_text(right_text)
|
||||
}
|
||||
}
|
||||
466
src/main.rs
466
src/main.rs
@@ -1,7 +1,8 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in releaspackage
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fmt::Display,
|
||||
io::BufReader,
|
||||
sync::mpsc::{Receiver, Sender, channel},
|
||||
thread::{sleep, spawn},
|
||||
@@ -10,12 +11,17 @@ use std::{
|
||||
|
||||
use adb_client::{ADBDeviceExt, ADBUSBDevice};
|
||||
use eframe::egui;
|
||||
use egui::{
|
||||
Align, Button, Color32, Label, Layout, RichText, Sense, Spinner, Style, TextEdit,
|
||||
TopBottomPanel, text::LayoutJob,
|
||||
};
|
||||
use egui::{Align, CentralPanel, Label, Spinner, TextEdit, TopBottomPanel};
|
||||
use egui_alignments::{center_horizontal, column};
|
||||
|
||||
use crate::{action::Action, adb_shell_text::ShellCommandText};
|
||||
mod action;
|
||||
mod action_bar;
|
||||
mod adb_shell_text;
|
||||
mod categories;
|
||||
mod listview;
|
||||
mod metadata;
|
||||
mod shortcuts;
|
||||
|
||||
const WORKER_THREAD_POLL: Duration = Duration::from_secs(5);
|
||||
const LABEL_EXTRACTOR: &[u8; 2124] = include_bytes!("./extractor.dex");
|
||||
@@ -24,17 +30,16 @@ type FrontendPayload = PackageDiff;
|
||||
|
||||
struct App {
|
||||
search_query: String,
|
||||
uninstallable: bool,
|
||||
reinstallable: bool,
|
||||
entries: BTreeMap<String, Entry>,
|
||||
entries: BTreeMap<String, listview::Entry>,
|
||||
categories: u8,
|
||||
|
||||
package_diff_rx: Receiver<FrontendPayload>,
|
||||
device_lost_rx: Receiver<()>,
|
||||
action_tx: Sender<Action>,
|
||||
// error_rx: Receiver<ShellRunError>,
|
||||
action_error_rx: Receiver<ShellRunError>,
|
||||
action_done_rx: Receiver<()>,
|
||||
disable_mode: bool,
|
||||
|
||||
disable_mode: bool,
|
||||
have_device: bool,
|
||||
busy: bool,
|
||||
}
|
||||
@@ -49,27 +54,48 @@ pub struct Package {
|
||||
label: String,
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
package: Package,
|
||||
expand_triggered: bool,
|
||||
enabled: bool,
|
||||
selected: bool,
|
||||
metadata: Option<&'static Metadata>,
|
||||
}
|
||||
|
||||
struct PackageDiff {
|
||||
added: Vec<Package>,
|
||||
removed: Vec<PackageIdentifier>,
|
||||
disabled: Vec<String>,
|
||||
re_enabled: Vec<String>,
|
||||
}
|
||||
|
||||
impl PackageDiff {
|
||||
fn same_as_before(&self) -> bool {
|
||||
self.added.is_empty()
|
||||
&& self.removed.is_empty()
|
||||
&& self.disabled.is_empty()
|
||||
&& self.re_enabled.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ShellRunError {
|
||||
Timeout,
|
||||
ParseError,
|
||||
Unrecoverable,
|
||||
UnsuccessfulOperation(PackageIdentifier),
|
||||
UninstallFailed(PackageIdentifier),
|
||||
BackupNotPossible(PackageIdentifier),
|
||||
RevertFailed(PackageIdentifier),
|
||||
DisableFailed(PackageIdentifier),
|
||||
}
|
||||
|
||||
impl Display for ShellRunError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ShellRunError::Timeout => f.write_str("timed out in running shell command on device"),
|
||||
ShellRunError::ParseError => {
|
||||
f.write_str("failed to parse the output of shell command from the device")
|
||||
}
|
||||
ShellRunError::Unrecoverable => f.write_str("unrecoverable error"),
|
||||
ShellRunError::UninstallFailed(id) => write!(f, "failed to uninstall package {id}"),
|
||||
ShellRunError::BackupNotPossible(id) => {
|
||||
write!(f, "failed to backup package {id} before uninstall")
|
||||
}
|
||||
ShellRunError::RevertFailed(id) => write!(f, "failed to revert package {id}"),
|
||||
ShellRunError::DisableFailed(id) => write!(f, "failed to disable package {id}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -78,41 +104,10 @@ pub struct Metadata {
|
||||
removal: u8,
|
||||
}
|
||||
|
||||
mod categories {
|
||||
pub const RECOMMENDED: u8 = 0b10000;
|
||||
pub const ADVANCED: u8 = 0b01000;
|
||||
pub const EXPERT: u8 = 0b00100;
|
||||
pub const UNSAFE: u8 = 0b00010;
|
||||
pub const UNIDENTIFIED: u8 = 0b00001;
|
||||
|
||||
pub const VALUES: [u8; 5] = [RECOMMENDED, ADVANCED, EXPERT, UNSAFE, UNIDENTIFIED];
|
||||
pub const NAMES: [&str; 5] = [
|
||||
"Recommended",
|
||||
"Advanced",
|
||||
"Expert",
|
||||
"Unsafe",
|
||||
"Unidentified",
|
||||
];
|
||||
|
||||
pub fn value_to_name(value: u8) -> &'static str {
|
||||
match value {
|
||||
RECOMMENDED => "Recommended",
|
||||
ADVANCED => "Advanced",
|
||||
EXPERT => "Expert",
|
||||
UNSAFE => "Unsafe",
|
||||
_ => "Unidentified",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Action {
|
||||
Uninstall(Package),
|
||||
Revert(PackageIdentifier),
|
||||
Disable(PackageIdentifier),
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Warn)
|
||||
.init();
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 300.0]),
|
||||
..Default::default()
|
||||
@@ -126,6 +121,7 @@ fn main() -> eframe::Result {
|
||||
let (device_lost_tx, device_lost_rx) = channel();
|
||||
let (action_tx, action_rx) = channel();
|
||||
let (action_done_tx, action_done_rx) = channel();
|
||||
let (action_result_tx, action_result_rx) = channel();
|
||||
|
||||
let ctx = cc.egui_ctx.clone();
|
||||
spawn(move || {
|
||||
@@ -134,6 +130,7 @@ fn main() -> eframe::Result {
|
||||
device_lost_tx,
|
||||
action_rx,
|
||||
action_done_tx,
|
||||
action_result_tx,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
@@ -142,8 +139,6 @@ fn main() -> eframe::Result {
|
||||
busy: false,
|
||||
disable_mode: false,
|
||||
have_device: false,
|
||||
uninstallable: false,
|
||||
reinstallable: false,
|
||||
search_query: "".to_owned(),
|
||||
device_lost_rx,
|
||||
package_diff_rx,
|
||||
@@ -151,64 +146,23 @@ fn main() -> eframe::Result {
|
||||
action_tx,
|
||||
categories: categories::RECOMMENDED,
|
||||
action_done_rx,
|
||||
action_error_rx: action_result_rx,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
impl Action {
|
||||
fn apply_on_device(self, device: &mut ADBUSBDevice) -> Result<(), ShellRunError> {
|
||||
match self {
|
||||
Action::Uninstall(pkg) => {
|
||||
if pkg.path.is_empty() {
|
||||
return Err(ShellRunError::BackupNotPossible(pkg.id));
|
||||
}
|
||||
|
||||
let _copy_command_no_output = device.shell_command_text(&format!(
|
||||
"cp {} /data/local/tmp/{}.apk",
|
||||
pkg.path, pkg.id
|
||||
))?;
|
||||
|
||||
let output =
|
||||
device.shell_command_text(&format!("pm uninstall --user 0 -k {}", pkg.id))?;
|
||||
|
||||
if !output.contains("Success") {
|
||||
return Err(ShellRunError::UnsuccessfulOperation(pkg.id));
|
||||
}
|
||||
}
|
||||
Action::Revert(id) => {
|
||||
let revert_command = format!("package install-existing {id}");
|
||||
let output = device.shell_command_text(&revert_command)?;
|
||||
|
||||
if !output.contains("inaccessible or not found") {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let revert_command = format!("pm install -r --user 0 /data/local/tmp/{id}.apk");
|
||||
let output = device.shell_command_text(&revert_command)?;
|
||||
if !output.contains("Success") {
|
||||
return Err(ShellRunError::RevertFailed(id));
|
||||
}
|
||||
}
|
||||
Action::Disable(id) => {
|
||||
let disable_command = format!("pm disable --user 0 {id}");
|
||||
let output = device.shell_command_text(&disable_command)?;
|
||||
eprintln!("disable output {output:?}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn worker_thread(
|
||||
package_diff_tx: Sender<PackageDiff>,
|
||||
device_lost_tx: Sender<()>,
|
||||
action_rx: Receiver<Action>,
|
||||
action_done_tx: Sender<()>,
|
||||
action_error_tx: Sender<ShellRunError>,
|
||||
ctx: egui::Context,
|
||||
) {
|
||||
let mut maybe_device: Option<ADBUSBDevice> = None;
|
||||
let mut pkg_set: BTreeSet<PackageIdentifier> = Default::default();
|
||||
let mut disabled_set: BTreeSet<PackageIdentifier> = Default::default();
|
||||
|
||||
loop {
|
||||
while maybe_device.is_none() {
|
||||
@@ -229,17 +183,22 @@ fn worker_thread(
|
||||
while let Some(device) = maybe_device.as_mut() {
|
||||
// do all the actions in bulk before the next render
|
||||
while let Ok(action) = action_rx.try_recv() {
|
||||
let _ = action.apply_on_device(device);
|
||||
if let Err(action_error) = action.apply_on_device(device) {
|
||||
action_error_tx
|
||||
.send(action_error)
|
||||
.expect("failed to send to ui");
|
||||
}
|
||||
}
|
||||
|
||||
action_done_tx.send(()).expect("failed to send to ui");
|
||||
match fetch_packages(device, &pkg_set) {
|
||||
Ok((diff, new_pkg_set)) => {
|
||||
if diff.added.is_empty() && diff.removed.is_empty() {
|
||||
match fetch_packages(device, &pkg_set, &disabled_set) {
|
||||
Ok((diff, new_pkg_set, new_disabled_set)) => {
|
||||
if diff.same_as_before() {
|
||||
sleep(WORKER_THREAD_POLL);
|
||||
continue;
|
||||
}
|
||||
pkg_set = new_pkg_set;
|
||||
disabled_set = new_disabled_set;
|
||||
package_diff_tx.send(diff).expect("failed to send to ui");
|
||||
}
|
||||
Err(ShellRunError::Timeout) => {}
|
||||
@@ -256,6 +215,42 @@ fn worker_thread(
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn reconcile(&mut self, package_diff: PackageDiff) {
|
||||
for package in package_diff.added {
|
||||
let maybe_meta = metadata::STORE.get(&package.id);
|
||||
self.entries.insert(
|
||||
package.id.clone(),
|
||||
listview::Entry {
|
||||
package,
|
||||
metadata: maybe_meta,
|
||||
expand_triggered: false,
|
||||
state: listview::State::Enabled,
|
||||
selected: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for package_id in package_diff.removed {
|
||||
if let Some(entry) = self.entries.get_mut(&package_id) {
|
||||
entry.state = listview::State::Uninstalled;
|
||||
};
|
||||
}
|
||||
|
||||
for package_id in package_diff.disabled {
|
||||
if let Some(entry) = self.entries.get_mut(&package_id) {
|
||||
entry.state = listview::State::Disabled;
|
||||
};
|
||||
}
|
||||
|
||||
for package_id in package_diff.re_enabled {
|
||||
if let Some(entry) = self.entries.get_mut(&package_id) {
|
||||
entry.state = listview::State::Enabled;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
ctx.set_pixels_per_point(1.5);
|
||||
@@ -263,35 +258,21 @@ impl eframe::App for App {
|
||||
if let Ok(()) = self.action_done_rx.try_recv() {
|
||||
self.busy = false;
|
||||
}
|
||||
|
||||
if let Ok(action_error) = self.action_error_rx.try_recv() {
|
||||
log::error!("{}", action_error.to_string());
|
||||
}
|
||||
|
||||
if let Ok(package_diff) = self.package_diff_rx.try_recv() {
|
||||
self.have_device = true;
|
||||
|
||||
for package in package_diff.added {
|
||||
let maybe_meta = metadata::STORE.get(&package.id);
|
||||
self.entries.insert(
|
||||
package.id.clone(),
|
||||
Entry {
|
||||
package,
|
||||
metadata: maybe_meta,
|
||||
expand_triggered: false,
|
||||
enabled: true,
|
||||
selected: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for package_id in package_diff.removed {
|
||||
if let Some(entry) = self.entries.get_mut(&package_id) {
|
||||
entry.enabled = false;
|
||||
};
|
||||
}
|
||||
self.reconcile(package_diff);
|
||||
}
|
||||
|
||||
if let Ok(()) = self.device_lost_rx.try_recv() {
|
||||
self.have_device = false;
|
||||
self.entries.clear();
|
||||
|
||||
println!("device lost");
|
||||
log::warn!("device lost");
|
||||
}
|
||||
|
||||
if !self.have_device {
|
||||
@@ -306,112 +287,20 @@ impl eframe::App for App {
|
||||
return;
|
||||
};
|
||||
|
||||
TopBottomPanel::bottom("action_bar").show(ctx, |ui| {
|
||||
ui.add_space(6.0);
|
||||
TopBottomPanel::bottom("action_bar").show(ctx, |ui| self.action_bar(ui));
|
||||
|
||||
ui.style_mut().spacing.button_padding = [6.0, 6.0].into();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (category, bits) in categories::NAMES.into_iter().zip(categories::VALUES) {
|
||||
let selected = self.categories & bits == bits;
|
||||
if ui
|
||||
.add(
|
||||
Button::selectable(selected, RichText::new(category).size(12.0))
|
||||
.corner_radius(10.0),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.categories ^= bits;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let button = if self.disable_mode {
|
||||
Button::new("disable")
|
||||
} else {
|
||||
Button::new("uninstall")
|
||||
};
|
||||
|
||||
let mut selected: Vec<&Entry> = vec![];
|
||||
for entry in self.entries.values().filter(|entry| entry.selected) {
|
||||
selected.push(entry);
|
||||
if entry.enabled {
|
||||
self.uninstallable = true;
|
||||
} else {
|
||||
self.reinstallable = true;
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
let button_size = [80.0, 30.0];
|
||||
if self.busy {
|
||||
ui.add_sized(button_size, Spinner::new());
|
||||
} else if self.uninstallable == self.reinstallable {
|
||||
ui.add_enabled_ui(false, |ui| {
|
||||
ui.add_sized(button_size, button);
|
||||
});
|
||||
} else if self.uninstallable {
|
||||
ui.add_enabled_ui(true, |ui| {
|
||||
if !ui.add_sized(button_size, button).clicked() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.disable_mode {
|
||||
for entry in selected.iter() {
|
||||
self.action_tx
|
||||
.send(Action::Disable(entry.package.id.clone()))
|
||||
.expect("failed to send message to backend");
|
||||
}
|
||||
} else {
|
||||
for entry in selected.iter() {
|
||||
self.action_tx
|
||||
.send(Action::Uninstall(entry.package.clone()))
|
||||
.expect("failed to send message to backend");
|
||||
}
|
||||
}
|
||||
self.busy = true;
|
||||
});
|
||||
} else if self.reinstallable {
|
||||
ui.add_enabled_ui(true, |ui| {
|
||||
if ui.add_sized(button_size, Button::new("revert")).clicked() {
|
||||
for entry in selected.iter() {
|
||||
self.action_tx
|
||||
.send(Action::Revert(entry.package.id.clone()))
|
||||
.expect("failed to send message to backend");
|
||||
}
|
||||
|
||||
self.busy = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.checkbox(&mut self.disable_mode, "disable mode")
|
||||
.on_hover_text("prefer disabling apps to uninstalling");
|
||||
|
||||
ui.separator();
|
||||
ui.label(format!("{} selected", selected.len()));
|
||||
ui.separator();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
|
||||
let mut search = None;
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
CentralPanel::default().show(ctx, |ui| {
|
||||
ui.take_available_width();
|
||||
ui.horizontal(|ui| {
|
||||
let search = ui.horizontal(|ui| {
|
||||
ui.take_available_width();
|
||||
search.replace(ui.add_sized(
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 20.0],
|
||||
TextEdit::singleline(&mut self.search_query).hint_text("Search"),
|
||||
))
|
||||
)
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
self.uninstallable = false;
|
||||
self.reinstallable = false;
|
||||
|
||||
for (id, entry) in self.entries.iter_mut() {
|
||||
let query_lower = self.search_query.to_lowercase();
|
||||
let entry_removal = entry
|
||||
@@ -422,49 +311,20 @@ impl eframe::App for App {
|
||||
|| entry.package.label.to_lowercase().contains(&query_lower))
|
||||
&& (entry_removal & self.categories == entry_removal)
|
||||
{
|
||||
render_entry(ui, entry);
|
||||
entry.render(ui);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
for (_id, entry) in self.entries.iter_mut() {
|
||||
entry.selected = false;
|
||||
}
|
||||
}
|
||||
if let Some(focusable_search) = search
|
||||
&& ui.input(|i| {
|
||||
i.key_pressed(egui::Key::S)
|
||||
|| i.key_pressed(egui::Key::Slash)
|
||||
|| (i.modifiers.ctrl && i.key_pressed(egui::Key::F))
|
||||
})
|
||||
{
|
||||
focusable_search.request_focus();
|
||||
}
|
||||
self.handle_shortcuts(ui, search.response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ShellCommandExt {
|
||||
fn shell_command_text(&mut self, command: &str) -> Result<String, ShellRunError>;
|
||||
}
|
||||
|
||||
impl ShellCommandExt for ADBUSBDevice {
|
||||
fn shell_command_text(&mut self, command: &str) -> Result<String, ShellRunError> {
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
self.shell_command(&[command], &mut buf)
|
||||
.map_err(|e| match e {
|
||||
adb_client::RustADBError::UsbError(rusb::Error::Timeout) => ShellRunError::Timeout,
|
||||
_ => ShellRunError::Unrecoverable,
|
||||
})?;
|
||||
String::from_utf8(buf).map_err(|_| ShellRunError::ParseError)
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_packages(
|
||||
device: &mut ADBUSBDevice,
|
||||
pkg_set: &BTreeSet<String>,
|
||||
) -> Result<(PackageDiff, BTreeSet<String>), ShellRunError> {
|
||||
disabled_set: &BTreeSet<String>,
|
||||
) -> Result<(PackageDiff, BTreeSet<String>, BTreeSet<String>), ShellRunError> {
|
||||
let raw_pkg_text = device.shell_command_text("pm list packages -f")?;
|
||||
|
||||
let mut current_set = BTreeSet::new();
|
||||
@@ -488,6 +348,24 @@ fn fetch_packages(
|
||||
|
||||
let removed = pkg_set.difference(¤t_set).cloned().collect();
|
||||
|
||||
// disabled
|
||||
let raw_pkg_text = device.shell_command_text("pm list packages -d")?;
|
||||
let mut current_disabled_set = BTreeSet::new();
|
||||
for line in raw_pkg_text.lines() {
|
||||
let id = line.strip_prefix("package:").unwrap_or(line);
|
||||
current_disabled_set.insert(id.to_string());
|
||||
}
|
||||
|
||||
let re_enabled = disabled_set
|
||||
.difference(¤t_disabled_set)
|
||||
.map(|v| v.to_string())
|
||||
.collect();
|
||||
|
||||
let disabled = current_disabled_set
|
||||
.difference(disabled_set)
|
||||
.map(|v| v.to_string())
|
||||
.collect();
|
||||
|
||||
let need_to_fetch_labels = !new_packages.is_empty();
|
||||
if need_to_fetch_labels {
|
||||
let raw_pkg_text = device
|
||||
@@ -510,82 +388,10 @@ fn fetch_packages(
|
||||
PackageDiff {
|
||||
added: new_packages.into_values().collect(),
|
||||
removed,
|
||||
disabled,
|
||||
re_enabled,
|
||||
},
|
||||
current_set,
|
||||
current_disabled_set,
|
||||
))
|
||||
}
|
||||
|
||||
fn create_button(entry: &'_ Entry) -> Button<'_> {
|
||||
let mut job = LayoutJob::default();
|
||||
let mut label = RichText::new(format!("{}\n", entry.package.label)).size(12.0);
|
||||
let mut package_id = RichText::new(&entry.package.id).monospace().size(10.0);
|
||||
let disabled_text_color = Color32::from_rgb(100, 100, 100);
|
||||
|
||||
if !entry.enabled {
|
||||
label = label.strikethrough().color(disabled_text_color);
|
||||
package_id = package_id.strikethrough().color(disabled_text_color);
|
||||
}
|
||||
|
||||
label.append_to(
|
||||
&mut job,
|
||||
&Style::default(),
|
||||
egui::FontSelection::Default,
|
||||
Align::Min,
|
||||
);
|
||||
package_id.append_to(
|
||||
&mut job,
|
||||
&Style::default(),
|
||||
egui::FontSelection::Default,
|
||||
Align::Min,
|
||||
);
|
||||
let button = Button::selectable(entry.selected, job);
|
||||
if !entry.enabled {
|
||||
button.fill(Color32::from_rgb(60, 60, 60))
|
||||
} else {
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry(ui: &mut egui::Ui, entry: &mut Entry) {
|
||||
let id = ui.make_persistent_id(format!("{}_state", entry.package.id));
|
||||
let mut state =
|
||||
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, false);
|
||||
|
||||
if entry.expand_triggered {
|
||||
state.toggle(ui);
|
||||
entry.expand_triggered = false;
|
||||
}
|
||||
|
||||
let header_res = ui.horizontal(|ui| {
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(20.0, 10.0);
|
||||
ui.with_layout(Layout::top_down_justified(egui::Align::LEFT), |ui| {
|
||||
let response = ui.add(create_button(entry).right_text(categories::value_to_name(
|
||||
entry.metadata.map(|m| m.removal).unwrap_or_default(),
|
||||
)));
|
||||
let id = ui.make_persistent_id(format!("{}_interact", entry.package.id));
|
||||
if ui
|
||||
.interact(response.rect, id, Sense::click())
|
||||
.double_clicked()
|
||||
{
|
||||
entry.expand_triggered = true;
|
||||
entry.selected ^= true;
|
||||
} else if ui.interact(response.rect, id, Sense::click()).clicked() {
|
||||
entry.selected ^= true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
state.show_body_indented(&header_res.response, ui, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
entry
|
||||
.metadata
|
||||
.map(|m| m.description)
|
||||
.unwrap_or("Description unavailable."),
|
||||
)
|
||||
.size(12.0),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
}
|
||||
|
||||
43
src/shortcuts.rs
Normal file
43
src/shortcuts.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
impl crate::App {
|
||||
pub fn handle_shortcuts(&mut self, ui: &mut egui::Ui, search_modal: egui::Response) {
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
for (_id, entry) in self.entries.iter_mut() {
|
||||
entry.selected = false;
|
||||
}
|
||||
}
|
||||
if ui.input(|i| {
|
||||
i.key_pressed(egui::Key::S)
|
||||
|| i.key_pressed(egui::Key::Slash)
|
||||
|| (i.modifiers.ctrl && i.key_pressed(egui::Key::F))
|
||||
}) {
|
||||
search_modal.request_focus();
|
||||
}
|
||||
|
||||
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl)
|
||||
&& let Some(path) = rfd::FileDialog::new()
|
||||
.set_file_name("zilch.ini")
|
||||
.save_file()
|
||||
{
|
||||
let mut enabled = vec![];
|
||||
let mut uninstalled = vec![];
|
||||
let mut disabled = vec![];
|
||||
for (id, entry) in self.entries.iter() {
|
||||
match entry.state {
|
||||
crate::listview::State::Enabled => enabled.push(id.clone()),
|
||||
crate::listview::State::Disabled => disabled.push(id.clone()),
|
||||
crate::listview::State::Uninstalled => uninstalled.push(id.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
let contents = format!(
|
||||
"disabled={}\nenabled={}\nuninstalled={}",
|
||||
disabled.join(","),
|
||||
enabled.join(","),
|
||||
uninstalled.join(",")
|
||||
);
|
||||
if let Err(e) = std::fs::write(&path, contents) {
|
||||
eprintln!("failed to write device state to {}: {e}", path.display());
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user