diff --git a/rm-main/src/ui/tabs/torrents/mod.rs b/rm-main/src/ui/tabs/torrents/mod.rs index 7445e4d..b3b8986 100644 --- a/rm-main/src/ui/tabs/torrents/mod.rs +++ b/rm-main/src/ui/tabs/torrents/mod.rs @@ -102,7 +102,7 @@ impl Component for TorrentsTab { } } A::AddMagnet => self.task_manager.add_magnet(), - A::Search => self.task_manager.search(self.table_manager.filter.clone()), + A::Search => self.task_manager.search(&self.table_manager.filter), A::MoveTorrent => { if let Some(torrent) = self.table_manager.current_torrent() { self.task_manager.move_torrent(torrent); @@ -126,7 +126,7 @@ impl Component for TorrentsTab { self.bottom_stats.set_free_space(free_space); } UpdateAction::SearchFilterApply(filter) => { - self.table_manager.filter.replace(filter); + self.table_manager.set_filter(filter); self.table_manager.table.state.borrow_mut().select(Some(0)); self.table_manager.update_rows_number(); self.bottom_stats diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index d7142b2..c2c8a2f 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -43,25 +43,81 @@ impl RustmissionTorrent { pub fn to_row_with_higlighted_indices( &self, - highlighted_indices: Vec, + highlighted_indices: &Vec, highlight_style: Style, headers: &Vec
, ) -> ratatui::widgets::Row { let mut torrent_name_line = Line::default(); - for (index, char) in self.torrent_name.char_indices() { - if highlighted_indices.contains(&index) { - torrent_name_line.push_span(Span::styled(char.to_string(), highlight_style)); + let char_indices: Vec = self.torrent_name.char_indices().map(|(i, _)| i).collect(); + let mut last_end = 0; + let mut flush_line = |start: usize, end: usize| { + let mut start = char_indices[start as usize]; + let mut end = char_indices[end as usize]; + torrent_name_line.push_span(Span::styled( + &self.torrent_name[last_end..start], + self.style, + )); + + while !self.torrent_name.is_char_boundary(start) { + start -= 1; + } + + while !self.torrent_name.is_char_boundary(end + 1) { + end += 1; + } + + torrent_name_line.push_span(Span::styled( + &self.torrent_name[start..=end], + highlight_style, + )); + last_end = end + 1; + }; + + let mut first: Option = None; + let mut second: Option = None; + for indice in highlighted_indices { + let fst = if let Some(fst) = first { + fst } else { - torrent_name_line.push_span(Span::styled(char.to_string(), self.style)) + first = Some(*indice); + continue; + }; + + let snd = if let Some(snd) = second { + snd + } else { + if fst + 1 == *indice { + second = Some(*indice); + } else { + flush_line(fst, fst); + first = Some(*indice); + } + continue; + }; + + if snd + 1 == *indice { + second = Some(*indice); + } else { + flush_line(fst, snd); + first = Some(*indice); + second = None; } } + if let (Some(first), None) = (first, second) { + flush_line(first, first); + } else if let (Some(first), Some(second)) = (first, second) { + flush_line(first, second); + } + + torrent_name_line.push_span(Span::styled(&self.torrent_name[last_end..], self.style)); + let mut cells = vec![]; for header in headers { if *header == Header::Name { - cells.push(torrent_name_line.clone()) + cells.push(std::mem::take(&mut torrent_name_line)) } else { cells.push(self.header_to_line(*header).style(self.style)) } diff --git a/rm-main/src/ui/tabs/torrents/table_manager.rs b/rm-main/src/ui/tabs/torrents/table_manager.rs index a636bb4..686450e 100644 --- a/rm-main/src/ui/tabs/torrents/table_manager.rs +++ b/rm-main/src/ui/tabs/torrents/table_manager.rs @@ -11,11 +11,17 @@ pub struct TableManager { ctx: app::Ctx, pub table: GenericTable, pub widths: Vec, - pub filter: Option, + pub filter: Option, pub torrents_displaying_no: u16, headers: Vec<&'static str>, } +pub struct Filter { + pub pattern: String, + indexes: Vec, + highlight_indices: Vec>, +} + impl TableManager { pub fn new(ctx: app::Ctx) -> Self { let table = GenericTable::new(vec![]); @@ -37,8 +43,7 @@ impl TableManager { pub fn update_rows_number(&mut self) { if let Some(filter) = &self.filter { - let rows = self.filtered_torrents_rows(&self.table.items, filter); - self.table.overwrite_len(rows.len()); + self.table.overwrite_len(filter.indexes.len()); } else { self.table.items.len(); } @@ -46,7 +51,18 @@ impl TableManager { pub fn rows(&self) -> Vec> { if let Some(filter) = &self.filter { - let rows = self.filtered_torrents_rows(&self.table.items, filter); + let highlight_style = Style::default().fg(self.ctx.config.general.accent_color); + let headers = &self.ctx.config.torrents_tab.headers; + let mut rows = vec![]; + for (i, which_torrent) in filter.indexes.iter().enumerate() { + let row = self.table.items[*which_torrent as usize].to_row_with_higlighted_indices( + &filter.highlight_indices[i], + highlight_style, + headers, + ); + rows.push(row); + } + self.table.overwrite_len(rows.len()); rows } else { @@ -63,25 +79,19 @@ impl TableManager { } pub fn current_torrent(&mut self) -> Option<&mut RustmissionTorrent> { - let matcher = SkimMatcherV2::default(); - let index = self.table.state.borrow().selected()?; + let selected_idx = self.table.state.borrow().selected()?; if let Some(filter) = &self.filter { - let mut loop_index = 0; - for rustmission_torrent in &mut self.table.items { - if matcher - .fuzzy_match(&rustmission_torrent.torrent_name, filter) - .is_some() - { - if index == loop_index { - return Some(rustmission_torrent); - } - loop_index += 1; - } + if filter.indexes.is_empty() { + return None; + } else { + self.table + .items + .get_mut(filter.indexes[selected_idx] as usize) } - return None; + } else { + self.table.items.get_mut(selected_idx) } - self.table.items.get_mut(index) } pub fn set_new_rows(&mut self, rows: Vec) { @@ -90,27 +100,24 @@ impl TableManager { self.update_rows_number(); } - fn filtered_torrents_rows<'a>( - &self, - torrents: &'a [RustmissionTorrent], - filter: &str, - ) -> Vec> { + pub fn set_filter(&mut self, filter: String) { let matcher = SkimMatcherV2::default(); - let mut rows = vec![]; - - let highlight_style = Style::default().fg(self.ctx.config.general.accent_color); - - for torrent in torrents { - if let Some((_, indices)) = matcher.fuzzy_indices(&torrent.torrent_name, filter) { - rows.push(torrent.to_row_with_higlighted_indices( - indices, - highlight_style, - &self.ctx.config.torrents_tab.headers, - )) + let mut indexes: Vec = vec![]; + let mut highlight_indices = vec![]; + for (i, torrent) in self.table.items.iter().enumerate() { + if let Some((_, indices)) = matcher.fuzzy_indices(&torrent.torrent_name, &filter) { + indexes.push(i as u16); + highlight_indices.push(indices); } } - rows + let filter = Filter { + pattern: filter, + indexes, + highlight_indices, + }; + + self.filter = Some(filter); } fn default_widths(headers: &Vec
) -> Vec { diff --git a/rm-main/src/ui/tabs/torrents/task_manager.rs b/rm-main/src/ui/tabs/torrents/task_manager.rs index 0e87927..c98ba5e 100644 --- a/rm-main/src/ui/tabs/torrents/task_manager.rs +++ b/rm-main/src/ui/tabs/torrents/task_manager.rs @@ -13,6 +13,7 @@ use rm_shared::{ use super::{ rustmission_torrent::RustmissionTorrent, + table_manager::Filter, tasks::{ add_magnet::AddMagnetBar, default::DefaultBar, @@ -130,7 +131,7 @@ impl TaskManager { self.ctx.send_update_action(UpdateAction::SwitchToInputMode); } - pub fn search(&mut self, filter: Option) { + pub fn search(&mut self, filter: &Option) { self.current_task = CurrentTask::FilterBar(FilterBar::new(self.ctx.clone(), filter)); self.ctx.send_update_action(UpdateAction::SwitchToInputMode); } diff --git a/rm-main/src/ui/tabs/torrents/tasks/filter.rs b/rm-main/src/ui/tabs/torrents/tasks/filter.rs index bd7f934..b25f214 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/filter.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/filter.rs @@ -5,7 +5,7 @@ use crate::{ app, ui::{ components::{Component, ComponentAction}, - tabs::torrents::input_manager::InputManager, + tabs::torrents::{input_manager::InputManager, table_manager::Filter}, to_input_request, }, }; @@ -17,12 +17,16 @@ pub struct FilterBar { } impl FilterBar { - pub fn new(ctx: app::Ctx, current_filter: Option) -> Self { - let input = InputManager::new_with_value( - ctx.clone(), - "Search: ".to_string(), - current_filter.unwrap_or_default(), - ); + pub fn new(ctx: app::Ctx, current_filter: &Option) -> Self { + let filter = { + if let Some(current_filter) = current_filter { + current_filter.pattern.clone() + } else { + "".to_string() + } + }; + + let input = InputManager::new_with_value(ctx.clone(), "Search: ".to_string(), filter); Self { ctx, input } } }