From 6d8dbb398eba68a0b8086f5eb44fd18773a18114 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 31 May 2024 15:19:47 -0600 Subject: [PATCH] Show search results in tab --- src/app.rs | 124 +--------------------------------------------------- src/menu.rs | 2 +- src/tab.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 118 insertions(+), 128 deletions(-) diff --git a/src/app.rs b/src/app.rs index 94aa21b..04481dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,7 +29,6 @@ use notify_debouncer_full::{ notify::{self, RecommendedWatcher, Watcher}, DebouncedEvent, Debouncer, FileIdMap, }; -use rayon::slice::ParallelSliceMut; use slotmap::Key as SlotMapKey; use std::{ any::TypeId, @@ -228,7 +227,6 @@ pub enum Message { SearchActivate, SearchClear, SearchInput(String), - SearchResults(String, Vec), SearchSubmit, SystemThemeModeChange(cosmic_theme::ThemeMode), TabActivate(Entity), @@ -342,7 +340,6 @@ pub struct App { search_active: bool, search_id: widget::Id, search_input: String, - search_results: Option>, watcher_opt: Option<(Debouncer, HashSet)>, nav_dnd_hover: Option<(Location, Instant)>, tab_dnd_hover: Option<(Entity, Instant)>, @@ -407,103 +404,8 @@ impl App { Command::batch(commands) } - fn search(&self) -> Command { - let input = self.search_input.clone(); - let pattern = regex::escape(&input); - let regex = match regex::RegexBuilder::new(&pattern) - .case_insensitive(true) - .build() - { - Ok(ok) => ok, - Err(err) => { - log::warn!("failed to parse regex {:?}: {}", pattern, err); - return Command::none(); - } - }; - Command::perform( - async move { - tokio::task::spawn_blocking(move || { - let start = Instant::now(); - - let results_arc = Arc::new(Mutex::new(Vec::new())); - //TODO: do we want to ignore files? - ignore::WalkBuilder::new(home_dir()) - //TODO: only use this on supported targets - .same_file_system(true) - .build_parallel() - .run(|| { - Box::new(|entry_res| { - let Ok(entry) = entry_res else { - // Skip invalid entries - return ignore::WalkState::Skip; - }; - - let Some(file_name) = entry.file_name().to_str() else { - // Skip anything with an invalid name - return ignore::WalkState::Skip; - }; - - if regex.is_match(file_name) { - let path = entry.path(); - - let metadata = match entry.metadata() { - Ok(ok) => ok, - Err(err) => { - log::warn!( - "failed to read metadata for entry at {:?}: {}", - path, - err - ); - return ignore::WalkState::Continue; - } - }; - - let mut results = results_arc.lock().unwrap(); - results.push(tab::item_from_entry( - path.to_path_buf(), - file_name.to_string(), - metadata, - IconSizes::default(), - )); - } - - ignore::WalkState::Continue - }) - }); - - let mut results = Arc::into_inner(results_arc).unwrap().into_inner().unwrap(); - let duration = start.elapsed(); - log::info!( - "searched for {:?} in {:?}, found {} results", - input, - duration, - results.len() - ); - - let start = Instant::now(); - - results.par_sort_unstable_by(|a, b| { - let get_modified = |x: &tab::Item| match &x.metadata { - ItemMetadata::Path { metadata, .. } => metadata.modified().ok(), - ItemMetadata::Trash { .. } => None, - }; - - // Sort with latest modified first - let a_modified = get_modified(a); - let b_modified = get_modified(b); - b_modified.cmp(&a_modified) - }); - - let duration = start.elapsed(); - log::info!("sorted results in {:?}", duration); - - message::app(Message::SearchResults(input, results)) - }) - .await - .unwrap_or(message::none()) - }, - |x| x, - ) + fn search(&mut self) -> Command { + self.open_tab(Location::Search(home_dir(), self.search_input.clone())) } fn selected_paths(&self, entity_opt: Option) -> Vec { @@ -1008,7 +910,6 @@ impl Application for App { search_active: false, search_id: widget::Id::unique(), search_input: String::new(), - search_results: None, watcher_opt: None, nav_dnd_hover: None, tab_dnd_hover: None, @@ -1112,7 +1013,6 @@ impl Application for App { if self.search_active { // Close search if open self.search_active = false; - self.search_results = None; return Command::none(); } if let Some(tab) = self.tab_model.data_mut::(entity) { @@ -1609,7 +1509,6 @@ impl Application for App { Message::SearchClear => { self.search_active = false; self.search_input.clear(); - self.search_results = None; } Message::SearchInput(input) => { if input != self.search_input { @@ -1622,11 +1521,6 @@ impl Application for App { */ } } - Message::SearchResults(input, results) => { - if input == self.search_input { - self.search_results = Some(results); - } - } Message::SearchSubmit => { if !self.search_input.is_empty() { return self.search(); @@ -2215,20 +2109,6 @@ impl Application for App { fn view(&self) -> Element { let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; - if let Some(results) = &self.search_results { - //TODO: pages? - let count = results.len().min(100); - if count == 0 { - //TODO: make it nicer - return widget::text("NO RESULTS").into(); - } - let mut column = widget::column::with_capacity(count); - for item in results.iter().take(count) { - column = column.push(widget::text(&item.name)); - } - return widget::scrollable(column.width(Length::Fill)).into(); - } - let mut tab_column = widget::column::with_capacity(1); if self.tab_model.iter().count() > 1 { diff --git a/src/menu.rs b/src/menu.rs index a1a26ef..43ea710 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -92,7 +92,7 @@ pub fn context_menu<'a>( let mut children: Vec> = Vec::new(); match tab.location { - Location::Path(_) => { + Location::Path(_) | Location::Search(_, _) => { if selected > 0 { children.push(menu_item(fl!("open"), Action::Open).into()); if selected == 1 { diff --git a/src/tab.rs b/src/tab.rs index 5032ba1..19f81c9 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -314,6 +314,97 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec { items } +pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec { + use rayon::prelude::ParallelSliceMut; + + let start = Instant::now(); + + let pattern = regex::escape(&term); + let regex = match regex::RegexBuilder::new(&pattern) + .case_insensitive(true) + .build() + { + Ok(ok) => ok, + Err(err) => { + log::warn!("failed to parse regex {:?}: {}", pattern, err); + return Vec::new(); + } + }; + + let items_arc = Arc::new(Mutex::new(Vec::new())); + //TODO: do we want to ignore files? + ignore::WalkBuilder::new(tab_path) + //TODO: only use this on supported targets + .same_file_system(true) + .build_parallel() + .run(|| { + Box::new(|entry_res| { + let Ok(entry) = entry_res else { + // Skip invalid entries + return ignore::WalkState::Skip; + }; + + let Some(file_name) = entry.file_name().to_str() else { + // Skip anything with an invalid name + return ignore::WalkState::Skip; + }; + + if regex.is_match(file_name) { + let path = entry.path(); + + let metadata = match entry.metadata() { + Ok(ok) => ok, + Err(err) => { + log::warn!("failed to read metadata for entry at {:?}: {}", path, err); + return ignore::WalkState::Continue; + } + }; + + let mut items = items_arc.lock().unwrap(); + items.push(item_from_entry( + path.to_path_buf(), + file_name.to_string(), + metadata, + IconSizes::default(), + )); + } + + ignore::WalkState::Continue + }) + }); + + let mut items = Arc::into_inner(items_arc).unwrap().into_inner().unwrap(); + let duration = start.elapsed(); + log::info!( + "searched for {:?} in {:?}, found {} items", + term, + duration, + items.len() + ); + + let start = Instant::now(); + + items.par_sort_unstable_by(|a, b| { + let get_modified = |x: &Item| match &x.metadata { + ItemMetadata::Path { metadata, .. } => metadata.modified().ok(), + ItemMetadata::Trash { .. } => None, + }; + + // Sort with latest modified first + let a_modified = get_modified(a); + let b_modified = get_modified(b); + b_modified.cmp(&a_modified) + }); + + let duration = start.elapsed(); + log::info!("sorted {} items in {:?}", items.len(), duration); + + //TODO: ideal number of search results, pages? + items.truncate(100); + + items +} + // This config statement is from trash::os_limited, inverted #[cfg(not(any( target_os = "windows", @@ -410,6 +501,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec { #[derive(Clone, Debug, Eq, PartialEq)] pub enum Location { Path(PathBuf), + Search(PathBuf, String), Trash, } @@ -417,6 +509,7 @@ impl std::fmt::Display for Location { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Path(path) => write!(f, "{}", path.display()), + Self::Search(path, term) => write!(f, "search {} for {}", path.display(), term), Self::Trash => write!(f, "trash"), } } @@ -426,6 +519,7 @@ impl Location { pub fn scan(&self, sizes: IconSizes) -> Vec { match self { Self::Path(path) => scan_path(path, sizes), + Self::Search(path, term) => scan_search(path, term, sizes), Self::Trash => scan_trash(sizes), } } @@ -755,6 +849,10 @@ impl Tab { Location::Path(path) => { format!("{}", path.display()) } + Location::Search(path, term) => { + //TODO: translate + format!("Search for {} in {}", term, path.display()) + } Location::Trash => { fl!("trash") } @@ -1319,6 +1417,9 @@ impl Tab { commands.push(Command::OpenFile(path.clone())); } } + Location::Search(path, term) => { + cd = Some(location); + } Location::Trash => { cd = Some(location); } @@ -1429,6 +1530,9 @@ impl Tab { } commands.push(Command::DropFiles(to, from)) } + Location::Search(_, _) => { + log::warn!(" Copy/cut to search not supported."); + } Location::Trash if matches!(from.kind, ClipboardKind::Cut) => { commands.push(Command::MoveToTrash(from.paths)) } @@ -1505,6 +1609,7 @@ impl Tab { if location != self.location { if match &location { Location::Path(path) => path.is_dir(), + Location::Search(path, _term) => path.is_dir(), Location::Trash => true, } { self.change_location(&location, history_i_opt); @@ -1596,7 +1701,7 @@ impl Tab { Some(items) } - pub fn location_view(&self) -> Element { + pub fn location_view(&self) -> Option> { let cosmic_theme::Spacing { space_xxxs, space_xxs, @@ -1642,7 +1747,7 @@ impl Tab { }) .on_submit(Message::Location(location.clone())), ); - return row.into(); + return Some(row.into()); } _ => { //TODO: allow editing other locations @@ -1717,6 +1822,9 @@ impl Tab { } children.reverse(); } + Location::Search(path, term) => { + return None; + } Location::Trash => { let mut row = widget::row::with_capacity(2) .align_items(Alignment::Center) @@ -1737,7 +1845,7 @@ impl Tab { for child in children { row = row.push(child); } - row.into() + Some(row.into()) } pub fn empty_view(&self, has_hidden: bool) -> Element { @@ -2373,7 +2481,7 @@ impl Tab { // Update cached size self.size_opt.set(Some(size)); - let location_view = self.location_view(); + let location_view_opt = self.location_view(); let (drag_list, mut item_view, can_scroll) = match self.config.view { View::Grid => self.grid_view(), View::List => self.list_view(), @@ -2433,7 +2541,9 @@ impl Tab { .position(widget::popover::Position::Point(point)); } let mut tab_column = widget::column::with_capacity(3); - tab_column = tab_column.push(location_view); + if let Some(location_view) = location_view_opt { + tab_column = tab_column.push(location_view); + } if can_scroll { tab_column = tab_column.push( widget::scrollable(popover)