diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index b5f55f8af..003908c94 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -16,7 +16,7 @@ impl Actor for Sort { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let pref = &mut cx.tab_mut().pref; - pref.sort_by = opt.by.unwrap_or(pref.sort_by); + pref.sort_by = opt.by.unwrap_or_else(|| pref.sort_by.clone()); pref.sort_reverse = opt.reverse.unwrap_or(pref.sort_reverse); pref.sort_dir_first = opt.dir_first.unwrap_or(pref.sort_dir_first); pref.sort_sensitive = opt.sensitive.unwrap_or(pref.sort_sensitive); @@ -29,7 +29,7 @@ impl Actor for Sort { render!(); false } else { - f.files.set_sorter(sorter); + f.files.set_sorter(sorter.clone()); render_and!(f.files.catchup_revision()) } }; diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 71feb9ab0..c6c45f78c 100644 --- a/yazi-config/src/mgr/mgr.rs +++ b/yazi-config/src/mgr/mgr.rs @@ -1,7 +1,7 @@ use anyhow::{Result, bail}; use serde::Deserialize; use yazi_codegen::DeserializeOver2; -use yazi_fs::{CWD, SortBy}; +use yazi_fs::{CWD, SortBys}; use yazi_shared::{SyncCell, url::UrlBuf}; use super::{MgrRatio, MouseEvents}; @@ -11,7 +11,7 @@ pub struct Mgr { pub ratio: SyncCell, // Sorting - pub sort_by: SyncCell, + pub sort_by: SortBys, pub sort_sensitive: SyncCell, pub sort_reverse: SyncCell, pub sort_dir_first: SyncCell, diff --git a/yazi-core/src/tab/preference.rs b/yazi-core/src/tab/preference.rs index c6ebf35e4..b92f0a4c0 100644 --- a/yazi-core/src/tab/preference.rs +++ b/yazi-core/src/tab/preference.rs @@ -1,10 +1,10 @@ use yazi_config::YAZI; -use yazi_fs::{FilesSorter, SortBy}; +use yazi_fs::{FilesSorter, SortBys}; #[derive(Clone, PartialEq)] pub struct Preference { // Sorting - pub sort_by: SortBy, + pub sort_by: SortBys, pub sort_sensitive: bool, pub sort_reverse: bool, pub sort_dir_first: bool, @@ -19,7 +19,7 @@ impl Default for Preference { fn default() -> Self { Self { // Sorting - sort_by: YAZI.mgr.sort_by.get(), + sort_by: YAZI.mgr.sort_by.clone(), sort_sensitive: YAZI.mgr.sort_sensitive.get(), sort_reverse: YAZI.mgr.sort_reverse.get(), sort_dir_first: YAZI.mgr.sort_dir_first.get(), @@ -35,7 +35,7 @@ impl Default for Preference { impl From<&Preference> for FilesSorter { fn from(value: &Preference) -> Self { Self { - by: value.sort_by, + by: value.sort_by.clone(), sensitive: value.sort_sensitive, reverse: value.sort_reverse, dir_first: value.sort_dir_first, diff --git a/yazi-core/src/tasks/preload.rs b/yazi-core/src/tasks/preload.rs index 42e7b7166..4e485bdeb 100644 --- a/yazi-core/src/tasks/preload.rs +++ b/yazi-core/src/tasks/preload.rs @@ -44,7 +44,7 @@ impl Tasks { } pub fn prework_sorted(&self, targets: &Files) { - if targets.sorter().by != SortBy::Size { + if targets.sorter().by.0.contains(&SortBy::Size) { return; } diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 4082bad02..bf8daae5b 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -143,7 +143,7 @@ impl Files { return; } - if self.sorter.by == SortBy::Size { + if self.sorter.by.0.contains(&SortBy::Size) { self.revision += 1; } self.sizes.extend(sizes); diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 286c8f517..4846e6bb5 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -3,15 +3,15 @@ use std::cmp::Ordering; use hashbrown::HashMap; use yazi_shared::{LcgRng, natsort, translit::Transliterator, url::UrnBuf}; -use crate::{File, SortBy}; +use crate::{File, SortBy, SortBys}; -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct FilesSorter { - pub by: SortBy, + pub by: SortBys, pub sensitive: bool, - pub reverse: bool, + pub reverse: bool, pub dir_first: bool, - pub translit: bool, + pub translit: bool, } impl FilesSorter { @@ -28,45 +28,7 @@ impl FilesSorter { } }; - match self.by { - SortBy::None => {} - SortBy::Mtime => items.sort_unstable_by(|a, b| { - let ord = self.cmp(a.mtime, b.mtime, self.promote(a, b)); - if ord == Ordering::Equal { by_alphabetical(a, b) } else { ord } - }), - SortBy::Btime => items.sort_unstable_by(|a, b| { - let ord = self.cmp(a.btime, b.btime, self.promote(a, b)); - if ord == Ordering::Equal { by_alphabetical(a, b) } else { ord } - }), - SortBy::Extension => items.sort_unstable_by(|a, b| { - let ord = if self.sensitive { - self.cmp(a.url.ext(), b.url.ext(), self.promote(a, b)) - } else { - self.cmp_insensitive( - a.url.ext().map_or(&[], |s| s.as_encoded_bytes()), - b.url.ext().map_or(&[], |s| s.as_encoded_bytes()), - self.promote(a, b), - ) - }; - if ord == Ordering::Equal { by_alphabetical(a, b) } else { ord } - }), - SortBy::Alphabetical => items.sort_unstable_by(by_alphabetical), - SortBy::Natural => self.sort_naturally(items), - SortBy::Size => items.sort_unstable_by(|a, b| { - let aa = if a.is_dir() { sizes.get(a.urn()).copied() } else { None }; - let bb = if b.is_dir() { sizes.get(b.urn()).copied() } else { None }; - let ord = self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len), self.promote(a, b)); - if ord == Ordering::Equal { by_alphabetical(a, b) } else { ord } - }), - SortBy::Random => { - let mut rng = LcgRng::default(); - items.sort_unstable_by(|a, b| self.cmp(rng.next(), rng.next(), self.promote(a, b))) - } - } - } - - fn sort_naturally(&self, items: &mut [File]) { - items.sort_unstable_by(|a, b| { + let by_natural = |a: &File, b: &File| { let promote = self.promote(a, b); if promote != Ordering::Equal { return promote; @@ -83,6 +45,45 @@ impl FilesSorter { }; if self.reverse { ordering.reverse() } else { ordering } + }; + + items.sort_unstable_by(|a, b| { + for key in &self.by.0 { + let ord = match key { + SortBy::None => Ordering::Equal, + SortBy::Mtime => self.cmp(a.mtime, b.mtime, self.promote(a, b)), + SortBy::Btime => self.cmp(a.btime, b.btime, self.promote(a, b)), + SortBy::Extension => { + if self.sensitive { + self.cmp(a.url.ext(), b.url.ext(), self.promote(a, b)) + } else { + self.cmp_insensitive( + a.url.ext().map_or(&[], |s| s.as_encoded_bytes()), + b.url.ext().map_or(&[], |s| s.as_encoded_bytes()), + self.promote(a, b), + ) + } + } + SortBy::Alphabetical => by_alphabetical(a, b), + SortBy::Natural => by_natural(a, b), + SortBy::Size => { + let aa = if a.is_dir() { sizes.get(a.urn()).copied() } else { None }; + let bb = if b.is_dir() { sizes.get(b.urn()).copied() } else { None }; + self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len), self.promote(a, b)) + } + SortBy::Random => { + let mut rng = LcgRng::default(); + self.cmp(rng.next(), rng.next(), self.promote(a, b)) + } + }; + + if ord != Ordering::Equal { + return ord; + } + } + + // fallback to alphabetical sorting if all other keys are equal + by_alphabetical(a, b) }); } diff --git a/yazi-fs/src/sorting.rs b/yazi-fs/src/sorting.rs index 14b8a976d..5116422ff 100644 --- a/yazi-fs/src/sorting.rs +++ b/yazi-fs/src/sorting.rs @@ -1,6 +1,6 @@ use std::{fmt::Display, str::FromStr}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] @@ -38,3 +38,60 @@ impl Display for SortBy { }) } } + +#[derive(Clone, Debug, PartialEq, Default)] +pub struct SortBys(pub Vec); + +#[derive(Deserialize)] +#[serde(untagged)] +enum SortBysDef { + Single(SortBy), + Multiple(Vec), +} + +impl<'de> Deserialize<'de> for SortBys { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let helper = SortBysDef::deserialize(deserializer)?; + Ok(match helper { + SortBysDef::Single(s) => SortBys(vec![s]), + SortBysDef::Multiple(v) => SortBys(v), + }) + } +} + +impl Serialize for SortBys { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self.0.len() { + 1 => { + // serialize the single SortBy as a single value + self.0[0].serialize(serializer) + } + _ => { + // empty or multi -> serialize as an array + self.0.serialize(serializer) + } + } + } +} + +impl Display for SortBys { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut iter = self.0.iter(); + f.write_str("[")?; + if let Some(first) = iter.next() { + f.write_str(&first.to_string())?; + for sort_by in iter { + f.write_str(", ")?; + f.write_str(&sort_by.to_string())?; + } + } + f.write_str("]")?; + Ok(()) + } +} diff --git a/yazi-parser/src/mgr/sort.rs b/yazi-parser/src/mgr/sort.rs index 93f359341..b57d53352 100644 --- a/yazi-parser/src/mgr/sort.rs +++ b/yazi-parser/src/mgr/sort.rs @@ -1,12 +1,12 @@ use std::str::FromStr; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_fs::SortBy; +use yazi_fs::{SortBy, SortBys}; use yazi_shared::event::CmdCow; #[derive(Debug, Default)] pub struct SortOpt { - pub by: Option, + pub by: Option, pub reverse: Option, pub dir_first: Option, pub sensitive: Option, @@ -17,8 +17,11 @@ impl TryFrom for SortOpt { type Error = anyhow::Error; fn try_from(c: CmdCow) -> Result { + let by = + (0..).map_while(|i| c.str(i)).map(SortBy::from_str).collect::, _>>()?; + Ok(Self { - by: c.first_str().map(SortBy::from_str).transpose()?, + by: if by.is_empty() { None } else { Some(SortBys(by)) }, reverse: c.maybe_bool("reverse"), dir_first: c.maybe_bool("dir-first"), sensitive: c.maybe_bool("sensitive"),