mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: sort naturally (#82)
This commit is contained in:
parent
a7a085e41c
commit
388ad43537
11 changed files with 136 additions and 87 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -381,6 +381,7 @@ dependencies = [
|
||||||
"crossterm 0.27.0",
|
"crossterm 0.27.0",
|
||||||
"futures",
|
"futures",
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.0",
|
||||||
|
"natord",
|
||||||
"notify",
|
"notify",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
|
@ -1133,6 +1134,12 @@ dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "natord"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use core::{emit, files::FilesSort, input::InputMode};
|
use core::{emit, files::FilesSorter, input::InputMode};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use config::{keymap::{Control, Exec, Key, KeymapLayer}, manager::SortBy, KEYMAP};
|
use config::{keymap::{Control, Exec, Key, KeymapLayer}, manager::SortBy, KEYMAP};
|
||||||
|
|
@ -130,7 +130,7 @@ impl Executor {
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
"sort" => {
|
"sort" => {
|
||||||
let b = cx.manager.current_mut().files.set_sort(FilesSort {
|
let b = cx.manager.current_mut().files.set_sorter(FilesSorter {
|
||||||
by: SortBy::try_from(exec.args.get(0).cloned().unwrap_or_default())
|
by: SortBy::try_from(exec.args.get(0).cloned().unwrap_or_default())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
reverse: exec.named.contains_key("reverse"),
|
reverse: exec.named.contains_key("reverse"),
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@
|
||||||
|
|
||||||
- sort_by: File sorting method
|
- sort_by: File sorting method
|
||||||
|
|
||||||
- `"alphabetical"`: Sort alphabetically
|
- `"alphabetical"`: Sort alphabetically, e.g. `1.md` < `10.md` < `2.md`
|
||||||
- `"created"`: Sort by creation time
|
- `"created"`: Sort by creation time
|
||||||
- `"modified"`: Sort by last modified time
|
- `"modified"`: Sort by last modified time
|
||||||
|
- `"natural"`: Sort naturally, e.g. `1.md` < `2.md` < `10.md`
|
||||||
- `"size"`: Sort by file size
|
- `"size"`: Sort by file size
|
||||||
|
|
||||||
- sort_reverse: Display files in reverse order
|
- sort_reverse: Display files in reverse order
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,16 @@ keymap = [
|
||||||
{ on = [ "c", "n" ], exec = "copy name_without_ext" },
|
{ on = [ "c", "n" ], exec = "copy name_without_ext" },
|
||||||
|
|
||||||
# Sorting
|
# Sorting
|
||||||
{ on = [ ",", "a" ], exec = "sort alphabetical" },
|
{ on = [ ",", "a" ], exec = "sort alphabetical --dir_first" },
|
||||||
{ on = [ ",", "A" ], exec = "sort alphabetical --reverse" },
|
{ on = [ ",", "A" ], exec = "sort alphabetical --reverse --dir_first" },
|
||||||
{ on = [ ",", "c" ], exec = "sort created" },
|
{ on = [ ",", "c" ], exec = "sort created --dir_first" },
|
||||||
{ on = [ ",", "C" ], exec = "sort created --reverse" },
|
{ on = [ ",", "C" ], exec = "sort created --reverse --dir_first" },
|
||||||
{ on = [ ",", "m" ], exec = "sort modified" },
|
{ on = [ ",", "m" ], exec = "sort modified --dir_first" },
|
||||||
{ on = [ ",", "M" ], exec = "sort modified --reverse" },
|
{ on = [ ",", "M" ], exec = "sort modified --reverse --dir_first" },
|
||||||
{ on = [ ",", "s" ], exec = "sort size" },
|
{ on = [ ",", "n" ], exec = "sort natural --dir_first" },
|
||||||
{ on = [ ",", "S" ], exec = "sort size --reverse" },
|
{ on = [ ",", "N" ], exec = "sort natural --reverse --dir_first" },
|
||||||
|
{ on = [ ",", "s" ], exec = "sort size --dir_first" },
|
||||||
|
{ on = [ ",", "S" ], exec = "sort size --reverse --dir_first" },
|
||||||
|
|
||||||
# Tabs
|
# Tabs
|
||||||
{ on = [ "t" ], exec = "tab_create --current" },
|
{ on = [ "t" ], exec = "tab_create --current" },
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub enum SortBy {
|
||||||
Alphabetical,
|
Alphabetical,
|
||||||
Created,
|
Created,
|
||||||
Modified,
|
Modified,
|
||||||
|
Natural,
|
||||||
Size,
|
Size,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ impl TryFrom<String> for SortBy {
|
||||||
"alphabetical" => Self::Alphabetical,
|
"alphabetical" => Self::Alphabetical,
|
||||||
"created" => Self::Created,
|
"created" => Self::Created,
|
||||||
"modified" => Self::Modified,
|
"modified" => Self::Modified,
|
||||||
|
"natural" => Self::Natural,
|
||||||
"size" => Self::Size,
|
"size" => Self::Size,
|
||||||
_ => bail!("invalid sort_by value: {s}"),
|
_ => bail!("invalid sort_by value: {s}"),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ async-channel = "^1"
|
||||||
crossterm = "^0"
|
crossterm = "^0"
|
||||||
futures = "^0"
|
futures = "^0"
|
||||||
indexmap = "^2"
|
indexmap = "^2"
|
||||||
|
natord = "^1"
|
||||||
notify = { version = "^6", default-features = false, features = [ "macos_fsevent" ] }
|
notify = { version = "^6", default-features = false, features = [ "macos_fsevent" ] }
|
||||||
parking_lot = "^0"
|
parking_lot = "^0"
|
||||||
ratatui = "^0"
|
ratatui = "^0"
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
use std::{cmp::Ordering, collections::BTreeMap, ops::{Deref, DerefMut}, path::{Path, PathBuf}};
|
use std::{collections::BTreeMap, ops::{Deref, DerefMut}, path::{Path, PathBuf}};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use config::{manager::SortBy, MANAGER};
|
use config::MANAGER;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
use super::File;
|
use super::{File, FilesSorter};
|
||||||
|
|
||||||
pub struct Files {
|
pub struct Files {
|
||||||
items: IndexMap<PathBuf, File>,
|
items: IndexMap<PathBuf, File>,
|
||||||
pub sort: FilesSort,
|
pub sorter: FilesSorter,
|
||||||
pub show_hidden: bool,
|
pub show_hidden: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ impl Default for Files {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
sort: Default::default(),
|
sorter: Default::default(),
|
||||||
show_hidden: MANAGER.show_hidden,
|
show_hidden: MANAGER.show_hidden,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,12 +53,12 @@ impl Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_sort(&mut self, sort: FilesSort) -> bool {
|
pub fn set_sorter(&mut self, sort: FilesSorter) -> bool {
|
||||||
if self.sort == sort {
|
if self.sorter == sort {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.sort = sort;
|
self.sorter = sort;
|
||||||
self.sort()
|
self.sorter.sort(&mut self.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_read(&mut self, mut items: BTreeMap<PathBuf, File>) -> bool {
|
pub fn update_read(&mut self, mut items: BTreeMap<PathBuf, File>) -> bool {
|
||||||
|
|
@ -80,7 +80,7 @@ impl Files {
|
||||||
|
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
self.items.extend(items);
|
self.items.extend(items);
|
||||||
self.sort();
|
self.sorter.sort(&mut self.items);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,14 +92,14 @@ impl Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.items.extend(items);
|
self.items.extend(items);
|
||||||
self.sort();
|
self.sorter.sort(&mut self.items);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_search(&mut self, items: BTreeMap<PathBuf, File>) -> bool {
|
pub fn update_search(&mut self, items: BTreeMap<PathBuf, File>) -> bool {
|
||||||
if !items.is_empty() {
|
if !items.is_empty() {
|
||||||
self.items.extend(items);
|
self.items.extend(items);
|
||||||
self.sort();
|
self.sorter.sort(&mut self.items);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,51 +110,6 @@ impl Files {
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort(&mut self) -> bool {
|
|
||||||
if self.items.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[allow(clippy::collapsible_else_if)]
|
|
||||||
fn cmp<T: Ord>(a: T, b: T, reverse: bool, promote: Ordering) -> Ordering {
|
|
||||||
if promote != Ordering::Equal {
|
|
||||||
promote
|
|
||||||
} else {
|
|
||||||
if reverse { b.cmp(&a) } else { a.cmp(&b) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn promote(a: &File, b: &File, dir_first: bool) -> Ordering {
|
|
||||||
if dir_first { b.meta.is_dir().cmp(&a.meta.is_dir()) } else { Ordering::Equal }
|
|
||||||
}
|
|
||||||
|
|
||||||
let reverse = self.sort.reverse;
|
|
||||||
let dir_first = self.sort.dir_first;
|
|
||||||
match self.sort.by {
|
|
||||||
SortBy::Alphabetical => {
|
|
||||||
self.items.sort_by(|_, a, _, b| cmp(&a.path, &b.path, reverse, promote(a, b, dir_first)))
|
|
||||||
}
|
|
||||||
SortBy::Created => self.items.sort_by(|_, a, _, b| {
|
|
||||||
if let (Ok(aa), Ok(bb)) = (a.meta.created(), b.meta.created()) {
|
|
||||||
return cmp(aa, bb, reverse, promote(a, b, dir_first));
|
|
||||||
}
|
|
||||||
Ordering::Equal
|
|
||||||
}),
|
|
||||||
SortBy::Modified => self.items.sort_by(|_, a, _, b| {
|
|
||||||
if let (Ok(aa), Ok(bb)) = (a.meta.modified(), b.meta.modified()) {
|
|
||||||
return cmp(aa, bb, reverse, promote(a, b, dir_first));
|
|
||||||
}
|
|
||||||
Ordering::Equal
|
|
||||||
}),
|
|
||||||
SortBy::Size => self.items.sort_by(|_, a, _, b| {
|
|
||||||
cmp(a.length.unwrap_or(0), b.length.unwrap_or(0), reverse, promote(a, b, dir_first))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Files {
|
impl Deref for Files {
|
||||||
|
|
@ -167,23 +122,6 @@ impl DerefMut for Files {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items }
|
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub struct FilesSort {
|
|
||||||
pub by: SortBy,
|
|
||||||
pub reverse: bool,
|
|
||||||
pub dir_first: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FilesSort {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
by: MANAGER.sort_by,
|
|
||||||
reverse: MANAGER.sort_reverse,
|
|
||||||
dir_first: MANAGER.sort_dir_first,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FilesOp {
|
pub enum FilesOp {
|
||||||
Read(PathBuf, BTreeMap<PathBuf, File>),
|
Read(PathBuf, BTreeMap<PathBuf, File>),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
mod file;
|
mod file;
|
||||||
mod files;
|
mod files;
|
||||||
|
mod sorter;
|
||||||
|
|
||||||
pub use file::*;
|
pub use file::*;
|
||||||
pub use files::*;
|
pub use files::*;
|
||||||
|
pub use sorter::*;
|
||||||
|
|
|
||||||
96
core/src/files/sorter.rs
Normal file
96
core/src/files/sorter.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
use std::{cmp::Ordering, path::PathBuf};
|
||||||
|
|
||||||
|
use config::{manager::SortBy, MANAGER};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
|
use super::File;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct FilesSorter {
|
||||||
|
pub by: SortBy,
|
||||||
|
pub reverse: bool,
|
||||||
|
pub dir_first: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FilesSorter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
by: MANAGER.sort_by,
|
||||||
|
reverse: MANAGER.sort_reverse,
|
||||||
|
dir_first: MANAGER.sort_dir_first,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilesSorter {
|
||||||
|
pub(super) fn sort(&self, items: &mut IndexMap<PathBuf, File>) -> bool {
|
||||||
|
if items.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.by {
|
||||||
|
SortBy::Alphabetical => {
|
||||||
|
items.sort_unstable_by(|_, a, _, b| self.cmp(&a.path, &b.path, self.promote(a, b)))
|
||||||
|
}
|
||||||
|
SortBy::Created => items.sort_unstable_by(|_, a, _, b| {
|
||||||
|
if let (Ok(aa), Ok(bb)) = (a.meta.created(), b.meta.created()) {
|
||||||
|
return self.cmp(aa, bb, self.promote(a, b));
|
||||||
|
}
|
||||||
|
Ordering::Equal
|
||||||
|
}),
|
||||||
|
SortBy::Modified => items.sort_unstable_by(|_, a, _, b| {
|
||||||
|
if let (Ok(aa), Ok(bb)) = (a.meta.modified(), b.meta.modified()) {
|
||||||
|
return self.cmp(aa, bb, self.promote(a, b));
|
||||||
|
}
|
||||||
|
Ordering::Equal
|
||||||
|
}),
|
||||||
|
SortBy::Natural => self.sort_naturally(items),
|
||||||
|
SortBy::Size => items.sort_unstable_by(|_, a, _, b| {
|
||||||
|
self.cmp(a.length.unwrap_or(0), b.length.unwrap_or(0), self.promote(a, b))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_naturally(&self, items: &mut IndexMap<PathBuf, File>) {
|
||||||
|
let mut indices = Vec::with_capacity(items.len());
|
||||||
|
let mut entities = Vec::with_capacity(items.len());
|
||||||
|
for (i, (path, file)) in items.into_iter().enumerate() {
|
||||||
|
indices.push(i);
|
||||||
|
entities.push((path.to_string_lossy(), file));
|
||||||
|
}
|
||||||
|
|
||||||
|
indices.sort_unstable_by(|&a, &b| {
|
||||||
|
let promote = self.promote(entities[a].1, entities[b].1);
|
||||||
|
if promote != Ordering::Equal {
|
||||||
|
promote
|
||||||
|
} else if self.reverse {
|
||||||
|
natord::compare(&entities[b].0, &entities[a].0)
|
||||||
|
} else {
|
||||||
|
natord::compare(&entities[a].0, &entities[b].0)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut new = IndexMap::with_capacity(indices.len());
|
||||||
|
for i in indices {
|
||||||
|
let file = entities[i].1.clone();
|
||||||
|
new.insert(file.path(), file);
|
||||||
|
}
|
||||||
|
*items = new;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[allow(clippy::collapsible_else_if)]
|
||||||
|
fn cmp<T: Ord>(&self, a: T, b: T, promote: Ordering) -> Ordering {
|
||||||
|
if promote != Ordering::Equal {
|
||||||
|
promote
|
||||||
|
} else {
|
||||||
|
if self.reverse { b.cmp(&a) } else { a.cmp(&b) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn promote(&self, a: &File, b: &File) -> Ordering {
|
||||||
|
if self.dir_first { b.meta.is_dir().cmp(&a.meta.is_dir()) } else { Ordering::Equal }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -212,7 +212,7 @@ impl Tasks {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn precache_size(&self, targets: &Files) -> bool {
|
pub fn precache_size(&self, targets: &Files) -> bool {
|
||||||
if targets.sort.by != SortBy::Size {
|
if targets.sorter.by != SortBy::Size {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel"],"language":"en","flagWords":[],"version":"0.2"}
|
{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord"],"language":"en","version":"0.2"}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue