From 9e7617634b4b5b48067781659b23ffb45743ff5d Mon Sep 17 00:00:00 2001 From: pilnyak Date: Mon, 9 Feb 2026 15:34:11 -0700 Subject: [PATCH] feat: add `sort_dir_by` option for separate directory sorting When `sort_dir_first = true`, directories and files are currently sorted by the same key. This adds a `sort_dir_by` config option that, when set, sorts directories independently from files. For example, setting `sort_by = "mtime"` with `sort_dir_by = "natural"` keeps directories alphabetically ordered at the top while files sort by modification time below. Includes: - New `sort_dir_by` config option (default: "none", preserving existing behavior) - `,d` keymap submenu for directory sort options - Lua API exposure via `preference.sort_dir_by` - `--dir-by` flag for the sort command --- yazi-actor/src/lives/preference.rs | 9 +++++++-- yazi-actor/src/mgr/sort.rs | 6 ++++++ yazi-config/preset/keymap-default.toml | 2 ++ yazi-config/preset/yazi-default.toml | 1 + yazi-config/src/mgr/mgr.rs | 1 + yazi-core/src/tab/preference.rs | 6 ++++++ yazi-fs/src/sorter.rs | 27 ++++++++++++++++++++++++++ yazi-parser/src/mgr/sort.rs | 2 ++ 8 files changed, 52 insertions(+), 2 deletions(-) diff --git a/yazi-actor/src/lives/preference.rs b/yazi-actor/src/lives/preference.rs index f9f29a5c..fd10c6de 100644 --- a/yazi-actor/src/lives/preference.rs +++ b/yazi-actor/src/lives/preference.rs @@ -11,7 +11,8 @@ pub(super) struct Preference { v_name: Option, v_linemode: Option, - v_sort_by: Option, + v_sort_by: Option, + v_sort_dir_by: Option, } impl Deref for Preference { @@ -28,7 +29,8 @@ impl Preference { v_name: None, v_linemode: None, - v_sort_by: None, + v_sort_by: None, + v_sort_dir_by: None, }) } } @@ -46,5 +48,8 @@ impl UserData for Preference { fields.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); fields.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); fields.add_field_method_get("sort_translit", |_, me| Ok(me.sort_translit)); + cached_field!(fields, sort_dir_by, |_, me| { + Ok(me.sort_dir_by.map(|s| s.to_string()).unwrap_or_default()) + }); } } diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index 7ad3b322..d22bde41 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -22,6 +22,12 @@ impl Actor for Sort { pref.sort_dir_first = opt.dir_first.unwrap_or(pref.sort_dir_first); pref.sort_sensitive = opt.sensitive.unwrap_or(pref.sort_sensitive); pref.sort_translit = opt.translit.unwrap_or(pref.sort_translit); + pref.sort_dir_by = match (opt.by, opt.dir_by) { + (_, Some(yazi_fs::SortBy::None)) => None, + (_, Some(v)) => Some(v), + (Some(_), None) => None, + (None, None) => pref.sort_dir_by, + }; let sorter = FilesSorter::from(&*pref); let hovered = cx.hovered().map(|f| f.urn().to_owned()); diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index bde93914..50fa3709 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -123,6 +123,8 @@ keymap = [ { on = [ ",", "S" ], run = [ "sort size --reverse=yes", "linemode size" ], desc = "Sort by size (reverse)" }, { on = [ ",", "r" ], run = "sort random --reverse=no", desc = "Sort randomly" }, + { on = [ ",", "d" ], run = [ "sort mtime --reverse=yes --dir-by=natural", "linemode mtime" ], desc = "Sort by modified time, dirs naturally" }, + # Goto { on = [ "g", "h" ], run = "cd ~", desc = "Go home" }, { on = [ "g", "c" ], run = "cd ~/.config", desc = "Go ~/.config" }, diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 4290c9e3..40c4dba9 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -9,6 +9,7 @@ sort_sensitive = false sort_reverse = false sort_dir_first = true sort_translit = false +sort_dir_by = "none" linemode = "none" show_hidden = false show_symlink = true diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 735a161d..cf1a87bc 100644 --- a/yazi-config/src/mgr/mgr.rs +++ b/yazi-config/src/mgr/mgr.rs @@ -16,6 +16,7 @@ pub struct Mgr { pub sort_reverse: SyncCell, pub sort_dir_first: SyncCell, pub sort_translit: SyncCell, + pub sort_dir_by: SyncCell, // Display pub linemode: String, diff --git a/yazi-core/src/tab/preference.rs b/yazi-core/src/tab/preference.rs index 99a07b18..8b472b93 100644 --- a/yazi-core/src/tab/preference.rs +++ b/yazi-core/src/tab/preference.rs @@ -14,6 +14,7 @@ pub struct Preference { pub sort_reverse: bool, pub sort_dir_first: bool, pub sort_translit: bool, + pub sort_dir_by: Option, } impl Default for Preference { @@ -30,6 +31,10 @@ impl Default for Preference { sort_reverse: YAZI.mgr.sort_reverse.get(), sort_dir_first: YAZI.mgr.sort_dir_first.get(), sort_translit: YAZI.mgr.sort_translit.get(), + sort_dir_by: match YAZI.mgr.sort_dir_by.get() { + SortBy::None => None, + v => Some(v), + }, } } } @@ -42,6 +47,7 @@ impl From<&Preference> for FilesSorter { reverse: value.sort_reverse, dir_first: value.sort_dir_first, translit: value.sort_translit, + dir_by: value.sort_dir_by, } } } diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 5f189d2f..21fa67fd 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -13,6 +13,7 @@ pub struct FilesSorter { pub reverse: bool, pub dir_first: bool, pub translit: bool, + pub dir_by: Option, } impl FilesSorter { @@ -21,6 +22,32 @@ impl FilesSorter { return; } + if self.dir_first && self.dir_by.is_some_and(|b| b != self.by) { + let dir_by = self.dir_by.unwrap(); + // Stable-partition: dirs first, then files + items.sort_by(|a, b| b.is_dir().cmp(&a.is_dir())); + let mid = items.iter().position(|f| !f.is_dir()).unwrap_or(items.len()); + let (dirs, files) = items.split_at_mut(mid); + + // Sort dirs with dir_by, non-reversed (already partitioned) + let dir_sorter = + Self { by: dir_by, reverse: false, dir_first: false, dir_by: None, ..*self }; + dir_sorter.sort_with_by(dirs, sizes); + + // Sort files with the main sort_by + let file_sorter = Self { dir_first: false, dir_by: None, ..*self }; + file_sorter.sort_with_by(files, sizes); + return; + } + + self.sort_with_by(items, sizes); + } + + fn sort_with_by(&self, items: &mut [File], sizes: &HashMap) { + if items.is_empty() { + return; + } + let by_alphabetical = |a: &File, b: &File| { if self.sensitive { self.cmp(a.urn().encoded_bytes(), b.urn().encoded_bytes(), self.promote(a, b)) diff --git a/yazi-parser/src/mgr/sort.rs b/yazi-parser/src/mgr/sort.rs index 2bccb47f..91ef5d8a 100644 --- a/yazi-parser/src/mgr/sort.rs +++ b/yazi-parser/src/mgr/sort.rs @@ -10,6 +10,7 @@ pub struct SortOpt { pub dir_first: Option, pub sensitive: Option, pub translit: Option, + pub dir_by: Option, } impl TryFrom for SortOpt { @@ -22,6 +23,7 @@ impl TryFrom for SortOpt { dir_first: c.get("dir-first").ok(), sensitive: c.get("sensitive").ok(), translit: c.get("translit").ok(), + dir_by: c.get::<&str>("dir-by").ok().map(str::parse).transpose()?, }) } }