Universal-Debloater-Alliance/universal-android-debloater-next-generation · error
User must be selected
Error message
User must be selected
What it means
This is a Rust `Option::expect` panic in `uad-gui`'s `filter_package_lists`. It indexes `self.phone_packages[self.selected_user.expect("User must be selected").index]`, panicking when `self.selected_user` is `None`. The code assumes a user (ADB phone user with an `.index`) is always chosen before packages are filtered; if no user is selected the index lookup cannot proceed and the app panics instead of degrading gracefully.
Solutions
- Guard before indexing: `let Some(user) = self.selected_user else { self.filtered_packages.clear(); return; };`
- Fall back to the first/default user: `let user = self.selected_user.as_ref().or_else(|| self.users.first())...`
- Ensure the user-selection handler runs (and sets `selected_user`) before any message that triggers `filter_package_lists` is processed
- Replace the expect with `.expect` removal plus bounds/None handling, e.g. `self.phone_packages.get(user.index).map(...)` returning empty vec on miss
Example fix
// before
self.filtered_packages = self.phone_packages
[self.selected_user.expect("User must be selected").index]
.iter()
// after
let Some(user) = self.selected_user.as_ref() else {
self.filtered_packages.clear();
return;
};
self.filtered_packages = self.phone_packages
.get(user.index)
.map(|pkgs| pkgs.iter().collect::<Vec<_>>())
.unwrap_or_default(); Defensive patterns
Strategy: validation
Validate before calling
if self.selected_user.is_none()
|| self.phone_packages.get(self.selected_user.as_ref().unwrap().index).is_none()
{
self.filtered_packages.clear();
return;
} Type guard
fn selected_user_index(state: &ListView) -> Option<usize> {
state.selected_user.as_ref().map(|u| u.index)
} Try / catch
// Prefer guard clause over catch:
let Some(user) = self.selected_user.as_ref() else { return; };
let Some(packages) = self.phone_packages.get(user.index) else { return; }; Prevention
- Never index Vecs with data from Options; use .get() with fallback
- Guard against device disconnect clearing selected_user while messages are queued
- Test the app on startup and after device reconnect before any user selection
- Centralize user selection so all filters read from one validated source
When it happens
Trigger: `filter_package_lists` runs while `self.selected_user` is `None`: before the user list is fetched/selected on first launch, after a device disconnect/reconnect clears the selection, or when a filter-change message is dispatched while `selected_user` was reset by a user-switch handler.
Common situations: First app start before the ADB user list loads; unplugging/replugging the phone clears the selected user but stale filter messages still fire; switching devices mid-session where the new device's users haven't been populated yet; a race between a background task message and view reset.
Related errors
- removal recommendation must be selected
- Layout must have at least 1 child
- {e}
- There must be 1 tab after serial
- Could not write config file to disk!
AI-assisted analysis of Universal-Debloater-Alliance/universal-android-debloater-next-generation@64465c850c (2026-09-12).
Data as JSON: /api/errors/47357238236e640b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/uad-gui/src/views/list.rs:752
)
.width(900)
.height(Length::Shrink)
.max_height(700)
.style(style::Container::Background)
.into()
}
fn filter_package_lists(&mut self) {
let list_filter: UadList = self.selected_list.expect("UAD-list type must be selected");
let package_filter: PackageState = self
.selected_package_state
.expect("pack-state must be selected");
let removal_filter: Removal = self
.selected_removal
.expect("removal recommendation must be selected");
self.filtered_packages = self.phone_packages
[self.selected_user.expect("User must be selected").index]
.iter()
// we must filter the indices associated with pack-rows,
// that's why `enumerate` is before `filter`.
.enumerate()
.filter(|(_, p)| {
(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()
|| matches_search(&p.name, &self.input_value, Some(&p.description)))
})
.map(|(i, _)| i)
.collect();
}
#[expect(clippy::unused_async, reason = "1 call-site")]
async fn load_packages<S: AsRef<str>>(
uad_list: PackageHashMap,View on GitHub (pinned to 64465c850c)