feat: support VFS for the file watcher (#3108)

This commit is contained in:
三咲雅 misaki masa 2025-08-25 21:40:38 +08:00 committed by GitHub
parent c27ef58116
commit 12172b7301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 770 additions and 493 deletions

23
Cargo.lock generated
View file

@ -3485,6 +3485,7 @@ dependencies = [
"yazi-proxy",
"yazi-shared",
"yazi-term",
"yazi-watcher",
"yazi-widgets",
]
@ -3617,10 +3618,8 @@ version = "25.6.11"
dependencies = [
"anyhow",
"crossterm 0.29.0",
"futures",
"hashbrown",
"indexmap",
"notify",
"parking_lot",
"ratatui",
"tokio",
@ -3638,6 +3637,7 @@ dependencies = [
"yazi-proxy",
"yazi-scheduler",
"yazi-shared",
"yazi-watcher",
"yazi-widgets",
]
@ -3714,6 +3714,7 @@ dependencies = [
"yazi-proxy",
"yazi-shared",
"yazi-term",
"yazi-watcher",
"yazi-widgets",
]
@ -3886,6 +3887,24 @@ dependencies = [
"yazi-shared",
]
[[package]]
name = "yazi-watcher"
version = "25.6.11"
dependencies = [
"anyhow",
"hashbrown",
"notify",
"parking_lot",
"tokio",
"tokio-stream",
"tracing",
"yazi-adapter",
"yazi-dds",
"yazi-fs",
"yazi-macro",
"yazi-shared",
]
[[package]]
name = "yazi-widgets"
version = "25.6.11"

View file

@ -1 +1 @@
{"version":"0.2","language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","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","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","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","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS"]}
{"flagWords":[],"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","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","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","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","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold"],"version":"0.2"}

View file

@ -25,6 +25,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" }
yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-term = { path = "../yazi-term", version = "25.6.11" }
yazi-watcher = { path = "../yazi-watcher", version = "25.6.11" }
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies

View file

@ -145,7 +145,7 @@ impl UserData for File {
return None;
}
let h = finder.filter.highlighted(me.url.file_name()?)?;
let h = finder.filter.highlighted(me.url.name()?)?;
Some(h.into_iter().map(Range::from).collect::<Vec<_>>())
})
});

View file

@ -46,7 +46,8 @@ impl UserData for Tab {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
cached_field!(fields, name, |lua, me| {
lua.create_string(me.current.url.name().as_encoded_bytes())
let url = &me.current.url;
lua.create_string(url.name().unwrap_or(url.loc.as_os_str()).as_encoded_bytes())
});
cached_field!(fields, mode, |_, me| Mode::make(&me.mode));

View file

@ -10,9 +10,10 @@ use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, provider::{self, local::{Gate, Local}}};
use yazi_macro::{err, succ};
use yazi_parser::VoidOpt;
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
use yazi_proxy::{AppProxy, HIDER, TasksProxy};
use yazi_shared::{OsStrJoin, event::Data, terminal_clear, url::{Component, UrlBuf}};
use yazi_term::tty::TTY;
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -119,7 +120,7 @@ impl BulkRename {
selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(),
);
if maybe_exists(&new).await && !provider::same(&old, &new).await.unwrap_or(false) {
if maybe_exists(&new).await && !provider::must_identical(&old, &new).await {
failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = provider::rename(&old, &new).await {
failed.push((o, n, e.into()));

View file

@ -84,7 +84,7 @@ impl Cd {
}
if let Some(p) = url.parent_url() {
FilesOp::Upserting(p.into(), [(url.urn_owned(), file)].into()).emit();
FilesOp::Upserting(p.into(), [(url.urn().to_owned(), file)].into()).emit();
}
MgrProxy::reveal(&url);
}

View file

@ -38,10 +38,10 @@ impl Actor for Copy {
}
}
"filename" => {
s.push(opt.separator.transform(u.name()));
s.push(opt.separator.transform(u.name().unwrap_or_default()));
}
"name_without_ext" => {
s.push(opt.separator.transform(u.file_stem().unwrap_or_default()));
s.push(opt.separator.transform(u.stem().unwrap_or_default()));
}
_ => bail!("Unknown copy type: {}", opt.r#type),
};

View file

@ -3,8 +3,9 @@ use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
use yazi_macro::succ;
use yazi_parser::mgr::CreateOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy};
use yazi_shared::{event::Data, url::{UrlBuf, UrnBuf}};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -57,7 +58,7 @@ impl Create {
}
if let Ok(f) = File::new(&new).await {
FilesOp::Upserting(parent.into(), [(f.urn_owned(), f)].into()).emit();
FilesOp::Upserting(parent.into(), [(f.urn().to_owned(), f)].into()).emit();
MgrProxy::reveal(&new)
}
Ok(())

View file

