mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
..
This commit is contained in:
parent
e42ec9f429
commit
e5ba8e7d27
36 changed files with 487 additions and 485 deletions
|
|
@ -1,15 +1,16 @@
|
|||
## Yazi - ⚡️ Blazing Fast Terminal File Manager
|
||||
|
||||
Yazi ("duck" in Chinese) is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and customizable file management experience.
|
||||
Yazi (means "duck") is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and customizable file management experience.
|
||||
|
||||
💡 A new article explaining its internal workings: [Why is Yazi Fast?](https://yazi-rs.github.io/blog/why-is-yazi-fast)
|
||||
|
||||
- 🚀 **Full Asynchronous Support**: All I/O operations are asynchronous, CPU tasks are spread across multiple threads, making the most of available resources.
|
||||
- 💪 **Powerful Async Task Scheduling and Management**: Provides real-time progress updates, task cancellation, and internal task priority assignment.
|
||||
- 🖼️ **Built-in Support for Multiple Image Protocols**: Also integrated with Überzug++, covering almost all terminals.
|
||||
- 🌟 **Built-in Code Highlighting and Image Encoding**: Combined with the pre-caching mechanism, greatly accelerates image and normal file loading.
|
||||
- 🌟 **Built-in Code Highlighting and Image Decoding**: Combined with the pre-loading mechanism, greatly accelerates image and normal file loading.
|
||||
- 🔌 **Concurrent Plugin System**: UI plugins (rewriting most of the UI), functional plugins (coming soon), custom previewer, and custom preloader; Just some pieces of Lua.
|
||||
- 🧰 Integration with fd, rg, fzf, zoxide
|
||||
- 💫 Vim-like Input component, and Select component
|
||||
- 💫 Vim-like input/select component, auto-completion for cd paths
|
||||
- 🏷️ Multi-Tab Support, Scrollable Preview (for videos, PDFs, archives, directories, code, etc.)
|
||||
- 🔄 Batch Renaming, Visual Mode, File Chooser
|
||||
- 🎨 Theme System, Custom Layouts, Trash Bin, CSI u
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"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","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize"],"language":"en","version":"0.2"}
|
||||
{"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","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","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting"],"version":"0.2"}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use std::{ffi::OsString, fs, path::{Path, PathBuf}, process};
|
||||
|
||||
use clap::Parser;
|
||||
use serde::Serialize;
|
||||
use yazi_shared::fs::{current_cwd, expand_path};
|
||||
|
||||
use super::Args;
|
||||
use crate::{Xdg, ARGS};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Boot {
|
||||
pub cwd: PathBuf,
|
||||
pub file: Option<OsString>,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ use yazi_shared::fs::{File, Url, FILES_TICKET};
|
|||
use super::FilesSorter;
|
||||
|
||||
pub struct Files {
|
||||
items: Vec<File>,
|
||||
hidden: Vec<File>,
|
||||
ticket: u64,
|
||||
pub(crate) version: u64,
|
||||
hidden: Vec<File>,
|
||||
items: Vec<File>,
|
||||
ticket: u64,
|
||||
pub(crate) revision: u64,
|
||||
|
||||
pub sizes: BTreeMap<Url, u64>,
|
||||
selected: BTreeSet<Url>,
|
||||
|
|
@ -23,10 +23,10 @@ pub struct Files {
|
|||
impl Default for Files {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
items: Default::default(),
|
||||
hidden: Default::default(),
|
||||
ticket: Default::default(),
|
||||
version: Default::default(),
|
||||
items: Default::default(),
|
||||
hidden: Default::default(),
|
||||
ticket: Default::default(),
|
||||
revision: Default::default(),
|
||||
|
||||
sizes: Default::default(),
|
||||
selected: Default::default(),
|
||||
|
|
@ -68,7 +68,7 @@ impl Files {
|
|||
#[inline]
|
||||
pub fn select(&mut self, url: &Url, state: Option<bool>) -> bool {
|
||||
let old = self.selected.contains(url);
|
||||
let new = if let Some(new) = state { new } else { !old };
|
||||
let new = state.unwrap_or(!old);
|
||||
|
||||
if new == old {
|
||||
return false;
|
||||
|
|
@ -126,132 +126,174 @@ impl Files {
|
|||
applied
|
||||
}
|
||||
|
||||
pub fn update_full(&mut self, mut items: Vec<File>) -> bool {
|
||||
if !self.show_hidden {
|
||||
(self.hidden, items) = items.into_iter().partition(|f| f.is_hidden());
|
||||
pub fn update_full(&mut self, files: Vec<File>) {
|
||||
if files.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
|
||||
self.sorter.sort(&mut items, &self.sizes);
|
||||
self.items = items;
|
||||
self.version += 1;
|
||||
true
|
||||
self.revision += 1;
|
||||
|
||||
(self.hidden, self.items) = if self.show_hidden {
|
||||
(vec![], files)
|
||||
} else {
|
||||
files.into_iter().partition(|f| f.is_hidden())
|
||||
};
|
||||
}
|
||||
|
||||
pub fn update_part(&mut self, version: u64, items: Vec<File>) -> bool {
|
||||
if !items.is_empty() {
|
||||
if version != self.ticket {
|
||||
return false;
|
||||
pub fn update_part(&mut self, files: Vec<File>, ticket: u64) {
|
||||
if !files.is_empty() {
|
||||
if ticket != self.ticket {
|
||||
return;
|
||||
}
|
||||
|
||||
self.revision += 1;
|
||||
if self.show_hidden {
|
||||
self.items.extend(items);
|
||||
self.hidden.clear();
|
||||
self.items.extend(files);
|
||||
} else {
|
||||
let (hidden, items): (Vec<_>, Vec<_>) = items.into_iter().partition(|f| f.is_hidden());
|
||||
self.items.extend(items);
|
||||
let (hidden, items): (Vec<_>, Vec<_>) = files.into_iter().partition(|f| f.is_hidden());
|
||||
self.hidden.extend(hidden);
|
||||
self.items.extend(items);
|
||||
}
|
||||
|
||||
self.sorter.sort(&mut self.items, &self.sizes);
|
||||
self.version += 1;
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
self.ticket = version;
|
||||
if self.items.is_empty() && self.hidden.is_empty() {
|
||||
return false;
|
||||
self.ticket = ticket;
|
||||
if !self.items.is_empty() || !self.hidden.is_empty() {
|
||||
self.revision += 1;
|
||||
self.hidden.clear();
|
||||
self.items.clear();
|
||||
}
|
||||
|
||||
self.items.clear();
|
||||
self.hidden.clear();
|
||||
self.version += 1;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn update_size(&mut self, items: BTreeMap<Url, u64>) -> bool {
|
||||
self.sizes.extend(items);
|
||||
pub fn update_size(&mut self, sizes: BTreeMap<Url, u64>) {
|
||||
if sizes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.sorter.by == SortBy::Size {
|
||||
self.sorter.sort(&mut self.items, &self.sizes);
|
||||
self.version += 1;
|
||||
self.revision += 1;
|
||||
}
|
||||
true
|
||||
self.sizes.extend(sizes);
|
||||
}
|
||||
|
||||
pub fn update_creating(&mut self, mut todo: BTreeMap<Url, File>) -> bool {
|
||||
if !self.show_hidden {
|
||||
todo.retain(|_, f| !f.is_hidden());
|
||||
pub fn update_creating(&mut self, files: Vec<File>) {
|
||||
if files.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let b = self.update_replacing(&mut todo);
|
||||
if todo.is_empty() {
|
||||
return b;
|
||||
}
|
||||
|
||||
self.items.extend(todo.into_values());
|
||||
self.sorter.sort(&mut self.items, &self.sizes);
|
||||
self.version += 1;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn update_deleting(&mut self, mut todo: BTreeSet<Url>) -> bool {
|
||||
let mut removed = Vec::with_capacity(todo.len());
|
||||
macro_rules! go {
|
||||
($name:expr) => {
|
||||
removed.clear();
|
||||
for i in 0..$name.len() {
|
||||
if todo.remove(&$name[i].url) {
|
||||
removed.push(i);
|
||||
if todo.is_empty() {
|
||||
($dist:expr, $src:expr) => {
|
||||
let mut todo: BTreeMap<_, _> = $src.into_iter().map(|f| (f.url(), f)).collect();
|
||||
for f in &$dist {
|
||||
if todo.remove(&f.url).is_some() && todo.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !todo.is_empty() {
|
||||
self.revision += 1;
|
||||
$dist.extend(todo.into_values());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let (hidden, items) = if self.show_hidden {
|
||||
(vec![], files)
|
||||
} else {
|
||||
files.into_iter().partition(|f| f.is_hidden())
|
||||
};
|
||||
|
||||
if !items.is_empty() {
|
||||
go!(self.items, items);
|
||||
}
|
||||
if !hidden.is_empty() {
|
||||
go!(self.hidden, hidden);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_deleting(&mut self, urls: Vec<Url>) {
|
||||
if urls.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
macro_rules! go {
|
||||
($dist:expr, $src:expr) => {
|
||||
let mut todo: BTreeSet<_> = $src.into_iter().collect();
|
||||
let len = $dist.len();
|
||||
|
||||
$dist.retain(|f| !todo.remove(&f.url));
|
||||
if $dist.len() != len {
|
||||
self.revision += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let (hidden, items) =
|
||||
if self.show_hidden { (vec![], urls) } else { urls.into_iter().partition(|u| u.is_hidden()) };
|
||||
|
||||
if !items.is_empty() {
|
||||
go!(self.items, items);
|
||||
}
|
||||
if !hidden.is_empty() {
|
||||
go!(self.hidden, hidden);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_updating(&mut self, files: BTreeMap<Url, File>) -> [BTreeMap<Url, File>; 2] {
|
||||
if files.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
macro_rules! go {
|
||||
($dist:expr, $src:expr) => {
|
||||
let len = $src.len();
|
||||
for i in 0..$dist.len() {
|
||||
if let Some(f) = $src.remove(&$dist[i].url) {
|
||||
$dist[i] = f;
|
||||
if $src.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in (0..removed.len()).rev() {
|
||||
$name.remove(removed[i]);
|
||||
if $src.len() != len {
|
||||
self.revision += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut b = false;
|
||||
if !todo.is_empty() {
|
||||
go!(self.items);
|
||||
b |= !removed.is_empty();
|
||||
}
|
||||
let (mut hidden, mut items) = if self.show_hidden {
|
||||
(BTreeMap::new(), files)
|
||||
} else {
|
||||
files.into_iter().partition(|(_, f)| f.is_hidden())
|
||||
};
|
||||
|
||||
if !todo.is_empty() {
|
||||
go!(self.hidden);
|
||||
b |= !removed.is_empty();
|
||||
if !items.is_empty() {
|
||||
go!(self.items, items);
|
||||
}
|
||||
b
|
||||
if !hidden.is_empty() {
|
||||
go!(self.hidden, hidden);
|
||||
}
|
||||
[hidden, items]
|
||||
}
|
||||
|
||||
pub fn update_replacing(&mut self, todo: &mut BTreeMap<Url, File>) -> bool {
|
||||
if todo.is_empty() {
|
||||
return false;
|
||||
pub fn update_upserting(&mut self, files: BTreeMap<Url, File>) {
|
||||
if files.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
macro_rules! go {
|
||||
($name:expr) => {
|
||||
for i in 0..$name.len() {
|
||||
if let Some(f) = todo.remove(&$name[i].url) {
|
||||
$name[i] = f;
|
||||
if todo.is_empty() {
|
||||
self.version += 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let [hidden, items] = self.update_updating(files);
|
||||
if hidden.is_empty() && items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let old = todo.len();
|
||||
go!(self.items);
|
||||
go!(self.hidden);
|
||||
|
||||
if old != todo.len() {
|
||||
self.version += 1;
|
||||
return true;
|
||||
self.revision += 1;
|
||||
if !hidden.is_empty() {
|
||||
self.hidden.extend(hidden.into_values());
|
||||
}
|
||||
if !items.is_empty() {
|
||||
self.items.extend(items.into_values());
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +360,7 @@ impl Files {
|
|||
return false;
|
||||
}
|
||||
self.sorter = sorter;
|
||||
self.version += 1;
|
||||
self.revision += 1;
|
||||
self.sorter.sort(&mut self.items, &self.sizes)
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +381,7 @@ impl Files {
|
|||
}
|
||||
|
||||
self.show_hidden = state;
|
||||
self.version += 1;
|
||||
self.revision += 1;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,19 @@ impl From<&Url> for Folder {
|
|||
|
||||
impl Folder {
|
||||
pub fn update(&mut self, op: FilesOp) -> bool {
|
||||
let b = match op {
|
||||
FilesOp::Full(_, items) => self.files.update_full(items),
|
||||
FilesOp::Part(_, ticket, items) => self.files.update_part(ticket, items),
|
||||
FilesOp::Size(_, items) => self.files.update_size(items),
|
||||
let revision = self.files.revision;
|
||||
match op {
|
||||
FilesOp::Full(_, files) => self.files.update_full(files),
|
||||
FilesOp::Part(_, files, ticket) => self.files.update_part(files, ticket),
|
||||
FilesOp::Size(_, sizes) => self.files.update_size(sizes),
|
||||
|
||||
FilesOp::Creating(_, items) => self.files.update_creating(items),
|
||||
FilesOp::Deleting(_, items) => self.files.update_deleting(items),
|
||||
FilesOp::Replacing(_, mut items) => self.files.update_replacing(&mut items),
|
||||
FilesOp::Creating(_, files) => self.files.update_creating(files),
|
||||
FilesOp::Deleting(_, urls) => self.files.update_deleting(urls),
|
||||
FilesOp::Updating(_, files) => _ = self.files.update_updating(files),
|
||||
FilesOp::Upserting(_, files) => self.files.update_upserting(files),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if !b {
|
||||
}
|
||||
if revision == self.files.revision {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::{PathBuf, MAIN_SEPARATOR};
|
|||
|
||||
use tokio::fs;
|
||||
use yazi_config::popup::InputCfg;
|
||||
use yazi_shared::{emit, event::Exec, fs::{File, FilesOp}, fs::Url};
|
||||
use yazi_shared::{event::Exec, fs::{File, FilesOp, Url}};
|
||||
|
||||
use crate::{input::Input, manager::Manager};
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ impl Manager {
|
|||
let child =
|
||||
Url::from(path.components().take(cwd.components().count() + 1).collect::<PathBuf>());
|
||||
if let Ok(f) = File::from(child.clone()).await {
|
||||
emit!(Files(FilesOp::Creating(cwd, f.into_map())));
|
||||
FilesOp::Creating(cwd, vec![f]).emit();
|
||||
Manager::_hover(Some(child));
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@ mod tab_close;
|
|||
mod tab_create;
|
||||
mod tab_swap;
|
||||
mod tab_switch;
|
||||
mod update_files;
|
||||
mod update_mimetype;
|
||||
mod yank;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::ffi::OsString;
|
||||
|
||||
use yazi_config::{popup::SelectCfg, OPEN};
|
||||
use yazi_plugin::external;
|
||||
use yazi_shared::{event::Exec, MIME_DIR};
|
||||
|
||||
use crate::{manager::Manager, select::Select, tasks::Tasks};
|
||||
|
|
@ -44,28 +43,30 @@ impl Manager {
|
|||
}
|
||||
|
||||
let opt = opt.into() as Opt;
|
||||
tokio::spawn(async move {
|
||||
let todo: Vec<_> = files.iter().filter(|(_, m)| m.is_none()).map(|(u, _)| u).collect();
|
||||
if let Ok(mut mimes) = external::file(&todo).await {
|
||||
files = files
|
||||
.into_iter()
|
||||
.map(|(u, m)| {
|
||||
let mime = m.or_else(|| mimes.remove(&u));
|
||||
(u, mime)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
// TODO: plugin system
|
||||
// tokio::spawn(async move {
|
||||
// let todo: Vec<_> = files.iter().filter(|(_, m)| m.is_none()).map(|(u, _)|
|
||||
// u).collect(); if let Ok(mut mimes) = external::file(&todo).await {
|
||||
// files = files
|
||||
// .into_iter()
|
||||
// .map(|(u, m)| {
|
||||
// let mime = m.or_else(|| mimes.remove(&u));
|
||||
// (u, mime)
|
||||
// })
|
||||
// .collect();
|
||||
// }
|
||||
|
||||
let files: Vec<_> =
|
||||
files.into_iter().filter_map(|(u, m)| m.map(|m| (u.into_os_string(), m))).collect();
|
||||
// let files: Vec<_> =
|
||||
// files.into_iter().filter_map(|(u, m)| m.map(|m| (u.into_os_string(),
|
||||
// m))).collect();
|
||||
|
||||
if opt.interactive {
|
||||
Self::open_interactive(files).await;
|
||||
return;
|
||||
}
|
||||
// if opt.interactive {
|
||||
// Self::open_interactive(files).await;
|
||||
// return;
|
||||
// }
|
||||
|
||||
Tasks::_open(files, None);
|
||||
});
|
||||
// Tasks::_open(files, None);
|
||||
// });
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ impl Manager {
|
|||
|
||||
self.active_mut().apply_files_attrs(false);
|
||||
|
||||
if let Some(f) = self.parent() {
|
||||
self.watcher.trigger_dirs(&[self.cwd(), &f.cwd]);
|
||||
if let Some(p) = self.parent() {
|
||||
self.watcher.trigger_dirs(&[self.cwd(), &p.cwd]);
|
||||
} else {
|
||||
self.watcher.trigger_dirs(&[self.cwd()]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}};
|
|||
use yazi_config::{popup::InputCfg, OPEN, PREVIEW};
|
||||
use yazi_plugin::external::{self, ShellOpt};
|
||||
use yazi_scheduler::{Scheduler, BLOCKER};
|
||||
use yazi_shared::{emit, event::Exec, fs::{max_common_root, File, FilesOp, Url}, term::Term, Defer};
|
||||
use yazi_shared::{event::Exec, fs::{max_common_root, File, FilesOp, Url}, term::Term, Defer};
|
||||
|
||||
use crate::{input::Input, manager::Manager};
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ impl Manager {
|
|||
}
|
||||
|
||||
let file = File::from(new.clone()).await?;
|
||||
emit!(Files(FilesOp::Replacing(file.parent().unwrap(), BTreeMap::from_iter([(old, file)]))));
|
||||
FilesOp::Upserting(file.parent().unwrap(), BTreeMap::from_iter([(old, file)])).emit();
|
||||
Ok(Self::_hover(Some(new)))
|
||||
}
|
||||
|
||||
|
|
|
|||
75
yazi-core/src/manager/commands/update_files.rs
Normal file
75
yazi-core/src/manager/commands/update_files.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use yazi_shared::{event::Exec, fs::FilesOp};
|
||||
|
||||
use crate::{folder::Folder, manager::Manager, tasks::Tasks};
|
||||
|
||||
pub struct Opt {
|
||||
op: FilesOp,
|
||||
}
|
||||
|
||||
impl TryFrom<&Exec> for Opt {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(e: &Exec) -> Result<Self, Self::Error> { Ok(Self { op: e.take_data().ok_or(())? }) }
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
fn handle_read(&mut self, op: FilesOp) -> bool {
|
||||
let url = op.url().clone();
|
||||
let cwd = self.cwd().to_owned();
|
||||
let hovered = self.hovered().map(|h| h.url());
|
||||
|
||||
let mut b = if cwd == url {
|
||||
self.current_mut().update(op)
|
||||
} else if matches!(self.parent(), Some(p) if p.cwd == url) {
|
||||
self.active_mut().parent.as_mut().unwrap().update(op)
|
||||
} else if matches!(self.hovered(), Some(h) if h.url == url) {
|
||||
self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url));
|
||||
self.active_mut().apply_files_attrs(true);
|
||||
self.active_mut().history.get_mut(&url).unwrap().update(op)
|
||||
} else {
|
||||
self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url)).update(op);
|
||||
false
|
||||
};
|
||||
|
||||
b |= self.active_mut().parent.as_mut().is_some_and(|p| p.hover(&cwd));
|
||||
b |= hovered.as_ref().is_some_and(|h| self.current_mut().hover(h));
|
||||
|
||||
if hovered.as_ref() != self.hovered().map(|h| &h.url) {
|
||||
Self::_hover(None);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn handle_ioerr(&mut self, op: FilesOp) -> bool {
|
||||
let url = op.url();
|
||||
let op = FilesOp::Full(url.clone(), Vec::new());
|
||||
|
||||
if url == self.cwd() {
|
||||
self.current_mut().update(op);
|
||||
self.active_mut().leave(());
|
||||
true
|
||||
} else if matches!(self.parent(), Some(p) if &p.cwd == url) {
|
||||
self.active_mut().parent.as_mut().unwrap().update(op)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_files(&mut self, opt: impl TryInto<Opt>, tasks: &Tasks) -> bool {
|
||||
let Ok(opt) = opt.try_into() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let calc = !matches!(opt.op, FilesOp::Size(..) | FilesOp::IOErr(_) | FilesOp::Deleting(..));
|
||||
let b = match opt.op {
|
||||
FilesOp::IOErr(..) => self.handle_ioerr(opt.op),
|
||||
_ => self.handle_read(opt.op),
|
||||
};
|
||||
|
||||
if calc {
|
||||
tasks.preload_sorted(&self.current().files);
|
||||
}
|
||||
|
||||
b
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use yazi_shared::fs::{File, FilesOp, Url};
|
||||
use yazi_shared::fs::{File, Url};
|
||||
|
||||
use super::{Tabs, Watcher};
|
||||
use crate::{folder::Folder, tab::Tab};
|
||||
|
|
@ -23,48 +23,6 @@ impl Manager {
|
|||
mimetype: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_read(&mut self, op: FilesOp) -> bool {
|
||||
let url = op.url().clone();
|
||||
let cwd = self.cwd().to_owned();
|
||||
let hovered = self.hovered().map(|h| h.url());
|
||||
|
||||
let mut b = if cwd == url {
|
||||
self.current_mut().update(op)
|
||||
} else if matches!(self.parent(), Some(p) if p.cwd == url) {
|
||||
self.active_mut().parent.as_mut().unwrap().update(op)
|
||||
} else if matches!(self.hovered(), Some(h) if h.url == url) {
|
||||
self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url));
|
||||
self.active_mut().apply_files_attrs(true);
|
||||
self.active_mut().history.get_mut(&url).unwrap().update(op)
|
||||
} else {
|
||||
self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url)).update(op);
|
||||
false
|
||||
};
|
||||
|
||||
b |= self.active_mut().parent.as_mut().is_some_and(|p| p.hover(&cwd));
|
||||
b |= hovered.as_ref().is_some_and(|h| self.current_mut().hover(h));
|
||||
|
||||
if hovered.as_ref() != self.hovered().map(|h| &h.url) {
|
||||
Self::_hover(None);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
pub fn update_ioerr(&mut self, op: FilesOp) -> bool {
|
||||
let url = op.url();
|
||||
let op = FilesOp::Full(url.clone(), Vec::new());
|
||||
|
||||
if url == self.cwd() {
|
||||
self.current_mut().update(op);
|
||||
self.active_mut().leave(());
|
||||
true
|
||||
} else if matches!(self.parent(), Some(p) if &p.cwd == url) {
|
||||
self.active_mut().parent.as_mut().unwrap().update(op)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
use std::{collections::BTreeSet, sync::Arc, time::Duration};
|
||||
use std::{collections::{BTreeMap, BTreeSet}, sync::Arc, time::Duration};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _Watcher};
|
||||
use parking_lot::RwLock;
|
||||
use tokio::{fs, pin, sync::mpsc::{self, UnboundedReceiver}};
|
||||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||
use yazi_shared::{emit, fs::{File, FilesOp, Url}};
|
||||
use tracing::error;
|
||||
use yazi_plugin::isolate;
|
||||
use yazi_shared::fs::{File, FilesOp, Url};
|
||||
|
||||
use crate::folder::Files;
|
||||
|
||||
pub struct Watcher {
|
||||
watcher: RecommendedWatcher,
|
||||
watched: Arc<RwLock<IndexMap<Url, Option<Url>>>>,
|
||||
watcher: RecommendedWatcher,
|
||||
watched: Arc<RwLock<BTreeSet<Url>>>,
|
||||
pub linked: Arc<RwLock<BTreeMap<Url, Url>>>, // from ==> to
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
|
|
@ -25,55 +27,45 @@ impl Watcher {
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(path) = event.paths.first().map(Url::from) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let parent = path.parent_url().unwrap_or_else(|| path.clone());
|
||||
match event.kind {
|
||||
EventKind::Create(_) => {
|
||||
tx.send(parent).ok();
|
||||
}
|
||||
EventKind::Modify(kind) => {
|
||||
match kind {
|
||||
ModifyKind::Data(_) => {}
|
||||
ModifyKind::Metadata(kind) => match kind {
|
||||
MetadataKind::Permissions => {}
|
||||
MetadataKind::Ownership => {}
|
||||
MetadataKind::Extended => {}
|
||||
_ => return,
|
||||
},
|
||||
ModifyKind::Name(_) => {}
|
||||
EventKind::Create(_) => {}
|
||||
EventKind::Modify(kind) => match kind {
|
||||
ModifyKind::Data(_) => {}
|
||||
ModifyKind::Metadata(md) => match md {
|
||||
MetadataKind::WriteTime => {}
|
||||
MetadataKind::Permissions => {}
|
||||
MetadataKind::Ownership => {}
|
||||
_ => return,
|
||||
};
|
||||
},
|
||||
ModifyKind::Name(_) => {}
|
||||
_ => return,
|
||||
},
|
||||
EventKind::Remove(_) => {}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
tx.send(path).ok();
|
||||
tx.send(parent).ok();
|
||||
}
|
||||
EventKind::Remove(_) => {
|
||||
tx.send(path).ok();
|
||||
tx.send(parent).ok();
|
||||
}
|
||||
_ => (),
|
||||
for path in event.paths {
|
||||
tx.send(Url::from(path)).ok();
|
||||
}
|
||||
}
|
||||
},
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
let instance = Self { watcher: watcher.unwrap(), watched: Default::default() };
|
||||
tokio::spawn(Self::on_changed(rx, instance.watched.clone()));
|
||||
let instance =
|
||||
Self { watcher: watcher.unwrap(), watched: Default::default(), linked: Default::default() };
|
||||
tokio::spawn(Self::on_changed(rx));
|
||||
instance
|
||||
}
|
||||
|
||||
pub(super) fn watch(&mut self, mut watched: BTreeSet<&Url>) {
|
||||
watched.retain(|&u| u.is_regular());
|
||||
pub(super) fn watch(&mut self, mut new: BTreeSet<&Url>) {
|
||||
new.retain(|&u| u.is_regular());
|
||||
let (to_unwatch, to_watch): (BTreeSet<_>, BTreeSet<_>) = {
|
||||
let guard = self.watched.read();
|
||||
let keys = guard.keys().collect::<BTreeSet<_>>();
|
||||
let old: BTreeSet<_> = guard.iter().collect();
|
||||
(
|
||||
keys.difference(&watched).map(|&x| x.clone()).collect(),
|
||||
watched.difference(&keys).map(|&x| x.clone()).collect(),
|
||||
old.difference(&new).map(|&x| x.clone()).collect(),
|
||||
new.difference(&old).map(|&x| x.clone()).collect(),
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -82,141 +74,103 @@ impl Watcher {
|
|||
}
|
||||
for u in to_watch {
|
||||
if self.watcher.watch(&u, RecursiveMode::NonRecursive).is_err() {
|
||||
watched.remove(&u);
|
||||
new.remove(&u);
|
||||
}
|
||||
}
|
||||
|
||||
let mut to_resolve = Vec::new();
|
||||
let mut guard = self.watched.write();
|
||||
*guard = watched
|
||||
.into_iter()
|
||||
.map(|k| {
|
||||
if let Some(old) = guard.remove_entry(k) {
|
||||
old
|
||||
} else {
|
||||
to_resolve.push(k.clone());
|
||||
(k.clone(), None)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let lock = self.watched.clone();
|
||||
tokio::spawn(async move {
|
||||
for k in to_resolve {
|
||||
match fs::canonicalize(&k).await {
|
||||
Ok(v) if v != *k => {
|
||||
lock.write().insert(k, Some(Url::from(v)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
*self.watched.write() = new.into_iter().cloned().collect();
|
||||
self.sync_linked();
|
||||
}
|
||||
|
||||
pub(super) fn trigger_dirs(&self, dirs: &[&Url]) {
|
||||
let dirs: Vec<_> = dirs.iter().filter(|&u| u.is_regular()).map(|&u| u.clone()).collect();
|
||||
if dirs.is_empty() {
|
||||
let urls: Vec<_> = dirs.iter().filter(|&u| u.is_regular()).map(|&u| u.clone()).collect();
|
||||
if urls.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let watched = self.watched.clone();
|
||||
tokio::spawn(async move {
|
||||
let watched = watched.read().clone();
|
||||
for dir in dirs {
|
||||
Self::dir_changed(&dir, &watched).await;
|
||||
for u in urls {
|
||||
let Ok(rx) = Files::from_dir(&u).await else {
|
||||
FilesOp::IOErr(u).emit();
|
||||
return;
|
||||
};
|
||||
|
||||
let files: Vec<_> = UnboundedReceiverStream::new(rx).collect().await;
|
||||
FilesOp::Full(u, files).emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn on_changed(
|
||||
rx: UnboundedReceiver<Url>,
|
||||
watched: Arc<RwLock<IndexMap<Url, Option<Url>>>>,
|
||||
) {
|
||||
fn sync_linked(&self) {
|
||||
let mut new = self.watched.read().clone();
|
||||
self.linked.write().retain(|k, _| new.remove(k));
|
||||
|
||||
let watched = self.watched.clone();
|
||||
let linked = self.linked.clone();
|
||||
macro_rules! go {
|
||||
($todo:expr) => {
|
||||
for from in $todo {
|
||||
match fs::canonicalize(&from).await {
|
||||
Ok(to) if to != *from && watched.read().contains(&from) => {
|
||||
linked.write().insert(from, Url::from(to));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let old: Vec<_> = linked.read().keys().cloned().collect();
|
||||
go!(new);
|
||||
go!(old);
|
||||
});
|
||||
}
|
||||
|
||||
async fn on_changed(rx: UnboundedReceiver<Url>) {
|
||||
// TODO: revert this once a new notification is implemented
|
||||
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10, Duration::from_millis(20));
|
||||
pin!(rx);
|
||||
|
||||
while let Some(urls) = rx.next().await {
|
||||
let (mut files, mut dirs): (Vec<_>, Vec<_>) = Default::default();
|
||||
for url in urls.into_iter().collect::<BTreeSet<_>>() {
|
||||
if fs::metadata(&url).await.map(|m| !m.is_dir()).unwrap_or(false) {
|
||||
files.push(url);
|
||||
} else {
|
||||
dirs.push(url);
|
||||
let urls = urls.into_iter().collect::<BTreeSet<_>>();
|
||||
let mut reload = Vec::with_capacity(urls.len());
|
||||
|
||||
for u in urls {
|
||||
let Some(parent) = u.parent_url() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(file) = File::from(u.clone()).await else {
|
||||
FilesOp::Deleting(parent, vec![u]).emit();
|
||||
continue;
|
||||
};
|
||||
|
||||
if !file.is_dir() {
|
||||
reload.push(file.clone());
|
||||
}
|
||||
FilesOp::Upserting(parent, BTreeMap::from_iter([(u, file)])).emit();
|
||||
}
|
||||
|
||||
let watched = watched.read().clone();
|
||||
|
||||
Self::files_changed(&files, &watched).await;
|
||||
for file in files {
|
||||
for u in Self::linked_urls(&file, &watched) {
|
||||
emit!(Files(FilesOp::IOErr(u.clone())));
|
||||
}
|
||||
emit!(Files(FilesOp::IOErr(file)));
|
||||
if reload.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for dir in dirs {
|
||||
Self::dir_changed(&dir, &watched).await;
|
||||
if let Err(e) = isolate::preload("mime.lua".to_string(), reload, true).await {
|
||||
error!("preload in watcher failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn files_changed(urls: &[Url], watched: &IndexMap<Url, Option<Url>>) {
|
||||
// TODO: plugin system
|
||||
// let Ok(mut mimes) = external::file(urls).await else {
|
||||
// return;
|
||||
// };
|
||||
|
||||
// let linked: Vec<_> = watched.iter().filter_map(|(k, v)|
|
||||
// v.as_ref().map(|v| (k, v))).fold( Vec::new(),
|
||||
// |mut aac, (k, v)| {
|
||||
// mimes
|
||||
// .iter()
|
||||
// .filter(|(u, _)| u.parent().map(|p| p == **v) == Some(true))
|
||||
// .for_each(|(u, m)| aac.push((k.join(u.file_name().unwrap()),
|
||||
// m.clone()))); aac
|
||||
// },
|
||||
// );
|
||||
|
||||
// mimes.extend(linked);
|
||||
// emit!(Mimetype(mimes));
|
||||
}
|
||||
|
||||
async fn dir_changed(url: &Url, watched: &IndexMap<Url, Option<Url>>) {
|
||||
let linked = Self::linked_urls(url, watched);
|
||||
let Ok(rx) = Files::from_dir(url).await else {
|
||||
emit!(Files(FilesOp::IOErr(url.clone())));
|
||||
for u in linked {
|
||||
emit!(Files(FilesOp::IOErr(u.clone())));
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
let linked_files = |files: &[File], linked: &Url| -> Vec<File> {
|
||||
let mut new = Vec::with_capacity(files.len());
|
||||
for file in files {
|
||||
let mut file = file.clone();
|
||||
file.url = linked.join(file.url.strip_prefix(url).unwrap());
|
||||
new.push(file);
|
||||
}
|
||||
new
|
||||
};
|
||||
|
||||
let files: Vec<_> = UnboundedReceiverStream::new(rx).collect().await;
|
||||
for u in linked {
|
||||
let files = linked_files(&files, u);
|
||||
emit!(Files(FilesOp::Full(u.clone(), files)));
|
||||
}
|
||||
emit!(Files(FilesOp::Full(url.clone(), files)));
|
||||
}
|
||||
|
||||
fn linked_urls<'a>(url: &'a Url, watched: &'a IndexMap<Url, Option<Url>>) -> Vec<&'a Url> {
|
||||
watched
|
||||
.iter()
|
||||
.filter_map(|(k, v)| v.as_ref().map(|v| (k, v)))
|
||||
.filter(|(_, v)| *v == url)
|
||||
.map(|(k, _)| k)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub fn relatives(&self, url: &Url) -> BTreeSet<Url> {
|
||||
let mut map = BTreeSet::from_iter([url.clone()]);
|
||||
|
||||
let linked = self.linked.read();
|
||||
if let Some(to) = linked.get(url) {
|
||||
map.extend(linked.iter().filter(|(_, v)| *v == to).map(|(k, _)| k).cloned());
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use yazi_shared::{emit, event::Exec, fs::{File, FilesOp}, fs::{expand_path, Url}, Layer};
|
||||
use yazi_shared::{emit, event::Exec, fs::{expand_path, File, FilesOp, Url}, Layer};
|
||||
|
||||
use crate::{manager::Manager, tab::Tab};
|
||||
|
||||
|
|
@ -34,10 +34,7 @@ impl Tab {
|
|||
};
|
||||
|
||||
let b = self.cd(parent.clone());
|
||||
emit!(Files(FilesOp::Creating(
|
||||
parent.clone(),
|
||||
File::from_dummy(opt.target.clone()).into_map()
|
||||
)));
|
||||
FilesOp::Creating(parent, vec![File::from_dummy(opt.target.clone())]).emit();
|
||||
Manager::_hover(Some(opt.target));
|
||||
b
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use tokio::pin;
|
|||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||
use yazi_config::popup::InputCfg;
|
||||
use yazi_plugin::external;
|
||||
use yazi_shared::{emit, event::Exec, fs::FilesOp};
|
||||
use yazi_shared::{event::Exec, fs::FilesOp};
|
||||
|
||||
use crate::{input::Input, manager::Manager, tab::Tab};
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ impl Tab {
|
|||
Tab::_cd(&cwd);
|
||||
first = false;
|
||||
}
|
||||
emit!(Files(FilesOp::Part(cwd.clone(), ticket, chunk)));
|
||||
FilesOp::Part(cwd.clone(), chunk, ticket).emit();
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ impl Finder {
|
|||
}
|
||||
|
||||
pub(super) fn catchup(&mut self, files: &Files) -> bool {
|
||||
if self.version == files.version {
|
||||
if self.version == files.revision {
|
||||
return false;
|
||||
}
|
||||
self.matched.clear();
|
||||
|
|
@ -72,7 +72,7 @@ impl Finder {
|
|||
i += 1;
|
||||
}
|
||||
|
||||
self.version = files.version;
|
||||
self.version = files.revision;
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crossterm::event::KeyEvent;
|
|||
use ratatui::backend::Backend;
|
||||
use yazi_config::{keymap::Key, ARGS};
|
||||
use yazi_core::input::InputMode;
|
||||
use yazi_shared::{emit, event::{Event, Exec}, fs::FilesOp, term::Term, Layer, COLLISION};
|
||||
use yazi_shared::{emit, event::{Event, Exec}, term::Term, Layer, COLLISION};
|
||||
|
||||
use crate::{lives::Lives, Ctx, Executor, Logs, Panic, Root, Signals};
|
||||
|
||||
|
|
@ -132,19 +132,6 @@ impl App {
|
|||
let manager = &mut self.cx.manager;
|
||||
let tasks = &mut self.cx.tasks;
|
||||
match event {
|
||||
Event::Files(op) => {
|
||||
let calc = !matches!(op, FilesOp::Size(..) | FilesOp::IOErr(_));
|
||||
let b = match op {
|
||||
FilesOp::IOErr(..) => manager.update_ioerr(op),
|
||||
_ => manager.update_read(op),
|
||||
};
|
||||
if b {
|
||||
emit!(Render);
|
||||
}
|
||||
if calc {
|
||||
tasks.preload_sorted(&manager.current().files);
|
||||
}
|
||||
}
|
||||
Event::Pages(page) => {
|
||||
let targets = self.cx.manager.current().paginate(page);
|
||||
tasks.preload_paged(targets, &self.cx.manager.mimetype);
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ impl<'a> Executor<'a> {
|
|||
};
|
||||
}
|
||||
|
||||
on!(MANAGER, update_files, &self.app.cx.tasks);
|
||||
on!(MANAGER, update_mimetype, &self.app.cx.tasks);
|
||||
on!(MANAGER, hover);
|
||||
on!(MANAGER, peek);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
local Archive = {}
|
||||
local M = {}
|
||||
|
||||
function Archive:peek()
|
||||
function M:peek()
|
||||
local _, bound = ya.preview_archive(self)
|
||||
if bound then
|
||||
ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" })
|
||||
end
|
||||
end
|
||||
|
||||
function Archive:seek(units)
|
||||
function M:seek(units)
|
||||
local h = cx.active.current.hovered
|
||||
if h and h.url == self.file.url then
|
||||
local step = math.floor(units * self.area.h / 10)
|
||||
|
|
@ -18,4 +18,4 @@ function Archive:seek(units)
|
|||
end
|
||||
end
|
||||
|
||||
return Archive
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
local Code = {}
|
||||
local M = {}
|
||||
|
||||
function Code:peek()
|
||||
function M:peek()
|
||||
local _, bound = ya.preview_code(self)
|
||||
if bound then
|
||||
ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" })
|
||||
end
|
||||
end
|
||||
|
||||
function Code:seek(units)
|
||||
function M:seek(units)
|
||||
local h = cx.active.current.hovered
|
||||
if h and h.url == self.file.url then
|
||||
local step = math.floor(units * self.area.h / 10)
|
||||
|
|
@ -18,4 +18,4 @@ function Code:seek(units)
|
|||
end
|
||||
end
|
||||
|
||||
return Code
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
local Folder_ = {}
|
||||
local M = {}
|
||||
|
||||
function Folder_:peek()
|
||||
function M:peek()
|
||||
local folder = Folder:by_kind(Folder.PREVIEW)
|
||||
if folder == nil or folder.cwd ~= self.file.url then
|
||||
return {}
|
||||
|
|
@ -24,7 +24,7 @@ function Folder_:peek()
|
|||
ya.preview_widgets(self, { ui.List(self.area, items) })
|
||||
end
|
||||
|
||||
function Folder_:seek(units)
|
||||
function M:seek(units)
|
||||
local folder = Folder:by_kind(Folder.PREVIEW)
|
||||
if folder and folder.cwd == self.file.url then
|
||||
local step = math.floor(units * self.area.h / 10)
|
||||
|
|
@ -36,4 +36,4 @@ function Folder_:seek(units)
|
|||
end
|
||||
end
|
||||
|
||||
return Folder_
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
local Image = {}
|
||||
local M = {}
|
||||
|
||||
function Image:cache() return ya.cache_file(self.file.url .. tostring(self.file.cha.modified)) end
|
||||
function M:cache() return ya.cache_file(self.file.url .. tostring(self.file.cha.modified)) end
|
||||
|
||||
function Image:peek()
|
||||
function M:peek()
|
||||
local cache = self:cache()
|
||||
ya.image_show(fs.symlink_metadata(cache) and cache or self.file.url, self.area)
|
||||
ya.preview_widgets(self, {})
|
||||
end
|
||||
|
||||
function Image:seek() end
|
||||
function M:seek() end
|
||||
|
||||
function Image:preload()
|
||||
function M:preload()
|
||||
local cache = self:cache()
|
||||
if fs.symlink_metadata(cache) then
|
||||
return 1
|
||||
|
|
@ -19,4 +19,4 @@ function Image:preload()
|
|||
return ya.image_precache(self.file.url, cache) and 1 or 2
|
||||
end
|
||||
|
||||
return Image
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
local Json = {}
|
||||
local M = {}
|
||||
|
||||
function Json:peek()
|
||||
function M:peek()
|
||||
local limit = self.area.h
|
||||
local child = Command.new("jq")
|
||||
:args({
|
||||
|
|
@ -35,7 +35,7 @@ function Json:peek()
|
|||
end
|
||||
end
|
||||
|
||||
function Json:seek(units)
|
||||
function M:seek(units)
|
||||
local h = cx.active.current.hovered
|
||||
if h and h.url == self.file.url then
|
||||
local step = math.floor(units * self.area.h / 10)
|
||||
|
|
@ -46,4 +46,4 @@ function Json:seek(units)
|
|||
end
|
||||
end
|
||||
|
||||
return Json
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
local Mime = {}
|
||||
local M = {}
|
||||
|
||||
function Mime:preload()
|
||||
function M:preload()
|
||||
local command = Command.new("file"):arg("--mime-type"):stdout(Command.PIPED):stderr(Command.PIPED)
|
||||
if ya.target_family() == "windows" then
|
||||
command:arg("-b")
|
||||
|
|
@ -13,23 +13,23 @@ function Mime:preload()
|
|||
urls[#urls + 1] = tostring(file.url)
|
||||
end
|
||||
|
||||
local i, data = 1, {}
|
||||
local i, mimes = 1, {}
|
||||
local output = command:args(urls):output()
|
||||
for line in output.stdout:gmatch("[^\r\n]+") do
|
||||
if i > #urls then
|
||||
break
|
||||
end
|
||||
if ya.mime_valid(line) then
|
||||
data[urls[i]] = line
|
||||
mimes[urls[i]] = line
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
if #data then
|
||||
ya.manager_emit("update_mimetype", {}, data)
|
||||
if #mimes then
|
||||
ya.manager_emit("update_mimetype", {}, mimes)
|
||||
return 3
|
||||
end
|
||||
return 2
|
||||
end
|
||||
|
||||
return Mime
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
local Noop = {}
|
||||
local M = {}
|
||||
|
||||
function Noop:peek() end
|
||||
function M:peek() end
|
||||
|
||||
function Noop:seek() end
|
||||
function M:seek() end
|
||||
|
||||
function Noop:preload() return 1 end
|
||||
function M:preload() return 1 end
|
||||
|
||||
return Noop
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
local Pdf = {}
|
||||
local M = {}
|
||||
|
||||
function Pdf:cache() return ya.cache_file(self.file.url .. self.skip .. tostring(self.file.cha.modified)) end
|
||||
function M:cache() return ya.cache_file(self.file.url .. self.skip .. tostring(self.file.cha.modified)) end
|
||||
|
||||
function Pdf:peek()
|
||||
function M:peek()
|
||||
if self:preload() == 1 then
|
||||
ya.image_show(self:cache(), self.area)
|
||||
ya.preview_widgets(self, {})
|
||||
end
|
||||
end
|
||||
|
||||
function Pdf:seek(units)
|
||||
function M:seek(units)
|
||||
local h = cx.active.current.hovered
|
||||
if h and h.url == self.file.url then
|
||||
local step = ya.clamp(-1, units, 1)
|
||||
|
|
@ -17,7 +17,7 @@ function Pdf:seek(units)
|
|||
end
|
||||
end
|
||||
|
||||
function Pdf:preload()
|
||||
function M:preload()
|
||||
local cache = self:cache()
|
||||
if fs.symlink_metadata(cache) then
|
||||
return 1
|
||||
|
|
@ -40,4 +40,4 @@ function Pdf:preload()
|
|||
return fs.write(cache, output.stdout) and 1 or 2
|
||||
end
|
||||
|
||||
return Pdf
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
local Video = {}
|
||||
local M = {}
|
||||
|
||||
function Video:cache() return ya.cache_file(self.file.url .. self.skip .. tostring(self.file.cha.modified)) end
|
||||
function M:cache() return ya.cache_file(self.file.url .. self.skip .. tostring(self.file.cha.modified)) end
|
||||
|
||||
function Video:peek()
|
||||
function M:peek()
|
||||
if self:preload() == 1 then
|
||||
ya.image_show(self:cache(), self.area)
|
||||
ya.preview_widgets(self, {})
|
||||
end
|
||||
end
|
||||
|
||||
function Video:seek(units)
|
||||
function M:seek(units)
|
||||
local h = cx.active.current.hovered
|
||||
if h and h.url == self.file.url then
|
||||
ya.manager_emit("peek", {
|
||||
|
|
@ -19,7 +19,7 @@ function Video:seek(units)
|
|||
end
|
||||
end
|
||||
|
||||
function Video:preload()
|
||||
function M:preload()
|
||||
local percentage = 5 + self.skip
|
||||
if percentage > 95 then
|
||||
ya.manager_emit("peek", { "90", only_if = tostring(self.file.url), upper_bound = "" })
|
||||
|
|
@ -52,4 +52,4 @@ function Video:preload()
|
|||
return status:success() and 1 or 2
|
||||
end
|
||||
|
||||
return Video
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use mlua::{Lua, LuaSerdeExt, SerializeOptions};
|
||||
use yazi_config::{MANAGER, PREVIEW, THEME};
|
||||
use yazi_config::{BOOT, MANAGER, PREVIEW, THEME};
|
||||
|
||||
const OPTIONS: SerializeOptions =
|
||||
SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false);
|
||||
|
|
@ -11,6 +11,16 @@ pub struct Config<'a> {
|
|||
impl<'a> Config<'a> {
|
||||
pub fn new(lua: &'a Lua) -> Self { Self { lua } }
|
||||
|
||||
pub fn install_boot(self) -> mlua::Result<Self> {
|
||||
self.lua.globals().set("BOOT", self.lua.to_value_with(&*BOOT, OPTIONS)?)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn install_manager(self) -> mlua::Result<Self> {
|
||||
self.lua.globals().set("MANAGER", self.lua.to_value_with(&*MANAGER, OPTIONS)?)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn install_theme(self) -> mlua::Result<Self> {
|
||||
self.lua.globals().set("THEME", self.lua.to_value_with(&*THEME, OPTIONS)?)?;
|
||||
Ok(self)
|
||||
|
|
@ -20,9 +30,4 @@ impl<'a> Config<'a> {
|
|||
self.lua.globals().set("PREVIEW", self.lua.to_value_with(&*PREVIEW, OPTIONS)?)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn install_manager(self) -> mlua::Result<Self> {
|
||||
self.lua.globals().set("MANAGER", self.lua.to_value_with(&*MANAGER, OPTIONS)?)?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
yazi-plugin/src/external/file.rs
vendored
40
yazi-plugin/src/external/file.rs
vendored
|
|
@ -1,40 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use futures::TryFutureExt;
|
||||
use tokio::process::Command;
|
||||
use tracing::error;
|
||||
use yazi_shared::{fs::Url, mime_valid};
|
||||
|
||||
async fn _file(files: &[&Url]) -> Result<BTreeMap<Url, String>> {
|
||||
if files.is_empty() {
|
||||
bail!("no files to get mime types for");
|
||||
}
|
||||
|
||||
let output = Command::new("file")
|
||||
.args([cfg!(windows).then_some("-b").unwrap_or("-bL"), "--mime-type"])
|
||||
.args(files)
|
||||
.kill_on_drop(true)
|
||||
.output()
|
||||
.inspect_err(|e| error!("failed to execute `file`: {}", e))
|
||||
.await?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
let mimes = BTreeMap::from_iter(
|
||||
files
|
||||
.iter()
|
||||
.zip(output.trim().lines())
|
||||
.filter(|(_, m)| mime_valid(m))
|
||||
.map(|(&f, m)| (f.clone(), m.to_string())),
|
||||
);
|
||||
|
||||
if mimes.is_empty() {
|
||||
error!("failed to get mime types: {:?}", files);
|
||||
bail!("failed to get mime types");
|
||||
}
|
||||
Ok(mimes)
|
||||
}
|
||||
|
||||
pub async fn file(files: &[impl AsRef<Url>]) -> Result<BTreeMap<Url, String>> {
|
||||
_file(&files.iter().map(AsRef::as_ref).collect::<Vec<_>>()).await
|
||||
}
|
||||
2
yazi-plugin/src/external/mod.rs
vendored
2
yazi-plugin/src/external/mod.rs
vendored
|
|
@ -1,5 +1,4 @@
|
|||
mod fd;
|
||||
mod file;
|
||||
mod fzf;
|
||||
mod highlighter;
|
||||
mod lsar;
|
||||
|
|
@ -8,7 +7,6 @@ mod shell;
|
|||
mod zoxide;
|
||||
|
||||
pub use fd::*;
|
||||
pub use file::*;
|
||||
pub use fzf::*;
|
||||
pub use highlighter::*;
|
||||
pub use lsar::*;
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ impl Loader {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let b = fs::read(BOOT.plugin_dir.join(name)).await.map(|v| v.into()).unwrap_or(Cow::from(
|
||||
match name {
|
||||
let b = fs::read(BOOT.plugin_dir.join(name)).await.map(|v| v.into()).or_else(|_| {
|
||||
Ok(Cow::from(match name {
|
||||
"noop.lua" => include_bytes!("../preset/plugins/noop.lua") as &[u8],
|
||||
"archive.lua" => include_bytes!("../preset/plugins/archive.lua"),
|
||||
"code.lua" => include_bytes!("../preset/plugins/code.lua"),
|
||||
|
|
@ -33,9 +33,9 @@ impl Loader {
|
|||
"mime.lua" => include_bytes!("../preset/plugins/mime.lua"),
|
||||
"pdf.lua" => include_bytes!("../preset/plugins/pdf.lua"),
|
||||
"video.lua" => include_bytes!("../preset/plugins/video.lua"),
|
||||
_ => bail!("plugin not found: {}", name),
|
||||
},
|
||||
));
|
||||
_ => bail!("plugin not found: {name}"),
|
||||
}))
|
||||
})?;
|
||||
|
||||
self.loaded.write().insert(name.to_owned(), b.into_owned());
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::Result;
|
||||
use mlua::{Lua, Table};
|
||||
use mlua::Lua;
|
||||
use yazi_config::BOOT;
|
||||
use yazi_shared::RoCell;
|
||||
|
||||
pub static LUA: RoCell<Lua> = RoCell::new();
|
||||
|
|
@ -7,7 +8,7 @@ pub static LUA: RoCell<Lua> = RoCell::new();
|
|||
pub fn init() {
|
||||
fn stage_1(lua: &Lua) -> Result<()> {
|
||||
crate::Loader::init();
|
||||
crate::Config::new(lua).install_theme()?.install_manager()?;
|
||||
crate::Config::new(lua).install_boot()?.install_manager()?.install_theme()?;
|
||||
crate::utils::install(lua)?;
|
||||
|
||||
// Base
|
||||
|
|
@ -29,14 +30,15 @@ pub fn init() {
|
|||
}
|
||||
|
||||
fn stage_2(lua: &Lua) {
|
||||
let ya: Table = lua.globals().get("ya").unwrap();
|
||||
ya.set("SYNC_ON", true).unwrap();
|
||||
let setup = br#"
|
||||
ya.SYNC_ON = true
|
||||
package.path = BOOT.plugin_dir .. "/?.yazi/init.lua;" .. BOOT.plugin_dir .. "/?.lua;" .. package.path
|
||||
"#;
|
||||
lua.load(setup as &[u8]).exec().unwrap();
|
||||
|
||||
// TODO: plugin system
|
||||
// PLUGIN.preload.iter().for_each(|p| {
|
||||
// let b = std::fs::read(p).unwrap_or_else(|_| panic!("failed to read
|
||||
// plugin: {p:?}")); lua.load(&b).exec().unwrap_or_else(|_| panic!("failed
|
||||
// to load plugin: {p:?}")); });
|
||||
if let Ok(b) = std::fs::read(BOOT.config_dir.join("init.lua")) {
|
||||
lua.load(b).exec().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let lua = Lua::new();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
|||
use parking_lot::RwLock;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
use yazi_shared::{emit, fs::{calculate_size, FilesOp, Url}, Throttle};
|
||||
use yazi_shared::{fs::{calculate_size, FilesOp, Url}, Throttle};
|
||||
|
||||
use crate::TaskOp;
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ impl Preload {
|
|||
}
|
||||
|
||||
let parent = buf[0].0.parent_url().unwrap();
|
||||
emit!(Files(FilesOp::Size(parent, BTreeMap::from_iter(buf))));
|
||||
FilesOp::Size(parent, BTreeMap::from_iter(buf)).emit();
|
||||
});
|
||||
|
||||
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crossterm::event::KeyEvent;
|
|||
use tokio::sync::{mpsc::UnboundedSender, oneshot};
|
||||
|
||||
use super::Exec;
|
||||
use crate::{fs::FilesOp, term::Term, Layer, RoCell};
|
||||
use crate::{term::Term, Layer, RoCell};
|
||||
|
||||
static TX: RoCell<UnboundedSender<Event>> = RoCell::new();
|
||||
|
||||
|
|
@ -15,7 +15,6 @@ pub enum Event {
|
|||
Call(Vec<Exec>, Layer),
|
||||
|
||||
// Manager
|
||||
Files(FilesOp),
|
||||
Pages(usize),
|
||||
}
|
||||
|
||||
|
|
@ -44,9 +43,6 @@ macro_rules! emit {
|
|||
$crate::event::Event::Call($exec, $layer).emit();
|
||||
};
|
||||
|
||||
(Files($op:expr)) => {
|
||||
$crate::event::Event::Files($op).emit();
|
||||
};
|
||||
(Pages($page:expr)) => {
|
||||
$crate::event::Event::Pages($page).emit();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{collections::BTreeMap, ffi::OsStr, fs::Metadata, ops::Deref};
|
||||
use std::{ffi::OsStr, fs::Metadata, ops::Deref};
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::fs;
|
||||
|
|
@ -52,13 +52,6 @@ impl File {
|
|||
let ck = if url.is_hidden() { ChaKind::HIDDEN } else { ChaKind::empty() };
|
||||
Self { url, cha: Cha::default().with_kind(ck), link_to: None }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_map(self) -> BTreeMap<Url, File> {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(self.url.clone(), self);
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
impl File {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
use std::{collections::{BTreeMap, BTreeSet}, sync::atomic::{AtomicU64, Ordering}};
|
||||
use std::{collections::BTreeMap, sync::atomic::{AtomicU64, Ordering}};
|
||||
|
||||
use super::File;
|
||||
use crate::{emit, fs::Url};
|
||||
use crate::{emit, event::Exec, fs::Url, Layer};
|
||||
|
||||
pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FilesOp {
|
||||
Full(Url, Vec<File>),
|
||||
Part(Url, u64, Vec<File>),
|
||||
Part(Url, Vec<File>, u64),
|
||||
Size(Url, BTreeMap<Url, u64>),
|
||||
IOErr(Url),
|
||||
|
||||
Creating(Url, BTreeMap<Url, File>),
|
||||
Deleting(Url, BTreeSet<Url>),
|
||||
Replacing(Url, BTreeMap<Url, File>),
|
||||
Creating(Url, Vec<File>),
|
||||
Deleting(Url, Vec<Url>),
|
||||
Updating(Url, BTreeMap<Url, File>),
|
||||
Upserting(Url, BTreeMap<Url, File>),
|
||||
}
|
||||
|
||||
impl FilesOp {
|
||||
|
|
@ -28,14 +29,41 @@ impl FilesOp {
|
|||
|
||||
Self::Creating(url, _) => url,
|
||||
Self::Deleting(url, _) => url,
|
||||
Self::Replacing(url, _) => url,
|
||||
Self::Updating(url, _) => url,
|
||||
Self::Upserting(url, _) => url,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn emit(self) {
|
||||
emit!(Call(Exec::call("update_files", vec![]).with_data(self).vec(), Layer::Manager));
|
||||
}
|
||||
|
||||
pub fn prepare(url: &Url) -> u64 {
|
||||
let ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
|
||||
emit!(Files(Self::Part(url.clone(), ticket, Vec::new())));
|
||||
Self::Part(url.clone(), Vec::new(), ticket).emit();
|
||||
ticket
|
||||
}
|
||||
|
||||
pub fn chroot(&self, new: Url) -> Self {
|
||||
todo!()
|
||||
// Full(Url, Vec<File>),
|
||||
// Part(Url, u64, Vec<File>),
|
||||
// Size(Url, BTreeMap<Url, u64>),
|
||||
// IOErr(Url),
|
||||
|
||||
// Creating(Url, BTreeMap<Url, File>),
|
||||
// Deleting(Url, BTreeSet<Url>),
|
||||
// Replacing(Url, BTreeMap<Url, File>),
|
||||
|
||||
// match self {
|
||||
// FilesOp::Full(_, vec) => {}
|
||||
// FilesOp::Part(_, _, vec) => todo!(),
|
||||
// FilesOp::Size(_, map) => todo!(),
|
||||
// FilesOp::IOErr(_) => todo!(),
|
||||
// FilesOp::Creating(_, map) => todo!(),
|
||||
// FilesOp::Deleting(_, set) => todo!(),
|
||||
// FilesOp::Replacing(_, map) => todo!(),
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue