diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 71feb9ab..905f4da0 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, SortBy, SortByMulti}; 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: SyncCell, pub sort_sensitive: SyncCell, pub sort_reverse: SyncCell, pub sort_dir_first: SyncCell, diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 286c8f51..dc93cb2f 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -3,7 +3,7 @@ use std::cmp::Ordering; use hashbrown::HashMap; use yazi_shared::{LcgRng, natsort, translit::Transliterator, url::UrnBuf}; -use crate::{File, SortBy}; +use crate::{File, SortBy, SortByMulti}; #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct FilesSorter { @@ -65,6 +65,105 @@ impl FilesSorter { } } + pub(super) fn sort_multi(&self, items: &mut [File], sizes: &HashMap, methods: &[SortBy]) { + if items.is_empty() || methods.is_empty() { + return; + } + + // If only one method, use the existing single-method sort + if methods.len() == 1 { + let mut single_sorter = *self; + single_sorter.by = methods[0]; + single_sorter.sort(items, sizes); + return; + } + + items.sort_unstable_by(|a, b| { + // Try each sorting method in order until we get a non-equal result + for &sort_method in methods { + let ordering = self.compare_by_method(a, b, sort_method, sizes); + if ordering != std::cmp::Ordering::Equal { + return ordering; + } + } + std::cmp::Ordering::Equal + }); + } + + fn compare_by_method(&self, a: &File, b: &File, method: SortBy, sizes: &HashMap) -> std::cmp::Ordering { + let promote = self.promote(a, b); + if promote != std::cmp::Ordering::Equal { + return promote; + } + + let ordering = match method { + SortBy::None => std::cmp::Ordering::Equal, + SortBy::Mtime => a.mtime.cmp(&b.mtime), + SortBy::Btime => a.btime.cmp(&b.btime), + SortBy::Extension => { + if self.sensitive { + a.url.ext().cmp(&b.url.ext()) + } else { + let a_ext = a.url.ext().map_or([].as_slice(), |s| s.as_encoded_bytes()); + let b_ext = b.url.ext().map_or([].as_slice(), |s| s.as_encoded_bytes()); + self.cmp_insensitive_no_promote(a_ext, b_ext) + } + } + SortBy::Alphabetical => { + if self.sensitive { + a.urn().encoded_bytes().cmp(b.urn().encoded_bytes()) + } else { + self.cmp_insensitive_no_promote(a.urn().encoded_bytes(), b.urn().encoded_bytes()) + } + } + SortBy::Natural => { + if self.translit { + natsort( + a.urn().encoded_bytes().transliterate().as_bytes(), + b.urn().encoded_bytes().transliterate().as_bytes(), + !self.sensitive, + ) + } else { + natsort(a.urn().encoded_bytes(), b.urn().encoded_bytes(), !self.sensitive) + } + } + 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 }; + aa.unwrap_or(a.len).cmp(&bb.unwrap_or(b.len)) + } + SortBy::Random => { + // For consistent results in multi-sort, we can't use true randomness + // Instead, use a hash-based comparison for deterministic "randomness" + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher_a = DefaultHasher::new(); + let mut hasher_b = DefaultHasher::new(); + a.urn().hash(&mut hasher_a); + b.urn().hash(&mut hasher_b); + hasher_a.finish().cmp(&hasher_b.finish()) + } + }; + + if self.reverse { ordering.reverse() } else { ordering } + } + + #[inline(always)] + fn cmp_insensitive_no_promote(&self, a: &[u8], b: &[u8]) -> std::cmp::Ordering { + let l = a.len().min(b.len()); + let (lhs, rhs) = if self.reverse { (b, a) } else { (a, b) }; + + for i in 0..l.min(lhs.len()).min(rhs.len()) { + match lhs[i].to_ascii_lowercase().cmp(&rhs[i].to_ascii_lowercase()) { + std::cmp::Ordering::Equal => (), + not_eq => return not_eq, + } + } + + if self.reverse { b.len().cmp(&a.len()) } else { a.len().cmp(&b.len()) } + } + fn sort_naturally(&self, items: &mut [File]) { items.sort_unstable_by(|a, b| { let promote = self.promote(a, b); diff --git a/yazi-fs/src/sorting.rs b/yazi-fs/src/sorting.rs index 14b8a976..642271fa 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, Deserializer, Serialize}; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] @@ -38,3 +38,75 @@ impl Display for SortBy { }) } } + +#[derive(Clone, Debug, Serialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum SortByMulti { + Single(SortBy), + Multiple(Vec), +} + +impl Default for SortByMulti { + fn default() -> Self { + Self::Single(SortBy::default()) + } +} + +impl SortByMulti { + pub fn methods(&self) -> &[SortBy] { + match self { + Self::Single(sort_by) => std::slice::from_ref(sort_by), + Self::Multiple(sort_methods) => sort_methods, + } + } + + pub fn primary(&self) -> SortBy { + match self { + Self::Single(sort_by) => *sort_by, + Self::Multiple(sort_methods) => sort_methods.first().copied().unwrap_or_default(), + } + } +} + +impl<'de> Deserialize<'de> for SortByMulti { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Helper { + Single(SortBy), + Multiple(Vec), + } + + match Helper::deserialize(deserializer)? { + Helper::Single(sort_by) => Ok(Self::Single(sort_by)), + Helper::Multiple(methods) => { + if methods.is_empty() { + Ok(Self::Single(SortBy::None)) + } else { + Ok(Self::Multiple(methods)) + } + } + } + } +} + +impl From for SortByMulti { + fn from(sort_by: SortBy) -> Self { + Self::Single(sort_by) + } +} + +impl Display for SortByMulti { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Single(sort_by) => sort_by.fmt(f), + Self::Multiple(methods) => { + let methods_str: Vec = methods.iter().map(|m| m.to_string()).collect(); + write!(f, "[{}]", methods_str.join(", ")) + } + } + } +}