@ -16,7 +16,7 @@ impl Actor for FilterDo {
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) };
let hovered = cx.hovered().map(|f| f.urn_owned());
let hovered = cx.hovered().map(|f| f.urn().to_owned());
cx.current_mut().files.set_filter(filter);
if cx.hovered().map(|f| f.urn()) != hovered.as_deref() {

View file

@ -18,7 +18,7 @@ impl Actor for Hidden {
let state = opt.state.bool(cx.tab().pref.show_hidden);
cx.tab_mut().pref.show_hidden = state;
let hovered = cx.hovered().map(|f| f.urn_owned());
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading {
render!();

View file

@ -1,10 +1,11 @@
use anyhow::Result;
use crossterm::{execute, terminal::SetTitle};
use yazi_config::YAZI;
use yazi_fs::CWD;
use yazi_core::tab::Folder;
use yazi_fs::{CWD, Files, FilesOp, cha::Cha};
use yazi_macro::{act, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use yazi_shared::{event::Data, url::UrlBuf};
use yazi_term::tty::TTY;
use crate::{Actor, Ctx};
@ -22,9 +23,9 @@ impl Actor for Refresh {
}
if let Some(p) = cx.parent() {
cx.mgr.watcher.trigger_dirs(&[cx.current(), p]);
Self::trigger_dirs(&[cx.current(), p]);
} else {
cx.mgr.watcher.trigger_dirs(&[cx.current()]);
Self::trigger_dirs(&[cx.current()]);
}
act!(mgr:peek, cx)?;
@ -35,3 +36,27 @@ impl Actor for Refresh {
succ!();
}
}
impl Refresh {
// TODO: performance improvement
pub fn trigger_dirs(folders: &[&Folder]) {
async fn go(cwd: UrlBuf, cha: Cha) {
let Some(cha) = Files::assert_stale(&cwd, cha).await else { return };
match Files::from_dir_bulk(&cwd).await {
Ok(files) => FilesOp::Full(cwd, files, cha).emit(),
Err(e) => FilesOp::issue_error(&cwd, e.kind()).await,
}
}
let futs: Vec<_> = folders
.iter()
.filter(|&f| f.url.is_internal())
.map(|&f| go(f.url.to_owned(), f.cha))
.collect();
if !futs.is_empty() {
tokio::spawn(futures::future::join_all(futs));
}
}
}

View file

@ -4,8 +4,9 @@ use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
use yazi_macro::{act, err, succ};
use yazi_parser::mgr::RenameOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy};
use yazi_shared::{Id, event::Data, url::{UrlBuf, UrnBuf}};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
@ -47,9 +48,8 @@ impl Actor for Rename {
return;
}
let new = UrlBuf::from(old.parent().unwrap().join(name));
if opt.force || !maybe_exists(&new).await || provider::same(&old, &new).await.unwrap_or(false)
{
let new = old.parent_url().unwrap().join(name);
if opt.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await {
Self::r#do(tab, old, new).await.ok();
} else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await {
Self::r#do(tab, old, new).await.ok();
@ -91,12 +91,12 @@ impl Rename {
return String::new();
}
let ext = url.extension();
let ext = url.ext();
match by {
"stem" => ext.map_or_else(String::new, |s| format!(".{}", s.to_string_lossy())),
"ext" if ext.is_some() => format!("{}.", url.file_stem().unwrap().to_string_lossy()),
"dot_ext" if ext.is_some() => url.file_stem().unwrap().to_string_lossy().into_owned(),
_ => url.name().to_string_lossy().into_owned(),
"ext" if ext.is_some() => format!("{}.", url.stem().unwrap().to_string_lossy()),
"dot_ext" if ext.is_some() => url.stem().unwrap().to_string_lossy().into_owned(),
_ => url.name().unwrap_or_default().to_string_lossy().into_owned(),
}
}
}

View file

@ -23,7 +23,7 @@ impl Actor for Sort {
pref.sort_translit = opt.translit.unwrap_or(pref.sort_translit);
let sorter = FilesSorter::from(&*pref);
let hovered = cx.hovered().map(|f| f.urn_owned());
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading {
render!();

View file

@ -1,9 +1,10 @@
use anyhow::Result;
use yazi_core::{mgr::LINKED, tab::Folder};
use yazi_core::tab::Folder;
use yazi_fs::FilesOp;
use yazi_macro::{act, render, succ};
use yazi_parser::mgr::UpdateFilesOpt;
use yazi_shared::event::Data;
use yazi_watcher::LINKED;
use crate::{Actor, Ctx};

View file

@ -1,9 +1,9 @@
use anyhow::Result;
use hashbrown::HashMap;
use yazi_core::mgr::LINKED;
use yazi_macro::{act, render, succ};
use yazi_parser::mgr::UpdateMimesOpt;
use yazi_shared::{event::Data, pool::InternStr, url::UrlCov};
use yazi_watcher::LINKED;
use crate::{Actor, Ctx};

View file

@ -14,11 +14,14 @@ pub enum Error {
impl Error {
pub fn install(lua: &Lua) -> mlua::Result<()> {
let new = lua.create_function(|_, msg: String| Ok(Error::Custom(msg.into())))?;
let new = lua.create_function(|_, msg: String| Ok(Error::custom(msg)))?;
lua.globals().raw_set("Error", lua.create_table_from([("custom", new)])?)
}
#[inline]
pub fn custom(msg: impl Into<SStr>) -> Self { Self::Custom(msg.into()) }
pub fn into_string(self) -> SStr {
match self {
Error::Io(e) => Cow::Owned(e.to_string()),

View file

@ -154,10 +154,7 @@ macro_rules! impl_file_fields {
$crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to.clone().map($crate::Url::new)));
$crate::cached_field!($fields, name, |lua, me| {
Some(me.name())
.filter(|s| !s.is_empty())
.map(|s| lua.create_string(s.as_encoded_bytes()))
.transpose()
me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
});
};
}

View file

@ -94,16 +94,16 @@ impl FromLua for Url {
impl UserData for Url {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, name, |lua, me| {
me.file_name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
});
cached_field!(fields, stem, |lua, me| {
me.file_stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
me.stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
});
cached_field!(fields, ext, |lua, me| {
me.extension().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
me.ext().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
});
cached_field!(fields, parent, |_, me| Ok(me.parent_url().map(Self::new)));
cached_field!(fields, urn, |_, me| Ok(Urn::new(me.urn_owned())));
cached_field!(fields, urn, |_, me| Ok(Urn::new(me.urn())));
cached_field!(fields, base, |_, me| Ok(me.base().map(Self::new)));
cached_field!(fields, domain, |lua, me| {
me.scheme.domain().map(|s| lua.create_string(s)).transpose()

View file

@ -17,7 +17,7 @@ impl From<Urn> for yazi_shared::url::UrnBuf {
}
impl Urn {
pub fn new(urn: yazi_shared::url::UrnBuf) -> Self { Self { inner: urn } }
pub fn new(urn: impl Into<yazi_shared::url::UrnBuf>) -> Self { Self { inner: urn.into() } }
}
impl FromLua for Urn {

View file

@ -76,7 +76,7 @@ impl Icon {
#[inline]
fn match_by_name(&self, file: &File) -> Option<&I> {
let name = file.name().to_str()?;
let name = file.name()?.to_str()?;
if file.is_dir() {
self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase()))
} else {
@ -90,7 +90,7 @@ impl Icon {
#[inline]
fn match_by_ext(&self, file: &File) -> Option<&I> {
let ext = file.url.extension()?.to_str()?;
let ext = file.url.ext()?.to_str()?;
self.exts.get(ext).or_else(|| self.exts.get(&ext.to_ascii_lowercase()))
}
}

View file

@ -19,15 +19,14 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" }
yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-scheduler = { path = "../yazi-scheduler", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-watcher = { path = "../yazi-watcher", version = "25.6.11" }
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] }
parking_lot = { workspace = true }
ratatui = { workspace = true }
tokio = { workspace = true }

View file

@ -9,8 +9,3 @@
yazi_macro::mod_pub!(cmp confirm help input mgr notify pick spot tab tasks which);
yazi_macro::mod_flat!(core);
pub fn init() {
mgr::WATCHED.with(<_>::default);
mgr::LINKED.with(<_>::default);
}

View file

@ -1,44 +0,0 @@
use std::{iter, ops::{Deref, DerefMut}};
use hashbrown::HashMap;
use yazi_shared::url::{Url, UrlBuf};
#[derive(Default)]
pub struct Linked(HashMap<UrlBuf, UrlBuf> /* from ==> to */);
impl Deref for Linked {
type Target = HashMap<UrlBuf, UrlBuf>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl DerefMut for Linked {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl Linked {
pub fn from_dir<'a, 'b, T>(&'a self, url: T) -> Box<dyn Iterator<Item = &'a UrlBuf> + 'b>
where
'a: 'b,
T: Into<Url<'b>>,
{
let url = url.into();
if url.scheme.is_virtual() {
Box::new(iter::empty())
} else if let Some(to) = self.get(&url) {
Box::new(self.iter().filter(move |(k, v)| *v == to && *k != url).map(|(k, _)| k))
} else {
Box::new(self.iter().filter(move |(_, v)| *v == url).map(|(k, _)| k))
}
}
pub fn from_file(&self, url: Url) -> Vec<UrlBuf> {
if url.scheme.is_virtual() {
vec![]
} else if let Some((parent, urn)) = url.pair() {
self.from_dir(parent).map(|u| u.join(&urn)).collect()
} else {
vec![]
}
}
}

View file

@ -2,8 +2,9 @@ use ratatui::layout::Rect;
use yazi_adapter::Dimension;
use yazi_config::popup::{Origin, Position};
use yazi_shared::url::UrlBuf;
use yazi_watcher::Watcher;
use super::{Mimetype, Tabs, Watcher, Yanked};
use super::{Mimetype, Tabs, Yanked};
use crate::tab::{Folder, Tab};
pub struct Mgr {

View file

@ -1 +1 @@
yazi_macro::mod_flat!(linked mgr mimetype tabs watcher yanked);
yazi_macro::mod_flat!(mgr mimetype tabs yanked);

View file

@ -1,216 +0,0 @@
use std::time::Duration;
use anyhow::Result;
use hashbrown::{HashMap, HashSet};
use notify::{PollWatcher, RecommendedWatcher, RecursiveMode, Watcher as _Watcher};
use parking_lot::RwLock;
use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use tracing::error;
use yazi_fs::{File, Files, FilesOp, cha::Cha, provider, realname_unchecked};
use yazi_proxy::WATCHER;
use yazi_shared::{RoCell, url::UrlBuf};
use super::Linked;
use crate::tab::Folder;
pub(crate) static WATCHED: RoCell<RwLock<HashSet<UrlBuf>>> = RoCell::new();
pub static LINKED: RoCell<RwLock<Linked>> = RoCell::new();
pub struct Watcher {
in_tx: watch::Sender<HashSet<UrlBuf>>,
out_tx: mpsc::UnboundedSender<UrlBuf>,
}
// FIXME: VFS
impl Watcher {
pub(super) fn serve() -> Self {
let (in_tx, in_rx) = watch::channel(Default::default());
let (out_tx, out_rx) = mpsc::unbounded_channel();
let out_tx_ = out_tx.clone();
let handler = move |res: Result<notify::Event, notify::Error>| {
let Ok(event) = res else { return };
if event.kind.is_access() {
return;
}
Self::push_files_impl(&out_tx_, event.paths.into_iter().map(UrlBuf::from));
};
let config = notify::Config::default().with_poll_interval(Duration::from_millis(500));
if yazi_adapter::WSL.get() || cfg!(target_os = "netbsd") {
tokio::spawn(Self::fan_in(in_rx, PollWatcher::new(handler, config).unwrap()));
} else {
tokio::spawn(Self::fan_in(in_rx, RecommendedWatcher::new(handler, config).unwrap()));
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
yazi_fs::mounts::Partitions::monitor(yazi_fs::mounts::PARTITIONS.clone(), || {
yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount())
});
tokio::spawn(Self::fan_out(out_rx));
Self { in_tx, out_tx }
}
pub fn watch<'a>(&mut self, it: impl Iterator<Item = &'a UrlBuf>) {
self.in_tx.send(it.filter(|u| u.is_regular()).cloned().collect()).ok();
}
pub fn push_files(&self, urls: Vec<UrlBuf>) {
Self::push_files_impl(&self.out_tx, urls.into_iter());
}
fn push_files_impl(out_tx: &mpsc::UnboundedSender<UrlBuf>, urls: impl Iterator<Item = UrlBuf>) {
let (mut parents, watched) = (HashSet::new(), WATCHED.read());
for u in urls {
let Some(p) = u.parent_url() else { continue };
if !watched.contains(&p) && !LINKED.read().from_dir(&p).any(|u| watched.contains(u)) {
continue;
}
if parents.contains(&p) {
out_tx.send(u).ok();
} else {
let p = p.to_owned();
out_tx.send(u).ok();
out_tx.send(p.to_owned()).ok();
parents.insert(p);
}
}
}
// TODO: performance improvement
pub fn trigger_dirs(&self, folders: &[&Folder]) {
async fn go(cwd: UrlBuf, cha: Cha) {
let Some(cha) = Files::assert_stale(&cwd, cha).await else { return };
match Files::from_dir_bulk(&cwd).await {
Ok(files) => FilesOp::Full(cwd, files, cha).emit(),
Err(e) => FilesOp::issue_error(&cwd, e.kind()).await,
}
}
let futs: Vec<_> = folders
.iter()
.filter(|&f| f.url.is_internal())
.map(|&f| go(f.url.to_owned(), f.cha))
.collect();
if !futs.is_empty() {
tokio::spawn(futures::future::join_all(futs));
}
}
async fn fan_in(
mut rx: watch::Receiver<HashSet<UrlBuf>>,
mut watcher: impl notify::Watcher + Send + 'static,
) {
loop {
let (to_unwatch, to_watch): (HashSet<_>, HashSet<_>) = {
let (new, old) = (&*rx.borrow_and_update(), &*WATCHED.read());
(old.difference(new).cloned().collect(), new.difference(old).cloned().collect())
};
watcher = Self::sync_watched(watcher, to_unwatch, to_watch).await;
if !rx.has_changed().unwrap_or(false) {
Self::sync_linked().await;
}
if rx.changed().await.is_err() {
break;
}
}
}
async fn fan_out(rx: UnboundedReceiver<UrlBuf>) {
// TODO: revert this once a new notification is implemented
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));
pin!(rx);
while let Some(chunk) = rx.next().await {
let urls: HashSet<_> = chunk.into_iter().collect();
let mut cached: HashMap<_, _> = HashMap::new();
let _permit = WATCHER.acquire().await.unwrap();
let mut ops = Vec::with_capacity(urls.len());
for u in urls {
let Some((parent, urn)) = u.pair() else { continue };
let Ok(file) = File::new(&u).await else {
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
continue;
};
let u = &file.url;
let eq = (!file.is_link() && provider::canonicalize(u).await.is_ok_and(|c| c == *u))
|| realname_unchecked(u, &mut cached).await.is_ok_and(|s| urn.as_urn() == s);
if !eq {
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
continue;
}
ops.push(FilesOp::Upserting(parent.into(), [(urn, file)].into()));
}
FilesOp::mutate(ops);
}
}
async fn sync_watched<W>(
mut watcher: W,
to_unwatch: HashSet<UrlBuf>,
to_watch: HashSet<UrlBuf>,
) -> W
where
W: notify::Watcher + Send + 'static,
{
use notify::ErrorKind::WatchNotFound;
if to_unwatch.is_empty() && to_watch.is_empty() {
return watcher;
}
tokio::task::spawn_blocking(move || {
for u in to_unwatch {
match watcher.unwatch(&u) {
Ok(()) => _ = WATCHED.write().remove(&u),
Err(e) if matches!(e.kind, WatchNotFound) => _ = WATCHED.write().remove(&u),
Err(e) => error!("Unwatch failed: {e:?}"),
}
}
for u in to_watch {
if watcher.watch(&u, RecursiveMode::NonRecursive).is_ok() {
WATCHED.write().insert(u);
}
}
watcher
})
.await
.unwrap()
}
async fn sync_linked() {
let mut new = WATCHED.read().clone();
let old = {
let mut linked = LINKED.write();
linked.retain(|k, _| new.remove(k));
linked.keys().cloned().collect()
};
async fn go(todo: HashSet<UrlBuf>) {
for from in todo {
let Ok(to) = provider::canonicalize(&from).await else { continue };
if to != from && WATCHED.read().contains(&from) {
LINKED.write().insert(from, to);
}
}
}
go(new).await;
go(old).await;
}
}

View file

@ -29,7 +29,9 @@ impl Finder {
pub fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
for i in !include as usize..files.len() {
let idx = (cursor + files.len() - i) % files.len();
if self.filter.matches(files[idx].name()) {
if let Some(s) = files[idx].name()
&& self.filter.matches(s)
{
return Some(idx as isize - cursor as isize);
}
}
@ -39,7 +41,9 @@ impl Finder {
pub fn next(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
for i in !include as usize..files.len() {
let idx = (cursor + i) % files.len();
if self.filter.matches(files[idx].name()) {
if let Some(s) = files[idx].name()
&& self.filter.matches(s)
{
return Some(idx as isize - cursor as isize);
}
}
@ -54,11 +58,11 @@ impl Finder {
let mut i = 0u8;
for file in folder.files.iter() {
if !self.filter.matches(file.name()) {
if file.name().is_none_or(|s| !self.filter.matches(s)) {
continue;
}
self.matched.insert(file.urn_owned(), i);
self.matched.insert(file.urn().to_owned(), i);
if self.matched.len() > 99 {
break;
}

View file

@ -103,7 +103,7 @@ impl Folder {
self.scroll(step)
};
self.trace = self.hovered().filter(|_| b).map(|h| h.urn_owned()).or(self.trace.take());
self.trace = self.hovered().filter(|_| b).map(|h| h.urn().to_owned()).or(self.trace.take());
b |= self.squeeze_offset();
self.sync_page(false);

View file

@ -8,7 +8,7 @@ use crate::mgr::Yanked;
impl Tasks {
pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) {
for u in src.iter() {
let to = dest.join(u.file_name().unwrap());
let to = dest.join(u.name().unwrap());
if force && *u == to {
debug!("file_cut: same file, skipping {:?}", to);
} else {
@ -19,7 +19,7 @@ impl Tasks {
pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) {
for u in src.iter() {
let to = dest.join(u.file_name().unwrap());
let to = dest.join(u.name().unwrap());
if force && *u == to {
debug!("file_copy: same file, skipping {:?}", to);
} else {
@ -30,7 +30,7 @@ impl Tasks {
pub fn file_link(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, relative: bool, force: bool) {
for u in src {
let to = dest.join(u.file_name().unwrap());
let to = dest.join(u.name().unwrap());
if force && *u == to {
debug!("file_link: same file, skipping {:?}", to);
} else {
@ -41,7 +41,7 @@ impl Tasks {
pub fn file_hardlink(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, force: bool, follow: bool) {
for u in src {
let to = dest.join(u.file_name().unwrap());
let to = dest.join(u.name().unwrap());
if force && *u == to {
debug!("file_hardlink: same file, skipping {:?}", to);
} else {

View file

@ -37,6 +37,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" }
yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-term = { path = "../yazi-term", version = "25.6.11" }
yazi-watcher = { path = "../yazi-watcher", version = "25.6.11" }
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies

View file

@ -32,9 +32,9 @@ async fn main() -> anyhow::Result<()> {
yazi_widgets::init();
yazi_plugin::init()?;
yazi_watcher::init();
yazi_core::init();
yazi_plugin::init()?;
yazi_dds::serve();
app::App::serve().await

View file

@ -1,7 +1,7 @@
use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::PathBuf};
use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use anyhow::Result;
use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn, UrnBuf};
use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn};
use crate::{cha::Cha, provider};
@ -47,7 +47,7 @@ impl File {
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
#[inline]
pub fn chdir(&self, wd: &UrlBuf) -> Self {
pub fn chdir(&self, wd: &Path) -> Self {
Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() }
}
}
@ -64,13 +64,10 @@ impl File {
pub fn urn(&self) -> &Urn { self.url.urn() }
#[inline]
pub fn urn_owned(&self) -> UrnBuf { self.url.urn_owned() }
pub fn name(&self) -> Option<&OsStr> { self.url.name() }
#[inline]
pub fn name(&self) -> &OsStr { self.url.name() }
#[inline]
pub fn stem(&self) -> Option<&OsStr> { self.url.file_stem() }
pub fn stem(&self) -> Option<&OsStr> { self.url.stem() }
}
impl Hash for File {

View file

@ -162,7 +162,7 @@ impl Files {
macro_rules! go {
($dist:expr, $src:expr, $inc:literal) => {
let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn_owned(), f)).collect();
let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn().to_owned(), f)).collect();
for f in &$dist {
if todo.remove(f.urn()).is_some() && todo.is_empty() {
break;
@ -293,7 +293,7 @@ impl Files {
}
self.update_deleting(
files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn_owned()).collect(),
files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn().to_owned()).collect(),
);
let (hidden, items) = self.update_updating(files);

View file

@ -7,7 +7,7 @@ use hashbrown::{HashMap, HashSet};
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
use yazi_shared::url::{Component, Url, UrlBuf};
use crate::{cha::Cha, provider};
use crate::{cha::Cha, provider::{self, local::Local}};
#[inline]
pub async fn maybe_exists<'a>(url: impl Into<Url<'a>>) -> bool {
@ -31,13 +31,13 @@ pub fn ok_or_not_found<T: Default>(result: io::Result<T>) -> io::Result<T> {
}
}
pub async fn realname(u: &UrlBuf) -> Option<OsString> {
let name = u.file_name()?;
if *u == provider::canonicalize(u).await.ok()? {
pub async fn realname(url: &UrlBuf) -> Option<OsString> {
let (path, name) = (url.as_path()?, url.name()?);
if path == Local::canonicalize(path).await.ok()? {
return None;
}
realname_unchecked(u, &mut HashMap::new())
realname_unchecked(path, &mut HashMap::new())
.await
.ok()
.filter(|s| s != name)

View file

@ -1,3 +1,5 @@
use std::path::Path;
use hashbrown::{HashMap, HashSet};
use yazi_macro::relay;
use yazi_shared::{Id, Ids, url::{UrlBuf, UrnBuf}};
@ -55,9 +57,9 @@ impl FilesOp {
let Some(o_p) = o.parent_url() else { continue };
let Some(n_p) = n.url.parent_url() else { continue };
if o_p != n_p {
parents.entry_ref(&o_p).or_default().0.insert(o.urn_owned());
parents.entry_ref(&o_p).or_default().0.insert(o.urn().to_owned());
}
parents.entry_ref(&n_p).or_default().1.insert(n.urn_owned(), n);
parents.entry_ref(&n_p).or_default().1.insert(n.urn().to_owned(), n);
}
for (p, (o, n)) in parents {
match (o.is_empty(), n.is_empty()) {
@ -94,7 +96,7 @@ impl FilesOp {
}
}
pub fn chdir(&self, wd: &UrlBuf) -> Self {
pub fn chdir(&self, wd: &Path) -> Self {
macro_rules! files {
($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect() }};
}
@ -102,7 +104,7 @@ impl FilesOp {
($map:expr) => {{ $map.iter().map(|(urn, file)| (urn.clone(), file.chdir(wd))).collect() }};
}
let w = wd.clone();
let w = UrlBuf::from(wd);
match self {
Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha),
Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket),

View file

@ -30,9 +30,9 @@ fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> {
.expect("Failed to create Loc from expanded path");
let url = UrlBuf { loc, scheme: url.scheme.clone() };
match absolute_url(&url) {
Cow::Borrowed(_) => url.into(),
Cow::Owned(u) => u.into(),
match absolute_url(url.as_url()) {
UrlCow::Borrowed(_) => url.into(),
UrlCow::Owned(u) => u.into(),
}
}
@ -62,7 +62,7 @@ fn expand_variables(p: &Path) -> Cow<'_, Path> {
}
}
fn absolute_url(url: &UrlBuf) -> Cow<'_, UrlBuf> {
fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> {
let b = url.loc.as_os_str().as_encoded_bytes();
let local = !url.scheme.is_virtual();
@ -89,7 +89,7 @@ fn absolute_url(url: &UrlBuf) -> Cow<'_, UrlBuf> {
UrlBuf { loc, scheme: url.scheme.clone() }.into()
} else if !url.is_absolute() {
let cwd = CWD.load();
let loc = LocBuf::with(cwd.loc.join(&url.loc), url.uri().count(), url.urn().count())
let loc = LocBuf::with(cwd.loc.join(url.loc), url.uri().count(), url.urn().count())
.expect("Failed to create Loc from relative path");
UrlBuf { loc, scheme: cwd.scheme.clone() }.into()
} else {

View file

@ -26,11 +26,11 @@ where
}
async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result<UrlBuf> {
let Some(stem) = url.file_stem().map(|s| s.to_owned()) else {
let Some(stem) = url.stem().map(|s| s.to_owned()) else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem"));
};
let dot_ext = url.extension().map_or_else(OsString::new, |e| {
let dot_ext = url.ext().map_or_else(OsString::new, |e| {
let mut s = OsString::with_capacity(e.len() + 1);
s.push(".");
s.push(e);

View file

@ -0,0 +1,91 @@
use std::{io, path::{Path, PathBuf}};
pub async fn valid_name_case(path: impl AsRef<Path>) -> io::Result<bool> {
let path = path.as_ref().to_owned();
tokio::task::spawn_blocking(move || valid_name_case_impl(path)).await?
}
#[cfg(any(
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
target_os = "freebsd"
))]
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
use std::{ffi::{CStr, CString, OsStr}, os::{fd::{AsRawFd, FromRawFd, OwnedFd}, unix::ffi::OsStrExt}};
use libc::{F_GETPATH, O_RDONLY, O_SYMLINK, PATH_MAX};
let cstr = CString::new(path.into_os_string().into_encoded_bytes())?;
let Some(name) = Path::new(OsStr::from_bytes(cstr.as_bytes())).file_name() else {
return Ok(true);
};
let fd = match unsafe { libc::open(cstr.as_ptr(), O_RDONLY | O_SYMLINK) } {
ret if ret < 0 => return Err(io::Error::last_os_error()),
ret => unsafe { OwnedFd::from_raw_fd(ret) },
};
let mut buf = [0u8; PATH_MAX as usize];
if unsafe { libc::fcntl(fd.as_raw_fd(), F_GETPATH, buf.as_mut_ptr()) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(
unsafe { CStr::from_ptr(buf.as_ptr() as *const i8) }
.to_bytes()
.ends_with(name.as_encoded_bytes()),
)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
use std::{ffi::{CString, OsStr}, fs::File, os::{fd::{AsRawFd, FromRawFd}, unix::ffi::OsStrExt}};
use libc::{O_NOFOLLOW, O_PATH};
let cstr = CString::new(path.into_os_string().into_encoded_bytes())?;
let path = Path::new(OsStr::from_bytes(cstr.as_bytes()));
let Some(name) = path.file_name() else { return Ok(true) };
let file = match unsafe { libc::open(cstr.as_ptr(), O_PATH | O_NOFOLLOW) } {
ret if ret < 0 => return Err(io::Error::last_os_error()),
ret => unsafe { File::from_raw_fd(ret) },
};
Ok(if file.metadata()?.is_symlink() {
std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))?.starts_with(path)
} else {
std::fs::canonicalize(path)?.file_name() == Some(name)
})
}
#[cfg(target_os = "windows")]
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
use std::{ffi::OsString, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}};
use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}};
let Some(name) = path.file_name() else { return Ok(true) };
let file = std::fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(&path)?;
let mut buf = [0u16; MAX_PATH as usize];
let len = unsafe {
GetFinalPathNameByHandleW(
file.as_raw_handle() as HANDLE,
buf.as_mut_ptr(),
buf.len() as u32,
VOLUME_NAME_DOS,
)
};
if len == 0 {
Err(io::Error::last_os_error())
} else {
Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize])).file_name() == Some(name))
}
}

View file

@ -0,0 +1,55 @@
use std::{io, path::{Path, PathBuf}};
#[inline]
pub async fn identical<P, Q>(a: P, b: Q) -> io::Result<bool>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let (a, b) = (a.as_ref().to_owned(), b.as_ref().to_owned());
tokio::task::spawn_blocking(move || identical_impl(a, b)).await?
}
#[cfg(unix)]
fn identical_impl(a: PathBuf, b: PathBuf) -> io::Result<bool> {
use std::os::unix::fs::MetadataExt;
let (a_, b_) = (std::fs::symlink_metadata(&a)?, std::fs::symlink_metadata(&b)?);
Ok(
a_.ino() == b_.ino()
&& a_.dev() == b_.dev()
&& std::fs::canonicalize(a)? == std::fs::canonicalize(b)?,
)
}
#[cfg(windows)]
fn identical_impl(a: PathBuf, b: PathBuf) -> io::Result<bool> {
use std::{fs::OpenOptions, mem, os::windows::{fs::OpenOptionsExt, io::AsRawHandle}};
use windows_sys::Win32::{Foundation::HANDLE, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_ID_INFO, FileIdInfo, GetFileInformationByHandleEx}};
fn file_id(path: PathBuf) -> io::Result<(u64, u128)> {
let file = OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let mut info: FILE_ID_INFO = unsafe { mem::zeroed() };
let ret = unsafe {
GetFileInformationByHandleEx(
file.as_raw_handle() as HANDLE,
FileIdInfo,
&mut info as *mut FILE_ID_INFO as _,
mem::size_of::<FILE_ID_INFO>() as u32,
)
};
if ret == 0 {
Err(io::Error::last_os_error())
} else {
Ok((info.VolumeSerialNumber, u128::from_le_bytes(info.FileId.Identifier)))
}
}
Ok(file_id(a)? == file_id(b)?)
}

View file

@ -202,63 +202,6 @@ impl Local {
tokio::fs::rename(from, to).await
}
#[inline]
pub async fn same<P, Q>(a: P, b: Q) -> io::Result<bool>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
Self::same_impl(a.as_ref(), b.as_ref()).await
}
#[cfg(unix)]
async fn same_impl(a: &Path, b: &Path) -> io::Result<bool> {
use std::os::unix::fs::MetadataExt;
let (a_, b_) = (tokio::fs::symlink_metadata(a).await?, tokio::fs::symlink_metadata(b).await?);
Ok(
a_.ino() == b_.ino()
&& a_.dev() == b_.dev()
&& tokio::fs::canonicalize(a).await? == tokio::fs::canonicalize(b).await?,
)
}
#[cfg(windows)]
async fn same_impl(a: &Path, b: &Path) -> io::Result<bool> {
use std::{ffi::OsString, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}};
use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}};
async fn final_name(path: &Path) -> io::Result<PathBuf> {
let path = path.to_owned();
tokio::task::spawn_blocking(move || {
let file = std::fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let mut buf = [0u16; MAX_PATH as usize];
let len = unsafe {
GetFinalPathNameByHandleW(
file.as_raw_handle() as HANDLE,
buf.as_mut_ptr(),
buf.len() as u32,
VOLUME_NAME_DOS,
)
};
if len == 0 {
Err(io::Error::last_os_error())
} else {
Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize])))
}
})
.await?
}
Ok(final_name(a).await? == final_name(b).await?)
}
#[inline]
pub async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()>
where

View file

@ -1 +1 @@
yazi_macro::mod_flat!(dir_entry gate local read_dir rw_file);
yazi_macro::mod_flat!(casefold dir_entry gate identical local read_dir rw_file);

View file

@ -2,7 +2,7 @@ use std::{io, path::{Path, PathBuf}};
use yazi_shared::url::{Url, UrlBuf};
use crate::{cha::Cha, provider::{ReadDir, ReadDirSync, RwFile, local::Local}};
use crate::{cha::Cha, provider::{ReadDir, ReadDirSync, RwFile, local::{self, Local}}};
#[inline]
pub fn cache<'a, U>(url: U) -> Option<PathBuf>
@ -86,6 +86,19 @@ where
}
}
#[inline]
pub async fn identical<'a, U, V>(a: U, b: V) -> io::Result<bool>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(a), Some(b)) = (a.into().as_path(), b.into().as_path()) {
local::identical(a, b).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn metadata<'a, U>(url: U) -> io::Result<std::fs::Metadata>
where
@ -98,6 +111,15 @@ where
}
}
#[inline]
pub async fn must_identical<'a, U, V>(a: U, b: V) -> bool
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
identical(a, b).await.unwrap_or(false)
}
#[inline]
pub async fn open<'a, U>(url: U) -> io::Result<RwFile>
where
@ -195,19 +217,6 @@ where
}
}
#[inline]
pub async fn same<'a, U, V>(a: U, b: V) -> io::Result<bool>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(a), Some(b)) = (a.into().as_path(), b.into().as_path()) {
Local::same(a, b).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()>
where

View file

@ -40,11 +40,11 @@ impl FilesSorter {
}),
SortBy::Extension => items.sort_unstable_by(|a, b| {
let ord = if self.sensitive {
self.cmp(a.url.extension(), b.url.extension(), self.promote(a, b))
self.cmp(a.url.ext(), b.url.ext(), self.promote(a, b))
} else {
self.cmp_insensitive(
a.url.extension().map_or(&[], |s| s.as_encoded_bytes()),
b.url.extension().map_or(&[], |s| s.as_encoded_bytes()),
a.url.ext().map_or(&[], |s| s.as_encoded_bytes()),
b.url.ext().map_or(&[], |s| s.as_encoded_bytes()),
self.promote(a, b),
)
};

View file

@ -9,7 +9,7 @@ impl UserData for MpscTx {
methods.add_async_method("send", |lua, me, value: Value| async move {
match me.0.send(value).await {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Custom(e.to_string().into())).into_lua_multi(&lua),
Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua),
}
});
}
@ -33,7 +33,7 @@ impl UserData for MpscUnboundedTx {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("send", |lua, me, value: Value| match me.0.send(value) {
Ok(()) => true.into_lua_multi(lua),
Err(e) => (false, Error::Custom(e.to_string().into())).into_lua_multi(lua),
Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(lua),
});
}
}
@ -60,7 +60,7 @@ impl UserData for OneshotTx {
};
match tx.send(value) {
Ok(()) => true.into_lua_multi(lua),
Err(_) => (false, Error::Custom("Oneshot receiver closed".into())).into_lua_multi(lua),
Err(_) => (false, Error::custom("Oneshot receiver closed")).into_lua_multi(lua),
}
});
}
@ -74,7 +74,7 @@ impl UserData for OneshotRx {
};
match rx.await {
Ok(value) => value.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Custom(e.to_string().into())).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua),
}
});
}

View file

@ -11,7 +11,7 @@ impl Utils {
pub(super) fn file_cache(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, t: Table| {
let file: FileRef = t.raw_get("file")?;
if file.url.parent() == Some(&YAZI.preview.cache_dir) {
if file.url.parent_url() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) {
return Ok(None);
}

View file

@ -8,27 +8,36 @@ use crate::bindings::ImageInfo;
impl Utils {
pub(super) fn image_info(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, url: UrlRef| async move {
match yazi_adapter::ImageInfo::new(&url).await {
let Some(path) = url.as_path() else {
return (Value::Nil, Error::custom("Only local files are supported")).into_lua_multi(&lua);
};
match yazi_adapter::ImageInfo::new(path).await {
Ok(info) => ImageInfo::from(info).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Custom(e.to_string().into())).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua),
}
})
}
pub(super) fn image_show(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move {
match ADAPTOR.get().image_show(&url, *rect).await {
let Some(path) = url.as_path() else {
return (Value::Nil, Error::custom("Only local files are supported")).into_lua_multi(&lua);
};
match ADAPTOR.get().image_show(path, *rect).await {
Ok(area) => Rect::from(area).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Custom(e.to_string().into())).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua),
}
})
}
pub(super) fn image_precache(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move {
match Image::precache(&src, &dist).await {
let Some((src, dist)) = src.as_path().zip(dist.as_path()) else {
return (Value::Nil, Error::custom("Only local files are supported")).into_lua_multi(&lua);
};
match Image::precache(src, dist).await {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Custom(e.to_string().into())).into_lua_multi(&lua),
Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua),
}
})
}

View file

@ -14,7 +14,11 @@ impl Utils {
let area: Area = t.raw_get("area")?;
let mut lock = PreviewLock::try_from(t)?;
let inner = match Highlighter::new(&lock.url).highlight(lock.skip, area.size()).await {
let Some(path) = lock.url.as_path() else {
return "Only local files are supported".into_lua_multi(&lua);
};
let inner = match Highlighter::new(path).highlight(lock.skip, area.size()).await {
Ok(text) => text,
Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua),
Err(e @ PeekError::Unexpected(_)) => {

View file

@ -24,9 +24,7 @@ impl Utils {
style: THEME.spot.border.into(),
titles: vec![(
ratatui::widgets::block::Position::Top,
ratatui::text::Line::raw(lock.url.name().to_string_lossy().into_owned())
.centered()
.style(THEME.spot.title),
ratatui::text::Line::raw("Spot").centered().style(THEME.spot.title),
)],
}),
Renderable::Table(Box::new(table)),

View file

@ -3,9 +3,4 @@ use yazi_shared::RoCell;
pub static HIDER: RoCell<Semaphore> = RoCell::new();
pub static WATCHER: RoCell<Semaphore> = RoCell::new();
pub(super) fn init_semaphore() {
HIDER.init(Semaphore::new(1));
WATCHER.init(Semaphore::new(1));
}
pub(super) fn init_semaphore() { HIDER.init(Semaphore::new(1)); }

View file

@ -188,7 +188,7 @@ impl File {
continue;
}
let to = dest.join(from.file_name().unwrap());
let to = dest.join(from.name().unwrap());
self.prog.send(TaskProg::New(task.id, cha.len))?;
if cha.is_orphan() || (cha.is_link() && !task.follow) {
@ -259,7 +259,7 @@ impl File {
continue;
}
let to = dest.join(from.file_name().unwrap());
let to = dest.join(from.name().unwrap());
self.prog.send(TaskProg::New(task.id, cha.len))?;
self.queue(FileIn::Hardlink(task.spawn(from, to, cha)), NORMAL).await?;
}

View file

@ -93,7 +93,7 @@ impl Prework {
let parent = buf[0].0.parent_url().unwrap();
FilesOp::Size(
parent.into(),
HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn_owned(), s))),
HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn().to_owned(), s))),
)
.emit();
});

