perf: general minor performance optimisations

Notably there is some code cleanup with the zooming functionality, I've
created a new module to reduce code duplication.
This commit is contained in:
Cheong Lau 2025-10-28 13:10:40 +10:00
parent 5f729829d7
commit bd1fa1f0a9
16 changed files with 971 additions and 1109 deletions

File diff suppressed because it is too large Load diff

View file

@ -47,7 +47,7 @@ pub fn extract(
controller: &Controller, controller: &Controller,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
let mime = mime_for_path(path, None, false); let mime = mime_for_path(path, None, false);
let password = password.clone(); let password = password.as_deref();
match mime.essence_str() { match mime.essence_str() {
"application/gzip" | "application/x-compressed-tar" => { "application/gzip" | "application/x-compressed-tar" => {
OpReader::new(path, controller.clone()) OpReader::new(path, controller.clone())
@ -107,7 +107,7 @@ pub fn extract(
fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>( fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
archive: &mut zip::ZipArchive<R>, archive: &mut zip::ZipArchive<R>,
directory: P, directory: P,
password: Option<String>, password: Option<&str>,
controller: Controller, controller: Controller,
) -> zip::result::ZipResult<()> { ) -> zip::result::ZipResult<()> {
use std::{ffi::OsString, fs}; use std::{ffi::OsString, fs};
@ -145,7 +145,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
controller.set_progress((i as f32) / total_files as f32); controller.set_progress((i as f32) / total_files as f32);
let mut file = match &password { let mut file = match password {
None => archive.by_index(i), None => archive.by_index(i),
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
}?; }?;
@ -207,7 +207,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
} }
continue; continue;
} }
let mut file = match &password { let mut file = match password {
None => archive.by_index(i), None => archive.by_index(i),
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
}?; }?;

View file

@ -25,7 +25,7 @@ pub struct ClipboardCopy {
} }
impl ClipboardCopy { impl ClipboardCopy {
pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: &[P]) -> Self { pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: impl IntoIterator<Item = P>) -> Self {
let available = vec![ let available = vec![
"text/plain".to_string(), "text/plain".to_string(),
"text/plain;charset=utf-8".to_string(), "text/plain;charset=utf-8".to_string(),

View file

@ -75,21 +75,17 @@ pub enum Favorite {
impl Favorite { impl Favorite {
pub fn from_path(path: PathBuf) -> Self { pub fn from_path(path: PathBuf) -> Self {
// Ensure that special folders are handled properly // Ensure that special folders are handled properly
for favorite in &[ [
Self::Home, Self::Home,
Self::Documents, Self::Documents,
Self::Downloads, Self::Downloads,
Self::Music, Self::Music,
Self::Pictures, Self::Pictures,
Self::Videos, Self::Videos,
] { ]
if let Some(favorite_path) = favorite.path_opt() { .into_iter()
if favorite_path == path { .find(|fav| fav.path_opt().as_ref() == Some(&path))
return favorite.clone(); .unwrap_or(Self::Path(path))
}
}
}
Self::Path(path)
} }
pub fn path_opt(&self) -> Option<PathBuf> { pub fn path_opt(&self) -> Option<PathBuf> {

View file

@ -31,9 +31,7 @@ use std::{
any::TypeId, any::TypeId,
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
env, fmt, fs, env, fmt, fs,
num::NonZeroU16,
path::PathBuf, path::PathBuf,
str::FromStr,
time::{self, Instant}, time::{self, Instant},
}; };
@ -48,6 +46,7 @@ use crate::{
menu, menu,
mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage}, mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage},
tab::{self, ItemMetadata, Location, Tab}, tab::{self, ItemMetadata, Location, Tab},
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -372,7 +371,7 @@ impl<M: Send + 'static> Dialog<M> {
let on_result_message = (self.on_result)(result); let on_result_message = (self.on_result)(result);
Task::batch([ Task::batch([
command, command,
Task::perform(async move { cosmic::action::app(on_result_message) }, |x| x), Task::future(async move { cosmic::action::app(on_result_message) }),
]) ])
} else { } else {
command command
@ -606,7 +605,7 @@ impl App {
row = row.push( row = row.push(
//TODO: easier way to create buttons with rich text //TODO: easier way to create buttons with rich text
widget::button::custom( widget::button::custom(
widget::row::with_children(vec![Element::from(&self.accept_label)]) widget::row::with_children([Element::from(&self.accept_label)])
.padding([0, space_s]) .padding([0, space_s])
.width(Length::Shrink) .width(Length::Shrink)
.height(space_l) .height(space_l)
@ -677,40 +676,37 @@ impl App {
let location = self.tab.location.clone(); let location = self.tab.location.clone();
let icon_sizes = self.tab.config.icon_sizes; let icon_sizes = self.tab.config.icon_sizes;
let mounter_items = self.mounter_items.clone(); let mounter_items = self.mounter_items.clone();
Task::perform( Task::future(async move {
async move { let location2 = location.clone();
let location2 = location.clone(); match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { Ok((parent_item_opt, mut items)) => {
Ok((parent_item_opt, mut items)) => { #[cfg(feature = "gvfs")]
#[cfg(feature = "gvfs")] {
{ let mounter_paths: Box<[_]> = mounter_items
let mounter_paths: Vec<_> = mounter_items .values()
.iter() .flatten()
.flat_map(|item| item.1.iter()) .filter_map(MounterItem::path)
.filter_map(MounterItem::path) .collect();
.collect(); if !mounter_paths.is_empty() {
if !mounter_paths.is_empty() { for item in &mut items {
for item in &mut items { item.is_mount_point =
item.is_mount_point = item.path_opt().is_some_and(|p| mounter_paths.contains(p));
item.path_opt().is_some_and(|p| mounter_paths.contains(p));
}
} }
} }
cosmic::action::app(Message::TabRescan(
location,
parent_item_opt,
items,
selection_paths,
))
}
Err(err) => {
log::warn!("failed to rescan: {err}");
cosmic::action::none()
} }
cosmic::action::app(Message::TabRescan(
location,
parent_item_opt,
items,
selection_paths,
))
} }
}, Err(err) => {
|x| x, log::warn!("failed to rescan: {err}");
) cosmic::action::none()
}
}
})
} }
fn search_get(&self) -> Option<&str> { fn search_get(&self) -> Option<&str> {
@ -835,12 +831,10 @@ impl App {
// Collect all mounter items // Collect all mounter items
let mut nav_items = Vec::new(); let mut nav_items = Vec::new();
for (key, items) in &self.mounter_items { for (key, items) in &self.mounter_items {
for item in items { nav_items.extend(items.iter().map(|item| (*key, item)));
nav_items.push((*key, item));
}
} }
// Sort by name lexically // Sort by name lexically
nav_items.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name())); nav_items.sort_unstable_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));
// Add items to nav model // Add items to nav model
for (i, (key, item)) in nav_items.into_iter().enumerate() { for (i, (key, item)) in nav_items.into_iter().enumerate() {
nav_model = nav_model.insert(|mut b| { nav_model = nav_model.insert(|mut b| {
@ -1040,7 +1034,7 @@ impl Application for App {
//TODO: should gallery view just be a dialog? //TODO: should gallery view just be a dialog?
if self.tab.gallery { if self.tab.gallery {
return Some( return Some(
widget::column::with_children(vec![ widget::column::with_children([
self.tab.gallery_view().map(Message::TabMessage), self.tab.gallery_view().map(Message::TabMessage),
// Draw button row as part of the overlay // Draw button row as part of the overlay
widget::container(self.button_view()) widget::container(self.button_view())
@ -1098,7 +1092,7 @@ impl Application for App {
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
) )
.control( .control(
widget::column::with_children(vec![ widget::column::with_children([
widget::text::body(fl!("folder-name")).into(), widget::text::body(fl!("folder-name")).into(),
widget::text_input("", name.as_str()) widget::text_input("", name.as_str())
.id(self.dialog_text_input.clone()) .id(self.dialog_text_input.clone())
@ -1392,10 +1386,9 @@ impl Application for App {
return self.search_set(Some(term)); return self.search_set(Some(term));
} }
TypeToSearch::EnterPath => { TypeToSearch::EnterPath => {
let location = self.tab.edit_location.as_ref().map_or_else( let location = (self.tab.edit_location)
|| self.tab.location.clone(), .as_ref()
|x| x.location.clone(), .map_or_else(|| &self.tab.location, |x| &x.location);
);
// Try to add text to end of location // Try to add text to end of location
if let Some(path) = location.path_opt() { if let Some(path) = location.path_opt() {
let mut path_string = path.to_string_lossy().to_string(); let mut path_string = path.to_string_lossy().to_string();
@ -1538,7 +1531,7 @@ impl Application for App {
if let Some(path) = item.path_opt() { if let Some(path) = item.path_opt() {
paths.push(path.clone()); paths.push(path.clone());
let _ = update_recently_used( let _ = update_recently_used(
&path.clone(), path,
Self::APP_ID.to_string(), Self::APP_ID.to_string(),
"cosmic-files".to_string(), "cosmic-files".to_string(),
None, None,
@ -1775,9 +1768,9 @@ impl Application for App {
// Filter // Filter
if let Some(filter_i) = self.filter_selected { if let Some(filter_i) = self.filter_selected {
if let Some(filter) = self.filters.get(filter_i) { if let Some(filter) = self.filters.get(filter_i) {
// Parse filters // Parse globs (Mime implements PartialEq with &str, so no need to parse)
let mut parsed_globs = Vec::new(); let mut parsed_globs = Vec::new();
let mut parsed_mimes = Vec::new(); let mut mimes = Vec::new();
for pattern in &filter.patterns { for pattern in &filter.patterns {
match pattern { match pattern {
DialogFilterPattern::Glob(value) => { DialogFilterPattern::Glob(value) => {
@ -1788,39 +1781,17 @@ impl Application for App {
} }
} }
} }
DialogFilterPattern::Mime(value) => { DialogFilterPattern::Mime(value) => mimes.push(value.as_str()),
match mime_guess::Mime::from_str(value) {
Ok(mime) => parsed_mimes.push(mime),
Err(err) => {
log::warn!("failed to parse mime {value:?}: {err}");
}
}
}
} }
} }
items.retain(|item| { items.retain(|item| {
if item.metadata.is_dir() { // Directories are always shown
// Directories are always shown item.metadata.is_dir()
return true;
}
// Check for mime type match (first because it is faster) // Check for mime type match (first because it is faster)
for mime in &parsed_mimes { || mimes.iter().copied().any(|mime| mime == item.mime)
if mime == &item.mime {
return true;
}
}
// Check for glob match (last because it is slower) // Check for glob match (last because it is slower)
for glob in &parsed_globs { || parsed_globs.iter().any(|glob| glob.matches(&item.name))
if glob.matches(&item.name) {
return true;
}
}
// No filters matched
false
}); });
} }
} }
@ -1869,47 +1840,18 @@ impl Application for App {
}); });
} }
Message::ZoomDefault => { Message::ZoomDefault => {
return self.with_dialog_config(|config| match config.view { return self.with_dialog_config(|config| {
tab::View::List => config.icon_sizes.list = 100.try_into().unwrap(), zoom_to_default(config.view, &mut config.icon_sizes);
tab::View::Grid => config.icon_sizes.grid = 100.try_into().unwrap(),
}); });
} }
Message::ZoomIn => { Message::ZoomIn => {
let zoom_in = |size: &mut NonZeroU16, min: u16, max: u16| { return self.with_dialog_config(|config| {
let mut step = min; zoom_in_view(config.view, &mut config.icon_sizes);
while step <= max {
if size.get() < step {
*size = step.try_into().unwrap();
break;
}
step += 25;
}
if size.get() > step {
*size = step.try_into().unwrap();
}
};
return self.with_dialog_config(|config| match config.view {
tab::View::List => zoom_in(&mut config.icon_sizes.list, 50, 500),
tab::View::Grid => zoom_in(&mut config.icon_sizes.grid, 50, 500),
}); });
} }
Message::ZoomOut => { Message::ZoomOut => {
let zoom_out = |size: &mut NonZeroU16, min: u16, max: u16| { return self.with_dialog_config(|config| {
let mut step = max; zoom_out_view(config.view, &mut config.icon_sizes);
while step >= min {
if size.get() > step {
*size = step.try_into().unwrap();
break;
}
step -= 25;
}
if size.get() < step {
*size = step.try_into().unwrap();
}
};
return self.with_dialog_config(|config| match config.view {
tab::View::List => zoom_out(&mut config.icon_sizes.list, 50, 500),
tab::View::Grid => zoom_out(&mut config.icon_sizes.grid, 50, 500),
}); });
} }
Message::Surface(action) => { Message::Surface(action) => {
@ -2090,21 +2032,19 @@ impl Application for App {
); );
} }
for (key, mounter) in MOUNTERS.iter() { subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| {
subscriptions.push( mounter
mounter .subscription()
.subscription() .with(*key)
.with(*key) .map(|(key, mounter_message)| {
.map(|(key, mounter_message)| { if let MounterMessage::Items(items) = mounter_message {
if let MounterMessage::Items(items) = mounter_message { Message::MounterItems(key, items)
Message::MounterItems(key, items) } else {
} else { log::warn!("{mounter_message:?} not supported in dialog mode");
log::warn!("{mounter_message:?} not supported in dialog mode"); Message::None
Message::None }
} })
}), }));
);
}
Subscription::batch(subscriptions) Subscription::batch(subscriptions)
} }

View file

@ -21,6 +21,7 @@ mod mouse_area;
pub mod operation; pub mod operation;
mod spawn_detached; mod spawn_detached;
use tab::Location; use tab::Location;
mod zoom;
use crate::config::State; use crate::config::State;
pub mod tab; pub mod tab;

View file

@ -32,7 +32,7 @@ macro_rules! menu_button {
($($x:expr),+ $(,)?) => ( ($($x:expr),+ $(,)?) => (
button::custom( button::custom(
Row::with_children( Row::with_children(
vec![$(Element::from($x)),+] [$(Element::from($x)),+]
) )
.height(Length::Fixed(24.0)) .height(Length::Fixed(24.0))
.align_y(Alignment::Center) .align_y(Alignment::Center)
@ -167,9 +167,9 @@ pub fn context_menu<'a>(
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open).into());
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
{ {
for (i, action) in entry.desktop_actions.into_iter().enumerate() { children.extend(entry.desktop_actions.into_iter().enumerate().map(
children.push(menu_item(action.name, Action::ExecEntryAction(i)).into()); |(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
} ));
} }
children.push(divider::horizontal::light().into()); children.push(divider::horizontal::light().into());
children.push(menu_item(fl!("rename"), Action::Rename).into()); children.push(menu_item(fl!("rename"), Action::Rename).into());
@ -207,11 +207,8 @@ pub fn context_menu<'a>(
children.push(menu_item(fl!("copy"), Action::Copy).into()); children.push(menu_item(fl!("copy"), Action::Copy).into());
children.push(divider::horizontal::light().into()); children.push(divider::horizontal::light().into());
let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;
.iter() selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m));
.filter_map(|mime_type| mime_type.parse::<Mime>().ok())
.collect::<Vec<_>>();
selected_types.retain(|t| !supported_archive_types.contains(t));
if selected_types.is_empty() { if selected_types.is_empty() {
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into()); children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into()); children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into());
@ -719,7 +716,7 @@ pub fn menu_bar<'a>(
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> { pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {
//TODO: only add some of these when in App mode //TODO: only add some of these when in App mode
let children = vec![ let children = [
menu_button!(text::body(fl!("open-in-new-tab"))) menu_button!(text::body(fl!("open-in-new-tab")))
.on_press(tab::Message::LocationMenuAction( .on_press(tab::Message::LocationMenuAction(
LocationMenuAction::OpenInNewTab(ancestor_index), LocationMenuAction::OpenInNewTab(ancestor_index),

View file

@ -83,7 +83,7 @@ pub fn exec_to_command(
for invalid in path_opt for invalid in path_opt
.iter() .iter()
.map(AsRef::as_ref) .map(AsRef::as_ref)
.filter(|path| from_file_or_dir(path).is_none()) .filter(|&path| from_file_or_dir(path).is_none())
{ {
log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}"); log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}");
} }
@ -221,7 +221,7 @@ fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
pub struct MimeAppCache { pub struct MimeAppCache {
apps: Vec<MimeApp>, apps: Vec<MimeApp>,
cache: FxHashMap<Mime, Vec<MimeApp>>, cache: FxHashMap<Mime, Vec<MimeApp>>,
icons: FxHashMap<Mime, Vec<widget::icon::Handle>>, icons: FxHashMap<Mime, Box<[widget::icon::Handle]>>,
terminals: Vec<MimeApp>, terminals: Vec<MimeApp>,
} }
@ -257,7 +257,7 @@ impl MimeAppCache {
// Load desktop applications by supported mime types // Load desktop applications by supported mime types
//TODO: hashmap for all apps by id? //TODO: hashmap for all apps by id?
let all_apps: Vec<_> = desktop::load_applications(locale, false, None).collect(); let all_apps: Box<[_]> = desktop::load_applications(locale, false, None).collect();
for app in &all_apps { for app in &all_apps {
//TODO: just collect apps that can be executed with a file argument? //TODO: just collect apps that can be executed with a file argument?
if !app.mime_types.is_empty() { if !app.mime_types.is_empty() {
@ -292,21 +292,17 @@ impl MimeAppCache {
let mut mimeapps_paths = Vec::new(); let mut mimeapps_paths = Vec::new();
let xdg_dirs = xdg::BaseDirectories::new(); let xdg_dirs = xdg::BaseDirectories::new();
for path in xdg_dirs.find_data_files("applications/mimeapps.list") { mimeapps_paths.extend(xdg_dirs.find_data_files("applications/mimeapps.list"));
mimeapps_paths.push(path);
}
for desktop in desktops.iter().rev() { for desktop in desktops.iter().rev() {
for path in xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list")) { mimeapps_paths
mimeapps_paths.push(path); .extend(xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list")));
}
}
for path in xdg_dirs.find_config_files("mimeapps.list") {
mimeapps_paths.push(path);
} }
mimeapps_paths.extend(xdg_dirs.find_config_files("mimeapps.list"));
for desktop in desktops.iter().rev() { for desktop in desktops.iter().rev() {
for path in xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list")) { mimeapps_paths.extend(xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list")));
mimeapps_paths.push(path);
}
} }
//TODO: handle directory specific behavior //TODO: handle directory specific behavior
@ -334,7 +330,7 @@ impl MimeAppCache {
.or_insert_with(|| Vec::with_capacity(1)); .or_insert_with(|| Vec::with_capacity(1));
if !apps.iter().any(|x| filename_eq(&x.path, filename)) { if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
if let Some(app) = if let Some(app) =
all_apps.iter().find(|x| filename_eq(&x.path, filename)) all_apps.iter().find(|&x| filename_eq(&x.path, filename))
{ {
apps.push(MimeApp::from(app)); apps.push(MimeApp::from(app));
} else { } else {
@ -406,12 +402,12 @@ impl MimeAppCache {
// Copy icons to special cache // Copy icons to special cache
//TODO: adjust dropdown API so this is no longer needed //TODO: adjust dropdown API so this is no longer needed
for (mime, apps) in &self.cache { self.icons.extend(self.cache.iter().map(|(mime, apps)| {
self.icons.insert( (
mime.clone(), mime.clone(),
apps.iter().map(|app| app.icon.clone()).collect(), apps.iter().map(|app| app.icon.clone()).collect(),
); )
} }));
let elapsed = start.elapsed(); let elapsed = start.elapsed();
log::info!("loaded mime app cache in {elapsed:?}"); log::info!("loaded mime app cache in {elapsed:?}");
@ -426,7 +422,7 @@ impl MimeAppCache {
} }
pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] { pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
self.icons.get(key).map_or(&[], Vec::as_slice) self.icons.get(key).map_or(&[], Box::as_ref)
} }
fn get_default_terminal(&self) -> Option<String> { fn get_default_terminal(&self) -> Option<String> {

View file

@ -41,10 +41,8 @@ impl MimeIconCache {
let icon_name = icon_names.remove(0); let icon_name = icon_names.remove(0);
let mut named = icon::from_name(icon_name).size(key.size); let mut named = icon::from_name(icon_name).size(key.size);
if !icon_names.is_empty() { if !icon_names.is_empty() {
let mut fallback_names = Vec::with_capacity(icon_names.len()); let fallback_names =
for fallback_name in icon_names { icon_names.into_iter().map(std::borrow::Cow::from).collect();
fallback_names.push(fallback_name.into());
}
named = named.fallback(Some(icon::IconFallback::Names(fallback_names))); named = named.fallback(Some(icon::IconFallback::Names(fallback_names)));
} }
Some(named.handle()) Some(named.handle())
@ -55,8 +53,8 @@ impl MimeIconCache {
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> = static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
LazyLock::new(|| Mutex::new(MimeIconCache::new())); LazyLock::new(|| Mutex::new(MimeIconCache::new()));
pub fn mime_for_path<P: AsRef<Path>>( pub fn mime_for_path(
path: P, path: impl AsRef<Path>,
metadata_opt: Option<&fs::Metadata>, metadata_opt: Option<&fs::Metadata>,
remote: bool, remote: bool,
) -> Mime { ) -> Mime {
@ -65,7 +63,7 @@ pub fn mime_for_path<P: AsRef<Path>>(
// Try the shared mime info cache first // Try the shared mime info cache first
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type(); let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
if remote { if remote {
if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) { if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) {
gb.file_name(file_name); gb.file_name(file_name);
} }
} else { } else {
@ -75,11 +73,22 @@ pub fn mime_for_path<P: AsRef<Path>>(
gb.metadata(metadata.clone()); gb.metadata(metadata.clone());
} }
let guess = gb.guess(); let guess = gb.guess();
if guess.uncertain() { let guessed_mime = guess.mime_type();
/// Checks if the `Mime` is a special variant returned by `xdg-mime`.
/// This includes directories, symlinks and zerosize files, which are returned as uncertain.
fn is_special_mime(mime: &Mime) -> bool {
*mime == "inode/directory" || *mime == "inode/symlink" || *mime == "application/x-zerosize"
}
// `xdg-mime-rs` sets the guess to uncertain if it returns special mime types.
// The guess could also be uncertain on platforms without shared-mime-info.
// Try mime_guess, but only if it is not one of the special mime types.
if guess.uncertain() && (remote || !is_special_mime(guessed_mime)) {
// If uncertain, try mime_guess. This could happen on platforms without shared-mime-info // If uncertain, try mime_guess. This could happen on platforms without shared-mime-info
mime_guess::from_path(path).first_or_octet_stream() mime_guess::from_path(path).first_or_octet_stream()
} else { } else {
guess.mime_type().clone() guessed_mime.clone()
} }
} }

View file

@ -30,45 +30,48 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
} }
fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems { fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
let mut items = MounterItems::new(); let mut items: MounterItems = (monitor.mounts().into_iter())
for (i, mount) in monitor.mounts().into_iter().enumerate() { .enumerate()
items.push(MounterItem::Gvfs(Item { .map(|(i, mount)| {
uri: MountExt::root(&mount).uri().to_string(), MounterItem::Gvfs(Item {
kind: ItemKind::Mount, uri: mount.root().uri().into(),
index: i, kind: ItemKind::Mount,
name: MountExt::name(&mount).to_string(), index: i,
is_mounted: true, name: mount.name().into(),
icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()), is_mounted: true,
icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16), icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()),
path_opt: MountExt::root(&mount).path(), icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16),
})); path_opt: MountExt::root(&mount).path(),
} })
for (i, volume) in monitor.volumes().into_iter().enumerate() { })
if volume.get_mount().is_some() { .collect();
items.extend(
(monitor.volumes().into_iter())
.enumerate()
// Volumes with mounts are already listed by mount // Volumes with mounts are already listed by mount
continue; .filter(|(_, volume)| volume.get_mount().is_none())
} .map(|(i, volume)| {
let uri = VolumeExt::activation_root(&volume) let uri = VolumeExt::activation_root(&volume)
.map(|f| f.uri().to_string()) .map(|f| f.uri().into())
.unwrap_or_default(); .unwrap_or_default();
items.push(MounterItem::Gvfs(Item { MounterItem::Gvfs(Item {
// TODO can we get URI for volumes with no mount? // TODO can we get URI for volumes with no mount?
uri, uri,
kind: ItemKind::Volume, kind: ItemKind::Volume,
index: i, index: i,
name: VolumeExt::name(&volume).to_string(), name: volume.name().into(),
is_mounted: false, is_mounted: false,
icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()), icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()),
icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16), icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16),
path_opt: None, path_opt: None,
})); })
} }),
);
items items
} }
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> { fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
let mut uri = uri.to_string(); let mut file = gio::File::for_uri(uri);
let mut file = gio::File::for_uri(&uri);
let force_dir = uri.starts_with("network:///"); let force_dir = uri.starts_with("network:///");
// Resolve the target-uri if it exists // Resolve the target-uri if it exists
@ -78,8 +81,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
gio::Cancellable::NONE, gio::Cancellable::NONE,
) { ) {
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) { if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
uri = resolved_uri.to_string(); file = gio::File::for_uri(resolved_uri.as_str());
file = gio::File::for_uri(&uri);
} }
} }
@ -89,10 +91,10 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
.map_err(err_str)? .map_err(err_str)?
{ {
let info = info_res.map_err(err_str)?; let info = info_res.map_err(err_str)?;
let name = info.name().to_string_lossy().to_string(); let name = info.name().to_string_lossy().into_owned();
let display_name = info.display_name().to_string(); let display_name = String::from(info.display_name());
let uri = file.child(info.name()).uri().to_string(); let uri = String::from(file.child(info.name()).uri());
//TODO: what is the best way to resolve shortcuts? //TODO: what is the best way to resolve shortcuts?
let location = Location::Network(uri, display_name.clone(), file.child(&name).path()); let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
@ -100,11 +102,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) { let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED); let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
let is_dir = matches!(info.file_type(), gio::FileType::Directory); let is_dir = matches!(info.file_type(), gio::FileType::Directory);
let size_opt = if is_dir { let size_opt = (!is_dir).then_some(info.size() as u64);
None
} else {
Some(info.size() as u64)
};
let mut children_opt = None; let mut children_opt = None;
if is_dir { if is_dir {
@ -189,31 +187,21 @@ fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOp
move |mount_op, message, default_user, default_domain, flags| { move |mount_op, message, default_user, default_domain, flags| {
let auth = MounterAuth { let auth = MounterAuth {
message: message.to_string(), message: message.to_string(),
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) { username_opt: flags
Some(default_user.to_string()) .contains(gio::AskPasswordFlags::NEED_USERNAME)
} else { .then(|| default_user.to_string()),
None domain_opt: flags
}, .contains(gio::AskPasswordFlags::NEED_DOMAIN)
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) { .then(|| default_domain.to_string()),
Some(default_domain.to_string()) password_opt: flags
} else { .contains(gio::AskPasswordFlags::NEED_PASSWORD)
None .then(String::new),
}, remember_opt: flags
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) { .contains(gio::AskPasswordFlags::SAVING_SUPPORTED)
Some(String::new()) .then_some(false),
} else { anonymous_opt: flags
None .contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED)
}, .then_some(false),
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
Some(false)
} else {
None
},
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
Some(false)
} else {
None
},
}; };
let (auth_tx, mut auth_rx) = mpsc::channel(1); let (auth_tx, mut auth_rx) = mpsc::channel(1);
event_tx event_tx
@ -458,7 +446,7 @@ impl Gvfs {
gio::Cancellable::NONE, gio::Cancellable::NONE,
) { ) {
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) { if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
uri = resolved_uri.to_string(); uri = resolved_uri.into();
file = gio::File::for_uri(&uri); file = gio::File::for_uri(&uri);
} }
} }
@ -597,12 +585,9 @@ impl Mounter for Gvfs {
fn unmount(&self, item: MounterItem) -> Task<()> { fn unmount(&self, item: MounterItem) -> Task<()> {
let command_tx = self.command_tx.clone(); let command_tx = self.command_tx.clone();
Task::perform( Task::future(async move {
async move { command_tx.send(Cmd::Unmount(item)).unwrap();
command_tx.send(Cmd::Unmount(item)).unwrap(); })
},
|x| x,
)
} }
fn subscription(&self) -> Subscription<MounterMessage> { fn subscription(&self) -> Subscription<MounterMessage> {

View file

@ -220,8 +220,9 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
let file_name = file_name.to_string(); let file_name = file_name.to_string();
COMPOUND_EXTENSIONS COMPOUND_EXTENSIONS
.iter() .iter()
.find(|&&ext| file_name.ends_with(ext)) .copied()
.map(|&ext| { .find(|&ext| file_name.ends_with(ext))
.map(|ext| {
( (
file_name.strip_suffix(ext).unwrap().to_string(), file_name.strip_suffix(ext).unwrap().to_string(),
Some(ext[1..].to_string()), Some(ext[1..].to_string()),
@ -251,7 +252,7 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
} }
}; };
to = to.join(&new_name); to.push(&new_name);
if !matches!(to.try_exists(), Ok(true)) { if !matches!(to.try_exists(), Ok(true)) {
break; break;
@ -329,7 +330,7 @@ pub enum Operation {
EmptyTrash, EmptyTrash,
/// Uncompress files /// Uncompress files
Extract { Extract {
paths: Vec<PathBuf>, paths: Box<[PathBuf]>,
to: PathBuf, to: PathBuf,
password: Option<String>, password: Option<String>,
}, },
@ -347,10 +348,10 @@ pub enum Operation {
}, },
/// Permanently delete items, skipping the trash /// Permanently delete items, skipping the trash
PermanentlyDelete { PermanentlyDelete {
paths: Vec<PathBuf>, paths: Box<[PathBuf]>,
}, },
RemoveFromRecents { RemoveFromRecents {
paths: Vec<PathBuf>, paths: Box<[PathBuf]>,
}, },
Rename { Rename {
from: PathBuf, from: PathBuf,
@ -1013,7 +1014,7 @@ impl Operation {
} }
Self::RemoveFromRecents { paths } => { Self::RemoveFromRecents { paths } => {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let path_refs = paths.iter().map(PathBuf::as_path).collect::<Vec<&Path>>(); let path_refs = paths.iter().map(PathBuf::as_path).collect::<Box<[_]>>();
recently_used_xbel::remove_recently_used(&path_refs) recently_used_xbel::remove_recently_used(&path_refs)
}) })
.await .await

View file

@ -52,7 +52,7 @@ impl Context {
pub async fn recursive_copy_or_move( pub async fn recursive_copy_or_move(
&mut self, &mut self,
from_to_pairs: Vec<(PathBuf, PathBuf)>, from_to_pairs: impl IntoIterator<Item = (PathBuf, PathBuf)>,
method: Method, method: Method,
) -> Result<bool, OperationError> { ) -> Result<bool, OperationError> {
let mut ops = Vec::new(); let mut ops = Vec::new();
@ -148,9 +148,8 @@ impl Context {
} }
// Add cleanup ops after standard ops, in reverse // Add cleanup ops after standard ops, in reverse
for cleanup_op in cleanup_ops.into_iter().rev() { cleanup_ops.reverse();
ops.push(cleanup_op); ops.append(&mut cleanup_ops);
}
let total_ops = ops.len(); let total_ops = ops.len();
for (current_ops, mut op) in ops.into_iter().enumerate() { for (current_ops, mut op) in ops.into_iter().enumerate() {

File diff suppressed because it is too large Load diff

View file

@ -149,9 +149,8 @@ impl ThumbnailCacher {
let info = reader.info(); let info = reader.info();
let text_chunks: FxHashMap<String, String> = info let text_chunks: FxHashMap<String, String> = info
.uncompressed_latin1_text .uncompressed_latin1_text
.clone() .iter()
.into_iter() .map(|chunk| (chunk.keyword.clone(), chunk.text.clone()))
.map(|chunk| (chunk.keyword, chunk.text))
.collect(); .collect();
( (
info.width, info.width,
@ -222,7 +221,7 @@ impl ThumbnailCacher {
// Thumb::URI is required and must match. // Thumb::URI is required and must match.
let thumb_uri = texts let thumb_uri = texts
.iter() .iter()
.find(|text| text.keyword == "Thumb::URI") .find(|&text| text.keyword == "Thumb::URI")
.map(|t| &t.text); .map(|t| &t.text);
if let Some(thumb_uri) = thumb_uri { if let Some(thumb_uri) = thumb_uri {
if *thumb_uri != self.file_uri { if *thumb_uri != self.file_uri {
@ -247,7 +246,7 @@ impl ThumbnailCacher {
// Thumb::MTime is required and must match. // Thumb::MTime is required and must match.
let thumb_mtime = texts let thumb_mtime = texts
.iter() .iter()
.find(|text| text.keyword == "Thumb::MTime") .find(|&text| text.keyword == "Thumb::MTime")
.map(|t| &t.text); .map(|t| &t.text);
if let Some(thumb_mtime) = thumb_mtime { if let Some(thumb_mtime) = thumb_mtime {
let modified = match metadata.modified() { let modified = match metadata.modified() {
@ -276,7 +275,7 @@ impl ThumbnailCacher {
// Thumb::Size isn't required, but it should be verified if present. // Thumb::Size isn't required, but it should be verified if present.
let thumb_size = texts let thumb_size = texts
.iter() .iter()
.find(|text| text.keyword == "Thumb::Size") .find(|&text| text.keyword == "Thumb::Size")
.map(|t| &t.text); .map(|t| &t.text);
if let Some(thumb_size) = thumb_size { if let Some(thumb_size) = thumb_size {
let size = metadata.len(); let size = metadata.len();
@ -301,7 +300,7 @@ fn thumbnail_uri(path: &Path) -> io::Result<String> {
// and they aren't by the url crate, but the thumbnailer used by // and they aren't by the url crate, but the thumbnailer used by
// Gnome Files does. In order to share thumbnails and not get duplicates // Gnome Files does. In order to share thumbnails and not get duplicates
// we should do the same. // we should do the same.
let url = url.to_string().replace('[', "%5B").replace(']', "%5D"); let url = url.as_str().replace('[', "%5B").replace(']', "%5D");
Ok(url) Ok(url)
} }

View file

@ -80,30 +80,32 @@ impl ThumbnailerCache {
let mut search_dirs = Vec::new(); let mut search_dirs = Vec::new();
let xdg_dirs = xdg::BaseDirectories::new(); let xdg_dirs = xdg::BaseDirectories::new();
if let Some(data_home) = xdg_dirs.get_data_home() { if let Some(mut data_home) = xdg_dirs.get_data_home() {
search_dirs.push(data_home.join("thumbnailers")); data_home.push("thumbnailers");
} search_dirs.push(data_home);
for data_dir in xdg_dirs.get_data_dirs() {
search_dirs.push(data_dir.join("thumbnailers"));
} }
search_dirs.extend(xdg_dirs.get_data_dirs().into_iter().map(|mut data_dir| {
data_dir.push("thumbnailers");
data_dir
}));
let mut thumbnailer_paths = Vec::new(); let mut thumbnailer_paths = Vec::new();
for dir in search_dirs { for dir in search_dirs {
log::trace!("looking for thumbnailers in {}", dir.display()); log::trace!("looking for thumbnailers in {}", dir.display());
match fs::read_dir(&dir) { match fs::read_dir(&dir) {
Ok(entries) => { Ok(entries) => {
for entry_res in entries { thumbnailer_paths.extend(entries.filter_map(|entry_res| {
match entry_res { entry_res
Ok(entry) => thumbnailer_paths.push(entry.path()), .inspect_err(|err| {
Err(err) => {
log::warn!( log::warn!(
"failed to read entry in directory {}: {}", "failed to read entry in directory {}: {}",
dir.display(), dir.display(),
err err
); )
} })
} .ok()
} .map(|entry| entry.path())
}));
} }
Err(err) => { Err(err) => {
log::warn!("failed to read directory {}: {}", dir.display(), err); log::warn!("failed to read directory {}: {}", dir.display(), err);

52
src/zoom.rs Normal file
View file

@ -0,0 +1,52 @@
use std::num::NonZeroU16;
use crate::{config::IconSizes, tab::View};
static DEFAULT_ZOOM: NonZeroU16 = NonZeroU16::new(100).unwrap();
static MIN_ZOOM: NonZeroU16 = NonZeroU16::new(50).unwrap();
static MAX_ZOOM: NonZeroU16 = NonZeroU16::new(500).unwrap();
const ZOOM_STEP: u16 = 25;
pub(crate) const fn zoom_to_default(view: View, icon_sizes: &mut IconSizes) {
let icon_size = select_resized_icon(view, icon_sizes);
*icon_size = DEFAULT_ZOOM;
}
pub(crate) fn zoom_in_view(view: View, icon_sizes: &mut IconSizes) {
let icon_size = select_resized_icon(view, icon_sizes);
let mut step = MIN_ZOOM;
while step <= MAX_ZOOM {
if *icon_size < step {
*icon_size = step;
break;
}
step = step.saturating_add(ZOOM_STEP);
}
if *icon_size > step {
*icon_size = step;
}
}
pub(crate) fn zoom_out_view(view: View, icon_sizes: &mut IconSizes) {
let icon_size = select_resized_icon(view, icon_sizes);
let mut step = MAX_ZOOM;
while step >= MIN_ZOOM {
if *icon_size > step {
*icon_size = step;
break;
}
step = NonZeroU16::new(step.get().saturating_sub(ZOOM_STEP)).unwrap();
}
if *icon_size < step {
*icon_size = step;
}
}
const fn select_resized_icon(view: View, icon_sizes: &mut IconSizes) -> &mut NonZeroU16 {
match view {
View::Grid => &mut icon_sizes.grid,
View::List => &mut icon_sizes.list,
}
}