feat: switch sorting by key; precalculate and cache directory size when sorting by size; use BTreeMap to improve performance

- Switch sorting by key
- Precalculate and cache directory size when sorting by size
- Use `BTreeMap` to improve performance
This commit is contained in:
三咲雅 · Misaki Masa 2023-07-25 11:31:34 +08:00 committed by GitHub
parent 688afeaf27
commit e2a4894111
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 276 additions and 94 deletions

10
Cargo.lock generated
View file

@ -1593,9 +1593,9 @@ dependencies = [
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.5" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]] [[package]]
name = "slab" name = "slab"
@ -2340,9 +2340,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -2364,7 +2364,7 @@ dependencies = [
[[package]] [[package]]
name = "yazi" name = "yazi"
version = "0.1.0" version = "0.1.2"
dependencies = [ dependencies = [
"ansi-to-tui", "ansi-to-tui",
"anyhow", "anyhow",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "yazi" name = "yazi"
version = "0.1.0" version = "0.1.2"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View file

@ -25,7 +25,7 @@ keymap = [
{ on = [ "<Enter>" ], exec = "open" }, { on = [ "<Enter>" ], exec = "open" },
# Selection # Selection
{ on = [ "<Space>" ], exec = "select --state=none" }, { on = [ "<Space>" ], exec = [ "select --state=none", "arrow 1" ] },
{ on = [ "v" ], exec = "visual_mode" }, { on = [ "v" ], exec = "visual_mode" },
{ on = [ "V" ], exec = "visual_mode --unset" }, { on = [ "V" ], exec = "visual_mode --unset" },
{ on = [ "<C-a>" ], exec = "select_all --state=true" }, { on = [ "<C-a>" ], exec = "select_all --state=true" },
@ -33,13 +33,11 @@ keymap = [
# Operation # Operation
{ on = [ "o" ], exec = "open" }, { on = [ "o" ], exec = "open" },
{ on = [ "O" ], exec = "open --select" }, { on = [ "O" ], exec = "open --interactive" },
{ on = [ "y" ], exec = "yank" }, { on = [ "y" ], exec = "yank" },
{ on = [ "x" ], exec = "yank --cut" }, { on = [ "x" ], exec = "yank --cut" },
{ on = [ "p" ], exec = "paste" }, { on = [ "p" ], exec = "paste" },
{ on = [ "P" ], exec = "paste --force" }, { on = [ "P" ], exec = "paste --force" },
{ on = [ "f" ], exec = "paste --follow" },
{ on = [ "F" ], exec = "paste --follow --force" },
{ on = [ "d" ], exec = "remove" }, { on = [ "d" ], exec = "remove" },
{ on = [ "D" ], exec = "remove --permanently" }, { on = [ "D" ], exec = "remove --permanently" },
{ on = [ "c" ], exec = "create" }, { on = [ "c" ], exec = "create" },
@ -51,6 +49,16 @@ keymap = [
{ on = [ "z" ], exec = "jump zoxide" }, { on = [ "z" ], exec = "jump zoxide" },
{ on = [ "Z" ], exec = "jump fzf" }, { on = [ "Z" ], exec = "jump fzf" },
# Sorting
{ on = [ ",", "a" ], exec = "sort alphabetical" },
{ on = [ ",", "A" ], exec = "sort alphabetical --reverse" },
{ on = [ ",", "c" ], exec = "sort created --reverse" },
{ on = [ ",", "C" ], exec = "sort created" },
{ on = [ ",", "m" ], exec = "sort modified --reverse" },
{ on = [ ",", "M" ], exec = "sort modified" },
{ on = [ ",", "s" ], exec = "sort size --reverse" },
{ on = [ ",", "S" ], exec = "sort size" },
# Tabs # Tabs
{ on = [ "t" ], exec = "tab_create --current" }, { on = [ "t" ], exec = "tab_create --current" },
@ -122,6 +130,7 @@ keymap = [
{ on = [ "a" ], exec = "insert --append" }, { on = [ "a" ], exec = "insert --append" },
{ on = [ "v" ], exec = "visual" }, { on = [ "v" ], exec = "visual" },
# Navigation
{ on = [ "h" ], exec = "move -1" }, { on = [ "h" ], exec = "move -1" },
{ on = [ "l" ], exec = "move 1" }, { on = [ "l" ], exec = "move 1" },
@ -137,14 +146,17 @@ keymap = [
{ on = [ "w" ], exec = "forward" }, { on = [ "w" ], exec = "forward" },
{ on = [ "e" ], exec = "forward --end-of-word" }, { on = [ "e" ], exec = "forward --end-of-word" },
# Deletion
{ on = [ "d" ], exec = "delete --cut" }, { on = [ "d" ], exec = "delete --cut" },
{ on = [ "c" ], exec = "delete --cut --insert" }, { on = [ "c" ], exec = "delete --cut --insert" },
{ on = [ "x" ], exec = [ "delete --cut", "move 1 --in-operating" ] }, { on = [ "x" ], exec = [ "delete --cut", "move 1 --in-operating" ] },
# Copy/Paste
{ on = [ "y" ], exec = [ "yank" ] }, { on = [ "y" ], exec = [ "yank" ] },
{ on = [ "p" ], exec = [ "paste" ] }, { on = [ "p" ], exec = [ "paste" ] },
{ on = [ "P" ], exec = [ "paste --before" ] }, { on = [ "P" ], exec = [ "paste --before" ] },
# Undo/Redo
{ on = [ "u" ], exec = [ "undo" ] }, { on = [ "u" ], exec = [ "undo" ] },
{ on = [ "<C-r>" ], exec = [ "redo" ] }, { on = [ "<C-r>" ], exec = [ "redo" ] },
] ]

View file

@ -1 +1 @@
{"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"],"language":"en"} {"language":"en","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"],"version":"0.2"}

View file

@ -42,7 +42,7 @@
- open: Open the selected files. - open: Open the selected files.
- `--select`: Open the selected files with an interactive ui to choose the opening method. - `--interactive`: Open the selected files with an interactive ui to choose the opening method.
- yank: Copy the selected files. - yank: Copy the selected files.

View file

@ -1,9 +1,10 @@
use anyhow::bail; use anyhow::bail;
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize, Clone, Copy)] #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(try_from = "String")] #[serde(try_from = "String")]
pub enum SortBy { pub enum SortBy {
#[default]
Alphabetical, Alphabetical,
Created, Created,
Modified, Modified,
@ -15,10 +16,10 @@ impl TryFrom<String> for SortBy {
fn try_from(s: String) -> Result<Self, Self::Error> { fn try_from(s: String) -> Result<Self, Self::Error> {
Ok(match s.as_str() { Ok(match s.as_str() {
"alphabetical" => Self::Alphabetical,
"created" => Self::Created, "created" => Self::Created,
"modified" => Self::Modified, "modified" => Self::Modified,
"size" => Self::Size, "size" => Self::Size,
"alphabetical" => Self::Alphabetical,
_ => bail!("invalid sort_by value: {}", s), _ => bail!("invalid sort_by value: {}", s),
}) })
} }

View file

@ -1,4 +1,4 @@
use std::{ops::{Deref, DerefMut}, path::{Path, PathBuf}}; use std::{collections::BTreeMap, ops::{Deref, DerefMut}, path::{Path, PathBuf}};
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
@ -10,13 +10,13 @@ use crate::config::{manager::SortBy, MANAGER};
#[derive(Default)] #[derive(Default)]
pub struct Files { pub struct Files {
items: IndexMap<PathBuf, File>, items: IndexMap<PathBuf, File>,
sort: FilesSort, pub sort: FilesSort,
pub show_hidden: bool, pub show_hidden: bool,
} }
impl Files { impl Files {
pub async fn read(paths: Vec<PathBuf>) -> IndexMap<PathBuf, File> { pub async fn read(paths: Vec<PathBuf>) -> BTreeMap<PathBuf, File> {
let mut items = IndexMap::new(); let mut items = BTreeMap::new();
for path in paths { for path in paths {
if let Ok(file) = File::from(&path).await { if let Ok(file) = File::from(&path).await {
items.insert(path, file); items.insert(path, file);
@ -25,9 +25,9 @@ impl Files {
items items
} }
pub async fn read_dir(path: &Path) -> Result<IndexMap<PathBuf, File>> { pub async fn read_dir(path: &Path) -> Result<BTreeMap<PathBuf, File>> {
let mut it = fs::read_dir(path).await?; let mut it = fs::read_dir(path).await?;
let mut items = IndexMap::new(); let mut items = BTreeMap::new();
while let Ok(Some(item)) = it.next_entry().await { while let Ok(Some(item)) = it.next_entry().await {
if let Ok(meta) = item.metadata().await { if let Ok(meta) = item.metadata().await {
let path = item.path(); let path = item.path();
@ -38,7 +38,75 @@ impl Files {
Ok(items) Ok(items)
} }
pub fn sort(&mut self) { #[inline]
pub fn duplicate(&self, idx: usize) -> Option<File> {
self.items.get_index(idx).map(|(_, file)| file.clone())
}
#[inline]
pub fn set_sort(&mut self, sort: FilesSort) -> bool {
if self.sort == sort {
return false;
}
self.sort = sort;
self.sort()
}
pub fn update_read(&mut self, mut items: BTreeMap<PathBuf, File>) -> bool {
if !self.show_hidden {
items.retain(|_, item| !item.is_hidden);
}
for (path, item) in &mut items {
if let Some(old) = self.items.get(path) {
item.is_selected = old.is_selected;
// Calculate the size of directories is expensive, so we keep the old value,
// before a new value is calculated and comes to.
if item.meta.is_dir() {
item.length = old.length;
}
}
}
self.items.clear();
self.items.extend(items);
self.sort();
true
}
pub fn update_sort(&mut self, mut items: BTreeMap<PathBuf, File>) -> bool {
for (path, item) in &mut items {
if let Some(old) = self.items.get(path) {
item.is_selected = old.is_selected;
}
}
self.items.extend(items);
self.sort();
true
}
pub fn update_search(&mut self, items: BTreeMap<PathBuf, File>) -> bool {
if !items.is_empty() {
self.items.extend(items);
self.sort();
return true;
}
if !self.items.is_empty() {
self.items.clear();
return true;
}
false
}
fn sort(&mut self) -> bool {
if self.items.is_empty() {
return false;
}
fn cmp<T: Ord>(a: T, b: T, reverse: bool) -> std::cmp::Ordering { fn cmp<T: Ord>(a: T, b: T, reverse: bool) -> std::cmp::Ordering {
if reverse { b.cmp(&a) } else { a.cmp(&b) } if reverse { b.cmp(&a) } else { a.cmp(&b) }
} }
@ -62,44 +130,8 @@ impl Files {
self.items.sort_by(|_, a, _, b| cmp(a.length.unwrap_or(0), b.length.unwrap_or(0), reverse)) self.items.sort_by(|_, a, _, b| cmp(a.length.unwrap_or(0), b.length.unwrap_or(0), reverse))
} }
} }
}
#[inline]
pub fn duplicate(&self, idx: usize) -> Option<File> {
self.items.get_index(idx).map(|(_, file)| file.clone())
}
pub fn update_read(&mut self, mut items: IndexMap<PathBuf, File>) -> bool {
if !self.show_hidden {
items.retain(|_, item| !item.is_hidden);
}
for (path, item) in &mut items {
if let Some(old) = self.items.get(path) {
item.length = old.length;
item.is_selected = old.is_selected;
}
}
self.items = items;
self.sort();
true true
} }
pub fn update_search(&mut self, items: IndexMap<PathBuf, File>) -> bool {
if !items.is_empty() {
self.items.extend(items);
self.sort();
return true;
}
if !self.items.is_empty() {
self.items.clear();
return true;
}
false
}
} }
impl Deref for Files { impl Deref for Files {
@ -112,7 +144,8 @@ 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 }
} }
struct FilesSort { #[derive(PartialEq)]
pub struct FilesSort {
pub by: SortBy, pub by: SortBy,
pub reverse: bool, pub reverse: bool,
} }
@ -123,9 +156,10 @@ impl Default for FilesSort {
#[derive(Debug)] #[derive(Debug)]
pub enum FilesOp { pub enum FilesOp {
Read(PathBuf, IndexMap<PathBuf, File>), Read(PathBuf, BTreeMap<PathBuf, File>),
Sort(PathBuf, BTreeMap<PathBuf, File>),
Search(PathBuf, BTreeMap<PathBuf, File>),
IOErr(PathBuf), IOErr(PathBuf),
Search(PathBuf, IndexMap<PathBuf, File>),
} }
impl FilesOp { impl FilesOp {
@ -133,15 +167,16 @@ impl FilesOp {
pub fn path(&self) -> PathBuf { pub fn path(&self) -> PathBuf {
match self { match self {
Self::Read(path, _) => path, Self::Read(path, _) => path,
Self::IOErr(path) => path, Self::Sort(path, _) => path,
Self::Search(path, _) => path, Self::Search(path, _) => path,
Self::IOErr(path) => path,
} }
.clone() .clone()
} }
#[inline] #[inline]
pub fn read_empty(path: &Path) -> Self { Self::Read(path.to_path_buf(), IndexMap::new()) } pub fn read_empty(path: &Path) -> Self { Self::Read(path.to_path_buf(), BTreeMap::new()) }
#[inline] #[inline]
pub fn search_empty(path: &Path) -> Self { Self::Search(path.to_path_buf(), IndexMap::new()) } pub fn search_empty(path: &Path) -> Self { Self::Search(path.to_path_buf(), BTreeMap::new()) }
} }

View file

@ -31,6 +31,7 @@ impl Folder {
pub fn update(&mut self, op: FilesOp) -> bool { pub fn update(&mut self, op: FilesOp) -> bool {
let b = match op { let b = match op {
FilesOp::Read(_, items) => self.files.update_read(items), FilesOp::Read(_, items) => self.files.update_read(items),
FilesOp::Sort(_, items) => self.files.update_sort(items),
FilesOp::Search(_, items) => self.files.update_search(items), FilesOp::Search(_, items) => self.files.update_search(items),
_ => unreachable!(), _ => unreachable!(),
}; };

View file

@ -114,7 +114,7 @@ impl Manager {
self.quit(tasks) self.quit(tasks)
} }
pub fn open(&mut self, select: bool) -> bool { pub fn open(&mut self, interactive: bool) -> bool {
let files = self.selected(); let files = self.selected();
if files.len() == 1 && files[0].meta.is_dir() { if files.len() == 1 && files[0].meta.is_dir() {
return self.active_mut().enter(); return self.active_mut().enter();
@ -143,7 +143,7 @@ impl Manager {
} }
let files = files.into_iter().filter_map(|(p, m)| m.map(|m| (p, m))).collect::<Vec<_>>(); let files = files.into_iter().filter_map(|(p, m)| m.map(|m| (p, m))).collect::<Vec<_>>();
if !select { if !interactive {
emit!(Open(files, None)); emit!(Open(files, None));
return; return;
} }
@ -251,6 +251,20 @@ impl Manager {
b b
} }
pub fn update_search(&mut self, op: FilesOp) -> bool {
let path = op.path();
if self.current().in_search && self.current().cwd == path {
return self.current_mut().update(op);
}
let rep = mem::replace(self.current_mut(), Folder::new_search(&path));
if !rep.in_search {
self.active_mut().history.insert(path, rep);
}
self.current_mut().update(op);
true
}
pub fn update_ioerr(&mut self, op: FilesOp) -> bool { pub fn update_ioerr(&mut self, op: FilesOp) -> bool {
let path = op.path(); let path = op.path();
let op = FilesOp::read_empty(&path); let op = FilesOp::read_empty(&path);
@ -267,20 +281,6 @@ impl Manager {
true true
} }
pub fn update_search(&mut self, op: FilesOp) -> bool {
let path = op.path();
if self.current().in_search && self.current().cwd == path {
return self.current_mut().update(op);
}
let rep = mem::replace(self.current_mut(), Folder::new_search(&path));
if !rep.in_search {
self.active_mut().history.insert(path, rep);
}
self.current_mut().update(op);
true
}
pub fn update_mimetype(&mut self, mut mimes: BTreeMap<PathBuf, String>, tasks: &Tasks) -> bool { pub fn update_mimetype(&mut self, mut mimes: BTreeMap<PathBuf, String>, tasks: &Tasks) -> bool {
mimes.retain(|f, m| self.mimetype.get(f) != Some(m)); mimes.retain(|f, m| self.mimetype.get(f) != Some(m));
if mimes.is_empty() { if mimes.is_empty() {

View file

@ -7,11 +7,11 @@ use super::{Folder, Mode, Preview};
use crate::{core::{external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp}, input::InputOpt, Event, Position, BLOCKER}, emit, misc::Defer}; use crate::{core::{external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp}, input::InputOpt, Event, Position, BLOCKER}, emit, misc::Defer};
pub struct Tab { pub struct Tab {
pub(super) mode: Mode,
pub(super) current: Folder, pub(super) current: Folder,
pub(super) parent: Option<Folder>, pub(super) parent: Option<Folder>,
search: Option<JoinHandle<Result<()>>>,
pub(super) mode: Mode, search: Option<JoinHandle<Result<()>>>,
pub(super) history: BTreeMap<PathBuf, Folder>, pub(super) history: BTreeMap<PathBuf, Folder>,
pub(super) preview: Preview, pub(super) preview: Preview,
@ -20,11 +20,11 @@ pub struct Tab {
impl Tab { impl Tab {
pub fn new(path: &Path) -> Self { pub fn new(path: &Path) -> Self {
Self { Self {
mode: Default::default(),
current: Folder::new(path), current: Folder::new(path),
parent: path.parent().map(|p| Folder::new(p)), parent: path.parent().map(|p| Folder::new(p)),
search: None,
mode: Default::default(), search: None,
history: Default::default(), history: Default::default(),
preview: Preview::new(), preview: Preview::new(),

View file

@ -1,4 +1,4 @@
use std::{collections::BTreeSet, path::{Path, PathBuf}, sync::Arc}; use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}, sync::Arc};
use indexmap::IndexMap; use indexmap::IndexMap;
use notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _Watcher}; use notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _Watcher};
@ -92,7 +92,7 @@ impl Watcher {
for ori in linked { for ori in linked {
emit!(Files(match &result { emit!(Files(match &result {
Ok(items) => { Ok(items) => {
let files = IndexMap::from_iter(items.iter().map(|(p, f)| { let files = BTreeMap::from_iter(items.iter().map(|(p, f)| {
let p = ori.join(p.strip_prefix(&path).unwrap()); let p = ori.join(p.strip_prefix(&path).unwrap());
let f = f.clone().set_path(&p); let f = f.clone().set_path(&p);
(p, f) (p, f)

View file

@ -1,11 +1,11 @@
use std::path::{Path, PathBuf}; use std::{collections::BTreeMap, path::{Path, PathBuf}, sync::Arc};
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use image::{imageops::FilterType, ImageFormat}; use image::{imageops::FilterType, ImageFormat};
use tokio::{fs, sync::mpsc}; use tokio::{fs, sync::mpsc};
use super::TaskOp; use super::TaskOp;
use crate::{config::PREVIEW, core::external, emit}; use crate::{config::PREVIEW, core::{external, files::{File, FilesOp}}, emit, misc::{calculate_size, Throttle}};
pub struct Precache { pub struct Precache {
rx: async_channel::Receiver<PrecacheOp>, rx: async_channel::Receiver<PrecacheOp>,
@ -20,6 +20,13 @@ pub(super) enum PrecacheOp {
Video(PrecacheOpVideo), Video(PrecacheOpVideo),
} }
#[derive(Debug)]
pub(super) struct PrecacheOpSize {
pub id: usize,
pub target: PathBuf,
pub throttle: Arc<Throttle<(PathBuf, File)>>,
}
#[derive(Debug)] #[derive(Debug)]
pub(super) struct PrecacheOpMime { pub(super) struct PrecacheOpMime {
pub id: usize, pub id: usize,
@ -88,6 +95,7 @@ impl Precache {
Ok(()) Ok(())
} }
#[inline]
fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) } fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) }
pub(super) async fn mime(&self, task: PrecacheOpMime) -> Result<()> { pub(super) async fn mime(&self, task: PrecacheOpMime) -> Result<()> {
@ -100,6 +108,22 @@ impl Precache {
self.done(task.id) self.done(task.id)
} }
pub(super) async fn size(&self, task: PrecacheOpSize) -> Result<()> {
self.sch.send(TaskOp::New(task.id, 0))?;
let length = Some(calculate_size(&task.target).await);
if let Ok(mut file) = File::from(&task.target).await {
file.length = length;
task.throttle.done((task.target, file), |buf| {
let parent = buf[0].0.parent().unwrap().to_path_buf();
emit!(Files(FilesOp::Sort(parent, BTreeMap::from_iter(buf))));
});
};
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
self.done(task.id)
}
pub(super) fn image(&self, id: usize, targets: Vec<PathBuf>) -> Result<()> { pub(super) fn image(&self, id: usize, targets: Vec<PathBuf>) -> Result<()> {
for target in targets { for target in targets {
self.sch.send(TaskOp::New(id, 0))?; self.sch.send(TaskOp::New(id, 0))?;

View file

@ -6,8 +6,8 @@ use parking_lot::RwLock;
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep}; use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep};
use tracing::{info, trace}; use tracing::{info, trace};
use super::{File, FileOpDelete, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, Process, ProcessOpOpen, Task, TaskOp, TaskStage}; use super::{File, FileOpDelete, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen, Task, TaskOp, TaskStage};
use crate::{config::open::Opener, emit, misc::unique_path}; use crate::{config::open::Opener, emit, misc::{unique_path, Throttle}};
#[derive(Default)] #[derive(Default)]
pub(super) struct Running { pub(super) struct Running {
@ -381,6 +381,24 @@ impl Scheduler {
}); });
} }
pub(super) fn precache_size(&self, targets: Vec<PathBuf>) {
let throttle = Arc::new(Throttle::new(targets.len(), Duration::from_millis(300)));
for target in targets {
let name = format!("Calculate the size of {:?}", target);
let id = self.running.write().add(name);
let _ = self.todo.send_blocking({
let precache = self.precache.clone();
let throttle = throttle.clone();
async move {
precache.size(PrecacheOpSize { id, target, throttle }).await.ok();
}
.boxed()
});
}
}
pub(super) fn precache_mime(&self, targets: Vec<PathBuf>) { pub(super) fn precache_mime(&self, targets: Vec<PathBuf>) {
let name = format!("Preload mimetype for {} files", targets.len()); let name = format!("Preload mimetype for {} files", targets.len());
let id = self.running.write().add(name); let id = self.running.write().add(name);

View file

@ -3,7 +3,7 @@ use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, path::{Path, Pa
use tracing::trace; use tracing::trace;
use super::{Scheduler, TASKS_PADDING, TASKS_PERCENT}; use super::{Scheduler, TASKS_PADDING, TASKS_PERCENT};
use crate::{config::{open::Opener, OPEN}, core::{files::File, input::InputOpt, Position}, emit, misc::{tty_size, MimeKind}}; use crate::{config::{manager::SortBy, open::Opener, OPEN}, core::{files::{File, Files}, input::InputOpt, Position}, emit, misc::{tty_size, MimeKind}};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Task { pub struct Task {
@ -180,6 +180,25 @@ impl Tasks {
false false
} }
#[inline]
pub fn precache_size(&self, targets: &Files) -> bool {
if targets.sort.by != SortBy::Size {
return false;
}
let targets = targets
.iter()
.filter(|(_, f)| f.meta.is_dir() && f.length.is_none())
.map(|(p, _)| p.clone())
.collect::<Vec<_>>();
if !targets.is_empty() {
self.scheduler.precache_size(targets);
}
false
}
#[inline] #[inline]
pub fn precache_mime(&self, targets: Vec<&File>, mimetype: &HashMap<PathBuf, String>) -> bool { pub fn precache_mime(&self, targets: Vec<&File>, mimetype: &HashMap<PathBuf, String>) -> bool {
let targets = targets let targets = targets

View file

@ -23,7 +23,7 @@ impl Which {
self.cands = KEYMAP self.cands = KEYMAP
.get(layer) .get(layer)
.into_iter() .into_iter()
.filter(|s| !s.on.is_empty() && s.on[0] == *key) .filter(|s| s.on.len() > 1 && s.on[0] == *key)
.cloned() .cloned()
.collect(); .collect();
self.visible = true; self.visible = true;

View file

@ -4,6 +4,7 @@ mod defer;
mod fns; mod fns;
mod fs; mod fs;
mod mime; mod mime;
mod throttle;
mod tty; mod tty;
pub use buffer::*; pub use buffer::*;
@ -12,4 +13,5 @@ pub use defer::*;
pub use fns::*; pub use fns::*;
pub use fs::*; pub use fs::*;
pub use mime::*; pub use mime::*;
pub use throttle::*;
pub use tty::*; pub use tty::*;

54
src/misc/throttle.rs Normal file
View file

@ -0,0 +1,54 @@
use std::{fmt::Debug, mem, sync::atomic::{AtomicU64, AtomicUsize, Ordering}, time::{self, Duration, SystemTime}};
use parking_lot::Mutex;
#[derive(Debug)]
pub struct Throttle<T> {
total: AtomicUsize,
interval: Duration,
last: AtomicU64,
buf: Mutex<Vec<T>>,
}
impl<T> Throttle<T> {
pub fn new(total: usize, interval: Duration) -> Self {
Self {
total: AtomicUsize::new(total),
interval,
last: AtomicU64::new(
SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_millis() as u64
- interval.as_millis() as u64,
),
buf: Default::default(),
}
}
pub fn done<F>(&self, data: T, f: F)
where
F: FnOnce(Vec<T>),
{
let total = self.total.fetch_sub(1, Ordering::Relaxed);
if total == 1 {
return self.flush(data, f);
}
let last = self.last.load(Ordering::Relaxed);
let now = SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_millis() as u64;
if now > self.interval.as_millis() as u64 + last {
self.last.store(now, Ordering::Relaxed);
return self.flush(data, f);
}
self.buf.lock().push(data);
}
#[inline]
fn flush<F>(&self, data: T, f: F)
where
F: FnOnce(Vec<T>),
{
let mut buf = mem::replace(&mut *self.buf.lock(), Vec::new());
buf.push(data);
f(buf)
}
}

View file

@ -100,14 +100,19 @@ impl App {
manager.refresh(); manager.refresh();
} }
Event::Files(op) => { Event::Files(op) => {
let calc = matches!(op, FilesOp::Read(..) | FilesOp::Search(..));
let b = match op { let b = match op {
FilesOp::Read(..) => manager.update_read(op), FilesOp::Read(..) => manager.update_read(op),
FilesOp::IOErr(..) => manager.update_ioerr(op), FilesOp::Sort(..) => manager.update_read(op),
FilesOp::Search(..) => manager.update_search(op), FilesOp::Search(..) => manager.update_search(op),
FilesOp::IOErr(..) => manager.update_ioerr(op),
}; };
if b { if b {
emit!(Render); emit!(Render);
} }
if calc {
tasks.precache_size(&manager.current().files);
}
} }
Event::Pages(page) => { Event::Pages(page) => {
if manager.current().page == page { if manager.current().page == page {

View file

@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use super::Ctx; use super::Ctx;
use crate::{config::{keymap::{Control, Exec, Key, KeymapLayer}, KEYMAP}, core::input::InputMode, emit, misc::optional_bool}; use crate::{config::{keymap::{Control, Exec, Key, KeymapLayer}, manager::SortBy, KEYMAP}, core::{files::FilesSort, input::InputMode}, emit, misc::optional_bool};
pub struct Executor; pub struct Executor;
@ -80,7 +80,7 @@ impl Executor {
} }
// Operation // Operation
"open" => cx.manager.open(exec.named.contains_key("select")), "open" => cx.manager.open(exec.named.contains_key("interactive")),
"yank" => cx.manager.yank(exec.named.contains_key("cut")), "yank" => cx.manager.yank(exec.named.contains_key("cut")),
"paste" => { "paste" => {
let dest = cx.manager.current().cwd.clone(); let dest = cx.manager.current().cwd.clone();
@ -115,6 +115,17 @@ impl Executor {
_ => false, _ => false,
}, },
// Sorting
"sort" => {
let b = cx.manager.current_mut().files.set_sort(FilesSort {
by: SortBy::try_from(exec.args.get(0).cloned().unwrap_or_default())
.unwrap_or_default(),
reverse: exec.named.contains_key("reverse"),
});
cx.tasks.precache_size(&cx.manager.current().files);
b
}
// Tabs // Tabs
"tab_create" => { "tab_create" => {
let path = if exec.named.contains_key("current") { let path = if exec.named.contains_key("current") {