Implement search backend
This commit is contained in:
parent
e6f8fdd1e6
commit
c5bff149c0
5 changed files with 302 additions and 74 deletions
42
Cargo.lock
generated
42
Cargo.lock
generated
|
|
@ -725,6 +725,16 @@ dependencies = [
|
||||||
"piper",
|
"piper",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.16.0"
|
version = "3.16.0"
|
||||||
|
|
@ -1123,6 +1133,7 @@ dependencies = [
|
||||||
"gio",
|
"gio",
|
||||||
"i18n-embed",
|
"i18n-embed",
|
||||||
"i18n-embed-fl",
|
"i18n-embed-fl",
|
||||||
|
"ignore",
|
||||||
"image",
|
"image",
|
||||||
"lexical-sort",
|
"lexical-sort",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -1133,6 +1144,8 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"open",
|
"open",
|
||||||
"paste",
|
"paste",
|
||||||
|
"rayon",
|
||||||
|
"regex",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"shlex",
|
"shlex",
|
||||||
|
|
@ -2288,6 +2301,19 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globset"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"bstr",
|
||||||
|
"log",
|
||||||
|
"regex-automata 0.4.6",
|
||||||
|
"regex-syntax 0.8.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glow"
|
name = "glow"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
|
|
@ -2795,6 +2821,22 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ignore"
|
||||||
|
version = "0.4.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"globset",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata 0.4.6",
|
||||||
|
"same-file",
|
||||||
|
"walkdir",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.24.9"
|
version = "0.24.9"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ env_logger = "0.11"
|
||||||
freedesktop_entry_parser = { version = "1.3", optional = true }
|
freedesktop_entry_parser = { version = "1.3", optional = true }
|
||||||
fs_extra = "1.3"
|
fs_extra = "1.3"
|
||||||
gio = { version = "0.19", optional = true }
|
gio = { version = "0.19", optional = true }
|
||||||
|
ignore = "0.4"
|
||||||
image = "0.24"
|
image = "0.24"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
open = "5.0.2"
|
open = "5.0.2"
|
||||||
|
|
@ -25,6 +26,8 @@ log = "0.4"
|
||||||
mime_guess = "2"
|
mime_guess = "2"
|
||||||
notify-debouncer-full = "0.3"
|
notify-debouncer-full = "0.3"
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
|
rayon = "1"
|
||||||
|
regex = "1"
|
||||||
serde = { version = "1", features = ["serde_derive"] }
|
serde = { version = "1", features = ["serde_derive"] }
|
||||||
shlex = { version = "1.3" }
|
shlex = { version = "1.3" }
|
||||||
tokio = { version = "1", features = ["sync"] }
|
tokio = { version = "1", features = ["sync"] }
|
||||||
|
|
|
||||||
181
src/app.rs
181
src/app.rs
|
|
@ -29,6 +29,7 @@ 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,
|
||||||
|
|
@ -37,7 +38,7 @@ use std::{
|
||||||
num::NonZeroU16,
|
num::NonZeroU16,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process,
|
process,
|
||||||
sync::Arc,
|
sync::{Arc, Mutex},
|
||||||
time::{self, Instant},
|
time::{self, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -85,6 +86,7 @@ pub enum Action {
|
||||||
Properties,
|
Properties,
|
||||||
Rename,
|
Rename,
|
||||||
RestoreFromTrash,
|
RestoreFromTrash,
|
||||||
|
SearchActivate,
|
||||||
SelectAll,
|
SelectAll,
|
||||||
Settings,
|
Settings,
|
||||||
TabClose,
|
TabClose,
|
||||||
|
|
@ -129,6 +131,7 @@ impl Action {
|
||||||
Action::Properties => Message::ToggleContextPage(ContextPage::Properties(None)),
|
Action::Properties => Message::ToggleContextPage(ContextPage::Properties(None)),
|
||||||
Action::Rename => Message::Rename(entity_opt),
|
Action::Rename => Message::Rename(entity_opt),
|
||||||
Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt),
|
Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt),
|
||||||
|
Action::SearchActivate => Message::SearchActivate,
|
||||||
Action::SelectAll => Message::TabMessage(entity_opt, tab::Message::SelectAll),
|
Action::SelectAll => Message::TabMessage(entity_opt, tab::Message::SelectAll),
|
||||||
Action::Settings => Message::ToggleContextPage(ContextPage::Settings),
|
Action::Settings => Message::ToggleContextPage(ContextPage::Settings),
|
||||||
Action::TabClose => Message::TabClose(entity_opt),
|
Action::TabClose => Message::TabClose(entity_opt),
|
||||||
|
|
@ -222,6 +225,11 @@ pub enum Message {
|
||||||
RescanTrash,
|
RescanTrash,
|
||||||
Rename(Option<Entity>),
|
Rename(Option<Entity>),
|
||||||
RestoreFromTrash(Option<Entity>),
|
RestoreFromTrash(Option<Entity>),
|
||||||
|
SearchActivate,
|
||||||
|
SearchClear,
|
||||||
|
SearchInput(String),
|
||||||
|
SearchResults(String, Vec<tab::Item>),
|
||||||
|
SearchSubmit,
|
||||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||||
TabActivate(Entity),
|
TabActivate(Entity),
|
||||||
TabNext,
|
TabNext,
|
||||||
|
|
@ -331,6 +339,10 @@ pub struct App {
|
||||||
pending_operations: BTreeMap<u64, (Operation, f32)>,
|
pending_operations: BTreeMap<u64, (Operation, f32)>,
|
||||||
complete_operations: BTreeMap<u64, Operation>,
|
complete_operations: BTreeMap<u64, Operation>,
|
||||||
failed_operations: BTreeMap<u64, (Operation, String)>,
|
failed_operations: BTreeMap<u64, (Operation, String)>,
|
||||||
|
search_active: bool,
|
||||||
|
search_id: widget::Id,
|
||||||
|
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)>,
|
||||||
|
|
@ -395,6 +407,105 @@ impl App {
|
||||||
Command::batch(commands)
|
Command::batch(commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn search(&self) -> Command<Message> {
|
||||||
|
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 selected_paths(&self, entity_opt: Option<Entity>) -> Vec<PathBuf> {
|
fn selected_paths(&self, entity_opt: Option<Entity>) -> Vec<PathBuf> {
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
|
|
@ -894,6 +1005,10 @@ impl Application for App {
|
||||||
pending_operations: BTreeMap::new(),
|
pending_operations: BTreeMap::new(),
|
||||||
complete_operations: BTreeMap::new(),
|
complete_operations: BTreeMap::new(),
|
||||||
failed_operations: BTreeMap::new(),
|
failed_operations: BTreeMap::new(),
|
||||||
|
search_active: false,
|
||||||
|
search_id: widget::Id::unique(),
|
||||||
|
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,
|
||||||
|
|
@ -994,6 +1109,12 @@ impl Application for App {
|
||||||
self.core.window.show_context = false;
|
self.core.window.show_context = false;
|
||||||
return Command::none();
|
return Command::none();
|
||||||
}
|
}
|
||||||
|
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::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
if tab.context_menu.is_some() {
|
if tab.context_menu.is_some() {
|
||||||
tab.context_menu = None;
|
tab.context_menu = None;
|
||||||
|
|
@ -1481,6 +1602,36 @@ impl Application for App {
|
||||||
self.operation(Operation::Restore { paths });
|
self.operation(Operation::Restore { paths });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::SearchActivate => {
|
||||||
|
self.search_active = true;
|
||||||
|
return widget::text_input::focus(self.search_id.clone());
|
||||||
|
}
|
||||||
|
Message::SearchClear => {
|
||||||
|
self.search_active = false;
|
||||||
|
self.search_input.clear();
|
||||||
|
self.search_results = None;
|
||||||
|
}
|
||||||
|
Message::SearchInput(input) => {
|
||||||
|
if input != self.search_input {
|
||||||
|
self.search_input = input;
|
||||||
|
/*TODO: live search? (probably needs subscription for streaming results)
|
||||||
|
// This performs live search
|
||||||
|
if !self.search_input.is_empty() {
|
||||||
|
return self.search();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::SystemThemeModeChange(_theme_mode) => {
|
Message::SystemThemeModeChange(_theme_mode) => {
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
}
|
}
|
||||||
|
|
@ -2045,13 +2196,39 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
||||||
vec![]
|
vec![if self.search_active {
|
||||||
|
widget::text_input::search_input("", &self.search_input)
|
||||||
|
.width(Length::Fixed(240.0))
|
||||||
|
.id(self.search_id.clone())
|
||||||
|
.on_clear(Message::SearchClear)
|
||||||
|
.on_input(Message::SearchInput)
|
||||||
|
.on_submit(Message::SearchSubmit)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
widget::button::icon(widget::icon::from_name("system-search-symbolic"))
|
||||||
|
.on_press(Message::SearchActivate)
|
||||||
|
.into()
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a view after each update.
|
/// Creates a view after each update.
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ pub fn key_binds() -> HashMap<KeyBind, Action> {
|
||||||
bind!([Ctrl], Key::Character("v".into()), Paste);
|
bind!([Ctrl], Key::Character("v".into()), Paste);
|
||||||
bind!([], Key::Named(Named::Space), Properties);
|
bind!([], Key::Named(Named::Space), Properties);
|
||||||
bind!([], Key::Named(Named::F2), Rename);
|
bind!([], Key::Named(Named::F2), Rename);
|
||||||
|
bind!([Ctrl], Key::Character("f".into()), SearchActivate);
|
||||||
bind!([Ctrl], Key::Character("a".into()), SelectAll);
|
bind!([Ctrl], Key::Character("a".into()), SelectAll);
|
||||||
bind!([Ctrl], Key::Character("w".into()), TabClose);
|
bind!([Ctrl], Key::Character("w".into()), TabClose);
|
||||||
bind!([Ctrl], Key::Character("t".into()), TabNew);
|
bind!([Ctrl], Key::Character("t".into()), TabNew);
|
||||||
|
|
|
||||||
149
src/tab.rs
149
src/tab.rs
|
|
@ -193,6 +193,77 @@ fn hidden_attribute(metadata: &Metadata) -> bool {
|
||||||
metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN
|
metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn item_from_entry(
|
||||||
|
path: PathBuf,
|
||||||
|
name: String,
|
||||||
|
metadata: fs::Metadata,
|
||||||
|
sizes: IconSizes,
|
||||||
|
) -> Item {
|
||||||
|
let hidden = name.starts_with(".") || hidden_attribute(&metadata);
|
||||||
|
|
||||||
|
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||||
|
if metadata.is_dir() {
|
||||||
|
(
|
||||||
|
//TODO: make this a static
|
||||||
|
"inode/directory".parse().unwrap(),
|
||||||
|
folder_icon(&path, sizes.grid()),
|
||||||
|
folder_icon(&path, sizes.list()),
|
||||||
|
folder_icon(&path, sizes.list_condensed()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let mime = mime_for_path(&path);
|
||||||
|
(
|
||||||
|
mime.clone(),
|
||||||
|
mime_icon(mime.clone(), sizes.grid()),
|
||||||
|
mime_icon(mime.clone(), sizes.list()),
|
||||||
|
mime_icon(mime, sizes.list_condensed()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let open_with = mime_apps(&mime);
|
||||||
|
|
||||||
|
let thumbnail_opt = if mime.type_() == mime::IMAGE {
|
||||||
|
if mime.subtype() == mime::SVG {
|
||||||
|
Some(ItemThumbnail::Svg)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(ItemThumbnail::NotImage)
|
||||||
|
};
|
||||||
|
|
||||||
|
let children = if metadata.is_dir() {
|
||||||
|
//TODO: calculate children in the background (and make it cancellable?)
|
||||||
|
match fs::read_dir(&path) {
|
||||||
|
Ok(entries) => entries.count(),
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to read directory {:?}: {}", path, err);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
Item {
|
||||||
|
name,
|
||||||
|
metadata: ItemMetadata::Path { metadata, children },
|
||||||
|
hidden,
|
||||||
|
path_opt: Some(path),
|
||||||
|
mime,
|
||||||
|
icon_handle_grid,
|
||||||
|
icon_handle_list,
|
||||||
|
icon_handle_list_condensed,
|
||||||
|
open_with,
|
||||||
|
thumbnail_opt,
|
||||||
|
button_id: widget::Id::unique(),
|
||||||
|
pos_opt: Cell::new(None),
|
||||||
|
rect_opt: Cell::new(None),
|
||||||
|
selected: false,
|
||||||
|
overlaps_drag_rect: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
match fs::read_dir(tab_path) {
|
match fs::read_dir(tab_path) {
|
||||||
|
|
@ -206,12 +277,14 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
let name = match entry.file_name().into_string() {
|
let name = match entry.file_name().into_string() {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(name_os) => {
|
Err(name_os) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"failed to parse entry in {:?}: {:?} is not valid UTF-8",
|
"failed to parse entry at {:?}: {:?} is not valid UTF-8",
|
||||||
tab_path,
|
path,
|
||||||
name_os,
|
name_os,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -221,80 +294,12 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
let metadata = match entry.metadata() {
|
let metadata = match entry.metadata() {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(
|
log::warn!("failed to read metadata for entry at {:?}: {}", path, err);
|
||||||
"failed to read metadata for entry in {:?}: {}",
|
|
||||||
tab_path,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let hidden = name.starts_with(".") || hidden_attribute(&metadata);
|
items.push(item_from_entry(path, name, metadata, sizes));
|
||||||
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
|
||||||
if metadata.is_dir() {
|
|
||||||
(
|
|
||||||
//TODO: make this a static
|
|
||||||
"inode/directory".parse().unwrap(),
|
|
||||||
folder_icon(&path, sizes.grid()),
|
|
||||||
folder_icon(&path, sizes.list()),
|
|
||||||
folder_icon(&path, sizes.list_condensed()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let mime = mime_for_path(&path);
|
|
||||||
(
|
|
||||||
mime.clone(),
|
|
||||||
mime_icon(mime.clone(), sizes.grid()),
|
|
||||||
mime_icon(mime.clone(), sizes.list()),
|
|
||||||
mime_icon(mime, sizes.list_condensed()),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let open_with = mime_apps(&mime);
|
|
||||||
|
|
||||||
let thumbnail_opt = if mime.type_() == mime::IMAGE {
|
|
||||||
if mime.subtype() == mime::SVG {
|
|
||||||
Some(ItemThumbnail::Svg)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(ItemThumbnail::NotImage)
|
|
||||||
};
|
|
||||||
|
|
||||||
let children = if metadata.is_dir() {
|
|
||||||
//TODO: calculate children in the background (and make it cancellable?)
|
|
||||||
match fs::read_dir(&path) {
|
|
||||||
Ok(entries) => entries.count(),
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read directory {:?}: {}", path, err);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
items.push(Item {
|
|
||||||
name,
|
|
||||||
metadata: ItemMetadata::Path { metadata, children },
|
|
||||||
hidden,
|
|
||||||
path_opt: Some(path),
|
|
||||||
mime,
|
|
||||||
icon_handle_grid,
|
|
||||||
icon_handle_list,
|
|
||||||
icon_handle_list_condensed,
|
|
||||||
open_with,
|
|
||||||
thumbnail_opt,
|
|
||||||
button_id: widget::Id::unique(),
|
|
||||||
pos_opt: Cell::new(None),
|
|
||||||
rect_opt: Cell::new(None),
|
|
||||||
selected: false,
|
|
||||||
overlaps_drag_rect: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue