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
This commit is contained in:
pilnyak 2026-02-09 15:34:11 -07:00
parent 1a121bbfb0
commit 9e7617634b
8 changed files with 52 additions and 2 deletions

View file

@ -11,7 +11,8 @@ pub(super) struct Preference {
v_name: Option<Value>,
v_linemode: Option<Value>,
v_sort_by: Option<Value>,
v_sort_by: Option<Value>,
v_sort_dir_by: Option<Value>,
}
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())
});
}
}

View file

@ -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());

View file

@ -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" },

View file

@ -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

View file

@ -16,6 +16,7 @@ pub struct Mgr {
pub sort_reverse: SyncCell<bool>,
pub sort_dir_first: SyncCell<bool>,
pub sort_translit: SyncCell<bool>,
pub sort_dir_by: SyncCell<SortBy>,
// Display
pub linemode: String,

View file

@ -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<SortBy>,
}
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,
}
}
}

View file

@ -13,6 +13,7 @@ pub struct FilesSorter {
pub reverse: bool,
pub dir_first: bool,
pub translit: bool,
pub dir_by: Option<SortBy>,
}
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<PathBufDyn, u64>) {
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))

View file

@ -10,6 +10,7 @@ pub struct SortOpt {
pub dir_first: Option<bool>,
pub sensitive: Option<bool>,
pub translit: Option<bool>,
pub dir_by: Option<SortBy>,
}
impl TryFrom<CmdCow> for SortOpt {
@ -22,6 +23,7 @@ impl TryFrom<CmdCow> 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()?,
})
}
}