View file

@ -2,7 +2,7 @@ use std::{cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Has
use anyhow::Result;
use crate::{loc::Loc, url::{Uri, Urn, UrnBuf}};
use crate::{loc::Loc, url::{Uri, Urn}};
#[derive(Clone, Default, Eq)]
pub struct LocBuf {
@ -160,9 +160,6 @@ impl LocBuf {
#[inline]
pub fn urn(&self) -> &Urn { self.as_loc().urn() }
#[inline]
pub fn urn_owned(&self) -> UrnBuf { self.as_loc().urn_owned() }
#[inline]
pub fn base(&self) -> &Urn { self.as_loc().base() }
@ -176,7 +173,13 @@ impl LocBuf {
pub fn has_trail(&self) -> bool { self.as_loc().has_trail() }
#[inline]
pub fn name(&self) -> &OsStr { self.as_loc().name() }
pub fn name(&self) -> Option<&OsStr> { self.as_loc().name() }
#[inline]
pub fn stem(&self) -> Option<&OsStr> { self.as_loc().stem() }
#[inline]
pub fn ext(&self) -> Option<&OsStr> { self.as_loc().ext() }
}
#[cfg(test)]
@ -189,21 +192,21 @@ mod tests {
let loc: LocBuf = Path::new("/").into();
assert_eq!(loc.uri().as_os_str(), OsStr::new("/"));
assert_eq!(loc.urn().as_os_str(), OsStr::new(""));
assert_eq!(loc.name(), OsStr::new(""));
assert_eq!(loc.name(), None);
assert_eq!(loc.base().as_os_str(), OsStr::new(""));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
let loc: LocBuf = Path::new("/root").into();
assert_eq!(loc.uri().as_os_str(), OsStr::new("root"));
assert_eq!(loc.urn().as_os_str(), OsStr::new("root"));
assert_eq!(loc.name(), OsStr::new("root"));
assert_eq!(loc.name().unwrap(), OsStr::new("root"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/"));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
let loc: LocBuf = Path::new("/root/code/foo/").into();
assert_eq!(loc.uri().as_os_str(), OsStr::new("foo"));
assert_eq!(loc.urn().as_os_str(), OsStr::new("foo"));
assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.name().unwrap(), OsStr::new("foo"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/"));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/"));
}
@ -213,42 +216,42 @@ mod tests {
let loc = LocBuf::with("/".into(), 0, 0)?;
assert_eq!(loc.uri().as_os_str(), OsStr::new(""));
assert_eq!(loc.urn().as_os_str(), OsStr::new(""));
assert_eq!(loc.name(), OsStr::new(""));
assert_eq!(loc.name(), None);
assert_eq!(loc.base().as_os_str(), OsStr::new("/"));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
let loc = LocBuf::with("/root/code/".into(), 1, 1)?;
assert_eq!(loc.uri().as_os_str(), OsStr::new("code"));
assert_eq!(loc.urn().as_os_str(), OsStr::new("code"));
assert_eq!(loc.name(), OsStr::new("code"));
assert_eq!(loc.name().unwrap(), OsStr::new("code"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/"));
let loc = LocBuf::with("/root/code/foo//".into(), 2, 1)?;
assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo"));
assert_eq!(loc.urn().as_os_str(), OsStr::new("foo"));
assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.name().unwrap(), OsStr::new("foo"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/"));
let loc = LocBuf::with("/root/code/foo//".into(), 2, 2)?;
assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo"));
assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo"));
assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.name().unwrap(), OsStr::new("foo"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/"));
let loc = LocBuf::with("/root/code/foo//bar/".into(), 2, 2)?;
assert_eq!(loc.uri().as_os_str(), OsStr::new("foo//bar"));
assert_eq!(loc.urn().as_os_str(), OsStr::new("foo//bar"));
assert_eq!(loc.name(), OsStr::new("bar"));
assert_eq!(loc.name().unwrap(), OsStr::new("bar"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/"));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/"));
let loc = LocBuf::with("/root/code/foo//bar/".into(), 3, 2)?;
assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo//bar"));
assert_eq!(loc.urn().as_os_str(), OsStr::new("foo//bar"));
assert_eq!(loc.name(), OsStr::new("bar"));
assert_eq!(loc.name().unwrap(), OsStr::new("bar"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/"));
Ok(())

View file

@ -2,7 +2,7 @@ use std::{ffi::OsStr, hash::{Hash, Hasher}, ops::Deref, path::Path};
use anyhow::{Result, bail};
use crate::{loc::LocBuf, url::{Uri, Urn, UrnBuf}};
use crate::{loc::LocBuf, url::{Uri, Urn}};
#[derive(Clone, Copy, Debug)]
pub struct Loc<'a> {
@ -151,9 +151,6 @@ impl<'a> Loc<'a> {
})
}
#[inline]
pub fn urn_owned(self) -> UrnBuf { self.urn().to_owned() }
#[inline]
pub fn base(self) -> &'a Urn {
Urn::new(unsafe {
@ -179,7 +176,13 @@ impl<'a> Loc<'a> {
pub fn has_trail(self) -> bool { self.bytes().len() != self.urn }
#[inline]
pub fn name(self) -> &'a OsStr { self.inner.file_name().unwrap_or(OsStr::new("")) }
pub fn name(self) -> Option<&'a OsStr> { self.inner.file_name() }
#[inline]
pub fn stem(self) -> Option<&'a OsStr> { self.inner.file_stem() }
#[inline]
pub fn ext(self) -> Option<&'a OsStr> { self.inner.extension() }
#[inline]
pub fn parent(self) -> Option<&'a Path> { self.inner.parent() }

View file

@ -1,10 +1,10 @@
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}, str::FromStr};
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, path::{Path, PathBuf}, str::FromStr};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use super::UrnBuf;
use crate::{loc::LocBuf, pool::Pool, url::{Components, Display, Encode, EncodeTilded, Scheme, Url, UrlCow, Urn}};
use crate::{loc::LocBuf, pool::Pool, url::{Components, Display, Encode, EncodeTilded, Scheme, Uri, Url, UrlCow, Urn}};
#[derive(Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)]
pub struct UrlBuf {
@ -12,12 +12,6 @@ pub struct UrlBuf {
pub scheme: Scheme,
}
impl Deref for UrlBuf {
type Target = LocBuf;
fn deref(&self) -> &Self::Target { &self.loc }
}
impl From<LocBuf> for UrlBuf {
fn from(loc: LocBuf) -> Self { Self { loc, scheme: Scheme::Regular } }
}
@ -146,9 +140,7 @@ impl UrlBuf {
}
#[inline]
pub fn as_path(&self) -> Option<&Path> {
Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual())
}
pub fn as_path(&self) -> Option<&Path> { self.as_url().as_path() }
#[inline]
pub fn into_path(self) -> Option<PathBuf> {
@ -163,11 +155,8 @@ impl UrlBuf {
Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() }
}
// TODO: use Urn instead of UrlBuf
#[inline]
pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> {
Some((self.parent_url()?, self.loc.urn_owned()))
}
pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> { self.as_url().pair() }
#[inline]
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
@ -232,6 +221,30 @@ impl UrlBuf {
// FIXME: remove
#[inline]
pub fn into_path2(self) -> PathBuf { self.loc.into_path() }
#[inline]
pub fn name(&self) -> Option<&OsStr> { self.as_url().name() }
#[inline]
pub fn stem(&self) -> Option<&OsStr> { self.as_url().stem() }
#[inline]
pub fn ext(&self) -> Option<&OsStr> { self.as_url().ext() }
#[inline]
pub fn uri(&self) -> &Uri { self.as_url().uri() }
#[inline]
pub fn urn(&self) -> &Urn { self.as_url().urn() }
#[inline]
pub fn is_absolute(&self) -> bool { self.as_url().is_absolute() }
#[inline]
pub fn has_root(&self) -> bool { self.as_url().has_root() }
#[inline]
pub fn has_trail(&self) -> bool { self.as_url().has_trail() }
}
impl Debug for UrlBuf {

View file

@ -1,4 +1,4 @@
use std::{hash::{Hash, Hasher}, ops::Deref};
use std::{hash::{Hash, Hasher}, ops::Deref, path::PathBuf};
use hashbrown::Equivalent;
use serde::{Deserialize, Serialize};
@ -50,12 +50,16 @@ impl Deref for UrlBufCov {
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<UrlBufCov> for UrlBuf {
fn from(value: UrlBufCov) -> Self { value.0 }
}
impl From<UrlBuf> for UrlBufCov {
fn from(value: UrlBuf) -> Self { Self(value) }
}
impl From<UrlBufCov> for UrlBuf {
fn from(value: UrlBufCov) -> Self { value.0 }
impl From<PathBuf> for UrlBufCov {
fn from(value: PathBuf) -> Self { Self(UrlBuf::from(value)) }
}
impl From<UrlCow<'_>> for UrlBufCov {

View file

@ -1,8 +1,8 @@
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, ops::Deref, path::Path};
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::Path};
use hashbrown::Equivalent;
use crate::{loc::{Loc, LocBuf}, url::{Components, Encode, Scheme, UrlBuf, UrnBuf}};
use crate::{loc::{Loc, LocBuf}, url::{Components, Encode, Scheme, Uri, UrlBuf, Urn, UrnBuf}};
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct Url<'a> {
@ -10,12 +10,6 @@ pub struct Url<'a> {
pub scheme: Scheme,
}
impl<'a> Deref for Url<'a> {
type Target = Loc<'a>;
fn deref(&self) -> &Self::Target { &self.loc }
}
// TODO: remove
impl<'a> From<&'a Url<'a>> for Url<'a> {
fn from(value: &'a Url) -> Self { Self { loc: value.loc.as_loc(), scheme: value.scheme.clone() } }
@ -69,6 +63,12 @@ impl<'a> Url<'a> {
#[inline]
pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) }
#[inline]
pub fn is_absolute(&self) -> bool { self.loc.is_absolute() }
#[inline]
pub fn has_root(&self) -> bool { self.loc.has_root() }
#[inline]
pub fn as_url(&'a self) -> Url<'a> { Self::from(self) }
@ -90,6 +90,21 @@ impl<'a> Url<'a> {
UrlBuf { loc, scheme: self.scheme.clone() }
}
#[inline]
pub fn uri(&self) -> &'a Uri { self.loc.uri() }
#[inline]
pub fn urn(&self) -> &'a Urn { self.loc.urn() }
#[inline]
pub fn name(self) -> Option<&'a OsStr> { self.loc.name() }
#[inline]
pub fn stem(&self) -> Option<&'a OsStr> { self.loc.stem() }
#[inline]
pub fn ext(&self) -> Option<&'a OsStr> { self.loc.ext() }
pub fn base(&self) -> Option<Self> {
use Scheme as S;
@ -163,9 +178,17 @@ impl<'a> Url<'a> {
#[inline]
pub fn pair(&self) -> Option<(Url<'a>, UrnBuf)> {
Some((self.parent_url()?, self.loc.urn_owned()))
Some((self.parent_url()?, self.loc.urn().to_owned()))
}
#[inline]
pub fn as_path(&self) -> Option<&Path> { Some(&*self.loc).filter(|_| !self.scheme.is_virtual()) }
pub fn as_path(&self) -> Option<&'a Path> {
Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual())
}
#[inline]
pub fn has_base(&self) -> bool { self.loc.has_base() }
#[inline]
pub fn has_trail(&self) -> bool { self.loc.has_trail() }
}

27
yazi-watcher/Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
[package]
name = "yazi-watcher"
version = "25.6.11"
edition = "2024"
license = "MIT"
authors = [ "sxyazi <sxyazi@gmail.com>" ]
description = "Yazi file watcher"
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
[dependencies]
yazi-adapter = { path = "../yazi-adapter", version = "25.6.11" }
yazi-dds = { path = "../yazi-dds", version = "25.6.11" }
yazi-fs = { path = "../yazi-fs", version = "25.6.11" }
yazi-macro = { path = "../yazi-macro", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
# Logging
tracing = { workspace = true }
# External dependencies
anyhow = { workspace = true }
hashbrown = { workspace = true }
notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] }
parking_lot = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }

View file

@ -0,0 +1,74 @@
use anyhow::Result;
use hashbrown::HashSet;
use tokio::sync::mpsc;
use tracing::error;
use yazi_shared::url::{Url, UrlBuf};
use crate::{LINKED, WATCHED, backend};
pub(crate) struct Backend {
local: backend::Local,
}
impl Backend {
pub(crate) fn serve(out_tx: mpsc::UnboundedSender<UrlBuf>) -> Self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
yazi_fs::mounts::Partitions::monitor(yazi_fs::mounts::PARTITIONS.clone(), || {
yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount())
});
Self { local: backend::Local::serve(out_tx) }
}
pub(crate) async fn sync(mut self, to_unwatch: Vec<UrlBuf>, to_watch: Vec<UrlBuf>) -> Backend {
if to_unwatch.is_empty() && to_watch.is_empty() {
return self;
}
tokio::task::spawn_blocking(move || {
for u in to_unwatch {
match self.unwatch(&u) {
Ok(()) => WATCHED.write().remove(&u),
Err(e) => error!("Unwatch failed: {e:?}"),
}
}
for u in to_watch {
match self.watch(&u) {
Ok(()) => WATCHED.write().insert(u),
Err(e) => error!("Watch failed: {e:?}"),
}
}
self
})
.await
.unwrap()
}
pub(crate) fn push_files<I, T>(out_tx: &mpsc::UnboundedSender<UrlBuf>, urls: I)
where
I: IntoIterator<Item = T>,
T: Into<UrlBuf>,
{
let (mut todo, watched) = (HashSet::new(), WATCHED.read());
for url in urls.into_iter().map(Into::into) {
let Some(parent) = url.parent_url() else { continue };
if todo.contains(&parent) {
todo.insert(url);
} else if watched.contains(&parent)
|| LINKED.read().from_dir(&parent).any(|p| watched.contains(Url::regular(p)))
{
todo.insert(parent.to_owned());
todo.insert(url);
}
}
todo.into_iter().for_each(|u| _ = out_tx.send(u));
}
fn watch<'a>(&mut self, url: impl Into<Url<'a>>) -> Result<()> {
if let Some(path) = url.into().as_path() { self.local.watch(path) } else { Ok(()) }
}
fn unwatch<'a>(&mut self, url: impl Into<Url<'a>>) -> Result<()> {
if let Some(path) = url.into().as_path() { self.local.unwatch(path) } else { Ok(()) }
}
}

View file

@ -0,0 +1,41 @@
use std::{path::Path, time::Duration};
use anyhow::Result;
use notify::{ErrorKind::WatchNotFound, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc;
use yazi_shared::url::UrlBuf;
use crate::backend::Backend;
pub(super) struct Local(Box<dyn notify::Watcher + Send>);
impl Local {
pub(super) fn serve(out_tx: mpsc::UnboundedSender<UrlBuf>) -> Self {
let handler = move |res: Result<notify::Event, notify::Error>| {
let Ok(event) = res else { return };
if event.kind.is_access() {
return;
}
Backend::push_files(&out_tx, event.paths);
};
let config = notify::Config::default().with_poll_interval(Duration::from_millis(500));
Self(if yazi_adapter::WSL.get() || cfg!(target_os = "netbsd") {
Box::new(PollWatcher::new(handler, config).unwrap())
} else {
Box::new(RecommendedWatcher::new(handler, config).unwrap())
})
}
pub(super) fn watch(&mut self, path: &Path) -> Result<()> {
Ok(self.0.watch(path, RecursiveMode::NonRecursive)?)
}
pub(super) fn unwatch(&mut self, path: &Path) -> Result<()> {
match self.0.unwatch(path) {
Ok(()) => Ok(()),
Err(e) if matches!(e.kind, WatchNotFound) => Ok(()),
Err(e) => Err(e)?,
}
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(backend local);

13
yazi-watcher/src/lib.rs Normal file
View file

@ -0,0 +1,13 @@
yazi_macro::mod_pub!(backend);
yazi_macro::mod_flat!(linked watched watcher);
pub static LINKED: yazi_shared::RoCell<parking_lot::RwLock<Linked>> = yazi_shared::RoCell::new();
pub static WATCHED: yazi_shared::RoCell<parking_lot::RwLock<Watched>> = yazi_shared::RoCell::new();
pub static WATCHER: yazi_shared::RoCell<tokio::sync::Semaphore> = yazi_shared::RoCell::new();
pub fn init() {
LINKED.with(<_>::default);
WATCHED.with(<_>::default);
WATCHER.init(tokio::sync::Semaphore::new(1));
}

View file

@ -0,0 +1,72 @@
use std::{iter, ops::{Deref, DerefMut}, path::{Path, PathBuf}};
use hashbrown::{HashMap, HashSet};
use parking_lot::RwLock;
use yazi_shared::url::Url;
use crate::Watched;
#[derive(Default)]
pub struct Linked(HashMap<PathBuf, PathBuf> /* from ==> to */);
impl Deref for Linked {
type Target = HashMap<PathBuf, PathBuf>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl DerefMut for Linked {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl Linked {
pub fn from_dir<'a, 'b, T>(&'a self, url: T) -> Box<dyn Iterator<Item = &'a Path> + 'b>
where
'a: 'b,
T: Into<Url<'b>>,
{
let url: Url = url.into();
let Some(path) = url.as_path() else {
return Box::new(iter::empty());
};
if let Some(to) = self.get(path) {
Box::new(self.iter().filter(move |(k, v)| *v == to && *k != path).map(|(k, _)| k.as_path()))
} else {
Box::new(self.iter().filter(move |(_, v)| *v == path).map(|(k, _)| k.as_path()))
}
}
pub fn from_file(&self, url: Url) -> Vec<PathBuf> {
let Some(path) = url.as_path() else { return vec![] };
if let Some((parent, name)) = path.parent().zip(path.file_name()) {
self.from_dir(Url::regular(parent)).map(|p| p.join(name)).collect()
} else {
vec![]
}
}
pub(super) async fn sync(linked: &'static RwLock<Self>, watched: &'static RwLock<Watched>) {
tokio::task::spawn_blocking(move || {
let mut new: HashSet<_> = watched.read().paths().map(ToOwned::to_owned).collect();
let mut linked = linked.write();
linked.retain(|k, _| new.remove(k));
for from in new {
linked.insert(from, PathBuf::new());
}
for (from, to) in linked.iter_mut() {
match std::fs::canonicalize(from) {
Ok(c) if c != *from && watched.read().contains(Url::regular(from)) => *to = c,
Ok(_) => *to = PathBuf::new(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => *to = PathBuf::new(),
Err(_) => {}
}
}
linked.retain(|_, v| !v.as_os_str().is_empty());
})
.await
.ok();
}
}

View file

@ -0,0 +1,30 @@
use std::path::Path;
use hashbrown::HashSet;
use yazi_shared::url::{Url, UrlBuf};
#[derive(Default)]
pub struct Watched(HashSet<UrlBuf>);
impl Watched {
#[inline]
pub(crate) fn contains<'a>(&self, url: impl Into<Url<'a>>) -> bool {
self.0.contains(&url.into())
}
#[inline]
pub(crate) fn diff(&self, new: &HashSet<UrlBuf>) -> (Vec<UrlBuf>, Vec<UrlBuf>) {
(self.0.difference(new).cloned().collect(), new.difference(&self.0).cloned().collect())
}
#[inline]
pub(crate) fn insert(&mut self, url: impl Into<UrlBuf>) { self.0.insert(url.into()); }
#[inline]
pub(crate) fn paths(&self) -> impl Iterator<Item = &Path> {
self.0.iter().filter_map(|u| u.as_path())
}
#[inline]
pub(crate) fn remove<'a>(&mut self, url: impl Into<Url<'a>>) { self.0.remove(&url.into()); }
}

View file

@ -0,0 +1,81 @@
use std::time::Duration;
use hashbrown::HashSet;
use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_fs::{File, FilesOp, provider::local};
use yazi_shared::url::UrlBuf;
use crate::{LINKED, Linked, WATCHED, WATCHER, backend::Backend};
pub struct Watcher {
in_tx: watch::Sender<HashSet<UrlBuf>>,
out_tx: mpsc::UnboundedSender<UrlBuf>,
}
impl Watcher {
pub fn serve() -> Self {
let (in_tx, in_rx) = watch::channel(Default::default());
let (out_tx, out_rx) = mpsc::unbounded_channel();
let backend = Backend::serve(out_tx.clone());
tokio::spawn(Self::fan_in(in_rx, backend));
tokio::spawn(Self::fan_out(out_rx));
Self { in_tx, out_tx }
}
pub fn watch<'a>(&mut self, it: impl Iterator<Item = &'a UrlBuf>) {
self.in_tx.send(it.filter(|u| u.is_regular()).cloned().collect()).ok();
}
pub fn push_files(&self, urls: Vec<UrlBuf>) { Backend::push_files(&self.out_tx, urls); }
async fn fan_in(mut rx: watch::Receiver<HashSet<UrlBuf>>, mut backend: Backend) {
loop {
let (to_unwatch, to_watch) = WATCHED.read().diff(&rx.borrow_and_update());
backend = backend.sync(to_unwatch, to_watch).await;
if !rx.has_changed().unwrap_or(false) {
Linked::sync(&LINKED, &WATCHED).await;
}
if rx.changed().await.is_err() {
break;
}
}
}
async fn fan_out(rx: UnboundedReceiver<UrlBuf>) {
// TODO: revert this once a new notification is implemented
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));
pin!(rx);
while let Some(chunk) = rx.next().await {
let urls: HashSet<_> = chunk.into_iter().collect();
let _permit = WATCHER.acquire().await.unwrap();
let mut ops = Vec::with_capacity(urls.len());
for u in urls {
let Some((parent, urn)) = u.pair() else { continue };
let Ok(file) = File::new(&u).await else {
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
continue;
};
if let Some(p) = file.url.as_path()
&& !local::valid_name_case(p).await.unwrap_or(false)
{
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
continue;
}
ops.push(FilesOp::Upserting(parent.into(), [(urn, file)].into()));
}
FilesOp::mutate(ops);
}
}
}