fix: TUI selection underflow and filtered-results desync
Three related TUI correctness bugs: - download_selector navigation (select_previous/next/last) computed items.len() - 1, panicking with an arithmetic underflow when the list was empty (e.g. a filter matched nothing). Index math is now in wrap-safe helpers that return no selection for an empty list. - update_search_results overwrote the filtered results view with the full unfiltered list on every ~100ms poll, so an active filter silently stopped working as new results streamed in. It now re-derives the filtered view from the current query. - The results pane rendered selection checkboxes by testing the display-row index against a selection set that stores unfiltered indices, so under an active filter the wrong rows showed as checked. It now maps the display index back through the filtered-index table. Filtering and index-mapping logic extracted into pure, unit-tested helpers.
This commit is contained in:
@@ -19,12 +19,33 @@ pub struct ResultsPaneParams<'a> {
|
||||
pub items: &'a [FileDisplayData],
|
||||
pub table_state: &'a mut TableState,
|
||||
pub selected_indices: &'a HashSet<usize>,
|
||||
/// Maps a rendered row index to its index in the unfiltered results list.
|
||||
/// `None` means the rendered rows are the unfiltered list (identity map).
|
||||
pub original_indices: Option<&'a [usize]>,
|
||||
pub filter_query: &'a str,
|
||||
pub is_filtering: bool,
|
||||
pub focused: bool,
|
||||
pub active_search_query: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Whether the rendered row `display_idx` is selected. `selected_indices` holds
|
||||
/// indices into the *unfiltered* results, so under an active filter the display
|
||||
/// index must be translated through `original_indices` first.
|
||||
fn row_is_selected(
|
||||
display_idx: usize,
|
||||
original_indices: Option<&[usize]>,
|
||||
selected_indices: &HashSet<usize>,
|
||||
) -> bool {
|
||||
let original = match original_indices {
|
||||
Some(map) => match map.get(display_idx) {
|
||||
Some(&o) => o,
|
||||
None => return false,
|
||||
},
|
||||
None => display_idx,
|
||||
};
|
||||
selected_indices.contains(&original)
|
||||
}
|
||||
|
||||
pub fn render_results_pane(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
@@ -34,6 +55,7 @@ pub fn render_results_pane(
|
||||
items,
|
||||
table_state,
|
||||
selected_indices,
|
||||
original_indices,
|
||||
filter_query,
|
||||
is_filtering,
|
||||
focused,
|
||||
@@ -83,11 +105,12 @@ No results. Select a search from the Searches pane [1]. Or start new search [s
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, file)| {
|
||||
let checkbox = if selected_indices.contains(&idx) {
|
||||
"[✓]"
|
||||
} else {
|
||||
"[ ]"
|
||||
};
|
||||
let checkbox =
|
||||
if row_is_selected(idx, original_indices, selected_indices) {
|
||||
"[✓]"
|
||||
} else {
|
||||
"[ ]"
|
||||
};
|
||||
|
||||
let bitrate_str = file
|
||||
.bitrate
|
||||
@@ -147,3 +170,34 @@ No results. Select a search from the Searches pane [1]. Or start new search [s
|
||||
|
||||
frame.render_stateful_widget(table, area, table_state);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::row_is_selected;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn identity_mapping_used_when_no_filter() {
|
||||
let selected: HashSet<usize> = [1].into_iter().collect();
|
||||
assert!(row_is_selected(1, None, &selected));
|
||||
assert!(!row_is_selected(0, None, &selected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtered_rows_resolve_through_original_indices() {
|
||||
// Filter shows original rows 2, 5, 7 as display rows 0, 1, 2.
|
||||
// The user selected original row 5.
|
||||
let original_indices = [2usize, 5, 7];
|
||||
let selected: HashSet<usize> = [5].into_iter().collect();
|
||||
assert!(!row_is_selected(0, Some(&original_indices), &selected));
|
||||
assert!(row_is_selected(1, Some(&original_indices), &selected));
|
||||
assert!(!row_is_selected(2, Some(&original_indices), &selected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_display_index_is_not_selected() {
|
||||
let original_indices = [2usize, 5];
|
||||
let selected: HashSet<usize> = [2, 5].into_iter().collect();
|
||||
assert!(!row_is_selected(9, Some(&original_indices), &selected));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user