diff --git a/src/app.rs b/src/app.rs index 07f8530..7ec3ce3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -56,7 +56,6 @@ use std::{ env, fmt, fs, future::Future, io, - num::NonZeroU16, path::{Path, PathBuf}, pin::Pin, process, @@ -92,7 +91,11 @@ use crate::{ self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK, Tab, }, }; -use crate::{config::State, dialog::DialogSettings}; +use crate::{ + config::State, + dialog::DialogSettings, + zoom::{zoom_in_view, zoom_out_view, zoom_to_default}, +}; static PERMANENT_DELETE_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("permanent-delete-button")); @@ -488,7 +491,7 @@ impl AsRef for ArchiveType { #[derive(Clone, Debug)] pub enum DialogPage { Compress { - paths: Vec, + paths: Box<[PathBuf]>, to: PathBuf, name: String, archive_type: ArchiveType, @@ -528,7 +531,7 @@ pub enum DialogPage { store_opt: Option, }, PermanentlyDelete { - paths: Vec, + paths: Box<[PathBuf]>, }, RenameItem { from: PathBuf, @@ -632,7 +635,7 @@ pub enum WindowKind { Desktop(Entity), DesktopViewOptions, Dialogs(widget::Id), - FileDialog(Option>), + FileDialog(Option>), Preview(Option, PreviewKind), } @@ -719,7 +722,7 @@ impl App { fn push_dialog(&mut self, page: DialogPage, focus_id: Option) -> Task { let t = self.dialog_pages.push_back(page); if let Some(focus_id) = focus_id { - Task::batch(vec![t, focus(focus_id)]) + Task::batch([t, focus(focus_id)]) } else { t } @@ -733,17 +736,14 @@ impl App { // player that is passed every path. let mut groups: FxHashMap> = FxHashMap::default(); let mut all_archives = true; - let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES - .iter() - .filter_map(|mime_type| mime_type.parse::().ok()) - .collect::>(); + let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES; for (mime, path) in paths.iter().map(|path| { ( mime_icon::mime_for_path(path, None, false), path.as_ref().to_owned(), ) }) { - if !supported_archive_types.contains(&mime) { + if all_archives && !supported_archive_types.iter().copied().any(|t| mime == t) { all_archives = false; } groups.entry(mime).or_default().push(path); @@ -772,7 +772,7 @@ impl App { io::ErrorKind::PermissionDenied => { // If permission is denied, try marking as executable, then running tasks.push(self.push_dialog( - DialogPage::SetExecutableAndLaunch { path: path.clone() }, + DialogPage::SetExecutableAndLaunch { path }, Some(SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID.clone()), )); } @@ -905,11 +905,7 @@ impl App { match exec.next() { Some(cmd) if !cmd.contains('=') => { let mut proc = tokio::process::Command::new(cmd); - for arg in exec { - if !arg.starts_with('%') { - proc.arg(arg); - } - } + proc.args(exec.filter(|arg| !arg.starts_with('%'))); let _ = proc.spawn(); } _ => (), @@ -957,7 +953,7 @@ impl App { .keys() .map(|k| (*k, (0., 0., 0., 0.))) .collect(); - let mut sorted_overlaps: Vec<_> = self.overlap.values().collect(); + let mut sorted_overlaps: Box<[_]> = self.overlap.values().collect(); sorted_overlaps .sort_by(|a, b| (b.1.width * b.1.height).total_cmp(&(a.1.width * b.1.height))); @@ -1105,7 +1101,7 @@ impl App { } // This wrapper ensures that local folders use trash and remote folders permanently delete with a dialog - fn delete(&mut self, paths: Vec) -> Task { + fn delete(&mut self, paths: impl IntoIterator) -> Task { let mut dialog_paths = Vec::new(); let mut trash_paths = Vec::new(); @@ -1129,7 +1125,7 @@ impl App { if !dialog_paths.is_empty() { tasks.push(self.update(Message::DialogPush( DialogPage::PermanentlyDelete { - paths: dialog_paths, + paths: dialog_paths.into_boxed_slice(), }, Some(PERMANENT_DELETE_BUTTON_ID.clone()), ))); @@ -1247,64 +1243,61 @@ impl App { let icon_sizes = self.config.tab.icon_sizes; let mounter_items = self.mounter_items.clone(); - Task::perform( - async move { - let location2 = location.clone(); - match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { - Ok((parent_item_opt, mut items)) => { - #[cfg(feature = "gvfs")] - { - let mounter_paths: Vec<_> = mounter_items - .iter() - .flat_map(|item| item.1.iter()) - .filter_map(MounterItem::path) - .collect(); - if !mounter_paths.is_empty() { - for item in &mut items { - item.is_mount_point = - item.path_opt().is_some_and(|p| mounter_paths.contains(p)); - } + Task::future(async move { + let location2 = location.clone(); + match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { + Ok((parent_item_opt, mut items)) => { + #[cfg(feature = "gvfs")] + { + let mounter_paths: Box<[_]> = mounter_items + .values() + .flatten() + .filter_map(MounterItem::path) + .collect(); + if !mounter_paths.is_empty() { + for item in &mut items { + item.is_mount_point = + item.path_opt().is_some_and(|p| mounter_paths.contains(p)); } } + } - cosmic::action::app(Message::TabRescan( - entity, - location, - parent_item_opt, - items, - selection_paths, - )) - } - Err(err) => { - log::warn!("failed to rescan: {err}"); - cosmic::action::none() - } + cosmic::action::app(Message::TabRescan( + entity, + location, + parent_item_opt, + items, + selection_paths, + )) } - }, - |x| x, - ) + Err(err) => { + log::warn!("failed to rescan: {err}"); + cosmic::action::none() + } + } + }) } fn rescan_trash(&mut self) -> Task { - let mut needs_reload = Vec::new(); - for entity in self.tab_model.iter() { - if let Some(tab) = self.tab_model.data::(entity) { - if matches!(&tab.location, Location::Trash) { - needs_reload.push((entity, Location::Trash)); - } - } - } + let needs_reload: Box<[_]> = self + .tab_model + .iter() + .filter_map(|entity| { + let tab = self.tab_model.data::(entity)?; + (tab.location == Location::Trash).then_some((entity, Location::Trash)) + }) + .collect(); + + let commands = needs_reload + .into_iter() + .map(|(entity, location)| self.update_tab(entity, location, None)); - let mut commands = Vec::with_capacity(needs_reload.len()); - for (entity, location) in needs_reload { - commands.push(self.update_tab(entity, location, None)); - } Task::batch(commands) } /// Refresh all tabs that are opened in [`Location::Recents`]. fn refresh_recents_tabs(&mut self) -> Task { - let commands: Vec<_> = self + let commands: Box<[_]> = self .tab_model .iter() .filter_map(|entity| { @@ -1312,27 +1305,28 @@ impl App { (tab.location == Location::Recents).then_some(entity) }) .collect(); - let commands: Vec<_> = commands + + let commands = commands .into_iter() - .map(|entity| self.update_tab(entity, Location::Recents, None)) - .collect(); + .map(|entity| self.update_tab(entity, Location::Recents, None)); + Task::batch(commands) } fn rescan_recents(&mut self) -> Task { - let mut needs_reload = Vec::new(); - for entity in self.tab_model.iter() { - if let Some(tab) = self.tab_model.data::(entity) { - if matches!(&tab.location, Location::Recents) { - needs_reload.push((entity, Location::Recents)); - } - } - } + let needs_reload: Box<[_]> = self + .tab_model + .iter() + .filter_map(|entity| { + let tab = self.tab_model.data::(entity)?; + (tab.location == Location::Recents).then_some((entity, Location::Recents)) + }) + .collect(); + + let commands = needs_reload + .into_iter() + .map(|(entity, location)| self.update_tab(entity, location, None)); - let mut commands = Vec::with_capacity(needs_reload.len()); - for (entity, location) in needs_reload { - commands.push(self.update_tab(entity, location, None)); - } Task::batch(commands) } @@ -1396,17 +1390,19 @@ impl App { Task::none() } - fn selected_paths(&self, entity_opt: Option) -> Vec { - let mut paths = Vec::new(); + fn selected_paths( + &self, + entity_opt: Option, + ) -> impl Iterator + use<'_> { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); - if let Some(tab) = self.tab_model.data::(entity) { - for location in tab.selected_locations() { - if let Some(path) = location.path_opt() { - paths.push(path.clone()); - } - } - } - paths + self.tab_model + .data::(entity) + .into_iter() + .flat_map(|tab| { + tab.selected_locations() + .into_iter() + .filter_map(Location::into_path_opt) + }) } fn set_cut(&mut self, entity_opt: Option) { @@ -1419,32 +1415,33 @@ impl App { fn update_config(&mut self) -> Task { self.update_nav_model(); // Tabs are collected first to placate the borrowck - let tabs: Vec<_> = self.tab_model.iter().collect(); + let tabs: Box<[_]> = self.tab_model.iter().collect(); // Update main conf and each tab with the new config - let commands: Vec<_> = - std::iter::once(cosmic::command::set_theme(self.config.app_theme.theme())) - .chain(tabs.into_iter().map(|entity| { - self.update(Message::TabMessage( - Some(entity), - tab::Message::Config(self.config.tab), - )) - })) - .collect(); + let commands = std::iter::once(cosmic::command::set_theme(self.config.app_theme.theme())) + .chain(tabs.into_iter().map(|entity| { + self.update(Message::TabMessage( + Some(entity), + tab::Message::Config(self.config.tab), + )) + })); Task::batch(commands) } fn update_desktop(&mut self) -> Task { - let mut needs_reload = Vec::new(); - for entity in self.tab_model.iter() { - if let Some(tab) = self.tab_model.data::(entity) { + let needs_reload: Box<[_]> = (self.tab_model.iter()) + .filter_map(|entity| { + let tab = self.tab_model.data::(entity)?; if let Location::Desktop(path, output, _) = &tab.location { - needs_reload.push(( + Some(( entity, Location::Desktop(path.clone(), output.clone(), self.config.desktop), - )); + )) + } else { + None } - } - } + }) + .collect(); + let mut commands = Vec::with_capacity(needs_reload.len()); for (entity, location) in needs_reload { if let Some(tab) = self.tab_model.data_mut::(entity) { @@ -1536,9 +1533,7 @@ impl App { // Collect all mounter items let mut nav_items = Vec::new(); for (key, items) in &self.mounter_items { - for item in items { - nav_items.push((*key, item)); - } + nav_items.extend(items.iter().map(|item| (*key, item))); } // Sort by name lexically nav_items.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name())); @@ -1578,20 +1573,17 @@ impl App { if self.pending_operations.is_empty() { #[cfg(feature = "notify")] if let Some(notification_arc) = self.notification_opt.take() { - return Task::perform( - async move { - tokio::task::spawn_blocking(move || { - //TODO: this is nasty - let notification_mutex = Arc::try_unwrap(notification_arc).unwrap(); - let notification = notification_mutex.into_inner().unwrap(); - notification.close(); - }) - .await - .unwrap(); - cosmic::action::app(Message::MaybeExit) - }, - |x| x, - ); + return Task::future(async move { + tokio::task::spawn_blocking(move || { + //TODO: this is nasty + let notification_mutex = Arc::try_unwrap(notification_arc).unwrap(); + let notification = notification_mutex.into_inner().unwrap(); + notification.close(); + }) + .await + .unwrap(); + cosmic::action::app(Message::MaybeExit) + }); } } @@ -1612,14 +1604,14 @@ impl App { fn update_watcher(&mut self) -> Task { if let Some((mut watcher, old_paths)) = self.watcher_opt.take() { - let mut new_paths = FxHashSet::default(); - for entity in self.tab_model.iter() { - if let Some(tab) = self.tab_model.data::(entity) { - if let Some(path) = tab.location.path_opt() { - new_paths.insert(path.clone()); - } - } - } + let new_paths: FxHashSet<_> = self + .tab_model + .iter() + .filter_map(|entity| { + let tab = self.tab_model.data::(entity)?; + tab.location.path_opt().cloned() + }) + .collect(); // Unwatch paths no longer used for path in &old_paths { @@ -1679,7 +1671,7 @@ impl App { table = table.push(widget::divider::horizontal::light()); } } - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(fl!("network-drive-description")).into(), table.into(), ]) @@ -1693,7 +1685,7 @@ impl App { } = theme::active().cosmic().spacing; let config = self.config.desktop; - let mut children = Vec::new(); + let mut column = widget::column::with_capacity(2); let mut section = widget::settings::section().title(fl!("show-on-desktop")); section = section.add( @@ -1729,17 +1721,17 @@ impl App { }, ), ); - children.push(section.into()); + column = column.push(section); let mut section = widget::settings::section().title(fl!("icon-size-and-spacing")); - let icon_size: u16 = config.icon_size.into(); + let icon_size = config.icon_size; section = section.add( widget::settings::item::builder(fl!("icon-size")) .description(format!("{icon_size}%")) .control( - widget::slider(50..=500, icon_size, move |icon_size| { + widget::slider(50..=500, icon_size.get(), move |_| { Message::DesktopConfig(DesktopConfig { - icon_size: NonZeroU16::new(icon_size).unwrap(), + icon_size, ..config }) }) @@ -1747,23 +1739,23 @@ impl App { ), ); - let grid_spacing: u16 = config.grid_spacing.into(); + let grid_spacing = config.grid_spacing; section = section.add( widget::settings::item::builder(fl!("grid-spacing")) .description(format!("{grid_spacing}%")) .control( - widget::slider(50..=500, grid_spacing, move |grid_spacing| { + widget::slider(50..=500, grid_spacing.get(), move |_| { Message::DesktopConfig(DesktopConfig { - grid_spacing: NonZeroU16::new(grid_spacing).unwrap(), + grid_spacing, ..config }) }) .step(25u16), ), ); - children.push(section.into()); + column = column.push(section); - widget::column::with_children(children) + column .padding([0, space_l, space_l, space_l]) .spacing(space_m) .into() @@ -1781,8 +1773,8 @@ impl App { let mut section = widget::settings::section().title(fl!("pending")); for (id, (op, controller)) in self.pending_operations.iter().rev() { let progress = controller.progress(); - section = section.add(widget::column::with_children(vec![ - widget::row::with_children(vec![ + section = section.add(widget::column::with_children([ + widget::row::with_children([ widget::progress_bar(0.0..=1.0, progress) .height(progress_bar_height) .into(), @@ -1828,9 +1820,9 @@ impl App { if !self.failed_operations.is_empty() { let mut section = widget::settings::section().title(fl!("failed")); - for (_id, (op, controller, error)) in self.failed_operations.iter().rev() { + for (op, controller, error) in self.failed_operations.values().rev() { let progress = controller.progress(); - section = section.add(widget::column::with_children(vec![ + section = section.add(widget::column::with_children([ widget::text::body(op.pending_text(progress, controller.state())).into(), widget::text::body(error).into(), ])); @@ -1840,7 +1832,7 @@ impl App { if !self.complete_operations.is_empty() { let mut section = widget::settings::section().title(fl!("complete")); - for (_id, op) in self.complete_operations.iter().rev() { + for op in self.complete_operations.values().rev() { section = section.add(widget::text::body(op.completed_text())); } children.push(section.into()); @@ -1984,50 +1976,49 @@ impl App { let mut dedupe = FxHashSet::default(); // start with exact matches - for mime_app in self.mime_app_cache.get(mime_type) { - let app_id = &mime_app.id; - if !dedupe.contains(app_id) { - results.push((mime_app, MimeAppMatch::Exact)); - dedupe.insert(app_id); - } - } + results.extend( + self.mime_app_cache + .get(mime_type) + .iter() + .filter(|&mime_app| dedupe.insert(&mime_app.id)) + .map(|mime_app| (mime_app, MimeAppMatch::Exact)), + ); // grab matches based off of subclass / parent mime type if let Some(parent_types) = mime_icon::parent_mime_types(mime_type) { for parent_type in parent_types { - for mime_app in self.mime_app_cache.get(&parent_type) { - let app_id = &mime_app.id; - if !dedupe.contains(app_id) { - results.push((mime_app, MimeAppMatch::Related)); - dedupe.insert(app_id); - } - } + results.extend( + self.mime_app_cache + .get(&parent_type) + .iter() + .filter(|&mime_app| dedupe.insert(&mime_app.id)) + .map(|mime_app| (mime_app, MimeAppMatch::Related)), + ); } } // Add other apps - for mime_app in self.mime_app_cache.apps() { - let app_id = &mime_app.id; - if !dedupe.contains(app_id) { - results.push((mime_app, MimeAppMatch::Other)); - dedupe.insert(app_id); - } - } + results.extend( + self.mime_app_cache + .apps() + .iter() + .filter(|&mime_app| dedupe.insert(&mime_app.id)) + .map(|mime_app| (mime_app, MimeAppMatch::Other)), + ); results } // Update favorites based on renaming or moving dirs. - fn update_favorites(&mut self, path_changes: &[(PathBuf, PathBuf)]) -> bool { + fn update_favorites(&mut self, path_changes: &[(impl AsRef, impl AsRef)]) -> bool { let mut favorites_changed = false; let favorites = self .config .favorites .iter() - .cloned() .map(|favorite| { - if let Favorite::Path(ref path) = favorite { - for (from, to) in path_changes { + if let Favorite::Path(path) = favorite { + for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) { if path.starts_with(from) { if let Ok(relative) = path.strip_prefix(from) { favorites_changed = true; @@ -2036,7 +2027,7 @@ impl App { } } } - favorite + favorite.clone() }) .collect(); @@ -2216,7 +2207,7 @@ impl Application for App { for location in flags.uris { if let Some(e) = app.nav_model.iter().find(|e| { app.nav_model.data::(*e).is_some_and( - |l| matches!(l, Location::Network(uri, ..) if *uri == location.to_string()), + |l| matches!(l, Location::Network(uri, ..) if *uri == *location.as_str()), ) }) { commands.push(cosmic::task::message(cosmic::Action::App( @@ -2225,7 +2216,7 @@ impl Application for App { } } - if app.tab_model.iter().next().is_none() { + if app.tab_model.entity_at(0).is_none() { if let Ok(current_dir) = env::current_dir() { commands.push(app.open_tab(Location::Path(current_dir), true, None)); } else { @@ -2278,10 +2269,10 @@ impl Application for App { let favorite_index_opt = self.nav_model.data::(entity); let location_opt = self.nav_model.data::(entity); - let mut items = Vec::new(); + let mut items = Vec::with_capacity(7); if location_opt - .and_then(|x| x.path_opt()) + .and_then(Location::path_opt) .is_some_and(|x| x.is_file()) { items.push(cosmic::widget::menu::Item::Button( @@ -2569,13 +2560,13 @@ impl Application for App { if let Location::Network(uri, _, _) = tab .items_opt .as_ref() - .and_then(|items| items.iter().find(|i| i.path_opt() == Some(&path))) + .and_then(|items| items.iter().find(|&i| i.path_opt() == Some(&path))) .unwrap() .location_opt - .clone() + .as_ref() .unwrap() { - Some((uri, name, path.clone())) + Some((uri.clone(), name, path.clone())) } else { None } @@ -2586,7 +2577,7 @@ impl Application for App { } else { Favorite::from_path(path) }; - if !favorites.iter().any(|f| f == &favorite) { + if !favorites.contains(&favorite) { favorites.push(favorite); } } @@ -2598,7 +2589,7 @@ impl Application for App { return self.update_config(); } Message::Compress(entity_opt) => { - let paths = self.selected_paths(entity_opt); + let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); if let Some(current_path) = paths.first() { if let Some(destination) = current_path.parent().zip(current_path.file_stem()) { let to = destination.0.to_path_buf(); @@ -2634,13 +2625,13 @@ impl Application for App { } } let paths = self.selected_paths(entity_opt); - let contents = ClipboardCopy::new(ClipboardKind::Copy, &paths); + let contents = ClipboardCopy::new(ClipboardKind::Copy, paths); return clipboard::write_data(contents); } Message::Cut(entity_opt) => { self.set_cut(entity_opt); let paths = self.selected_paths(entity_opt); - let contents = ClipboardCopy::new(ClipboardKind::Cut { is_dnd: false }, &paths); + let contents = ClipboardCopy::new(ClipboardKind::Cut { is_dnd: false }, paths); return clipboard::write_data(contents); } Message::CloseToast(id) => { @@ -2678,7 +2669,7 @@ impl Application for App { } } } else { - let paths = self.selected_paths(entity_opt); + let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); if !paths.is_empty() { return self.delete(paths); } @@ -2741,12 +2732,11 @@ impl Application for App { return command.map(|_id| cosmic::Action::None); } - let mut tasks = Vec::new(); - for (id, kind) in &self.windows { - if matches!(kind, WindowKind::Dialogs(_)) { - tasks.push(window::close(*id)); - } - } + let tasks = self + .windows + .iter() + .filter(|(_, kind)| matches!(*kind, WindowKind::Dialogs(_))) + .map(|(id, _)| window::close(*id)); return Task::batch(tasks); } } @@ -2770,7 +2760,7 @@ impl Application for App { let name = format!("{name}{extension}"); let to = to.join(name); tasks.push(self.operation(Operation::Compress { - paths, + paths: paths.into_vec(), to, archive_type, password, @@ -2809,13 +2799,10 @@ impl Application for App { auth, auth_tx, } => { - tasks.push(Task::perform( - async move { - auth_tx.send(auth).await.unwrap(); - cosmic::action::none() - }, - |x| x, - )); + tasks.push(Task::future(async move { + auth_tx.send(auth).await.unwrap(); + cosmic::action::none() + })); } DialogPage::NetworkError { mounter_key: _, @@ -2915,7 +2902,7 @@ impl Application for App { ]); } Message::ExtractHere(entity_opt) => { - let paths = self.selected_paths(entity_opt); + let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); if let Some(destination) = paths .first() .and_then(|first| first.parent()) @@ -2929,7 +2916,8 @@ impl Application for App { } } Message::ExtractTo(entity_opt) => { - return self.extract_to(&self.selected_paths(entity_opt)); + let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); + return self.extract_to(&selected_paths); } Message::ExtractToResult(result) => { match result { @@ -2990,20 +2978,17 @@ impl Application for App { } TypeToSearch::EnterPath => { if let Some(tab) = self.tab_model.data_mut::(entity) { - let location = tab.edit_location.as_ref().map_or_else( - || tab.location.clone(), - |x| x.location.clone(), - ); + let location = tab + .edit_location + .as_ref() + .map_or_else(|| &tab.location, |x| &x.location); // Try to add text to end of location if let Some(path) = location.path_opt() { let mut path_string = - path.to_string_lossy().to_string(); + path.to_string_lossy().into_owned(); path_string.push_str(&text); - tab.edit_location = Some( - location - .with_path(PathBuf::from(path_string)) - .into(), - ); + tab.edit_location = + Some(location.with_path(path_string.into()).into()); } } } @@ -3062,23 +3047,21 @@ impl Application for App { let mut commands = Vec::new(); { let home_location = Location::Path(home_dir()); - let entities: Vec<_> = self.tab_model.iter().collect(); + let entities: Box<[_]> = self.tab_model.iter().collect(); for entity in entities { - let title_opt = match self.tab_model.data_mut::(entity) { - Some(tab) => { - if unmounted.iter().any(|unmounted| { + let title_opt = self.tab_model.data_mut::(entity).and_then(|tab| { + unmounted + .iter() + .any(|unmounted| { tab.location .path_opt() .is_some_and(|location| location.starts_with(unmounted)) - }) { + }) + .then(|| { tab.change_location(&home_location, None); - Some(tab.title()) - } else { - None - } - } - None => None, - }; + tab.title() + }) + }); if let Some(title) = title_opt { self.tab_model.text_set(entity, title); commands.push(self.update_tab(entity, home_location.clone(), None)); @@ -3150,7 +3133,11 @@ impl Application for App { ); } Message::NetworkResult(mounter_key, uri, res) => { - if self.network_drive_connecting == Some((mounter_key, uri.clone())) { + if self + .network_drive_connecting + .as_ref() + .is_some_and(|(m, u)| *m == mounter_key && *u == uri) + { self.network_drive_connecting = None; } match res { @@ -3176,10 +3163,10 @@ impl Application for App { Message::NewItem(entity_opt, dir) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { - if let Some(path) = &tab.location.path_opt() { + if let Some(path) = tab.location.path_opt() { return Task::batch([ self.dialog_pages.push_back(DialogPage::NewItem { - parent: (*path).clone(), + parent: path.clone(), name: String::new(), dir, }), @@ -3196,10 +3183,10 @@ impl Application for App { log::debug!("{events:?}"); let mut needs_reload = Vec::new(); - let entities: Vec<_> = self.tab_model.iter().collect(); + let entities: Box<[_]> = self.tab_model.iter().collect(); for entity in entities { if let Some(tab) = self.tab_model.data_mut::(entity) { - if let Some(path) = &tab.location.path_opt() { + if let Some(path) = tab.location.path_opt() { let mut contains_change = false; for event in &events { for event_path in &event.paths { @@ -3253,10 +3240,9 @@ impl Application for App { } } - let mut commands = Vec::with_capacity(needs_reload.len()); - for (entity, location) in needs_reload { - commands.push(self.update_tab(entity, location, None)); - } + let commands = needs_reload + .into_iter() + .map(|(entity, location)| self.update_tab(entity, location, None)); return Task::batch(commands); } Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take() @@ -3271,21 +3257,21 @@ impl Application for App { }, Message::OpenTerminal(entity_opt) => { if let Some(terminal) = self.mime_app_cache.terminal() { - let mut paths = Vec::new(); + let mut paths = Box::from([]); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { - if let Some(path) = &tab.location.path_opt() { + if let Some(path) = tab.location.path_opt() { if let Some(items) = tab.items_opt() { - for item in items { - if item.selected { - if let Some(path) = item.path_opt() { - paths.push(path.clone()); - } - } - } + paths = + items + .iter() + .filter_map(|item| { + if item.selected { item.path_opt() } else { None } + }) + .collect(); } if paths.is_empty() { - paths.push((*path).clone()); + paths = Box::from([path]); } } } @@ -3294,7 +3280,7 @@ impl Application for App { .command::<&str>(&[]) .and_then(|v| v.into_iter().next()) { - command.current_dir(&path); + command.current_dir(path); if let Err(err) = spawn_detached(&mut command) { log::warn!( "failed to open {} with terminal {:?}: {}", @@ -3310,20 +3296,19 @@ impl Application for App { } } Message::OpenInNewTab(entity_opt) => { - return Task::batch(self.selected_paths(entity_opt).into_iter().filter_map( - |path| { - if path.is_dir() { - Some(self.open_tab(Location::Path(path), false, None)) - } else { - None - } - }, - )); + let selected_paths: Box<[_]> = self + .selected_paths(entity_opt) + .filter(|p| p.is_dir()) + .collect(); + return Task::batch( + selected_paths + .into_iter() + .map(|path| self.open_tab(Location::Path(path), false, None)), + ); } Message::OpenInNewWindow(entity_opt) => match env::current_exe() { Ok(exe) => self .selected_paths(entity_opt) - .into_iter() .filter(|p| p.is_dir()) .for_each(|path| match process::Command::new(&exe).arg(path).spawn() { Ok(_child) => {} @@ -3336,13 +3321,12 @@ impl Application for App { } }, Message::OpenItemLocation(entity_opt) => { - return Task::batch(self.selected_paths(entity_opt).into_iter().filter_map( - |path| { - path.parent().map(Path::to_path_buf).map(|parent| { - self.open_tab(Location::Path(parent), true, Some(vec![path])) - }) - }, - )); + let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); + return Task::batch(selected_paths.into_iter().filter_map(|path| { + path.parent() + .map(Path::to_path_buf) + .map(|parent| self.open_tab(Location::Path(parent), true, Some(vec![path]))) + })); } Message::OpenWithBrowse => match self.dialog_pages.pop_front() { Some(( @@ -3428,7 +3412,7 @@ impl Application for App { } } Message::PasteContents(to, mut contents) => { - contents.paths.retain(|p| p != &to); + contents.paths.retain(|p| *p != to); if !contents.paths.is_empty() { return match contents.kind { ClipboardKind::Copy => self.operation(Operation::Copy { @@ -3483,18 +3467,16 @@ impl Application for App { // If a favorite for a path has been renamed or moved, update it. if let Operation::Rename { ref from, ref to } = op { - if self.update_favorites(&[(from.clone(), to.clone())]) { + if self.update_favorites([(from, to)].as_slice()) { commands.push(self.update_config()); } } else if let Operation::Move { ref paths, ref to, .. } = op { - let path_changes: Vec<_> = paths + let path_changes: Box<[_]> = paths .iter() - .filter_map(|from| { - from.file_name().map(|name| (from.clone(), to.join(name))) - }) + .filter_map(|from| from.file_name().map(|name| (from, to.join(name)))) .collect(); if self.update_favorites(&path_changes) { commands.push(self.update_config()); @@ -3510,8 +3492,8 @@ impl Application for App { // Close progress notification if all relevant operations are finished if !self .pending_operations - .iter() - .any(|(_id, (op, _))| op.show_progress_notification()) + .values() + .any(|(op, _)| op.show_progress_notification()) { self.progress_operations.clear(); } @@ -3550,8 +3532,8 @@ impl Application for App { // Close progress notification if all relevant operations are finished if !self .pending_operations - .iter() - .any(|(_id, (op, _))| op.show_progress_notification()) + .values() + .any(|(op, _)| op.show_progress_notification()) { self.progress_operations.clear(); } @@ -3578,7 +3560,7 @@ impl Application for App { } } Message::PermanentlyDelete(entity_opt) => { - let paths = self.selected_paths(entity_opt); + let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); if !paths.is_empty() { return self.push_dialog( DialogPage::PermanentlyDelete { paths }, @@ -3595,7 +3577,7 @@ impl Application for App { return cosmic::task::message(Message::SetShowDetails(show_details)); } Mode::Desktop => { - let selected_paths = self.selected_paths(entity_opt); + let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); let mut commands = Vec::with_capacity(selected_paths.len()); for path in selected_paths { let mut settings = window::Settings { @@ -3629,7 +3611,7 @@ impl Application for App { } } Message::RemoveFromRecents(entity_opt) => { - let paths = self.selected_paths(entity_opt); + let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); return self.operation(Operation::RemoveFromRecents { paths }); } Message::RescanRecents => { @@ -3653,35 +3635,34 @@ impl Application for App { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { if let Some(items) = tab.items_opt() { - let mut selected = Vec::new(); - for item in items { - if item.selected { - if let Some(path) = item.path_opt() { - selected.push(path.clone()); + let selected: Box<[_]> = items + .iter() + .filter_map(|item| { + if item.selected { + item.path_opt().cloned() + } else { + None } - } - } + }) + .collect(); if !selected.is_empty() { //TODO: batch rename - let mut tasks = Vec::new(); - for path in selected { - let parent = match path.parent() { - Some(some) => some.to_path_buf(), - None => continue, - }; - let name = match path.file_name().and_then(|x| x.to_str()) { - Some(some) => some.to_string(), - None => continue, - }; - let dir = path.is_dir(); - tasks.push(self.dialog_pages.push_back(DialogPage::RenameItem { - from: path, - parent, - name, - dir, - })); - } - tasks.push(widget::text_input::focus(self.dialog_text_input.clone())); + let tasks = selected + .into_iter() + .filter_map(|path| { + let parent = path.parent()?.to_path_buf(); + let name = path.file_name()?.to_str()?.to_string(); + let dir = path.is_dir(); + Some(self.dialog_pages.push_back(DialogPage::RenameItem { + from: path, + parent, + name, + dir, + })) + }) + .chain(std::iter::once(widget::text_input::focus( + self.dialog_text_input.clone(), + ))); return Task::batch(tasks); } } @@ -3691,13 +3672,10 @@ impl Application for App { if let Some((dialog_page, task)) = self.dialog_pages.pop_front() { match dialog_page { DialogPage::Replace { tx, .. } => { - return Task::perform( - async move { - let _ = tx.send(replace_result).await; - cosmic::action::none() - }, - |x| x, - ); + return Task::future(async move { + let _ = tx.send(replace_result).await; + cosmic::action::none() + }); } other => { log::warn!("tried to send replace result to the wrong dialog"); @@ -3786,15 +3764,15 @@ impl Application for App { return Task::batch(tasks); } Message::TabNext => { - let len = self.tab_model.iter().count(); + let len = self.tab_model.len(); let pos = self .tab_model .position(self.tab_model.active()) + .expect("should always be at least one tab open") // Wraparound to 0 if i + 1 > num of tabs - .map(|i| (i as usize + 1) % len) - .expect("should always be at least one tab open"); + + 1 % len as u16; - let entity = self.tab_model.iter().nth(pos); + let entity = self.tab_model.entity_at(pos); if let Some(entity) = entity { return self.update(Message::TabActivate(entity)); } @@ -3803,17 +3781,12 @@ impl Application for App { let pos = self .tab_model .position(self.tab_model.active()) - .and_then(|i| (i as usize).checked_sub(1)) + .expect("should always be at least one tab open") + .checked_sub(1) // Subtraction underflow => last tab; i.e. it wraps around - .unwrap_or_else(|| { - self.tab_model - .iter() - .count() - .checked_sub(1) - .unwrap_or_default() - }); + .unwrap_or_else(|| (self.tab_model.len() as u16).saturating_sub(1)); - let entity = self.tab_model.iter().nth(pos); + let entity = self.tab_model.entity_at(pos); if let Some(entity) = entity { return self.update(Message::TabActivate(entity)); } @@ -3892,7 +3865,7 @@ impl Application for App { tab::Command::AddToSidebar(path) => { let mut favorites = self.config.favorites.clone(); let favorite = Favorite::from_path(path); - if !favorites.iter().any(|f| f == &favorite) { + if !favorites.contains(&favorite) { favorites.push(favorite); } config_set!(favorites, favorites); @@ -4006,7 +3979,7 @@ impl Application for App { } tab::Command::OpenFile(paths) => commands.push(self.open_file(&paths)), tab::Command::OpenInNewTab(path) => { - commands.push(self.open_tab(Location::Path(path.clone()), false, None)); + commands.push(self.open_tab(Location::Path(path), false, None)); } tab::Command::OpenInNewWindow(path) => match env::current_exe() { Ok(exe) => match process::Command::new(&exe).arg(path).spawn() { @@ -4082,13 +4055,10 @@ impl Application for App { if !self.must_save_sort_names & changed { self.must_save_sort_names = true; - return cosmic::Task::perform( - async move { - tokio::time::sleep(Duration::from_secs(1)).await; - cosmic::action::app(Message::SaveSortNames) - }, - |x| x, - ); + return cosmic::Task::future(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + cosmic::action::app(Message::SaveSortNames) + }); } } } @@ -4212,10 +4182,7 @@ impl Application for App { self.core.set_main_window_id(None); return Task::batch([ window::close(window_id), - Task::perform( - async move { cosmic::action::app(Message::MaybeExit) }, - |x| x, - ), + Task::future(async move { cosmic::action::app(Message::MaybeExit) }), ]); } } @@ -4240,58 +4207,23 @@ impl Application for App { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let mut config = self.config.tab; if let Some(tab) = self.tab_model.data::(entity) { - match tab.config.view { - tab::View::List => config.icon_sizes.list = 100.try_into().unwrap(), - tab::View::Grid => config.icon_sizes.grid = 100.try_into().unwrap(), - } + zoom_to_default(tab.config.view, &mut config.icon_sizes); } return self.update(Message::TabConfig(config)); } Message::ZoomIn(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); - let zoom_in = |size: &mut NonZeroU16, min: u16, max: u16| { - let mut step = min; - while step <= max { - if size.get() < step { - *size = step.try_into().unwrap(); - break; - } - step += 25; - } - if size.get() > step { - *size = step.try_into().unwrap(); - } - }; let mut config = self.config.tab; if let Some(tab) = self.tab_model.data::(entity) { - match tab.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), - } + zoom_in_view(tab.config.view, &mut config.icon_sizes); } return self.update(Message::TabConfig(config)); } Message::ZoomOut(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); - let zoom_out = |size: &mut NonZeroU16, min: u16, max: u16| { - let mut step = max; - while step >= min { - if size.get() > step { - *size = step.try_into().unwrap(); - break; - } - step -= 25; - } - if size.get() < step { - *size = step.try_into().unwrap(); - } - }; let mut config = self.config.tab; if let Some(tab) = self.tab_model.data::(entity) { - match tab.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), - } + zoom_out_view(tab.config.view, &mut config.icon_sizes); } return self.update(Message::TabConfig(config)); } @@ -4435,8 +4367,8 @@ impl Application for App { if let Some(path) = self .nav_model .data::(entity) - .and_then(|x| x.path_opt()) - .map(ToOwned::to_owned) + .and_then(Location::path_opt) + .cloned() { return self.open_file(&[path]); } @@ -4445,7 +4377,7 @@ impl Application for App { if let Some(path) = self .nav_model .data::(entity) - .and_then(|x| x.path_opt()) + .and_then(Location::path_opt) .cloned() { match tab::item_from_path(&path, IconSizes::default()) { @@ -4544,7 +4476,7 @@ impl Application for App { if let Some(path) = self .nav_model .data::(entity) - .and_then(|location| location.path_opt()) + .and_then(Location::path_opt) { match tab::item_from_path(path, IconSizes::default()) { Ok(item) => { @@ -4588,7 +4520,8 @@ impl Application for App { Message::OutputEvent(output_event, output) => { match output_event { OutputEvent::Created(output_info_opt) => { - log::info!("output {}: created", output.id()); + let output_id = output.id(); + log::info!("output {output_id}: created"); let surface_id = WindowId::unique(); if let Some(old_surface_id) = @@ -4596,9 +4529,7 @@ impl Application for App { { //TODO: remove old surface? log::warn!( - "output {}: already had surface ID {:?}", - output.id(), - old_surface_id + "output {output_id}: already had surface ID {old_surface_id:?}" ); } @@ -4609,12 +4540,12 @@ impl Application for App { output_name } None => { - log::warn!("output {}: no output name", output.id()); + log::warn!("output {output_id}: no output name"); String::new() } }, None => { - log::warn!("output {}: no output info", output.id()); + log::warn!("output {output_id}: no output info"); String::new() } }; @@ -4705,14 +4636,14 @@ impl Application for App { Message::Eject => { #[cfg(feature = "gvfs")] { - let paths = self.selected_paths(None); - if let Some(p) = paths.first() { + let mut paths = self.selected_paths(None); + if let Some(p) = paths.next() { { for (k, mounter_items) in &self.mounter_items { if let Some(mounter) = MOUNTERS.get(k) { if let Some(item) = mounter_items .iter() - .find(|item| item.path().is_some_and(|path| path == *p)) + .find(|&item| item.path().is_some_and(|path| path == p)) { return mounter .unmount(item.clone()) @@ -4742,9 +4673,9 @@ impl Application for App { self.must_save_sort_names = false; if let Some(state_handler) = self.state_handler.as_ref() { if let Err(err) = state_handler - .set::>( + .set::<&FxOrderMap>( "sort_names", - self.state.sort_names.clone(), + &self.state.sort_names, ) { log::warn!("Failed to save sort names: {err:?}"); @@ -4795,7 +4726,7 @@ impl Application for App { ) .title(fl!("add-network-drive")) .header(text_input) - .footer(widget::row::with_children(vec![ + .footer(widget::row::with_children([ widget::horizontal_space().into(), button.into(), ])) @@ -4895,9 +4826,9 @@ impl Application for App { widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control( - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(fl!("file-name")).into(), - widget::row::with_children(vec![ + widget::row::with_children([ widget::text_input("", name.as_str()) .id(self.dialog_text_input.clone()) .on_input(move |name| { @@ -4936,8 +4867,8 @@ impl Application for App { ); if *archive_type == ArchiveType::Zip { - let password_unwrapped = password.clone().unwrap_or_else(String::default); - dialog = dialog.control(widget::column::with_children(vec![ + let password_unwrapped = password.clone().unwrap_or_default(); + dialog = dialog.control(widget::column::with_children([ widget::text::body(fl!("password")).into(), widget::text_input("", password_unwrapped) .password() @@ -5023,7 +4954,7 @@ impl Application for App { auth_tx, } => { //TODO: use URI! - let mut controls = Vec::with_capacity(4); + let mut controls = widget::column::with_capacity(4); let mut id_assigned = false; if let Some(username) = &auth.username_opt { @@ -5045,7 +4976,7 @@ impl Application for App { input = input.id(self.dialog_text_input.clone()); id_assigned = true; } - controls.push(input.into()); + controls = controls.push(input); } if let Some(domain) = &auth.domain_opt { @@ -5067,7 +4998,7 @@ impl Application for App { input = input.id(self.dialog_text_input.clone()); id_assigned = true; } - controls.push(input.into()); + controls = controls.push(input); } if let Some(password) = &auth.password_opt { @@ -5089,15 +5020,15 @@ impl Application for App { if !id_assigned { input = input.id(self.dialog_text_input.clone()); } - controls.push(input.into()); + controls = controls.push(input); } if let Some(remember) = &auth.remember_opt { //TODO: what should submit do? //TODO: button for showing password - controls.push( - widget::checkbox(fl!("remember-password"), *remember) - .on_toggle(move |value| { + controls = controls.push( + widget::checkbox(fl!("remember-password"), *remember).on_toggle( + move |value| { Message::DialogUpdate(DialogPage::NetworkAuth { mounter_key: *mounter_key, uri: uri.clone(), @@ -5107,8 +5038,8 @@ impl Application for App { }, auth_tx: auth_tx.clone(), }) - }) - .into(), + }, + ), ); } @@ -5119,7 +5050,7 @@ impl Application for App { let mut widget = widget::dialog() .title(title) .body(body) - .control(widget::column::with_children(controls).spacing(space_s)) + .control(controls.spacing(space_s)) .primary_action( widget::button::suggested(fl!("connect")).on_press(Message::DialogComplete), ) @@ -5205,7 +5136,7 @@ impl Application for App { widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control( - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(if *dir { fl!("folder-name") } else { @@ -5244,8 +5175,8 @@ impl Application for App { let item_height = 32.0; let mut displayed_default = false; let mut last_kind = MimeAppMatch::Exact; - for (i, (app, kind)) in available_apps.iter().enumerate() { - if *kind != last_kind { + for (i, &(app, kind)) in available_apps.iter().enumerate() { + if kind != last_kind { match kind { MimeAppMatch::Related => { column = column.add(widget::text::heading(fl!("related-apps"))); @@ -5255,12 +5186,12 @@ impl Application for App { } _ => {} } - last_kind = *kind; + last_kind = kind; } column = column.add( widget::mouse_area( widget::button::custom( - widget::row::with_children(vec![ + widget::row::with_children([ icon(app.icon.clone()).size(32).into(), if app.is_default && !displayed_default { displayed_default = true; @@ -5270,7 +5201,7 @@ impl Application for App { )) .into() } else { - widget::text::body(app.name.to_string()).into() + widget::text::body(app.name.clone()).into() }, widget::horizontal_space().into(), if *selected == i { @@ -5380,7 +5311,7 @@ impl Application for App { None } else { let path = parent.join(name); - if from != &path && path.exists() { + if *from != path && path.exists() { if path.is_dir() { dialog = dialog .tertiary_action(widget::text::body(fl!("folder-already-exists"))); @@ -5406,7 +5337,7 @@ impl Application for App { widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control( - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(if *dir { fl!("folder-name") } else { @@ -5591,8 +5522,8 @@ impl Application for App { let progress_bar = widget::progress_bar(0.0..=1.0, total_progress).height(progress_bar_height); - let container = widget::layer_container(widget::column::with_children(vec![ - widget::row::with_children(vec![ + let container = widget::layer_container(widget::column::with_children([ + widget::row::with_children([ progress_bar.into(), if all_paused { widget::tooltip( @@ -5626,7 +5557,7 @@ impl Application for App { .into(), widget::text::body(title).into(), widget::Space::with_height(space_s).into(), - widget::row::with_children(vec![ + widget::row::with_children([ widget::button::link(fl!("details")) .on_press(Message::ToggleContextPage(ContextPage::EditHistory)) .padding(0) @@ -5714,7 +5645,7 @@ impl Application for App { } } - if self.tab_model.iter().count() > 1 { + if self.tab_model.len() > 1 { tab_column = tab_column.push( widget::container( widget::tab_bar::horizontal(&self.tab_model) @@ -5787,14 +5718,14 @@ impl Application for App { tab_column.push(widget::toaster(&self.toasts, widget::horizontal_space())); return if let Some(margin) = self.margin.get(&id) { if margin.0 >= 0. || margin.2 >= 0. { - tab_column = widget::column::with_children(vec![ + tab_column = widget::column::with_children([ vertical_space().height(margin.0).into(), tab_column.into(), vertical_space().height(margin.2).into(), ]); } if margin.1 >= 0. || margin.3 >= 0. { - Element::from(widget::row::with_children(vec![ + Element::from(widget::row::with_children([ horizontal_space().width(margin.1).into(), tab_column.into(), horizontal_space().width(margin.3).into(), @@ -6140,24 +6071,21 @@ impl Application for App { ); } - for (key, mounter) in MOUNTERS.iter() { - subscriptions.push( - mounter.subscription().with(*key).map( - |(key, mounter_message)| match mounter_message { - MounterMessage::Items(items) => Message::MounterItems(key, items), - MounterMessage::MountResult(item, res) => { - Message::MountResult(key, item, res) - } - MounterMessage::NetworkAuth(uri, auth, auth_tx) => { - Message::NetworkAuth(key, uri, auth, auth_tx) - } - MounterMessage::NetworkResult(uri, res) => { - Message::NetworkResult(key, uri, res) - } - }, - ), - ); - } + subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| { + mounter + .subscription() + .with(*key) + .map(|(key, mounter_message)| match mounter_message { + MounterMessage::Items(items) => Message::MounterItems(key, items), + MounterMessage::MountResult(item, res) => Message::MountResult(key, item, res), + MounterMessage::NetworkAuth(uri, auth, auth_tx) => { + Message::NetworkAuth(key, uri, auth, auth_tx) + } + MounterMessage::NetworkResult(uri, res) => { + Message::NetworkResult(key, uri, res) + } + }) + })); if !self.pending_operations.is_empty() { //TODO: inhibit suspend/shutdown? @@ -6221,15 +6149,14 @@ impl Application for App { selected_preview = Some(entity_opt.unwrap_or_else(|| self.tab_model.active())); } } - for entity in self.tab_model.iter() { - if let Some(tab) = self.tab_model.data::(entity) { - subscriptions.push( - tab.subscription(selected_preview == Some(entity)) - .with(entity) - .map(|(entity, tab_msg)| Message::TabMessage(Some(entity), tab_msg)), - ); - } - } + subscriptions.extend(self.tab_model.iter().filter_map(|entity| { + let tab = self.tab_model.data::(entity)?; + Some( + tab.subscription(selected_preview == Some(entity)) + .with(entity) + .map(|(entity, tab_msg)| Message::TabMessage(Some(entity), tab_msg)), + ) + })); Subscription::batch(subscriptions) } @@ -6285,7 +6212,8 @@ pub(crate) mod test_utils { // Random alphanumeric String of length `len` fn rand_string(len: usize) -> String { - (0..len).map(|_| fastrand::alphanumeric()).collect() + let mut rng = fastrand::Rng::new(); + iter::repeat_with(|| rng.alphanumeric()).take(len).collect() } /// Create a small, temporary file hierarchy. @@ -6313,14 +6241,21 @@ pub(crate) mod test_utils { ); // All paths for directories and nested directories - let paths = (0..dirs).flat_map(|_| { + let paths = iter::repeat_with(|| { let root = root.as_ref(); let current = rand_string(name_len); iter::once(root.join(¤t)).chain( - (0..nested).map(move |_| root.join(format!("{current}/{}", rand_string(name_len)))), + iter::repeat_with(move || { + let mut path = root.join(¤t); + path.push(rand_string(name_len)); + path + }) + .take(nested), ) - }); + }) + .take(dirs) + .flatten(); // Create directories from `paths` and add a few files for path in paths { @@ -6381,17 +6316,17 @@ pub(crate) mod test_utils { } /// Filter `path` for directories - pub fn filter_dirs(path: &Path) -> io::Result> { + pub fn filter_dirs(path: &Path) -> io::Result + use<>> { Ok(path.read_dir()?.filter_map(|entry| { entry.ok().and_then(|entry| { let path = entry.path(); - if path.is_dir() { Some(path) } else { None } + path.is_dir().then_some(path) }) })) } // Filter `path` for files - pub fn filter_files(path: &Path) -> io::Result> { + pub fn filter_files(path: &Path) -> io::Result + use<>> { Ok(path.read_dir()?.filter_map(|entry| { entry.ok().and_then(|entry| { let path = entry.path(); @@ -6491,11 +6426,10 @@ pub(crate) mod test_utils { let items_len = tab.items_opt().map(Vec::len).unwrap_or_default(); assert_eq!(entries.len(), items_len); - let empty = Vec::new(); assert!( entries .into_iter() - .zip(tab.items_opt().unwrap_or(&empty)) + .zip(tab.items_opt().map_or([].as_slice(), Vec::as_slice)) .all(|(a, b)| eq_path_item(&a, b)), "Path ({}) and Tab path ({}) don't have equal contents", path.display(), diff --git a/src/archive.rs b/src/archive.rs index 75af4cf..5f0fe63 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -47,7 +47,7 @@ pub fn extract( controller: &Controller, ) -> Result<(), OperationError> { let mime = mime_for_path(path, None, false); - let password = password.clone(); + let password = password.as_deref(); match mime.essence_str() { "application/gzip" | "application/x-compressed-tar" => { OpReader::new(path, controller.clone()) @@ -107,7 +107,7 @@ pub fn extract( fn zip_extract>( archive: &mut zip::ZipArchive, directory: P, - password: Option, + password: Option<&str>, controller: Controller, ) -> zip::result::ZipResult<()> { use std::{ffi::OsString, fs}; @@ -145,7 +145,7 @@ fn zip_extract>( 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), Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), }?; @@ -207,7 +207,7 @@ fn zip_extract>( } continue; } - let mut file = match &password { + let mut file = match password { None => archive.by_index(i), Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), }?; diff --git a/src/clipboard.rs b/src/clipboard.rs index b0144b9..0c17a09 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -25,7 +25,7 @@ pub struct ClipboardCopy { } impl ClipboardCopy { - pub fn new>(kind: ClipboardKind, paths: &[P]) -> Self { + pub fn new>(kind: ClipboardKind, paths: impl IntoIterator) -> Self { let available = vec![ "text/plain".to_string(), "text/plain;charset=utf-8".to_string(), diff --git a/src/config.rs b/src/config.rs index 8689121..d537a3f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -75,21 +75,17 @@ pub enum Favorite { impl Favorite { pub fn from_path(path: PathBuf) -> Self { // Ensure that special folders are handled properly - for favorite in &[ + [ Self::Home, Self::Documents, Self::Downloads, Self::Music, Self::Pictures, Self::Videos, - ] { - if let Some(favorite_path) = favorite.path_opt() { - if favorite_path == path { - return favorite.clone(); - } - } - } - Self::Path(path) + ] + .into_iter() + .find(|fav| fav.path_opt().as_ref() == Some(&path)) + .unwrap_or(Self::Path(path)) } pub fn path_opt(&self) -> Option { diff --git a/src/dialog.rs b/src/dialog.rs index 0a4f063..44f3a45 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -31,9 +31,7 @@ use std::{ any::TypeId, collections::{HashMap, VecDeque}, env, fmt, fs, - num::NonZeroU16, path::PathBuf, - str::FromStr, time::{self, Instant}, }; @@ -48,6 +46,7 @@ use crate::{ menu, mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage}, tab::{self, ItemMetadata, Location, Tab}, + zoom::{zoom_in_view, zoom_out_view, zoom_to_default}, }; #[derive(Clone, Debug)] @@ -372,7 +371,7 @@ impl Dialog { let on_result_message = (self.on_result)(result); Task::batch([ command, - Task::perform(async move { cosmic::action::app(on_result_message) }, |x| x), + Task::future(async move { cosmic::action::app(on_result_message) }), ]) } else { command @@ -606,7 +605,7 @@ impl App { row = row.push( //TODO: easier way to create buttons with rich text 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]) .width(Length::Shrink) .height(space_l) @@ -677,40 +676,37 @@ impl App { let location = self.tab.location.clone(); let icon_sizes = self.tab.config.icon_sizes; let mounter_items = self.mounter_items.clone(); - Task::perform( - async move { - let location2 = location.clone(); - match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { - Ok((parent_item_opt, mut items)) => { - #[cfg(feature = "gvfs")] - { - let mounter_paths: Vec<_> = mounter_items - .iter() - .flat_map(|item| item.1.iter()) - .filter_map(MounterItem::path) - .collect(); - if !mounter_paths.is_empty() { - for item in &mut items { - item.is_mount_point = - item.path_opt().is_some_and(|p| mounter_paths.contains(p)); - } + Task::future(async move { + let location2 = location.clone(); + match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { + Ok((parent_item_opt, mut items)) => { + #[cfg(feature = "gvfs")] + { + let mounter_paths: Box<[_]> = mounter_items + .values() + .flatten() + .filter_map(MounterItem::path) + .collect(); + if !mounter_paths.is_empty() { + for item in &mut items { + item.is_mount_point = + 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, + )) } - }, - |x| x, - ) + Err(err) => { + log::warn!("failed to rescan: {err}"); + cosmic::action::none() + } + } + }) } fn search_get(&self) -> Option<&str> { @@ -835,12 +831,10 @@ impl App { // Collect all mounter items let mut nav_items = Vec::new(); for (key, items) in &self.mounter_items { - for item in items { - nav_items.push((*key, item)); - } + nav_items.extend(items.iter().map(|item| (*key, item))); } // 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 for (i, (key, item)) in nav_items.into_iter().enumerate() { nav_model = nav_model.insert(|mut b| { @@ -1040,7 +1034,7 @@ impl Application for App { //TODO: should gallery view just be a dialog? if self.tab.gallery { return Some( - widget::column::with_children(vec![ + widget::column::with_children([ self.tab.gallery_view().map(Message::TabMessage), // Draw button row as part of the overlay widget::container(self.button_view()) @@ -1098,7 +1092,7 @@ impl Application for App { widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control( - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(fl!("folder-name")).into(), widget::text_input("", name.as_str()) .id(self.dialog_text_input.clone()) @@ -1392,10 +1386,9 @@ impl Application for App { return self.search_set(Some(term)); } TypeToSearch::EnterPath => { - let location = self.tab.edit_location.as_ref().map_or_else( - || self.tab.location.clone(), - |x| x.location.clone(), - ); + let location = (self.tab.edit_location) + .as_ref() + .map_or_else(|| &self.tab.location, |x| &x.location); // Try to add text to end of location if let Some(path) = location.path_opt() { 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() { paths.push(path.clone()); let _ = update_recently_used( - &path.clone(), + path, Self::APP_ID.to_string(), "cosmic-files".to_string(), None, @@ -1775,9 +1768,9 @@ impl Application for App { // Filter if let Some(filter_i) = self.filter_selected { 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_mimes = Vec::new(); + let mut mimes = Vec::new(); for pattern in &filter.patterns { match pattern { DialogFilterPattern::Glob(value) => { @@ -1788,39 +1781,17 @@ impl Application for App { } } } - DialogFilterPattern::Mime(value) => { - match mime_guess::Mime::from_str(value) { - Ok(mime) => parsed_mimes.push(mime), - Err(err) => { - log::warn!("failed to parse mime {value:?}: {err}"); - } - } - } + DialogFilterPattern::Mime(value) => mimes.push(value.as_str()), } } items.retain(|item| { - if item.metadata.is_dir() { - // Directories are always shown - return true; - } - + // Directories are always shown + item.metadata.is_dir() // Check for mime type match (first because it is faster) - for mime in &parsed_mimes { - if mime == &item.mime { - return true; - } - } - + || mimes.iter().copied().any(|mime| mime == item.mime) // Check for glob match (last because it is slower) - for glob in &parsed_globs { - if glob.matches(&item.name) { - return true; - } - } - - // No filters matched - false + || parsed_globs.iter().any(|glob| glob.matches(&item.name)) }); } } @@ -1869,47 +1840,18 @@ impl Application for App { }); } Message::ZoomDefault => { - return self.with_dialog_config(|config| match config.view { - tab::View::List => config.icon_sizes.list = 100.try_into().unwrap(), - tab::View::Grid => config.icon_sizes.grid = 100.try_into().unwrap(), + return self.with_dialog_config(|config| { + zoom_to_default(config.view, &mut config.icon_sizes); }); } Message::ZoomIn => { - let zoom_in = |size: &mut NonZeroU16, min: u16, max: u16| { - let mut step = min; - 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), + return self.with_dialog_config(|config| { + zoom_in_view(config.view, &mut config.icon_sizes); }); } Message::ZoomOut => { - let zoom_out = |size: &mut NonZeroU16, min: u16, max: u16| { - let mut step = max; - 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), + return self.with_dialog_config(|config| { + zoom_out_view(config.view, &mut config.icon_sizes); }); } Message::Surface(action) => { @@ -2090,21 +2032,19 @@ impl Application for App { ); } - for (key, mounter) in MOUNTERS.iter() { - subscriptions.push( - mounter - .subscription() - .with(*key) - .map(|(key, mounter_message)| { - if let MounterMessage::Items(items) = mounter_message { - Message::MounterItems(key, items) - } else { - log::warn!("{mounter_message:?} not supported in dialog mode"); - Message::None - } - }), - ); - } + subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| { + mounter + .subscription() + .with(*key) + .map(|(key, mounter_message)| { + if let MounterMessage::Items(items) = mounter_message { + Message::MounterItems(key, items) + } else { + log::warn!("{mounter_message:?} not supported in dialog mode"); + Message::None + } + }) + })); Subscription::batch(subscriptions) } diff --git a/src/lib.rs b/src/lib.rs index 9b18488..6ffbe2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ mod mouse_area; pub mod operation; mod spawn_detached; use tab::Location; +mod zoom; use crate::config::State; pub mod tab; diff --git a/src/menu.rs b/src/menu.rs index db16472..6af7687 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -32,7 +32,7 @@ macro_rules! menu_button { ($($x:expr),+ $(,)?) => ( button::custom( Row::with_children( - vec![$(Element::from($x)),+] + [$(Element::from($x)),+] ) .height(Length::Fixed(24.0)) .align_y(Alignment::Center) @@ -167,9 +167,9 @@ pub fn context_menu<'a>( children.push(menu_item(fl!("open"), Action::Open).into()); #[cfg(feature = "desktop")] { - for (i, action) in entry.desktop_actions.into_iter().enumerate() { - children.push(menu_item(action.name, Action::ExecEntryAction(i)).into()); - } + children.extend(entry.desktop_actions.into_iter().enumerate().map( + |(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(), + )); } children.push(divider::horizontal::light().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(divider::horizontal::light().into()); - let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES - .iter() - .filter_map(|mime_type| mime_type.parse::().ok()) - .collect::>(); - selected_types.retain(|t| !supported_archive_types.contains(t)); + let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES; + selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m)); if selected_types.is_empty() { children.push(menu_item(fl!("extract-here"), Action::ExtractHere).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> { //TODO: only add some of these when in App mode - let children = vec![ + let children = [ menu_button!(text::body(fl!("open-in-new-tab"))) .on_press(tab::Message::LocationMenuAction( LocationMenuAction::OpenInNewTab(ancestor_index), diff --git a/src/mime_app.rs b/src/mime_app.rs index fba710b..1ad8643 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -83,7 +83,7 @@ pub fn exec_to_command( for invalid in path_opt .iter() .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:?}"); } @@ -221,7 +221,7 @@ fn filename_eq(path_opt: &Option, filename: &str) -> bool { pub struct MimeAppCache { apps: Vec, cache: FxHashMap>, - icons: FxHashMap>, + icons: FxHashMap>, terminals: Vec, } @@ -257,7 +257,7 @@ impl MimeAppCache { // Load desktop applications by supported mime types //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 { //TODO: just collect apps that can be executed with a file argument? if !app.mime_types.is_empty() { @@ -292,21 +292,17 @@ impl MimeAppCache { let mut mimeapps_paths = Vec::new(); let xdg_dirs = xdg::BaseDirectories::new(); - for path in xdg_dirs.find_data_files("applications/mimeapps.list") { - mimeapps_paths.push(path); - } + mimeapps_paths.extend(xdg_dirs.find_data_files("applications/mimeapps.list")); + for desktop in desktops.iter().rev() { - for path in xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list")) { - mimeapps_paths.push(path); - } - } - for path in xdg_dirs.find_config_files("mimeapps.list") { - mimeapps_paths.push(path); + mimeapps_paths + .extend(xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list"))); } + + mimeapps_paths.extend(xdg_dirs.find_config_files("mimeapps.list")); + for desktop in desktops.iter().rev() { - for path in xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list")) { - mimeapps_paths.push(path); - } + mimeapps_paths.extend(xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list"))); } //TODO: handle directory specific behavior @@ -334,7 +330,7 @@ impl MimeAppCache { .or_insert_with(|| Vec::with_capacity(1)); if !apps.iter().any(|x| filename_eq(&x.path, filename)) { 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)); } else { @@ -406,12 +402,12 @@ impl MimeAppCache { // Copy icons to special cache //TODO: adjust dropdown API so this is no longer needed - for (mime, apps) in &self.cache { - self.icons.insert( + self.icons.extend(self.cache.iter().map(|(mime, apps)| { + ( mime.clone(), apps.iter().map(|app| app.icon.clone()).collect(), - ); - } + ) + })); let elapsed = start.elapsed(); log::info!("loaded mime app cache in {elapsed:?}"); @@ -426,7 +422,7 @@ impl MimeAppCache { } 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 { diff --git a/src/mime_icon.rs b/src/mime_icon.rs index 73674ee..52b1a27 100644 --- a/src/mime_icon.rs +++ b/src/mime_icon.rs @@ -41,10 +41,8 @@ impl MimeIconCache { let icon_name = icon_names.remove(0); let mut named = icon::from_name(icon_name).size(key.size); if !icon_names.is_empty() { - let mut fallback_names = Vec::with_capacity(icon_names.len()); - for fallback_name in icon_names { - fallback_names.push(fallback_name.into()); - } + let fallback_names = + icon_names.into_iter().map(std::borrow::Cow::from).collect(); named = named.fallback(Some(icon::IconFallback::Names(fallback_names))); } Some(named.handle()) @@ -55,8 +53,8 @@ impl MimeIconCache { static MIME_ICON_CACHE: LazyLock> = LazyLock::new(|| Mutex::new(MimeIconCache::new())); -pub fn mime_for_path>( - path: P, +pub fn mime_for_path( + path: impl AsRef, metadata_opt: Option<&fs::Metadata>, remote: bool, ) -> Mime { @@ -65,7 +63,7 @@ pub fn mime_for_path>( // Try the shared mime info cache first let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type(); 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); } } else { @@ -75,11 +73,22 @@ pub fn mime_for_path>( gb.metadata(metadata.clone()); } 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 mime_guess::from_path(path).first_or_octet_stream() } else { - guess.mime_type().clone() + guessed_mime.clone() } } diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 7c84039..4d2a44d 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -30,45 +30,48 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { } fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems { - let mut items = MounterItems::new(); - for (i, mount) in monitor.mounts().into_iter().enumerate() { - items.push(MounterItem::Gvfs(Item { - uri: MountExt::root(&mount).uri().to_string(), - kind: ItemKind::Mount, - index: i, - name: MountExt::name(&mount).to_string(), - is_mounted: true, - icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()), - 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() { + let mut items: MounterItems = (monitor.mounts().into_iter()) + .enumerate() + .map(|(i, mount)| { + MounterItem::Gvfs(Item { + uri: mount.root().uri().into(), + kind: ItemKind::Mount, + index: i, + name: mount.name().into(), + is_mounted: true, + icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()), + icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16), + path_opt: MountExt::root(&mount).path(), + }) + }) + .collect(); + items.extend( + (monitor.volumes().into_iter()) + .enumerate() // Volumes with mounts are already listed by mount - continue; - } - let uri = VolumeExt::activation_root(&volume) - .map(|f| f.uri().to_string()) - .unwrap_or_default(); - items.push(MounterItem::Gvfs(Item { - // TODO can we get URI for volumes with no mount? - uri, - kind: ItemKind::Volume, - index: i, - name: VolumeExt::name(&volume).to_string(), - is_mounted: false, - icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()), - icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16), - path_opt: None, - })); - } + .filter(|(_, volume)| volume.get_mount().is_none()) + .map(|(i, volume)| { + let uri = VolumeExt::activation_root(&volume) + .map(|f| f.uri().into()) + .unwrap_or_default(); + MounterItem::Gvfs(Item { + // TODO can we get URI for volumes with no mount? + uri, + kind: ItemKind::Volume, + index: i, + name: volume.name().into(), + is_mounted: false, + icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()), + icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16), + path_opt: None, + }) + }), + ); items } fn network_scan(uri: &str, sizes: IconSizes) -> Result, 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:///"); // Resolve the target-uri if it exists @@ -78,8 +81,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { gio::Cancellable::NONE, ) { if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) { - uri = resolved_uri.to_string(); - file = gio::File::for_uri(&uri); + file = gio::File::for_uri(resolved_uri.as_str()); } } @@ -89,10 +91,10 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { .map_err(err_str)? { let info = info_res.map_err(err_str)?; - let name = info.name().to_string_lossy().to_string(); - let display_name = info.display_name().to_string(); + let name = info.name().to_string_lossy().into_owned(); + 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? let location = Location::Network(uri, display_name.clone(), file.child(&name).path()); @@ -100,11 +102,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) { let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED); let is_dir = matches!(info.file_type(), gio::FileType::Directory); - let size_opt = if is_dir { - None - } else { - Some(info.size() as u64) - }; + let size_opt = (!is_dir).then_some(info.size() as u64); let mut children_opt = None; if is_dir { @@ -189,31 +187,21 @@ fn mount_op(uri: String, event_tx: mpsc::UnboundedSender) -> gio::MountOp move |mount_op, message, default_user, default_domain, flags| { let auth = MounterAuth { message: message.to_string(), - username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) { - Some(default_user.to_string()) - } else { - None - }, - domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) { - Some(default_domain.to_string()) - } else { - None - }, - password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) { - Some(String::new()) - } else { - None - }, - 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 - }, + username_opt: flags + .contains(gio::AskPasswordFlags::NEED_USERNAME) + .then(|| default_user.to_string()), + domain_opt: flags + .contains(gio::AskPasswordFlags::NEED_DOMAIN) + .then(|| default_domain.to_string()), + password_opt: flags + .contains(gio::AskPasswordFlags::NEED_PASSWORD) + .then(String::new), + remember_opt: flags + .contains(gio::AskPasswordFlags::SAVING_SUPPORTED) + .then_some(false), + anonymous_opt: flags + .contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) + .then_some(false), }; let (auth_tx, mut auth_rx) = mpsc::channel(1); event_tx @@ -458,7 +446,7 @@ impl Gvfs { gio::Cancellable::NONE, ) { 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); } } @@ -597,12 +585,9 @@ impl Mounter for Gvfs { fn unmount(&self, item: MounterItem) -> Task<()> { let command_tx = self.command_tx.clone(); - Task::perform( - async move { - command_tx.send(Cmd::Unmount(item)).unwrap(); - }, - |x| x, - ) + Task::future(async move { + command_tx.send(Cmd::Unmount(item)).unwrap(); + }) } fn subscription(&self) -> Subscription { diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 23fd18f..49dd415 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -220,8 +220,9 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf { let file_name = file_name.to_string(); COMPOUND_EXTENSIONS .iter() - .find(|&&ext| file_name.ends_with(ext)) - .map(|&ext| { + .copied() + .find(|&ext| file_name.ends_with(ext)) + .map(|ext| { ( file_name.strip_suffix(ext).unwrap().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)) { break; @@ -329,7 +330,7 @@ pub enum Operation { EmptyTrash, /// Uncompress files Extract { - paths: Vec, + paths: Box<[PathBuf]>, to: PathBuf, password: Option, }, @@ -347,10 +348,10 @@ pub enum Operation { }, /// Permanently delete items, skipping the trash PermanentlyDelete { - paths: Vec, + paths: Box<[PathBuf]>, }, RemoveFromRecents { - paths: Vec, + paths: Box<[PathBuf]>, }, Rename { from: PathBuf, @@ -1013,7 +1014,7 @@ impl Operation { } Self::RemoveFromRecents { paths } => { tokio::task::spawn_blocking(move || { - let path_refs = paths.iter().map(PathBuf::as_path).collect::>(); + let path_refs = paths.iter().map(PathBuf::as_path).collect::>(); recently_used_xbel::remove_recently_used(&path_refs) }) .await diff --git a/src/operation/recursive.rs b/src/operation/recursive.rs index c6caac5..5233028 100644 --- a/src/operation/recursive.rs +++ b/src/operation/recursive.rs @@ -52,7 +52,7 @@ impl Context { pub async fn recursive_copy_or_move( &mut self, - from_to_pairs: Vec<(PathBuf, PathBuf)>, + from_to_pairs: impl IntoIterator, method: Method, ) -> Result { let mut ops = Vec::new(); @@ -148,9 +148,8 @@ impl Context { } // Add cleanup ops after standard ops, in reverse - for cleanup_op in cleanup_ops.into_iter().rev() { - ops.push(cleanup_op); - } + cleanup_ops.reverse(); + ops.append(&mut cleanup_ops); let total_ops = ops.len(); for (current_ops, mut op) in ops.into_iter().enumerate() { diff --git a/src/tab.rs b/src/tab.rs index c07a28e..4101525 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -54,7 +54,7 @@ use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, cell::{Cell, RefCell}, - cmp::Ordering, + cmp::{Ordering, Reverse}, collections::HashMap, error::Error, fmt::{self, Display}, @@ -483,18 +483,11 @@ impl Display for FormatTime<'_> { }; if datetime.date_naive() == now.date_naive() { - write!( - f, - "{}, {}", - fl!("today"), - self.time_formatter.format(&icu_datetime).to_string() - ) + f.write_str(fl!("today").as_str())?; + f.write_str(", ")?; + self.time_formatter.format(&icu_datetime).fmt(f) } else { - write!( - f, - "{}", - self.date_time_formatter.format(&icu_datetime).to_string() - ) + self.date_time_formatter.format(&icu_datetime).fmt(f) } } } @@ -606,11 +599,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS let remote = file_info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE); let is_dir = matches!(file_info.file_type(), gio::FileType::Directory); - let size_opt = if is_dir { - None - } else { - Some(file_info.size() as u64) - }; + let size_opt = (!is_dir).then_some(file_info.size() as u64); let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = if is_dir { ( @@ -673,8 +662,10 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS } } + let hidden = file_name.starts_with('.'); + Item { - name: file_name.clone().to_string(), + name: file_name.into(), display_name, is_mount_point: false, metadata: ItemMetadata::GvfsPath { @@ -682,7 +673,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS size_opt, children_opt, }, - hidden: file_name.starts_with('.'), + hidden, location_opt: Some(Location::Path(path)), mime, icon_handle_grid, @@ -790,7 +781,7 @@ pub fn item_from_entry( widget::icon::from_name(&*icon_name) .size(sizes.list()) .handle(), - widget::icon::from_name(&*icon_name) + widget::icon::from_name(icon_name) .size(sizes.list_condensed()) .handle(), ) @@ -833,11 +824,7 @@ pub fn item_from_entry( icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - thumbnail_opt: if remote { - Some(ItemThumbnail::NotImage) - } else { - None - }, + thumbnail_opt: remote.then_some(ItemThumbnail::NotImage), button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), @@ -870,7 +857,7 @@ pub fn item_from_path>(path: P, sizes: IconSizes) -> Result Vec { let mut items = Vec::new(); - let mut hidden_files = Vec::new(); + let mut hidden_files = Box::from([]); let mut remote_scannable = false; #[cfg(feature = "gvfs")] @@ -880,19 +867,15 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec { let file = gio::File::for_path(tab_path); // gio crate expects a comma delimited string - let mut attr_string = String::new(); - for attr in [ - gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, - gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE, - gio::FILE_ATTRIBUTE_TIME_MODIFIED, - gio::FILE_ATTRIBUTE_STANDARD_SIZE, - gio::FILE_ATTRIBUTE_STANDARD_TYPE, - gio::FILE_ATTRIBUTE_STANDARD_NAME, - ] { - attr_string.push_str(attr); - attr_string.push(','); - } - attr_string.pop(); + let attr_string = [ + gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(), + gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE.as_str(), + gio::FILE_ATTRIBUTE_TIME_MODIFIED.as_str(), + gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(), + gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(), + gio::FILE_ATTRIBUTE_STANDARD_NAME.as_str(), + ] + .join(","); match gio::prelude::FileExt::enumerate_children( &file, @@ -902,10 +885,12 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec { ) { Ok(res) => { remote_scannable = true; - for file in res.flatten() { - let full_path = Path::new(tab_path).join(file.name()); - items.push(item_from_gvfs_info(full_path, file, sizes)); - } + items = res + .filter_map(|file| { + let file = file.ok()?; + Some(item_from_gvfs_info(tab_path.join(file.name()), file, sizes)) + }) + .collect(); } Err(err) => { log::warn!( @@ -922,60 +907,62 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec { if !remote_scannable { match fs::read_dir(tab_path) { Ok(entries) => { - for entry_res in entries { - let entry = match entry_res { - Ok(ok) => ok, - Err(err) => { - log::warn!("failed to read entry in {}: {}", tab_path.display(), err); - continue; + items = entries + .filter_map(|entry_res| { + let entry = entry_res + .inspect_err(|err| { + log::warn!( + "failed to read entry in {}: {}", + tab_path.display(), + err + ) + }) + .ok()?; + + let path = entry.path(); + + let name = entry + .file_name() + .into_string() + .inspect_err(|name_os| { + log::warn!( + "failed to parse entry at {}: {:?} is not valid UTF-8", + path.display(), + name_os + ) + }) + .ok()?; + + if name == ".hidden" && path.is_file() { + hidden_files = parse_hidden_file(&path); } - }; - let path = entry.path(); + let metadata = fs::metadata(&path) + .inspect_err(|err| { + log::warn!( + "failed to read metadata for entry at {}: {}", + path.display(), + err + ) + }) + .ok()?; - let name = match entry.file_name().into_string() { - Ok(ok) => ok, - Err(name_os) => { - log::warn!( - "failed to parse entry at {}: {:?} is not valid UTF-8", - path.display(), - name_os - ); - continue; - } - }; - - if name == ".hidden" && path.is_file() { - hidden_files = parse_hidden_file(&path); - } - - let metadata = match fs::metadata(&path) { - Ok(ok) => ok, - Err(err) => { - log::warn!( - "failed to read metadata for entry at {}: {}", - path.display(), - err - ); - continue; - } - }; - - items.push(item_from_entry(path, name, metadata, sizes)); - } + Some(item_from_entry(path, name, metadata, sizes)) + }) + .collect(); } Err(err) => { log::warn!("failed to read directory {}: {}", tab_path.display(), err); } } } - items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) { + items.sort_unstable_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name), }); for item in &mut items { - if hidden_files.iter().any(|hidden| &item.name == hidden) { + if hidden_files.contains(&item.name) { item.hidden = true; } } @@ -1074,70 +1061,69 @@ pub fn scan_trash(_sizes: IconSizes) -> Vec { ) ))] pub fn scan_trash(sizes: IconSizes) -> Vec { - let mut items: Vec = Vec::new(); - match trash::os_limited::list() { - Ok(entries) => { - for entry in entries { - let metadata = match trash::os_limited::metadata(&entry) { - Ok(ok) => ok, - Err(err) => { - log::warn!("failed to get metadata for trash item {entry:?}: {err}"); - continue; + let entries = match trash::os_limited::list() { + Ok(entry) => entry, + Err(err) => { + log::warn!("failed to read trash items: {err}"); + return Vec::new(); + } + }; + let mut items: Vec<_> = entries + .into_iter() + .filter_map(|entry| { + let metadata = trash::os_limited::metadata(&entry) + .inspect_err(|err| { + log::warn!("failed to get metadata for trash item {entry:?}: {err}") + }) + .ok()?; + let original_path = entry.original_path(); + let name = entry.name.to_string_lossy().into_owned(); + let display_name = Item::display_name(&name); + + let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = + match metadata.size { + trash::TrashItemSize::Entries(_) => ( + //TODO: make this a static + "inode/directory".parse().unwrap(), + folder_icon(&original_path, sizes.grid()), + folder_icon(&original_path, sizes.list()), + folder_icon(&original_path, sizes.list_condensed()), + ), + trash::TrashItemSize::Bytes(_) => { + // This passes remote = true so it does not read from the original path + let mime = mime_for_path(&original_path, None, true); + ( + mime.clone(), + mime_icon(mime.clone(), sizes.grid()), + mime_icon(mime.clone(), sizes.list()), + mime_icon(mime, sizes.list_condensed()), + ) } }; - let original_path = entry.original_path(); - let name = entry.name.to_string_lossy().to_string(); - let display_name = Item::display_name(&name); - - let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = - match metadata.size { - trash::TrashItemSize::Entries(_) => ( - //TODO: make this a static - "inode/directory".parse().unwrap(), - folder_icon(&original_path, sizes.grid()), - folder_icon(&original_path, sizes.list()), - folder_icon(&original_path, sizes.list_condensed()), - ), - trash::TrashItemSize::Bytes(_) => { - // This passes remote = true so it does not read from the original path - let mime = mime_for_path(&original_path, None, true); - ( - mime.clone(), - mime_icon(mime.clone(), sizes.grid()), - mime_icon(mime.clone(), sizes.list()), - mime_icon(mime, sizes.list_condensed()), - ) - } - }; - - items.push(Item { - name, - display_name, - is_mount_point: false, - metadata: ItemMetadata::Trash { metadata, entry }, - hidden: false, - location_opt: None, - mime, - icon_handle_grid, - icon_handle_list, - icon_handle_list_condensed, - thumbnail_opt: Some(ItemThumbnail::NotImage), - button_id: widget::Id::unique(), - pos_opt: Cell::new(None), - rect_opt: Cell::new(None), - selected: false, - highlighted: false, - overlaps_drag_rect: false, - dir_size: DirSize::NotDirectory, - cut: false, - }); - } - } - Err(err) => { - log::warn!("failed to read trash items: {err}"); - } - } + Some(Item { + name, + display_name, + is_mount_point: false, + metadata: ItemMetadata::Trash { metadata, entry }, + hidden: false, + location_opt: None, + mime, + icon_handle_grid, + icon_handle_list, + icon_handle_list_condensed, + thumbnail_opt: Some(ItemThumbnail::NotImage), + button_id: widget::Id::unique(), + pos_opt: Cell::new(None), + rect_opt: Cell::new(None), + selected: false, + highlighted: false, + overlaps_drag_rect: false, + dir_size: DirSize::NotDirectory, + cut: false, + }) + }) + .collect(); items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, @@ -1158,71 +1144,53 @@ fn uri_to_path(uri: String) -> Option { } pub fn scan_recents(sizes: IconSizes) -> Vec { - let mut recents = Vec::new(); - - match recently_used_xbel::parse_file() { - Ok(recent_files) => { - for bookmark in recent_files.bookmarks { - let uri = bookmark.href; - let path = match uri_to_path(uri) { - None => continue, - Some(path) => path, - }; - let last_edit = match bookmark.modified.parse::>() { - Ok(last_edit) => last_edit, - Err(_) => continue, - }; - let last_visit = match bookmark.visited.parse::>() { - Ok(last_visit) => last_visit, - Err(_) => continue, - }; - let path_exist = path.exists(); - - if path_exist { - let file_name = path.file_name(); - - if let Some(name) = file_name { - let name = name.to_string_lossy().to_string(); - - let metadata = match path.metadata() { - Ok(ok) => ok, - Err(err) => { - log::warn!( - "failed to read metadata for entry at {}: {}", - path.display(), - err - ); - continue; - } - }; - - let item = item_from_entry(path, name, metadata, sizes); - recents.push(( - item, - if last_edit.le(&last_visit) { - last_edit - } else { - last_visit - }, - )); - } - } else { - log::warn!("recent file path not exist: {}", path.display()); - } - } - } + let recent_files = match recently_used_xbel::parse_file() { + Ok(recent_files) => recent_files, Err(err) => { log::warn!("Error reading recent files: {err:?}"); + return Vec::new(); } - } + }; + let mut recents: Vec<_> = recent_files + .bookmarks + .into_iter() + .filter_map(|bookmark| { + let path = uri_to_path(bookmark.href)?; + let last_edit = bookmark.modified.parse::>().ok()?; + let last_visit = bookmark.visited.parse::>().ok()?; - recents.sort_by(|a, b| b.1.cmp(&a.1)); + if path.exists() { + let file_name = path.file_name()?; + let name = file_name.to_string_lossy().to_string(); + + let metadata = match path.metadata() { + Ok(ok) => ok, + Err(err) => { + log::warn!( + "failed to read metadata for entry at {}: {}", + path.display(), + err + ); + return None; + } + }; + + let item = item_from_entry(path, name, metadata, sizes); + Some((item, last_edit.min(last_visit))) + } else { + log::warn!("recent file path not exist: {}", path.display()); + None + } + }) + .collect(); + + recents.sort_by_key(|recent| Reverse(recent.1)); recents.into_iter().take(50).map(|(item, _)| item).collect() } pub fn scan_network(uri: &str, sizes: IconSizes) -> Vec { - for (_key, mounter) in MOUNTERS.iter() { + for mounter in MOUNTERS.values() { match mounter.network_scan(uri, sizes) { Some(Ok(items)) => return items, Some(Err(err)) => { @@ -1250,12 +1218,12 @@ pub fn scan_desktop( } if desktop_config.show_mounted_drives { - for (_mounter_key, mounter) in MOUNTERS.iter() { - for mounter_item in mounter.items(sizes).unwrap_or_default() { - let Some(path) = mounter_item.path() else { - continue; - }; - + for mounter in MOUNTERS.values() { + let Some(mounter_items) = mounter.items(sizes) else { + continue; + }; + items.extend(mounter_items.into_iter().filter_map(|mounter_item| { + let path = mounter_item.path()?; // Get most item data from path let mut item = match item_from_path(&path, sizes) { Ok(item) => item, @@ -1265,7 +1233,7 @@ pub fn scan_desktop( path.display(), err ); - continue; + return None; } }; @@ -1275,13 +1243,13 @@ pub fn scan_desktop( //TODO: use icon size for mounter item icon if let Some(icon) = mounter_item.icon(false) { - item.icon_handle_grid = icon.clone(); - item.icon_handle_list = icon.clone(); + item.icon_handle_grid.clone_from(&icon); + item.icon_handle_list.clone_from(&icon); item.icon_handle_list_condensed = icon; } - items.push(item); - } + Some(item) + })); } } @@ -1415,17 +1383,17 @@ impl Location { } pub fn ancestors(&self) -> Vec<(Self, String)> { - let mut ancestors = Vec::new(); - if let Some(path) = self.path_opt() { - for ancestor in path.ancestors() { - let (name, found_home) = folder_name(ancestor); - ancestors.push((self.with_path(ancestor.to_path_buf()), name)); - if found_home { - break; - } - } - } - ancestors + self.path_opt().map_or_else(Default::default, |path| { + path.ancestors() + .scan(false, |found_home, ancestor| { + (!*found_home).then(|| { + let (name, is_home) = folder_name(ancestor); + *found_home = is_home; + (self.with_path(ancestor.to_path_buf()), name) + }) + }) + .collect() + }) } pub const fn path_opt(&self) -> Option<&PathBuf> { @@ -1438,6 +1406,16 @@ impl Location { } } + pub(crate) fn into_path_opt(self) -> Option { + match self { + Self::Desktop(path, ..) => Some(path), + Self::Path(path) => Some(path), + Self::Search(path, ..) => Some(path), + Self::Network(_, _, path) => path, + _ => None, + } + } + pub fn with_path(&self, path: PathBuf) -> Self { match self { Self::Desktop(_, display, desktop_config) => { @@ -1690,13 +1668,7 @@ impl ItemMetadata { pub fn file_size(&self) -> Option { match self { - Self::Path { metadata, .. } => { - if metadata.is_dir() { - None - } else { - Some(metadata.len()) - } - } + Self::Path { metadata, .. } => (!metadata.is_dir()).then_some(metadata.len()), Self::Trash { metadata, .. } => match metadata.size { TrashItemSize::Bytes(size) => Some(size), TrashItemSize::Entries(_) => None, @@ -2180,8 +2152,8 @@ impl Item { #[cfg(feature = "gvfs")] ItemMetadata::GvfsPath { children_opt, .. } => { // grab the fs::metadata object for gvfs paths since this is run on-demand - if let Some(path) = &self.path_opt() { - file_metadata = fs::metadata(*path).ok(); + if let Some(path) = self.path_opt() { + file_metadata = fs::metadata(path).ok(); } dir_children_count = *children_opt; @@ -2320,9 +2292,7 @@ impl Item { if !settings.is_empty() { let mut section = widget::settings::section(); - for setting in settings { - section = section.add(setting); - } + section = section.extend(settings); column = column.push(section); } @@ -2521,7 +2491,7 @@ fn folder_name>(path: P) -> (String, bool) { // This is not optimized but it helps ensure the same display names match item_from_path(path, IconSizes::default()) { Ok(item) => item.display_name, - Err(_err) => name.to_string_lossy().to_string(), + Err(_err) => name.to_string_lossy().into_owned(), } } } @@ -2533,10 +2503,9 @@ fn folder_name>(path: P) -> (String, bool) { } // parse .hidden file and return files path -fn parse_hidden_file(path: &PathBuf) -> Vec { - let file = match File::open(path) { - Ok(f) => f, - Err(_) => return Vec::new(), +fn parse_hidden_file(path: &PathBuf) -> Box<[String]> { + let Ok(file) = File::open(path) else { + return Default::default(); }; BufReader::new(file) @@ -2645,11 +2614,9 @@ impl Tab { if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { item.cut = false; - if let Some(location) = &item.location_opt { - if locations - .iter() - .any(|s| location.path_opt().is_some_and(|b| b == s)) - { + if let Some(location_path) = item.location_opt.as_ref().and_then(Location::path_opt) + { + if locations.contains(location_path) { item.cut = true; } } @@ -2658,17 +2625,20 @@ impl Tab { } pub fn selected_locations(&self) -> Vec { - let mut locations = Vec::new(); if let Some(ref items) = self.items_opt { - for item in items { - if item.selected { - if let Some(location) = &item.location_opt { - locations.push(location.clone()); + items + .iter() + .filter_map(|item| { + if item.selected { + item.location_opt.clone() + } else { + None } - } - } + }) + .collect() + } else { + Vec::new() } - locations } pub fn select_all(&mut self) { @@ -3065,13 +3035,15 @@ impl Tab { // a sorted tab. let min = indices .iter() - .position(|&offset| offset == range_min) + .copied() + .position(|offset| offset == range_min) .unwrap_or_default(); // We can't skip `min_real` elements here because the index of // `max` may actually be before `min` in a sorted tab let max = indices .iter() - .position(|&offset| offset == range_max) + .copied() + .position(|offset| offset == range_max) .unwrap_or(indices.len()); let min_real = min.min(max); let max_real = max.max(min); @@ -3096,7 +3068,7 @@ impl Tab { let dont_unset = mod_ctrl || self.column_sort().is_some_and(|l| { l.iter() - .any(|(e_i, e)| Some(e_i) == click_i_opt.as_ref() && e.selected) + .any(|&(e_i, e)| Some(e_i) == click_i_opt && e.selected) }); if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { @@ -3291,7 +3263,7 @@ impl Tab { match path.map_or_else( || { let items = self.items_opt.as_deref()?; - items.iter().find(|item| item.selected).and_then(|item| { + items.iter().find(|&item| item.selected).and_then(|item| { let location = item.location_opt.as_ref()?; let path = location.path_opt()?; cosmic::desktop::load_desktop_file(&[language.into()], path.into()) @@ -3372,9 +3344,7 @@ impl Tab { if let Some(edit_location) = &mut self.edit_location { edit_location.select(true); } else if self.gallery { - for command in self.update(Message::GalleryNext, modifiers) { - commands.push(command); - } + commands.append(&mut self.update(Message::GalleryNext, modifiers)); } else { if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_last_pos_opt()) @@ -3407,9 +3377,7 @@ impl Tab { } Message::ItemLeft => { if self.gallery { - for command in self.update(Message::GalleryPrevious, modifiers) { - commands.push(command); - } + commands.append(&mut self.update(Message::GalleryPrevious, modifiers)); } else { if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_first_pos_opt()) @@ -3460,9 +3428,7 @@ impl Tab { } Message::ItemRight => { if self.gallery { - for command in self.update(Message::GalleryNext, modifiers) { - commands.push(command); - } + commands.append(&mut self.update(Message::GalleryNext, modifiers)); } else { if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_last_pos_opt()) @@ -3498,9 +3464,7 @@ impl Tab { if let Some(edit_location) = &mut self.edit_location { edit_location.select(false); } else if self.gallery { - for command in self.update(Message::GalleryPrevious, modifiers) { - commands.push(command); - } + commands.append(&mut self.update(Message::GalleryPrevious, modifiers)); } else { if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_first_pos_opt()) @@ -3594,13 +3558,12 @@ impl Tab { } } Message::Reload => { - let mut selected_paths = Vec::new(); //TODO: support keeping selected locations without paths - for location in self.selected_locations() { - if let Some(path) = location.path_opt() { - selected_paths.push(path.clone()); - } - } + let selected_paths = self + .selected_locations() + .into_iter() + .filter_map(Location::into_path_opt) + .collect(); let location = self.location.clone(); self.change_location(&location, None); commands.push(Command::ChangeLocation( @@ -3835,8 +3798,8 @@ impl Tab { ItemThumbnail::Text(_text) => None, }; if let Some(handle) = handle_opt { - item.icon_handle_grid = handle.clone(); - item.icon_handle_list = handle.clone(); + item.icon_handle_grid.clone_from(&handle); + item.icon_handle_list.clone_from(&handle); item.icon_handle_list_condensed = handle; } item.thumbnail_opt = Some(thumbnail); @@ -3908,13 +3871,10 @@ impl Tab { self.dnd_hovered = Some((loc.clone(), Instant::now())); if loc != self.location { commands.push(Command::Iced( - cosmic::Task::perform( - async move { - tokio::time::sleep(HOVER_DURATION).await; - Message::DndHover(loc) - }, - |x| x, - ) + cosmic::Task::future(async move { + tokio::time::sleep(HOVER_DURATION).await; + Message::DndHover(loc) + }) .into(), )); } @@ -3940,7 +3900,7 @@ impl Tab { let location = Location::Path(path); if let Some(ref mut item) = self.parent_item_opt { if item.location_opt.as_ref() == Some(&location) { - item.dir_size = dir_size.clone(); + item.dir_size.clone_from(&dir_size); } } if let Some(ref mut items) = self.items_opt { @@ -4409,7 +4369,7 @@ impl Tab { .into() }; - let heading_row = widget::row::with_children(vec![ + let heading_row = widget::row::with_children([ heading_item(fl!("name"), Length::Fill, HeadingOptions::Name), if self.location == Location::Trash { heading_item( @@ -4443,7 +4403,7 @@ impl Tab { if let Some(edit_location) = &self.edit_location { if let Some(location) = edit_location.resolve() { //TODO: allow editing other locations - if let Some(path) = location.path_opt().cloned() { + if let Some(path) = location.path_opt() { row = row.push( widget::button::custom( widget::icon::from_name("window-close-symbolic").size(16), @@ -4452,7 +4412,7 @@ impl Tab { .padding(space_xxs) .class(theme::Button::Icon), ); - let text_input = widget::text_input("", path.to_string_lossy().to_string()) + let text_input = widget::text_input("", path.to_string_lossy().into_owned()) .id(self.edit_location_id.clone()) .on_input(move |input| { Message::EditLocation(Some( @@ -4632,9 +4592,7 @@ impl Tab { } } - for child in children { - row = row.push(child); - } + row = row.extend(children); let mut column = widget::column::with_capacity(4).padding([0, space_s]); column = column.push(row); column = column.push(accent_rule); @@ -4663,31 +4621,29 @@ impl Tab { pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; - mouse_area::MouseArea::new(widget::column::with_children(vec![ - widget::container( - widget::column::with_children(match self.mode { - Mode::App | Mode::Dialog(_) => vec![ - widget::icon::from_name("folder-symbolic") - .size(64) - .icon() - .into(), - widget::text::body(if has_hidden { - fl!("empty-folder-hidden") - } else if matches!(self.location, Location::Search(..)) { - fl!("no-results") - } else { - fl!("empty-folder") - }) + mouse_area::MouseArea::new(widget::column::with_children([widget::container( + match self.mode { + Mode::App | Mode::Dialog(_) => widget::column::with_children([ + widget::icon::from_name("folder-symbolic") + .size(64) + .icon() .into(), - ], - Mode::Desktop => Vec::new(), - }) - .align_x(Alignment::Center) - .spacing(space_xxs), - ) - .center(Length::Fill) - .into(), - ])) + widget::text::body(if has_hidden { + fl!("empty-folder-hidden") + } else if matches!(self.location, Location::Search(..)) { + fl!("no-results") + } else { + fl!("empty-folder") + }) + .into(), + ]), + Mode::Desktop => widget::column(), + } + .align_x(Alignment::Center) + .spacing(space_xxs), + ) + .center(Length::Fill) + .into()])) .on_press(|_| Message::Click(None)) .into() } @@ -4769,7 +4725,7 @@ impl Tab { let mut drag_e_i = 0; let mut drag_s_i = 0; - let mut children = Vec::new(); + let mut column = widget::column::with_capacity(2); if let Some(items) = self.column_sort() { let mut count = 0; let mut col = 0; @@ -4921,7 +4877,7 @@ impl Tab { return (None, self.empty_view(hidden > 0), false); } - children.push(grid.into()); + column = column.push(grid); //TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that { @@ -4945,10 +4901,9 @@ impl Tab { let spacer_height = height.saturating_sub(max_bottom + top_deduct); if spacer_height > 0 { - children.push( - widget::container(Space::with_height(Length::Fixed(spacer_height as f32))) - .into(), - ); + column = column.push(widget::container(Space::with_height(Length::Fixed( + spacer_height as f32, + )))); } } } @@ -4997,13 +4952,11 @@ impl Tab { )), ]; - let mut column = widget::column::with_capacity(buttons.len()) - .align_x(Alignment::Center) - .height(Length::Fixed(item_height as f32)) - .width(Length::Fixed(item_width as f32)); - for button in buttons { - column = column.push(button) - } + let column = + widget::column::with_children(buttons.into_iter().map(Element::from)) + .align_x(Alignment::Center) + .height(Length::Fixed(item_height as f32)) + .width(Length::Fixed(item_width as f32)); dnd_grid = dnd_grid.push(column); dnd_item_i += 1; @@ -5018,13 +4971,12 @@ impl Tab { Element::from(dnd_grid) }); - let mut mouse_area = - mouse_area::MouseArea::new(widget::column::with_children(children).width(Length::Fill)) - .on_press(|_| Message::Click(None)) - .on_auto_scroll(Message::AutoScroll) - .on_drag_end(|_| Message::DragEnd) - .show_drag_rect(self.mode.multiple()) - .on_release(|_| Message::ClickRelease(None)); + let mut mouse_area = mouse_area::MouseArea::new(column.width(Length::Fill)) + .on_press(|_| Message::Click(None)) + .on_auto_scroll(Message::AutoScroll) + .on_drag_end(|_| Message::DragEnd) + .show_drag_rect(self.mode.multiple()) + .on_release(|_| Message::ClickRelease(None)); if self.watch_drag { mouse_area = mouse_area.on_drag(Message::Drag); } @@ -5063,7 +5015,7 @@ impl Tab { }; let row_height = icon_size + 2 * space_xxs; - let mut children: Vec> = Vec::new(); + let mut column = widget::column::with_capacity(3); let mut y: f32 = 0.0; let rule_padding = theme::active().cosmic().corner_radii.radius_xs[0] as u16; @@ -5091,11 +5043,8 @@ impl Tab { } if count > 0 { - children.push( - widget::container(horizontal_rule(1)) - .padding([0, rule_padding]) - .into(), - ); + column = column + .push(widget::container(horizontal_rule(1)).padding([0, rule_padding])); y += 1.0; } @@ -5186,12 +5135,12 @@ impl Tab { }; let row = if condensed { - widget::row::with_children(vec![ + widget::row::with_children([ widget::icon::icon(item.icon_handle_list_condensed.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(item.display_name.clone()).into(), //TODO: translate? widget::text::caption(format!("{modified_text} - {size_text}")) @@ -5203,12 +5152,12 @@ impl Tab { .align_y(Alignment::Center) .spacing(space_xxs) } else if is_search { - widget::row::with_children(vec![ + widget::row::with_children([ widget::icon::icon(item.icon_handle_list_condensed.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(item.display_name.clone()).into(), widget::text::caption(match item.path_opt() { Some(path) => path.display().to_string(), @@ -5229,7 +5178,7 @@ impl Tab { .align_y(Alignment::Center) .spacing(space_xxs) } else { - widget::row::with_children(vec![ + widget::row::with_children([ widget::icon::icon(item.icon_handle_list.clone()) .content_fit(ContentFit::Contain) .size(icon_size) @@ -5295,12 +5244,12 @@ impl Tab { let dnd_row = if !item.selected { Element::from(Space::with_height(Length::Fixed(f32::from(row_height)))) } else if condensed { - widget::row::with_children(vec![ + widget::row::with_children([ widget::icon::icon(item.icon_handle_list_condensed.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(item.display_name.clone()).into(), //TODO: translate? widget::text::body(format!("{modified_text} - {size_text}")) @@ -5312,12 +5261,12 @@ impl Tab { .spacing(space_xxs) .into() } else if is_search { - widget::row::with_children(vec![ + widget::row::with_children([ widget::icon::icon(item.icon_handle_list_condensed.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), - widget::column::with_children(vec![ + widget::column::with_children([ widget::text::body(item.display_name.clone()).into(), widget::text::caption(match item.path_opt() { Some(path) => path.display().to_string(), @@ -5338,7 +5287,7 @@ impl Tab { .spacing(space_xxs) .into() } else { - widget::row::with_children(vec![ + widget::row::with_children([ widget::icon::icon(item.icon_handle_list.clone()) .content_fit(ContentFit::Contain) .size(icon_size) @@ -5378,7 +5327,7 @@ impl Tab { count += 1; y += f32::from(row_height); - children.push(button_row); + column = column.push(button_row); } if count == 0 { @@ -5397,23 +5346,19 @@ impl Tab { let spacer_height = size.height - y - f32::from(top_deduct); if spacer_height > 0. { - children.push( - widget::container(Space::with_height(Length::Fixed(spacer_height))).into(), - ); + column = column.push(widget::container(Space::with_height(spacer_height))); } } let drag_col = (!drag_items.is_empty()) .then(|| Element::from(widget::column::with_children(drag_items))); - let mut mouse_area = mouse_area::MouseArea::new( - widget::column::with_children(children).padding([0, space_s]), - ) - .with_id(Id::new("list-view")) - .on_press(|_| Message::Click(None)) - .on_auto_scroll(Message::AutoScroll) - .on_drag_end(|_| Message::DragEnd) - .show_drag_rect(self.mode.multiple()) - .on_release(|_| Message::ClickRelease(None)); + let mut mouse_area = mouse_area::MouseArea::new(column.padding([0, space_s])) + .with_id(Id::new("list-view")) + .on_press(|_| Message::Click(None)) + .on_auto_scroll(Message::AutoScroll) + .on_drag_end(|_| Message::DragEnd) + .show_drag_rect(self.mode.multiple()) + .on_release(|_| Message::ClickRelease(None)); if self.watch_drag { mouse_area = mouse_area.on_drag(Message::Drag); } @@ -5452,9 +5397,14 @@ impl Tab { .map(|items| { items .iter() - .filter(|item| item.selected) - .filter_map(|item| item.path_opt().cloned()) - .collect::>() + .filter_map(|item| { + if item.selected { + item.path_opt().cloned() + } else { + None + } + }) + .collect::>() }) .unwrap_or_default(); let item_view = @@ -5533,7 +5483,7 @@ impl Tab { if let Some(items) = self.items_opt() { if !items.is_empty() { tab_column = tab_column.push( - widget::layer_container(widget::row::with_children(vec![ + widget::layer_container(widget::row::with_children([ widget::horizontal_space().into(), widget::button::standard(fl!("empty-trash")) .on_press(Message::EmptyTrash) @@ -5549,7 +5499,7 @@ impl Tab { } Location::Network(uri, _display_name, _path) if uri == "network:///" => { tab_column = tab_column.push( - widget::layer_container(widget::row::with_children(vec![ + widget::layer_container(widget::row::with_children([ widget::horizontal_space().into(), widget::button::standard(fl!("add-network-drive")) .on_press(Message::AddNetworkDrive) @@ -5715,7 +5665,7 @@ impl Tab { // Load directory size for selected items if let Some(item) = items .iter() - .find(|item| item.selected) + .find(|&item| item.selected) .or(self.parent_item_opt.as_ref()) { // Item must have a path @@ -5872,7 +5822,7 @@ impl Tab { .cloned() { subscriptions.push(Subscription::run_with_id( - ("tab_complete", path.to_string_lossy().to_string()), + ("tab_complete", path.to_string_lossy().into_owned()), stream::channel(1, |mut output| async move { let message = { let path = path.clone(); @@ -6201,7 +6151,7 @@ mod tests { let top_level = filter_dirs(path)?; let mut result = Vec::new(); for dir in top_level { - let nested_dirs: Vec = filter_dirs(&dir)?.collect(); + let nested_dirs = filter_dirs(&dir)?; result.push(dir); result.extend(nested_dirs); } @@ -6496,10 +6446,11 @@ mod tests { debug!("Shuffled numbers for paths: {base_nums:?}"); let paths: Vec<_> = base_nums .iter() - .map(|&base| path.join(std::iter::repeat_n(base, 255).collect::())) + .copied() + .map(|base| path.join(std::iter::repeat_n(base, 255).collect::())) .collect(); - for (file, &base) in paths.iter().zip(base_nums.iter()) { + for (file, base) in paths.iter().zip(base_nums.into_iter()) { trace!("Creating long file name for {base}"); fs::File::create(file)?; } diff --git a/src/thumbnail_cacher.rs b/src/thumbnail_cacher.rs index 447b848..82cafb3 100644 --- a/src/thumbnail_cacher.rs +++ b/src/thumbnail_cacher.rs @@ -149,9 +149,8 @@ impl ThumbnailCacher { let info = reader.info(); let text_chunks: FxHashMap = info .uncompressed_latin1_text - .clone() - .into_iter() - .map(|chunk| (chunk.keyword, chunk.text)) + .iter() + .map(|chunk| (chunk.keyword.clone(), chunk.text.clone())) .collect(); ( info.width, @@ -222,7 +221,7 @@ impl ThumbnailCacher { // Thumb::URI is required and must match. let thumb_uri = texts .iter() - .find(|text| text.keyword == "Thumb::URI") + .find(|&text| text.keyword == "Thumb::URI") .map(|t| &t.text); if let Some(thumb_uri) = thumb_uri { if *thumb_uri != self.file_uri { @@ -247,7 +246,7 @@ impl ThumbnailCacher { // Thumb::MTime is required and must match. let thumb_mtime = texts .iter() - .find(|text| text.keyword == "Thumb::MTime") + .find(|&text| text.keyword == "Thumb::MTime") .map(|t| &t.text); if let Some(thumb_mtime) = thumb_mtime { let modified = match metadata.modified() { @@ -276,7 +275,7 @@ impl ThumbnailCacher { // Thumb::Size isn't required, but it should be verified if present. let thumb_size = texts .iter() - .find(|text| text.keyword == "Thumb::Size") + .find(|&text| text.keyword == "Thumb::Size") .map(|t| &t.text); if let Some(thumb_size) = thumb_size { let size = metadata.len(); @@ -301,7 +300,7 @@ fn thumbnail_uri(path: &Path) -> io::Result { // 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 // 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) } diff --git a/src/thumbnailer.rs b/src/thumbnailer.rs index 3627fe5..8a22ba9 100644 --- a/src/thumbnailer.rs +++ b/src/thumbnailer.rs @@ -80,30 +80,32 @@ impl ThumbnailerCache { let mut search_dirs = Vec::new(); let xdg_dirs = xdg::BaseDirectories::new(); - if let Some(data_home) = xdg_dirs.get_data_home() { - search_dirs.push(data_home.join("thumbnailers")); - } - for data_dir in xdg_dirs.get_data_dirs() { - search_dirs.push(data_dir.join("thumbnailers")); + if let Some(mut data_home) = xdg_dirs.get_data_home() { + data_home.push("thumbnailers"); + search_dirs.push(data_home); } + 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(); for dir in search_dirs { log::trace!("looking for thumbnailers in {}", dir.display()); match fs::read_dir(&dir) { Ok(entries) => { - for entry_res in entries { - match entry_res { - Ok(entry) => thumbnailer_paths.push(entry.path()), - Err(err) => { + thumbnailer_paths.extend(entries.filter_map(|entry_res| { + entry_res + .inspect_err(|err| { log::warn!( "failed to read entry in directory {}: {}", dir.display(), err - ); - } - } - } + ) + }) + .ok() + .map(|entry| entry.path()) + })); } Err(err) => { log::warn!("failed to read directory {}: {}", dir.display(), err); diff --git a/src/zoom.rs b/src/zoom.rs new file mode 100644 index 0000000..600d0b5 --- /dev/null +++ b/src/zoom.rs @@ -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, + } +}