diff --git a/src/app.rs b/src/app.rs index e51d46c..80f879e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,7 +16,8 @@ use cosmic::iced::{ use cosmic::iced_winit::commands::overlap_notify::overlap_notify; use cosmic::{ app::{self, context_drawer, Core, Task}, - cosmic_config, cosmic_theme, executor, + cosmic_config::{self, ConfigSet}, + cosmic_theme, executor, iced::{ self, clipboard::dnd::DndAction, @@ -358,6 +359,7 @@ pub enum Message { Rename(Option), ReplaceResult(ReplaceResult), RestoreFromTrash(Option), + SaveSortNames, ScrollTab(i16), SearchActivate, SearchClear, @@ -574,6 +576,7 @@ pub struct App { mime_app_cache: MimeAppCache, modifiers: Modifiers, mounter_items: HashMap, + must_save_sort_names: bool, network_drive_connecting: Option<(MounterKey, String)>, network_drive_input: String, #[cfg(feature = "notify")] @@ -873,7 +876,11 @@ impl App { activate: bool, selection_paths: Option>, ) -> (Entity, Task) { - let mut tab = Tab::new(location.clone(), self.config.tab); + let mut tab = Tab::new( + location.clone(), + self.config.tab, + Some(&self.config.sort_names), + ); tab.mode = match self.mode { Mode::App => tab::Mode::App, Mode::Desktop => { @@ -1949,6 +1956,7 @@ impl Application for App { mime_app_cache: MimeAppCache::new(), modifiers: Modifiers::empty(), mounter_items: HashMap::new(), + must_save_sort_names: false, network_drive_connecting: None, network_drive_input: String::new(), #[cfg(feature = "notify")] @@ -3587,6 +3595,19 @@ impl Application for App { commands.push(window::toggle_maximize(*window_id)); } } + tab::Command::SetSort(location, heading_options, direction) => { + self.config + .sort_names + .insert(location, (heading_options, direction)); + 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 Task::batch(commands); @@ -3599,11 +3620,22 @@ impl Application for App { }; return self.open_tab(location, true, None); } - Message::TabRescan(entity, location, parent_item_opt, items, selection_paths) => { + Message::TabRescan(entity, mut location, parent_item_opt, items, selection_paths) => { + location = location.normalize(); if let Some(tab) = self.tab_model.data_mut::(entity) { + tab.location = tab.location.normalize(); if location == tab.location { tab.parent_item_opt = parent_item_opt; tab.set_items(items); + let sort = self + .config + .sort_names + .get(&location.to_string()) + .unwrap_or_else(|| &(HeadingOptions::Name, true)); + + tab.sort_name = sort.0; + tab.sort_direction = sort.1; + if let Some(selection_paths) = selection_paths { tab.select_paths(selection_paths); } @@ -4194,6 +4226,21 @@ impl Application for App { cosmic::app::Action::Surface(a), )); } + Message::SaveSortNames => { + if self.must_save_sort_names { + self.must_save_sort_names = false; + if let Some(config_handler) = self.config_handler.as_ref() { + if let Err(err) = config_handler + .set::>( + "sort_names", + self.config.sort_names.clone(), + ) + { + log::warn!("Failed to save sort names: {:?}", err); + } + } + } + } } Task::none() @@ -5764,7 +5811,7 @@ pub(crate) mod test_utils { // New tab with items let location = Location::Path(path.to_owned()); let (parent_item_opt, items) = location.scan(IconSizes::default()); - let mut tab = Tab::new(location, TabConfig::default()); + let mut tab = Tab::new(location, TabConfig::default(), None); tab.parent_item_opt = parent_item_opt; tab.set_items(items); diff --git a/src/config.rs b/src/config.rs index b3c467a..6dc69b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only -use std::{any::TypeId, num::NonZeroU16, path::PathBuf}; +use std::{any::TypeId, collections::HashMap, num::NonZeroU16, path::PathBuf}; use cosmic::{ cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry}, @@ -9,7 +9,10 @@ use cosmic::{ }; use serde::{Deserialize, Serialize}; -use crate::{app::App, tab::View}; +use crate::{ + app::App, + tab::{HeadingOptions, Location, View}, +}; pub const CONFIG_VERSION: u64 = 1; @@ -110,6 +113,7 @@ pub struct Config { pub show_details: bool, pub tab: TabConfig, pub type_to_search: TypeToSearch, + pub sort_names: HashMap, } impl Config { @@ -158,6 +162,12 @@ impl Default for Config { show_details: false, tab: TabConfig::default(), type_to_search: TypeToSearch::Recursive, + sort_names: HashMap::from_iter(dirs::download_dir().into_iter().map(|dir| { + ( + Location::Path(dir).normalize().to_string(), + (HeadingOptions::Modified, false), + ) + })), } } } diff --git a/src/dialog.rs b/src/dialog.rs index 7506e37..c6ac63d 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -916,7 +916,7 @@ impl Application for App { folders_first: false, ..Default::default() }; - let mut tab = Tab::new(location, tab_config); + let mut tab = Tab::new(location, tab_config, None); tab.mode = tab::Mode::Dialog(flags.kind.clone()); tab.sort_name = tab::HeadingOptions::Modified; tab.sort_direction = false; diff --git a/src/tab.rs b/src/tab.rs index 81ae75e..63e058a 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -54,10 +54,11 @@ use std::{ error::Error, fmt::{self, Display}, fs::{self, File, Metadata}, + hash::Hash, io::{BufRead, BufReader}, os::unix::fs::MetadataExt, path::{Path, PathBuf}, - sync::{atomic, Arc, Mutex, RwLock}, + sync::{atomic, Arc, LazyLock, Mutex, RwLock}, time::{Duration, Instant, SystemTime}, }; use tokio::sync::mpsc; @@ -90,6 +91,16 @@ const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32); const DRAG_SCROLL_DISTANCE: f32 = 15.0; +static SORT_OPTION_FALLBACK: LazyLock> = + LazyLock::new(|| { + HashMap::from_iter(dirs::download_dir().into_iter().map(|dir| { + ( + Location::Path(dir).normalize().to_string(), + (HeadingOptions::Modified, false), + ) + })) + }); + static MODE_NAMES: Lazy> = Lazy::new(|| { vec![ // Mode 0 @@ -1506,6 +1517,7 @@ pub enum Command { Preview(PreviewKind), SetOpenWith(Mime, String), SetPermissions(PathBuf, u32), + SetSort(String, HeadingOptions, bool), WindowDrag, WindowToggleMaximize, } @@ -2351,7 +2363,17 @@ fn parse_hidden_file(path: &PathBuf) -> Vec { } impl Tab { - pub fn new(location: Location, config: TabConfig) -> Self { + pub fn new( + location: Location, + config: TabConfig, + sorting_options: Option<&HashMap>, + ) -> Self { + let location_str = location.to_string(); + let (sort_name, sort_direction) = sorting_options + .and_then(|opts| opts.get(&location_str)) + .or_else(|| SORT_OPTION_FALLBACK.get(&location_str)) + .cloned() + .unwrap_or_else(|| (HeadingOptions::Name, true)); let location = location.normalize(); let location_ancestors = location.ancestors(); let location_title = location.title(); @@ -2372,8 +2394,8 @@ impl Tab { history_i: 0, history, config, - sort_name: HeadingOptions::Name, - sort_direction: true, + sort_name, + sort_direction, gallery: false, parent_item_opt: None, items_opt: None, @@ -3669,6 +3691,13 @@ impl Tab { if !matches!(self.location, Location::Search(..)) { self.sort_name = heading_option; self.sort_direction = dir; + if !matches!(self.location, Location::Desktop(..)) { + commands.push(Command::SetSort( + self.location.normalize().to_string(), + heading_option, + self.sort_direction, + )); + } } } Message::TabComplete(path, completions) => { @@ -3721,6 +3750,15 @@ impl Tab { // Default modified to descending, and others to ascending. heading_option != HeadingOptions::Modified }; + + if !matches!(self.location, Location::Desktop(..)) { + commands.push(Command::SetSort( + self.location.normalize().to_string(), + heading_option, + heading_sort, + )); + } + self.sort_direction = heading_sort; self.sort_name = heading_option; } @@ -5941,7 +5979,7 @@ mod tests { fn tab_history() -> io::Result<(TempDir, Tab, Vec)> { let fs = simple_fs(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?; let path = fs.path(); - let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default(), None); // All directories (simple_fs only produces one nested layer) let dirs: Vec = filter_dirs(path)? @@ -6038,7 +6076,7 @@ mod tests { .next() .expect("temp directory should have at least one directory"); - let mut tab = Tab::new(Location::Path(path.to_owned()), TabConfig::default()); + let mut tab = Tab::new(Location::Path(path.to_owned()), TabConfig::default(), None); debug!( "Emitting Message::Location(Location::Path(\"{}\"))", next_dir.display() @@ -6170,7 +6208,7 @@ mod tests { fn tab_empty_history_does_nothing_on_prev_next() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; let path = fs.path(); - let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default(), None); // Tab's location shouldn't change if GoPrev or GoNext is triggered debug!("Emitting Message::GoPrevious",); @@ -6192,7 +6230,7 @@ mod tests { .next() .expect("should be at least one directory"); - let mut tab = Tab::new(Location::Path(next_dir.clone()), TabConfig::default()); + let mut tab = Tab::new(Location::Path(next_dir.clone()), TabConfig::default(), None); // This will eventually yield false once root is hit while next_dir.pop() { debug!("Emitting Message::LocationUp",); @@ -6225,7 +6263,7 @@ mod tests { } debug!("Creating tab for directory of long file names"); - Tab::new(Location::Path(path.into()), TabConfig::default()); + Tab::new(Location::Path(path.into()), TabConfig::default(), None); Ok(()) }