Show search results in tab
This commit is contained in:
parent
c5bff149c0
commit
6d8dbb398e
3 changed files with 118 additions and 128 deletions
124
src/app.rs
124
src/app.rs
|
|
@ -29,7 +29,6 @@ use notify_debouncer_full::{
|
||||||
notify::{self, RecommendedWatcher, Watcher},
|
notify::{self, RecommendedWatcher, Watcher},
|
||||||
DebouncedEvent, Debouncer, FileIdMap,
|
DebouncedEvent, Debouncer, FileIdMap,
|
||||||
};
|
};
|
||||||
use rayon::slice::ParallelSliceMut;
|
|
||||||
use slotmap::Key as SlotMapKey;
|
use slotmap::Key as SlotMapKey;
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
|
|
@ -228,7 +227,6 @@ pub enum Message {
|
||||||
SearchActivate,
|
SearchActivate,
|
||||||
SearchClear,
|
SearchClear,
|
||||||
SearchInput(String),
|
SearchInput(String),
|
||||||
SearchResults(String, Vec<tab::Item>),
|
|
||||||
SearchSubmit,
|
SearchSubmit,
|
||||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||||
TabActivate(Entity),
|
TabActivate(Entity),
|
||||||
|
|
@ -342,7 +340,6 @@ pub struct App {
|
||||||
search_active: bool,
|
search_active: bool,
|
||||||
search_id: widget::Id,
|
search_id: widget::Id,
|
||||||
search_input: String,
|
search_input: String,
|
||||||
search_results: Option<Vec<tab::Item>>,
|
|
||||||
watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>,
|
watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>,
|
||||||
nav_dnd_hover: Option<(Location, Instant)>,
|
nav_dnd_hover: Option<(Location, Instant)>,
|
||||||
tab_dnd_hover: Option<(Entity, Instant)>,
|
tab_dnd_hover: Option<(Entity, Instant)>,
|
||||||
|
|
@ -407,103 +404,8 @@ impl App {
|
||||||
Command::batch(commands)
|
Command::batch(commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search(&self) -> Command<Message> {
|
fn search(&mut self) -> Command<Message> {
|
||||||
let input = self.search_input.clone();
|
self.open_tab(Location::Search(home_dir(), 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 selected_paths(&self, entity_opt: Option<Entity>) -> Vec<PathBuf> {
|
fn selected_paths(&self, entity_opt: Option<Entity>) -> Vec<PathBuf> {
|
||||||
|
|
@ -1008,7 +910,6 @@ impl Application for App {
|
||||||
search_active: false,
|
search_active: false,
|
||||||
search_id: widget::Id::unique(),
|
search_id: widget::Id::unique(),
|
||||||
search_input: String::new(),
|
search_input: String::new(),
|
||||||
search_results: None,
|
|
||||||
watcher_opt: None,
|
watcher_opt: None,
|
||||||
nav_dnd_hover: None,
|
nav_dnd_hover: None,
|
||||||
tab_dnd_hover: None,
|
tab_dnd_hover: None,
|
||||||
|
|
@ -1112,7 +1013,6 @@ impl Application for App {
|
||||||
if self.search_active {
|
if self.search_active {
|
||||||
// Close search if open
|
// Close search if open
|
||||||
self.search_active = false;
|
self.search_active = false;
|
||||||
self.search_results = None;
|
|
||||||
return Command::none();
|
return Command::none();
|
||||||
}
|
}
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
|
|
@ -1609,7 +1509,6 @@ impl Application for App {
|
||||||
Message::SearchClear => {
|
Message::SearchClear => {
|
||||||
self.search_active = false;
|
self.search_active = false;
|
||||||
self.search_input.clear();
|
self.search_input.clear();
|
||||||
self.search_results = None;
|
|
||||||
}
|
}
|
||||||
Message::SearchInput(input) => {
|
Message::SearchInput(input) => {
|
||||||
if input != self.search_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 => {
|
Message::SearchSubmit => {
|
||||||
if !self.search_input.is_empty() {
|
if !self.search_input.is_empty() {
|
||||||
return self.search();
|
return self.search();
|
||||||
|
|
@ -2215,20 +2109,6 @@ impl Application for App {
|
||||||
fn view(&self) -> Element<Self::Message> {
|
fn view(&self) -> Element<Self::Message> {
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
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);
|
let mut tab_column = widget::column::with_capacity(1);
|
||||||
|
|
||||||
if self.tab_model.iter().count() > 1 {
|
if self.tab_model.iter().count() > 1 {
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ pub fn context_menu<'a>(
|
||||||
|
|
||||||
let mut children: Vec<Element<_>> = Vec::new();
|
let mut children: Vec<Element<_>> = Vec::new();
|
||||||
match tab.location {
|
match tab.location {
|
||||||
Location::Path(_) => {
|
Location::Path(_) | Location::Search(_, _) => {
|
||||||
if selected > 0 {
|
if selected > 0 {
|
||||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||||
if selected == 1 {
|
if selected == 1 {
|
||||||
|
|
|
||||||
120
src/tab.rs
120
src/tab.rs
|
|
@ -314,6 +314,97 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec<Item> {
|
||||||
|
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
|
// This config statement is from trash::os_limited, inverted
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "windows",
|
target_os = "windows",
|
||||||
|
|
@ -410,6 +501,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Location {
|
pub enum Location {
|
||||||
Path(PathBuf),
|
Path(PathBuf),
|
||||||
|
Search(PathBuf, String),
|
||||||
Trash,
|
Trash,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,6 +509,7 @@ impl std::fmt::Display for Location {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Path(path) => write!(f, "{}", path.display()),
|
Self::Path(path) => write!(f, "{}", path.display()),
|
||||||
|
Self::Search(path, term) => write!(f, "search {} for {}", path.display(), term),
|
||||||
Self::Trash => write!(f, "trash"),
|
Self::Trash => write!(f, "trash"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -426,6 +519,7 @@ impl Location {
|
||||||
pub fn scan(&self, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan(&self, sizes: IconSizes) -> Vec<Item> {
|
||||||
match self {
|
match self {
|
||||||
Self::Path(path) => scan_path(path, sizes),
|
Self::Path(path) => scan_path(path, sizes),
|
||||||
|
Self::Search(path, term) => scan_search(path, term, sizes),
|
||||||
Self::Trash => scan_trash(sizes),
|
Self::Trash => scan_trash(sizes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -755,6 +849,10 @@ impl Tab {
|
||||||
Location::Path(path) => {
|
Location::Path(path) => {
|
||||||
format!("{}", path.display())
|
format!("{}", path.display())
|
||||||
}
|
}
|
||||||
|
Location::Search(path, term) => {
|
||||||
|
//TODO: translate
|
||||||
|
format!("Search for {} in {}", term, path.display())
|
||||||
|
}
|
||||||
Location::Trash => {
|
Location::Trash => {
|
||||||
fl!("trash")
|
fl!("trash")
|
||||||
}
|
}
|
||||||
|
|
@ -1319,6 +1417,9 @@ impl Tab {
|
||||||
commands.push(Command::OpenFile(path.clone()));
|
commands.push(Command::OpenFile(path.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Location::Search(path, term) => {
|
||||||
|
cd = Some(location);
|
||||||
|
}
|
||||||
Location::Trash => {
|
Location::Trash => {
|
||||||
cd = Some(location);
|
cd = Some(location);
|
||||||
}
|
}
|
||||||
|
|
@ -1429,6 +1530,9 @@ impl Tab {
|
||||||
}
|
}
|
||||||
commands.push(Command::DropFiles(to, from))
|
commands.push(Command::DropFiles(to, from))
|
||||||
}
|
}
|
||||||
|
Location::Search(_, _) => {
|
||||||
|
log::warn!(" Copy/cut to search not supported.");
|
||||||
|
}
|
||||||
Location::Trash if matches!(from.kind, ClipboardKind::Cut) => {
|
Location::Trash if matches!(from.kind, ClipboardKind::Cut) => {
|
||||||
commands.push(Command::MoveToTrash(from.paths))
|
commands.push(Command::MoveToTrash(from.paths))
|
||||||
}
|
}
|
||||||
|
|
@ -1505,6 +1609,7 @@ impl Tab {
|
||||||
if location != self.location {
|
if location != self.location {
|
||||||
if match &location {
|
if match &location {
|
||||||
Location::Path(path) => path.is_dir(),
|
Location::Path(path) => path.is_dir(),
|
||||||
|
Location::Search(path, _term) => path.is_dir(),
|
||||||
Location::Trash => true,
|
Location::Trash => true,
|
||||||
} {
|
} {
|
||||||
self.change_location(&location, history_i_opt);
|
self.change_location(&location, history_i_opt);
|
||||||
|
|
@ -1596,7 +1701,7 @@ impl Tab {
|
||||||
Some(items)
|
Some(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn location_view(&self) -> Element<Message> {
|
pub fn location_view(&self) -> Option<Element<Message>> {
|
||||||
let cosmic_theme::Spacing {
|
let cosmic_theme::Spacing {
|
||||||
space_xxxs,
|
space_xxxs,
|
||||||
space_xxs,
|
space_xxs,
|
||||||
|
|
@ -1642,7 +1747,7 @@ impl Tab {
|
||||||
})
|
})
|
||||||
.on_submit(Message::Location(location.clone())),
|
.on_submit(Message::Location(location.clone())),
|
||||||
);
|
);
|
||||||
return row.into();
|
return Some(row.into());
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
//TODO: allow editing other locations
|
//TODO: allow editing other locations
|
||||||
|
|
@ -1717,6 +1822,9 @@ impl Tab {
|
||||||
}
|
}
|
||||||
children.reverse();
|
children.reverse();
|
||||||
}
|
}
|
||||||
|
Location::Search(path, term) => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Location::Trash => {
|
Location::Trash => {
|
||||||
let mut row = widget::row::with_capacity(2)
|
let mut row = widget::row::with_capacity(2)
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
|
|
@ -1737,7 +1845,7 @@ impl Tab {
|
||||||
for child in children {
|
for child in children {
|
||||||
row = row.push(child);
|
row = row.push(child);
|
||||||
}
|
}
|
||||||
row.into()
|
Some(row.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn empty_view(&self, has_hidden: bool) -> Element<Message> {
|
pub fn empty_view(&self, has_hidden: bool) -> Element<Message> {
|
||||||
|
|
@ -2373,7 +2481,7 @@ impl Tab {
|
||||||
// Update cached size
|
// Update cached size
|
||||||
self.size_opt.set(Some(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 {
|
let (drag_list, mut item_view, can_scroll) = match self.config.view {
|
||||||
View::Grid => self.grid_view(),
|
View::Grid => self.grid_view(),
|
||||||
View::List => self.list_view(),
|
View::List => self.list_view(),
|
||||||
|
|
@ -2433,7 +2541,9 @@ impl Tab {
|
||||||
.position(widget::popover::Position::Point(point));
|
.position(widget::popover::Position::Point(point));
|
||||||
}
|
}
|
||||||
let mut tab_column = widget::column::with_capacity(3);
|
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 {
|
if can_scroll {
|
||||||
tab_column = tab_column.push(
|
tab_column = tab_column.push(
|
||||||
widget::scrollable(popover)
|
widget::scrollable(popover)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue