This commit is contained in:
sxyazi 2023-12-24 00:33:01 +08:00
parent e42ec9f429
commit e5ba8e7d27
No known key found for this signature in database
36 changed files with 487 additions and 485 deletions

View file

@ -1,15 +1,16 @@
## Yazi - ⚡️ Blazing Fast Terminal File Manager ## 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) 💡 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. - 🚀 **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. - 💪 **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 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 - 🧰 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.) - 🏷️ Multi-Tab Support, Scrollable Preview (for videos, PDFs, archives, directories, code, etc.)
- 🔄 Batch Renaming, Visual Mode, File Chooser - 🔄 Batch Renaming, Visual Mode, File Chooser
- 🎨 Theme System, Custom Layouts, Trash Bin, CSI u - 🎨 Theme System, Custom Layouts, Trash Bin, CSI u

View file

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

View file

@ -1,12 +1,13 @@
use std::{ffi::OsString, fs, path::{Path, PathBuf}, process}; use std::{ffi::OsString, fs, path::{Path, PathBuf}, process};
use clap::Parser; use clap::Parser;
use serde::Serialize;
use yazi_shared::fs::{current_cwd, expand_path}; use yazi_shared::fs::{current_cwd, expand_path};
use super::Args; use super::Args;
use crate::{Xdg, ARGS}; use crate::{Xdg, ARGS};
#[derive(Debug)] #[derive(Debug, Serialize)]
pub struct Boot { pub struct Boot {
pub cwd: PathBuf, pub cwd: PathBuf,
pub file: Option<OsString>, pub file: Option<OsString>,

View file

@ -8,10 +8,10 @@ use yazi_shared::fs::{File, Url, FILES_TICKET};
use super::FilesSorter; use super::FilesSorter;
pub struct Files { pub struct Files {
items: Vec<File>, hidden: Vec<File>,
hidden: Vec<File>, items: Vec<File>,
ticket: u64, ticket: u64,
pub(crate) version: u64, pub(crate) revision: u64,
pub sizes: BTreeMap<Url, u64>, pub sizes: BTreeMap<Url, u64>,
selected: BTreeSet<Url>, selected: BTreeSet<Url>,
@ -23,10 +23,10 @@ pub struct Files {
impl Default for Files { impl Default for Files {
fn default() -> Self { fn default() -> Self {
Self { Self {
items: Default::default(), items: Default::default(),
hidden: Default::default(), hidden: Default::default(),
ticket: Default::default(), ticket: Default::default(),
version: Default::default(), revision: Default::default(),
sizes: Default::default(), sizes: Default::default(),
selected: Default::default(), selected: Default::default(),
@ -68,7 +68,7 @@ impl Files {
#[inline] #[inline]
pub fn select(&mut self, url: &Url, state: Option<bool>) -> bool { pub fn select(&mut self, url: &Url, state: Option<bool>) -> bool {
let old = self.selected.contains(url); 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 { if new == old {
return false; return false;
@ -126,132 +126,174 @@ impl Files {
applied applied
} }
pub fn update_full(&mut self, mut items: Vec<File>) -> bool { pub fn update_full(&mut self, files: Vec<File>) {
if !self.show_hidden { if files.is_empty() {
(self.hidden, items) = items.into_iter().partition(|f| f.is_hidden()); return;
} }
self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
self.sorter.sort(&mut items, &self.sizes); self.revision += 1;
self.items = items;
self.version += 1; (self.hidden, self.items) = if self.show_hidden {
true (vec![], files)
} else {
files.into_iter().partition(|f| f.is_hidden())
};
} }
pub fn update_part(&mut self, version: u64, items: Vec<File>) -> bool { pub fn update_part(&mut self, files: Vec<File>, ticket: u64) {
if !items.is_empty() { if !files.is_empty() {
if version != self.ticket { if ticket != self.ticket {
return false; return;
} }
self.revision += 1;
if self.show_hidden { if self.show_hidden {
self.items.extend(items); self.hidden.clear();
self.items.extend(files);
} else { } else {
let (hidden, items): (Vec<_>, Vec<_>) = items.into_iter().partition(|f| f.is_hidden()); let (hidden, items): (Vec<_>, Vec<_>) = files.into_iter().partition(|f| f.is_hidden());
self.items.extend(items);
self.hidden.extend(hidden); self.hidden.extend(hidden);
self.items.extend(items);
} }
return;
self.sorter.sort(&mut self.items, &self.sizes);
self.version += 1;
return true;
} }
self.ticket = version; self.ticket = ticket;
if self.items.is_empty() && self.hidden.is_empty() { if !self.items.is_empty() || !self.hidden.is_empty() {
return false; 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 { pub fn update_size(&mut self, sizes: BTreeMap<Url, u64>) {
self.sizes.extend(items); if sizes.is_empty() {
return;
}
if self.sorter.by == SortBy::Size { if self.sorter.by == SortBy::Size {
self.sorter.sort(&mut self.items, &self.sizes); self.revision += 1;
self.version += 1;
} }
true self.sizes.extend(sizes);
} }
pub fn update_creating(&mut self, mut todo: BTreeMap<Url, File>) -> bool { pub fn update_creating(&mut self, files: Vec<File>) {
if !self.show_hidden { if files.is_empty() {
todo.retain(|_, f| !f.is_hidden()); 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 { macro_rules! go {
($name:expr) => { ($dist:expr, $src:expr) => {
removed.clear(); let mut todo: BTreeMap<_, _> = $src.into_iter().map(|f| (f.url(), f)).collect();
for i in 0..$name.len() { for f in &$dist {
if todo.remove(&$name[i].url) { if todo.remove(&f.url).is_some() && todo.is_empty() {
removed.push(i); break;
if todo.is_empty() { }
}
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; break;
} }
} }
} }
for i in (0..removed.len()).rev() { if $src.len() != len {
$name.remove(removed[i]); self.revision += 1;
} }
}; };
} }
let mut b = false; let (mut hidden, mut items) = if self.show_hidden {
if !todo.is_empty() { (BTreeMap::new(), files)
go!(self.items); } else {
b |= !removed.is_empty(); files.into_iter().partition(|(_, f)| f.is_hidden())
} };
if !todo.is_empty() { if !items.is_empty() {
go!(self.hidden); go!(self.items, items);
b |= !removed.is_empty();
} }
b if !hidden.is_empty() {
go!(self.hidden, hidden);
}
[hidden, items]
} }
pub fn update_replacing(&mut self, todo: &mut BTreeMap<Url, File>) -> bool { pub fn update_upserting(&mut self, files: BTreeMap<Url, File>) {
if todo.is_empty() { if files.is_empty() {
return false; return;
} }
macro_rules! go { let [hidden, items] = self.update_updating(files);
($name:expr) => { if hidden.is_empty() && items.is_empty() {
for i in 0..$name.len() { return;
if let Some(f) = todo.remove(&$name[i].url) {
$name[i] = f;
if todo.is_empty() {
self.version += 1;
return true;
}
}
}
};
} }
let old = todo.len(); self.revision += 1;
go!(self.items); if !hidden.is_empty() {
go!(self.hidden); self.hidden.extend(hidden.into_values());
}
if old != todo.len() { if !items.is_empty() {
self.version += 1; self.items.extend(items.into_values());
return true;
} }
false
} }
} }
@ -318,7 +360,7 @@ impl Files {
return false; return false;
} }
self.sorter = sorter; self.sorter = sorter;
self.version += 1; self.revision += 1;
self.sorter.sort(&mut self.items, &self.sizes) self.sorter.sort(&mut self.items, &self.sizes)
} }
@ -339,7 +381,7 @@ impl Files {
} }
self.show_hidden = state; self.show_hidden = state;
self.version += 1; self.revision += 1;
true true
} }
} }

View file

@ -25,17 +25,19 @@ impl From<&Url> for Folder {
impl Folder { impl Folder {
pub fn update(&mut self, op: FilesOp) -> bool { pub fn update(&mut self, op: FilesOp) -> bool {
let b = match op { let revision = self.files.revision;
FilesOp::Full(_, items) => self.files.update_full(items), match op {
FilesOp::Part(_, ticket, items) => self.files.update_part(ticket, items), FilesOp::Full(_, files) => self.files.update_full(files),
FilesOp::Size(_, items) => self.files.update_size(items), 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::Creating(_, files) => self.files.update_creating(files),
FilesOp::Deleting(_, items) => self.files.update_deleting(items), FilesOp::Deleting(_, urls) => self.files.update_deleting(urls),
FilesOp::Replacing(_, mut items) => self.files.update_replacing(&mut items), FilesOp::Updating(_, files) => _ = self.files.update_updating(files),
FilesOp::Upserting(_, files) => self.files.update_upserting(files),
_ => unreachable!(), _ => unreachable!(),
}; }
if !b { if revision == self.files.revision {
return false; return false;
} }

View file

@ -2,7 +2,7 @@ use std::path::{PathBuf, MAIN_SEPARATOR};
use tokio::fs; use tokio::fs;
use yazi_config::popup::InputCfg; 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}; use crate::{input::Input, manager::Manager};
@ -42,7 +42,7 @@ impl Manager {
let child = let child =
Url::from(path.components().take(cwd.components().count() + 1).collect::<PathBuf>()); Url::from(path.components().take(cwd.components().count() + 1).collect::<PathBuf>());
if let Ok(f) = File::from(child.clone()).await { 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)); Manager::_hover(Some(child));
} }
Ok::<(), anyhow::Error>(()) Ok::<(), anyhow::Error>(())

View file

@ -15,5 +15,6 @@ mod tab_close;
mod tab_create; mod tab_create;
mod tab_swap; mod tab_swap;
mod tab_switch; mod tab_switch;
mod update_files;
mod update_mimetype; mod update_mimetype;
mod yank; mod yank;

View file

@ -1,7 +1,6 @@
use std::ffi::OsString; use std::ffi::OsString;
use yazi_config::{popup::SelectCfg, OPEN}; use yazi_config::{popup::SelectCfg, OPEN};
use yazi_plugin::external;
use yazi_shared::{event::Exec, MIME_DIR}; use yazi_shared::{event::Exec, MIME_DIR};
use crate::{manager::Manager, select::Select, tasks::Tasks}; use crate::{manager::Manager, select::Select, tasks::Tasks};
@ -44,28 +43,30 @@ impl Manager {
} }
let opt = opt.into() as Opt; let opt = opt.into() as Opt;
tokio::spawn(async move { // TODO: plugin system
let todo: Vec<_> = files.iter().filter(|(_, m)| m.is_none()).map(|(u, _)| u).collect(); // tokio::spawn(async move {
if let Ok(mut mimes) = external::file(&todo).await { // let todo: Vec<_> = files.iter().filter(|(_, m)| m.is_none()).map(|(u, _)|
files = files // u).collect(); if let Ok(mut mimes) = external::file(&todo).await {
.into_iter() // files = files
.map(|(u, m)| { // .into_iter()
let mime = m.or_else(|| mimes.remove(&u)); // .map(|(u, m)| {
(u, mime) // let mime = m.or_else(|| mimes.remove(&u));
}) // (u, mime)
.collect(); // })
} // .collect();
// }
let files: Vec<_> = // let files: Vec<_> =
files.into_iter().filter_map(|(u, m)| m.map(|m| (u.into_os_string(), m))).collect(); // files.into_iter().filter_map(|(u, m)| m.map(|m| (u.into_os_string(),
// m))).collect();
if opt.interactive { // if opt.interactive {
Self::open_interactive(files).await; // Self::open_interactive(files).await;
return; // return;
} // }
Tasks::_open(files, None); // Tasks::_open(files, None);
}); // });
false false
} }
} }

View file

@ -22,8 +22,8 @@ impl Manager {
self.active_mut().apply_files_attrs(false); self.active_mut().apply_files_attrs(false);
if let Some(f) = self.parent() { if let Some(p) = self.parent() {
self.watcher.trigger_dirs(&[self.cwd(), &f.cwd]); self.watcher.trigger_dirs(&[self.cwd(), &p.cwd]);
} else { } else {
self.watcher.trigger_dirs(&[self.cwd()]); self.watcher.trigger_dirs(&[self.cwd()]);
} }

View file

@ -5,7 +5,7 @@ use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}};
use yazi_config::{popup::InputCfg, OPEN, PREVIEW}; use yazi_config::{popup::InputCfg, OPEN, PREVIEW};
use yazi_plugin::external::{self, ShellOpt}; use yazi_plugin::external::{self, ShellOpt};
use yazi_scheduler::{Scheduler, BLOCKER}; 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}; use crate::{input::Input, manager::Manager};
@ -25,7 +25,7 @@ impl Manager {
} }
let file = File::from(new.clone()).await?; 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))) Ok(Self::_hover(Some(new)))
} }

View 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
}
}

View file

@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use yazi_shared::fs::{File, FilesOp, Url}; use yazi_shared::fs::{File, Url};
use super::{Tabs, Watcher}; use super::{Tabs, Watcher};
use crate::{folder::Folder, tab::Tab}; use crate::{folder::Folder, tab::Tab};
@ -23,48 +23,6 @@ impl Manager {
mimetype: Default::default(), 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 { impl Manager {

View file

@ -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 notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _Watcher};
use parking_lot::RwLock; use parking_lot::RwLock;
use tokio::{fs, pin, sync::mpsc::{self, UnboundedReceiver}}; use tokio::{fs, pin, sync::mpsc::{self, UnboundedReceiver}};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; 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; use crate::folder::Files;
pub struct Watcher { pub struct Watcher {
watcher: RecommendedWatcher, watcher: RecommendedWatcher,
watched: Arc<RwLock<IndexMap<Url, Option<Url>>>>, watched: Arc<RwLock<BTreeSet<Url>>>,
pub linked: Arc<RwLock<BTreeMap<Url, Url>>>, // from ==> to
} }
impl Watcher { impl Watcher {
@ -25,55 +27,45 @@ impl Watcher {
return; 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 { match event.kind {
EventKind::Create(_) => { EventKind::Create(_) => {}
tx.send(parent).ok(); EventKind::Modify(kind) => match kind {
} ModifyKind::Data(_) => {}
EventKind::Modify(kind) => { ModifyKind::Metadata(md) => match md {
match kind { MetadataKind::WriteTime => {}
ModifyKind::Data(_) => {} MetadataKind::Permissions => {}
ModifyKind::Metadata(kind) => match kind { MetadataKind::Ownership => {}
MetadataKind::Permissions => {}
MetadataKind::Ownership => {}
MetadataKind::Extended => {}
_ => return,
},
ModifyKind::Name(_) => {}
_ => return, _ => return,
}; },
ModifyKind::Name(_) => {}
_ => return,
},
EventKind::Remove(_) => {}
_ => return,
}
tx.send(path).ok(); for path in event.paths {
tx.send(parent).ok(); tx.send(Url::from(path)).ok();
}
EventKind::Remove(_) => {
tx.send(path).ok();
tx.send(parent).ok();
}
_ => (),
} }
} }
}, },
Default::default(), Default::default(),
); );
let instance = Self { watcher: watcher.unwrap(), watched: Default::default() }; let instance =
tokio::spawn(Self::on_changed(rx, instance.watched.clone())); Self { watcher: watcher.unwrap(), watched: Default::default(), linked: Default::default() };
tokio::spawn(Self::on_changed(rx));
instance instance
} }
pub(super) fn watch(&mut self, mut watched: BTreeSet<&Url>) { pub(super) fn watch(&mut self, mut new: BTreeSet<&Url>) {
watched.retain(|&u| u.is_regular()); new.retain(|&u| u.is_regular());
let (to_unwatch, to_watch): (BTreeSet<_>, BTreeSet<_>) = { let (to_unwatch, to_watch): (BTreeSet<_>, BTreeSet<_>) = {
let guard = self.watched.read(); 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(), old.difference(&new).map(|&x| x.clone()).collect(),
watched.difference(&keys).map(|&x| x.clone()).collect(), new.difference(&old).map(|&x| x.clone()).collect(),
) )
}; };
@ -82,141 +74,103 @@ impl Watcher {
} }
for u in to_watch { for u in to_watch {
if self.watcher.watch(&u, RecursiveMode::NonRecursive).is_err() { if self.watcher.watch(&u, RecursiveMode::NonRecursive).is_err() {
watched.remove(&u); new.remove(&u);
} }
} }
let mut to_resolve = Vec::new(); *self.watched.write() = new.into_iter().cloned().collect();
let mut guard = self.watched.write(); self.sync_linked();
*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)));
}
_ => {}
}
}
});
} }
pub(super) fn trigger_dirs(&self, dirs: &[&Url]) { pub(super) fn trigger_dirs(&self, dirs: &[&Url]) {
let dirs: Vec<_> = dirs.iter().filter(|&u| u.is_regular()).map(|&u| u.clone()).collect(); let urls: Vec<_> = dirs.iter().filter(|&u| u.is_regular()).map(|&u| u.clone()).collect();
if dirs.is_empty() { if urls.is_empty() {
return; return;
} }
let watched = self.watched.clone();
tokio::spawn(async move { tokio::spawn(async move {
let watched = watched.read().clone(); for u in urls {
for dir in dirs { let Ok(rx) = Files::from_dir(&u).await else {
Self::dir_changed(&dir, &watched).await; FilesOp::IOErr(u).emit();
return;
};
let files: Vec<_> = UnboundedReceiverStream::new(rx).collect().await;
FilesOp::Full(u, files).emit();
} }
}); });
} }
async fn on_changed( fn sync_linked(&self) {
rx: UnboundedReceiver<Url>, let mut new = self.watched.read().clone();
watched: Arc<RwLock<IndexMap<Url, Option<Url>>>>, 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 // TODO: revert this once a new notification is implemented
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10, Duration::from_millis(20)); let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10, Duration::from_millis(20));
pin!(rx); pin!(rx);
while let Some(urls) = rx.next().await { while let Some(urls) = rx.next().await {
let (mut files, mut dirs): (Vec<_>, Vec<_>) = Default::default(); let urls = urls.into_iter().collect::<BTreeSet<_>>();
for url in urls.into_iter().collect::<BTreeSet<_>>() { let mut reload = Vec::with_capacity(urls.len());
if fs::metadata(&url).await.map(|m| !m.is_dir()).unwrap_or(false) {
files.push(url); for u in urls {
} else { let Some(parent) = u.parent_url() else {
dirs.push(url); 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(); if reload.is_empty() {
continue;
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 let Err(e) = isolate::preload("mime.lua".to_string(), reload, true).await {
for dir in dirs { error!("preload in watcher failed: {e}");
Self::dir_changed(&dir, &watched).await;
} }
} }
} }
}
async fn files_changed(urls: &[Url], watched: &IndexMap<Url, Option<Url>>) {
// TODO: plugin system impl Watcher {
// let Ok(mut mimes) = external::file(urls).await else { pub fn relatives(&self, url: &Url) -> BTreeSet<Url> {
// return; let mut map = BTreeSet::from_iter([url.clone()]);
// };
let linked = self.linked.read();
// let linked: Vec<_> = watched.iter().filter_map(|(k, v)| if let Some(to) = linked.get(url) {
// v.as_ref().map(|v| (k, v))).fold( Vec::new(), map.extend(linked.iter().filter(|(_, v)| *v == to).map(|(k, _)| k).cloned());
// |mut aac, (k, v)| { }
// mimes
// .iter() map
// .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()
}
} }

View file

@ -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}; use crate::{manager::Manager, tab::Tab};
@ -34,10 +34,7 @@ impl Tab {
}; };
let b = self.cd(parent.clone()); let b = self.cd(parent.clone());
emit!(Files(FilesOp::Creating( FilesOp::Creating(parent, vec![File::from_dummy(opt.target.clone())]).emit();
parent.clone(),
File::from_dummy(opt.target.clone()).into_map()
)));
Manager::_hover(Some(opt.target)); Manager::_hover(Some(opt.target));
b b
} }

View file

@ -5,7 +5,7 @@ use tokio::pin;
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use yazi_config::popup::InputCfg; use yazi_config::popup::InputCfg;
use yazi_plugin::external; 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}; use crate::{input::Input, manager::Manager, tab::Tab};
@ -66,7 +66,7 @@ impl Tab {
Tab::_cd(&cwd); Tab::_cd(&cwd);
first = false; first = false;
} }
emit!(Files(FilesOp::Part(cwd.clone(), ticket, chunk))); FilesOp::Part(cwd.clone(), chunk, ticket).emit();
} }
Ok(()) Ok(())
})); }));

View file

@ -53,7 +53,7 @@ impl Finder {
} }
pub(super) fn catchup(&mut self, files: &Files) -> bool { pub(super) fn catchup(&mut self, files: &Files) -> bool {
if self.version == files.version { if self.version == files.revision {
return false; return false;
} }
self.matched.clear(); self.matched.clear();
@ -72,7 +72,7 @@ impl Finder {
i += 1; i += 1;
} }
self.version = files.version; self.version = files.revision;
true true
} }

View file

@ -5,7 +5,7 @@ use crossterm::event::KeyEvent;
use ratatui::backend::Backend; use ratatui::backend::Backend;
use yazi_config::{keymap::Key, ARGS}; use yazi_config::{keymap::Key, ARGS};
use yazi_core::input::InputMode; 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}; use crate::{lives::Lives, Ctx, Executor, Logs, Panic, Root, Signals};
@ -132,19 +132,6 @@ impl App {
let manager = &mut self.cx.manager; let manager = &mut self.cx.manager;
let tasks = &mut self.cx.tasks; let tasks = &mut self.cx.tasks;
match event { 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) => { Event::Pages(page) => {
let targets = self.cx.manager.current().paginate(page); let targets = self.cx.manager.current().paginate(page);
tasks.preload_paged(targets, &self.cx.manager.mimetype); tasks.preload_paged(targets, &self.cx.manager.mimetype);

View file

@ -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, update_mimetype, &self.app.cx.tasks);
on!(MANAGER, hover); on!(MANAGER, hover);
on!(MANAGER, peek); on!(MANAGER, peek);

View file

@ -1,13 +1,13 @@
local Archive = {} local M = {}
function Archive:peek() function M:peek()
local _, bound = ya.preview_archive(self) local _, bound = ya.preview_archive(self)
if bound then if bound then
ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" }) ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" })
end end
end end
function Archive:seek(units) function M:seek(units)
local h = cx.active.current.hovered local h = cx.active.current.hovered
if h and h.url == self.file.url then if h and h.url == self.file.url then
local step = math.floor(units * self.area.h / 10) local step = math.floor(units * self.area.h / 10)
@ -18,4 +18,4 @@ function Archive:seek(units)
end end
end end
return Archive return M

View file

@ -1,13 +1,13 @@
local Code = {} local M = {}
function Code:peek() function M:peek()
local _, bound = ya.preview_code(self) local _, bound = ya.preview_code(self)
if bound then if bound then
ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" }) ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" })
end end
end end
function Code:seek(units) function M:seek(units)
local h = cx.active.current.hovered local h = cx.active.current.hovered
if h and h.url == self.file.url then if h and h.url == self.file.url then
local step = math.floor(units * self.area.h / 10) local step = math.floor(units * self.area.h / 10)
@ -18,4 +18,4 @@ function Code:seek(units)
end end
end end
return Code return M

View file

@ -1,6 +1,6 @@
local Folder_ = {} local M = {}
function Folder_:peek() function M:peek()
local folder = Folder:by_kind(Folder.PREVIEW) local folder = Folder:by_kind(Folder.PREVIEW)
if folder == nil or folder.cwd ~= self.file.url then if folder == nil or folder.cwd ~= self.file.url then
return {} return {}
@ -24,7 +24,7 @@ function Folder_:peek()
ya.preview_widgets(self, { ui.List(self.area, items) }) ya.preview_widgets(self, { ui.List(self.area, items) })
end end
function Folder_:seek(units) function M:seek(units)
local folder = Folder:by_kind(Folder.PREVIEW) local folder = Folder:by_kind(Folder.PREVIEW)
if folder and folder.cwd == self.file.url then if folder and folder.cwd == self.file.url then
local step = math.floor(units * self.area.h / 10) local step = math.floor(units * self.area.h / 10)
@ -36,4 +36,4 @@ function Folder_:seek(units)
end end
end end
return Folder_ return M

View file

@ -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() local cache = self:cache()
ya.image_show(fs.symlink_metadata(cache) and cache or self.file.url, self.area) ya.image_show(fs.symlink_metadata(cache) and cache or self.file.url, self.area)
ya.preview_widgets(self, {}) ya.preview_widgets(self, {})
end end
function Image:seek() end function M:seek() end
function Image:preload() function M:preload()
local cache = self:cache() local cache = self:cache()
if fs.symlink_metadata(cache) then if fs.symlink_metadata(cache) then
return 1 return 1
@ -19,4 +19,4 @@ function Image:preload()
return ya.image_precache(self.file.url, cache) and 1 or 2 return ya.image_precache(self.file.url, cache) and 1 or 2
end end
return Image return M

View file

@ -1,6 +1,6 @@
local Json = {} local M = {}
function Json:peek() function M:peek()
local limit = self.area.h local limit = self.area.h
local child = Command.new("jq") local child = Command.new("jq")
:args({ :args({
@ -35,7 +35,7 @@ function Json:peek()
end end
end end
function Json:seek(units) function M:seek(units)
local h = cx.active.current.hovered local h = cx.active.current.hovered
if h and h.url == self.file.url then if h and h.url == self.file.url then
local step = math.floor(units * self.area.h / 10) local step = math.floor(units * self.area.h / 10)
@ -46,4 +46,4 @@ function Json:seek(units)
end end
end end
return Json return M

View file

@ -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) local command = Command.new("file"):arg("--mime-type"):stdout(Command.PIPED):stderr(Command.PIPED)
if ya.target_family() == "windows" then if ya.target_family() == "windows" then
command:arg("-b") command:arg("-b")
@ -13,23 +13,23 @@ function Mime:preload()
urls[#urls + 1] = tostring(file.url) urls[#urls + 1] = tostring(file.url)
end end
local i, data = 1, {} local i, mimes = 1, {}
local output = command:args(urls):output() local output = command:args(urls):output()
for line in output.stdout:gmatch("[^\r\n]+") do for line in output.stdout:gmatch("[^\r\n]+") do
if i > #urls then if i > #urls then
break break
end end
if ya.mime_valid(line) then if ya.mime_valid(line) then
data[urls[i]] = line mimes[urls[i]] = line
end end
i = i + 1 i = i + 1
end end
if #data then if #mimes then
ya.manager_emit("update_mimetype", {}, data) ya.manager_emit("update_mimetype", {}, mimes)
return 3 return 3
end end
return 2 return 2
end end
return Mime return M

View file

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

View file

@ -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 if self:preload() == 1 then
ya.image_show(self:cache(), self.area) ya.image_show(self:cache(), self.area)
ya.preview_widgets(self, {}) ya.preview_widgets(self, {})
end end
end end
function Pdf:seek(units) function M:seek(units)
local h = cx.active.current.hovered local h = cx.active.current.hovered
if h and h.url == self.file.url then if h and h.url == self.file.url then
local step = ya.clamp(-1, units, 1) local step = ya.clamp(-1, units, 1)
@ -17,7 +17,7 @@ function Pdf:seek(units)
end end
end end
function Pdf:preload() function M:preload()
local cache = self:cache() local cache = self:cache()
if fs.symlink_metadata(cache) then if fs.symlink_metadata(cache) then
return 1 return 1
@ -40,4 +40,4 @@ function Pdf:preload()
return fs.write(cache, output.stdout) and 1 or 2 return fs.write(cache, output.stdout) and 1 or 2
end end
return Pdf return M

View file

@ -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 if self:preload() == 1 then
ya.image_show(self:cache(), self.area) ya.image_show(self:cache(), self.area)
ya.preview_widgets(self, {}) ya.preview_widgets(self, {})
end end
end end
function Video:seek(units) function M:seek(units)
local h = cx.active.current.hovered local h = cx.active.current.hovered
if h and h.url == self.file.url then if h and h.url == self.file.url then
ya.manager_emit("peek", { ya.manager_emit("peek", {
@ -19,7 +19,7 @@ function Video:seek(units)
end end
end end
function Video:preload() function M:preload()
local percentage = 5 + self.skip local percentage = 5 + self.skip
if percentage > 95 then if percentage > 95 then
ya.manager_emit("peek", { "90", only_if = tostring(self.file.url), upper_bound = "" }) 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 return status:success() and 1 or 2
end end
return Video return M

View file

@ -1,5 +1,5 @@
use mlua::{Lua, LuaSerdeExt, SerializeOptions}; use mlua::{Lua, LuaSerdeExt, SerializeOptions};
use yazi_config::{MANAGER, PREVIEW, THEME}; use yazi_config::{BOOT, MANAGER, PREVIEW, THEME};
const OPTIONS: SerializeOptions = const OPTIONS: SerializeOptions =
SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false);
@ -11,6 +11,16 @@ pub struct Config<'a> {
impl<'a> Config<'a> { impl<'a> Config<'a> {
pub fn new(lua: &'a Lua) -> Self { Self { lua } } 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> { pub fn install_theme(self) -> mlua::Result<Self> {
self.lua.globals().set("THEME", self.lua.to_value_with(&*THEME, OPTIONS)?)?; self.lua.globals().set("THEME", self.lua.to_value_with(&*THEME, OPTIONS)?)?;
Ok(self) Ok(self)
@ -20,9 +30,4 @@ impl<'a> Config<'a> {
self.lua.globals().set("PREVIEW", self.lua.to_value_with(&*PREVIEW, OPTIONS)?)?; self.lua.globals().set("PREVIEW", self.lua.to_value_with(&*PREVIEW, OPTIONS)?)?;
Ok(self) Ok(self)
} }
pub fn install_manager(self) -> mlua::Result<Self> {
self.lua.globals().set("MANAGER", self.lua.to_value_with(&*MANAGER, OPTIONS)?)?;
Ok(self)
}
} }

View file

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

View file

@ -1,5 +1,4 @@
mod fd; mod fd;
mod file;
mod fzf; mod fzf;
mod highlighter; mod highlighter;
mod lsar; mod lsar;
@ -8,7 +7,6 @@ mod shell;
mod zoxide; mod zoxide;
pub use fd::*; pub use fd::*;
pub use file::*;
pub use fzf::*; pub use fzf::*;
pub use highlighter::*; pub use highlighter::*;
pub use lsar::*; pub use lsar::*;

View file

@ -22,8 +22,8 @@ impl Loader {
return Ok(()); return Ok(());
} }
let b = fs::read(BOOT.plugin_dir.join(name)).await.map(|v| v.into()).unwrap_or(Cow::from( let b = fs::read(BOOT.plugin_dir.join(name)).await.map(|v| v.into()).or_else(|_| {
match name { Ok(Cow::from(match name {
"noop.lua" => include_bytes!("../preset/plugins/noop.lua") as &[u8], "noop.lua" => include_bytes!("../preset/plugins/noop.lua") as &[u8],
"archive.lua" => include_bytes!("../preset/plugins/archive.lua"), "archive.lua" => include_bytes!("../preset/plugins/archive.lua"),
"code.lua" => include_bytes!("../preset/plugins/code.lua"), "code.lua" => include_bytes!("../preset/plugins/code.lua"),
@ -33,9 +33,9 @@ impl Loader {
"mime.lua" => include_bytes!("../preset/plugins/mime.lua"), "mime.lua" => include_bytes!("../preset/plugins/mime.lua"),
"pdf.lua" => include_bytes!("../preset/plugins/pdf.lua"), "pdf.lua" => include_bytes!("../preset/plugins/pdf.lua"),
"video.lua" => include_bytes!("../preset/plugins/video.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()); self.loaded.write().insert(name.to_owned(), b.into_owned());
Ok(()) Ok(())

View file

@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use mlua::{Lua, Table}; use mlua::Lua;
use yazi_config::BOOT;
use yazi_shared::RoCell; use yazi_shared::RoCell;
pub static LUA: RoCell<Lua> = RoCell::new(); pub static LUA: RoCell<Lua> = RoCell::new();
@ -7,7 +8,7 @@ pub static LUA: RoCell<Lua> = RoCell::new();
pub fn init() { pub fn init() {
fn stage_1(lua: &Lua) -> Result<()> { fn stage_1(lua: &Lua) -> Result<()> {
crate::Loader::init(); 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)?; crate::utils::install(lua)?;
// Base // Base
@ -29,14 +30,15 @@ pub fn init() {
} }
fn stage_2(lua: &Lua) { fn stage_2(lua: &Lua) {
let ya: Table = lua.globals().get("ya").unwrap(); let setup = br#"
ya.set("SYNC_ON", true).unwrap(); 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 if let Ok(b) = std::fs::read(BOOT.config_dir.join("init.lua")) {
// PLUGIN.preload.iter().for_each(|p| { lua.load(b).exec().unwrap();
// 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:?}")); });
} }
let lua = Lua::new(); let lua = Lua::new();

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use parking_lot::RwLock; use parking_lot::RwLock;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::error; 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; use crate::TaskOp;
@ -74,7 +74,7 @@ impl Preload {
} }
let parent = buf[0].0.parent_url().unwrap(); 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))?; self.sch.send(TaskOp::Adv(task.id, 1, 0))?;

View file

@ -2,7 +2,7 @@ use crossterm::event::KeyEvent;
use tokio::sync::{mpsc::UnboundedSender, oneshot}; use tokio::sync::{mpsc::UnboundedSender, oneshot};
use super::Exec; use super::Exec;
use crate::{fs::FilesOp, term::Term, Layer, RoCell}; use crate::{term::Term, Layer, RoCell};
static TX: RoCell<UnboundedSender<Event>> = RoCell::new(); static TX: RoCell<UnboundedSender<Event>> = RoCell::new();
@ -15,7 +15,6 @@ pub enum Event {
Call(Vec<Exec>, Layer), Call(Vec<Exec>, Layer),
// Manager // Manager
Files(FilesOp),
Pages(usize), Pages(usize),
} }
@ -44,9 +43,6 @@ macro_rules! emit {
$crate::event::Event::Call($exec, $layer).emit(); $crate::event::Event::Call($exec, $layer).emit();
}; };
(Files($op:expr)) => {
$crate::event::Event::Files($op).emit();
};
(Pages($page:expr)) => { (Pages($page:expr)) => {
$crate::event::Event::Pages($page).emit(); $crate::event::Event::Pages($page).emit();
}; };

View file

@ -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 anyhow::Result;
use tokio::fs; use tokio::fs;
@ -52,13 +52,6 @@ impl File {
let ck = if url.is_hidden() { ChaKind::HIDDEN } else { ChaKind::empty() }; let ck = if url.is_hidden() { ChaKind::HIDDEN } else { ChaKind::empty() };
Self { url, cha: Cha::default().with_kind(ck), link_to: None } 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 { impl File {

View 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 super::File;
use crate::{emit, fs::Url}; use crate::{emit, event::Exec, fs::Url, Layer};
pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0); pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)] #[derive(Debug)]
pub enum FilesOp { pub enum FilesOp {
Full(Url, Vec<File>), Full(Url, Vec<File>),
Part(Url, u64, Vec<File>), Part(Url, Vec<File>, u64),
Size(Url, BTreeMap<Url, u64>), Size(Url, BTreeMap<Url, u64>),
IOErr(Url), IOErr(Url),
Creating(Url, BTreeMap<Url, File>), Creating(Url, Vec<File>),
Deleting(Url, BTreeSet<Url>), Deleting(Url, Vec<Url>),
Replacing(Url, BTreeMap<Url, File>), Updating(Url, BTreeMap<Url, File>),
Upserting(Url, BTreeMap<Url, File>),
} }
impl FilesOp { impl FilesOp {
@ -28,14 +29,41 @@ impl FilesOp {
Self::Creating(url, _) => url, Self::Creating(url, _) => url,
Self::Deleting(url, _) => url, Self::Deleting(url, _) => url,
Self::Replacing(url, _) => url, Self::Updating(url, _) => url,
Self::Upserting(url, _) => url,
} }
} }
#[inline] #[inline]
pub fn emit(self) {
emit!(Call(Exec::call("update_files", vec![]).with_data(self).vec(), Layer::Manager));
}
pub fn prepare(url: &Url) -> u64 { pub fn prepare(url: &Url) -> u64 {
let ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); 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 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!(),
// }
}
} }