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-proxy",
"yazi-shared", "yazi-shared",
"yazi-term", "yazi-term",
"yazi-watcher",
"yazi-widgets", "yazi-widgets",
] ]
@ -3617,10 +3618,8 @@ version = "25.6.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"crossterm 0.29.0", "crossterm 0.29.0",
"futures",
"hashbrown", "hashbrown",
"indexmap", "indexmap",
"notify",
"parking_lot", "parking_lot",
"ratatui", "ratatui",
"tokio", "tokio",
@ -3638,6 +3637,7 @@ dependencies = [
"yazi-proxy", "yazi-proxy",
"yazi-scheduler", "yazi-scheduler",
"yazi-shared", "yazi-shared",
"yazi-watcher",
"yazi-widgets", "yazi-widgets",
] ]
@ -3714,6 +3714,7 @@ dependencies = [
"yazi-proxy", "yazi-proxy",
"yazi-shared", "yazi-shared",
"yazi-term", "yazi-term",
"yazi-watcher",
"yazi-widgets", "yazi-widgets",
] ]
@ -3886,6 +3887,24 @@ dependencies = [
"yazi-shared", "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]] [[package]]
name = "yazi-widgets" name = "yazi-widgets"
version = "25.6.11" 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-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-term = { path = "../yazi-term", 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" } yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies # External dependencies

View file

@ -145,7 +145,7 @@ impl UserData for File {
return None; 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<_>>()) 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) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id))); fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
cached_field!(fields, name, |lua, me| { 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)); 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_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, provider::{self, local::{Gate, Local}}};
use yazi_macro::{err, succ}; use yazi_macro::{err, succ};
use yazi_parser::VoidOpt; 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_shared::{OsStrJoin, event::Data, terminal_clear, url::{Component, UrlBuf}};
use yazi_term::tty::TTY; use yazi_term::tty::TTY;
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -119,7 +120,7 @@ impl BulkRename {
selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(), 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"))); failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = provider::rename(&old, &new).await { } else if let Err(e) = provider::rename(&old, &new).await {
failed.push((o, n, e.into())); failed.push((o, n, e.into()));

View file

@ -84,7 +84,7 @@ impl Cd {
} }
if let Some(p) = url.parent_url() { 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); MgrProxy::reveal(&url);
} }

View file

@ -38,10 +38,10 @@ impl Actor for Copy {
} }
} }
"filename" => { "filename" => {
s.push(opt.separator.transform(u.name())); s.push(opt.separator.transform(u.name().unwrap_or_default()));
} }
"name_without_ext" => { "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), _ => 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_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::mgr::CreateOpt; 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_shared::{event::Data, url::{UrlBuf, UrnBuf}};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -57,7 +58,7 @@ impl Create {
} }
if let Ok(f) = File::new(&new).await { 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) MgrProxy::reveal(&new)
} }
Ok(()) Ok(())

View file

@ -16,7 +16,7 @@ impl Actor for FilterDo {
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { 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 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); cx.current_mut().files.set_filter(filter);
if cx.hovered().map(|f| f.urn()) != hovered.as_deref() { 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); let state = opt.state.bool(cx.tab().pref.show_hidden);
cx.tab_mut().pref.show_hidden = state; 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| { let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading { if f.stage == FolderStage::Loading {
render!(); render!();

View file

@ -1,10 +1,11 @@
use anyhow::Result; use anyhow::Result;
use crossterm::{execute, terminal::SetTitle}; use crossterm::{execute, terminal::SetTitle};
use yazi_config::YAZI; 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_macro::{act, succ};
use yazi_parser::VoidOpt; use yazi_parser::VoidOpt;
use yazi_shared::event::Data; use yazi_shared::{event::Data, url::UrlBuf};
use yazi_term::tty::TTY; use yazi_term::tty::TTY;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -22,9 +23,9 @@ impl Actor for Refresh {
} }
if let Some(p) = cx.parent() { if let Some(p) = cx.parent() {
cx.mgr.watcher.trigger_dirs(&[cx.current(), p]); Self::trigger_dirs(&[cx.current(), p]);
} else { } else {
cx.mgr.watcher.trigger_dirs(&[cx.current()]); Self::trigger_dirs(&[cx.current()]);
} }
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
@ -35,3 +36,27 @@ impl Actor for Refresh {
succ!(); 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_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
use yazi_macro::{act, err, succ}; use yazi_macro::{act, err, succ};
use yazi_parser::mgr::RenameOpt; 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_shared::{Id, event::Data, url::{UrlBuf, UrnBuf}};
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -47,9 +48,8 @@ impl Actor for Rename {
return; return;
} }
let new = UrlBuf::from(old.parent().unwrap().join(name)); let new = old.parent_url().unwrap().join(name);
if opt.force || !maybe_exists(&new).await || provider::same(&old, &new).await.unwrap_or(false) if opt.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await {
{
Self::r#do(tab, old, new).await.ok(); Self::r#do(tab, old, new).await.ok();
} else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { } else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await {
Self::r#do(tab, old, new).await.ok(); Self::r#do(tab, old, new).await.ok();
@ -91,12 +91,12 @@ impl Rename {
return String::new(); return String::new();
} }
let ext = url.extension(); let ext = url.ext();
match by { match by {
"stem" => ext.map_or_else(String::new, |s| format!(".{}", s.to_string_lossy())), "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()), "ext" if ext.is_some() => format!("{}.", url.stem().unwrap().to_string_lossy()),
"dot_ext" if ext.is_some() => url.file_stem().unwrap().to_string_lossy().into_owned(), "dot_ext" if ext.is_some() => url.stem().unwrap().to_string_lossy().into_owned(),
_ => url.name().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); pref.sort_translit = opt.translit.unwrap_or(pref.sort_translit);
let sorter = FilesSorter::from(&*pref); 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| { let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading { if f.stage == FolderStage::Loading {
render!(); render!();

View file

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

View file

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

View file

@ -14,11 +14,14 @@ pub enum Error {
impl Error { impl Error {
pub fn install(lua: &Lua) -> mlua::Result<()> { 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)])?) 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 { pub fn into_string(self) -> SStr {
match self { match self {
Error::Io(e) => Cow::Owned(e.to_string()), 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, link_to, |_, me| Ok(me.link_to.clone().map($crate::Url::new)));
$crate::cached_field!($fields, name, |lua, me| { $crate::cached_field!($fields, name, |lua, me| {
Some(me.name()) me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
.filter(|s| !s.is_empty())
.map(|s| lua.create_string(s.as_encoded_bytes()))
.transpose()
}); });
}; };
} }

View file

@ -94,16 +94,16 @@ impl FromLua for Url {
impl UserData for Url { impl UserData for Url {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, name, |lua, me| { 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| { 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| { 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, 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, base, |_, me| Ok(me.base().map(Self::new)));
cached_field!(fields, domain, |lua, me| { cached_field!(fields, domain, |lua, me| {
me.scheme.domain().map(|s| lua.create_string(s)).transpose() 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 { 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 { impl FromLua for Urn {

View file

@ -76,7 +76,7 @@ impl Icon {
#[inline] #[inline]
fn match_by_name(&self, file: &File) -> Option<&I> { 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() { if file.is_dir() {
self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase())) self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase()))
} else { } else {
@ -90,7 +90,7 @@ impl Icon {
#[inline] #[inline]
fn match_by_ext(&self, file: &File) -> Option<&I> { 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())) 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-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-scheduler = { path = "../yazi-scheduler", version = "25.6.11" } yazi-scheduler = { path = "../yazi-scheduler", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", 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" } yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true } crossterm = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] }
parking_lot = { workspace = true } parking_lot = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
tokio = { 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_pub!(cmp confirm help input mgr notify pick spot tab tasks which);
yazi_macro::mod_flat!(core); 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_adapter::Dimension;
use yazi_config::popup::{Origin, Position}; use yazi_config::popup::{Origin, Position};
use yazi_shared::url::UrlBuf; 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}; use crate::tab::{Folder, Tab};
pub struct Mgr { 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> { pub fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
for i in !include as usize..files.len() { for i in !include as usize..files.len() {
let idx = (cursor + files.len() - i) % 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); 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> { pub fn next(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
for i in !include as usize..files.len() { for i in !include as usize..files.len() {
let idx = (cursor + i) % 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); return Some(idx as isize - cursor as isize);
} }
} }
@ -54,11 +58,11 @@ impl Finder {
let mut i = 0u8; let mut i = 0u8;
for file in folder.files.iter() { for file in folder.files.iter() {
if !self.filter.matches(file.name()) { if file.name().is_none_or(|s| !self.filter.matches(s)) {
continue; continue;
} }
self.matched.insert(file.urn_owned(), i); self.matched.insert(file.urn().to_owned(), i);
if self.matched.len() > 99 { if self.matched.len() > 99 {
break; break;
} }

View file

@ -103,7 +103,7 @@ impl Folder {
self.scroll(step) 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(); b |= self.squeeze_offset();
self.sync_page(false); self.sync_page(false);

View file

@ -8,7 +8,7 @@ use crate::mgr::Yanked;
impl Tasks { impl Tasks {
pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) { pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) {
for u in src.iter() { for u in src.iter() {
let to = dest.join(u.file_name().unwrap()); let to = dest.join(u.name().unwrap());
if force && *u == to { if force && *u == to {
debug!("file_cut: same file, skipping {:?}", to); debug!("file_cut: same file, skipping {:?}", to);
} else { } else {
@ -19,7 +19,7 @@ impl Tasks {
pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) {
for u in src.iter() { for u in src.iter() {
let to = dest.join(u.file_name().unwrap()); let to = dest.join(u.name().unwrap());
if force && *u == to { if force && *u == to {
debug!("file_copy: same file, skipping {:?}", to); debug!("file_copy: same file, skipping {:?}", to);
} else { } else {
@ -30,7 +30,7 @@ impl Tasks {
pub fn file_link(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, relative: bool, force: bool) { pub fn file_link(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, relative: bool, force: bool) {
for u in src { for u in src {
let to = dest.join(u.file_name().unwrap()); let to = dest.join(u.name().unwrap());
if force && *u == to { if force && *u == to {
debug!("file_link: same file, skipping {:?}", to); debug!("file_link: same file, skipping {:?}", to);
} else { } else {
@ -41,7 +41,7 @@ impl Tasks {
pub fn file_hardlink(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, force: bool, follow: bool) { pub fn file_hardlink(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, force: bool, follow: bool) {
for u in src { for u in src {
let to = dest.join(u.file_name().unwrap()); let to = dest.join(u.name().unwrap());
if force && *u == to { if force && *u == to {
debug!("file_hardlink: same file, skipping {:?}", to); debug!("file_hardlink: same file, skipping {:?}", to);
} else { } 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-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-term = { path = "../yazi-term", 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" } yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies # External dependencies

View file

@ -32,9 +32,9 @@ async fn main() -> anyhow::Result<()> {
yazi_widgets::init(); yazi_widgets::init();
yazi_plugin::init()?; yazi_watcher::init();
yazi_core::init(); yazi_plugin::init()?;
yazi_dds::serve(); yazi_dds::serve();
app::App::serve().await 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 anyhow::Result;
use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn, UrnBuf}; use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn};
use crate::{cha::Cha, provider}; use crate::{cha::Cha, provider};
@ -47,7 +47,7 @@ impl File {
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
#[inline] #[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() } 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() } pub fn urn(&self) -> &Urn { self.url.urn() }
#[inline] #[inline]
pub fn urn_owned(&self) -> UrnBuf { self.url.urn_owned() } pub fn name(&self) -> Option<&OsStr> { self.url.name() }
#[inline] #[inline]
pub fn name(&self) -> &OsStr { self.url.name() } pub fn stem(&self) -> Option<&OsStr> { self.url.stem() }
#[inline]
pub fn stem(&self) -> Option<&OsStr> { self.url.file_stem() }
} }
impl Hash for File { impl Hash for File {

View file

@ -162,7 +162,7 @@ impl Files {
macro_rules! go { macro_rules! go {
($dist:expr, $src:expr, $inc:literal) => { ($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 { for f in &$dist {
if todo.remove(f.urn()).is_some() && todo.is_empty() { if todo.remove(f.urn()).is_some() && todo.is_empty() {
break; break;
@ -293,7 +293,7 @@ impl Files {
} }
self.update_deleting( 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); 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 tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
use yazi_shared::url::{Component, Url, UrlBuf}; use yazi_shared::url::{Component, Url, UrlBuf};
use crate::{cha::Cha, provider}; use crate::{cha::Cha, provider::{self, local::Local}};
#[inline] #[inline]
pub async fn maybe_exists<'a>(url: impl Into<Url<'a>>) -> bool { 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> { pub async fn realname(url: &UrlBuf) -> Option<OsString> {
let name = u.file_name()?; let (path, name) = (url.as_path()?, url.name()?);
if *u == provider::canonicalize(u).await.ok()? { if path == Local::canonicalize(path).await.ok()? {
return None; return None;
} }
realname_unchecked(u, &mut HashMap::new()) realname_unchecked(path, &mut HashMap::new())
.await .await
.ok() .ok()
.filter(|s| s != name) .filter(|s| s != name)

View file

@ -1,3 +1,5 @@
use std::path::Path;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use yazi_macro::relay; use yazi_macro::relay;
use yazi_shared::{Id, Ids, url::{UrlBuf, UrnBuf}}; 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(o_p) = o.parent_url() else { continue };
let Some(n_p) = n.url.parent_url() else { continue }; let Some(n_p) = n.url.parent_url() else { continue };
if o_p != n_p { 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 { for (p, (o, n)) in parents {
match (o.is_empty(), n.is_empty()) { 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 { macro_rules! files {
($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect() }}; ($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() }}; ($map:expr) => {{ $map.iter().map(|(urn, file)| (urn.clone(), file.chdir(wd))).collect() }};
} }
let w = wd.clone(); let w = UrlBuf::from(wd);
match self { match self {
Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha), Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha),
Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket), 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"); .expect("Failed to create Loc from expanded path");
let url = UrlBuf { loc, scheme: url.scheme.clone() }; let url = UrlBuf { loc, scheme: url.scheme.clone() };
match absolute_url(&url) { match absolute_url(url.as_url()) {
Cow::Borrowed(_) => url.into(), UrlCow::Borrowed(_) => url.into(),
Cow::Owned(u) => u.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 b = url.loc.as_os_str().as_encoded_bytes();
let local = !url.scheme.is_virtual(); let local = !url.scheme.is_virtual();
@ -89,7 +89,7 @@ fn absolute_url(url: &UrlBuf) -> Cow<'_, UrlBuf> {
UrlBuf { loc, scheme: url.scheme.clone() }.into() UrlBuf { loc, scheme: url.scheme.clone() }.into()
} else if !url.is_absolute() { } else if !url.is_absolute() {
let cwd = CWD.load(); 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"); .expect("Failed to create Loc from relative path");
UrlBuf { loc, scheme: cwd.scheme.clone() }.into() UrlBuf { loc, scheme: cwd.scheme.clone() }.into()
} else { } else {

View file

@ -26,11 +26,11 @@ where
} }
async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result<UrlBuf> { 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")); 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); let mut s = OsString::with_capacity(e.len() + 1);
s.push("."); s.push(".");
s.push(e); 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 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] #[inline]
pub async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()> pub async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()>
where 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 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] #[inline]
pub fn cache<'a, U>(url: U) -> Option<PathBuf> 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] #[inline]
pub async fn metadata<'a, U>(url: U) -> io::Result<std::fs::Metadata> pub async fn metadata<'a, U>(url: U) -> io::Result<std::fs::Metadata>
where 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] #[inline]
pub async fn open<'a, U>(url: U) -> io::Result<RwFile> pub async fn open<'a, U>(url: U) -> io::Result<RwFile>
where 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] #[inline]
pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()> pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()>
where where

View file

@ -40,11 +40,11 @@ impl FilesSorter {
}), }),
SortBy::Extension => items.sort_unstable_by(|a, b| { SortBy::Extension => items.sort_unstable_by(|a, b| {
let ord = if self.sensitive { 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 { } else {
self.cmp_insensitive( self.cmp_insensitive(
a.url.extension().map_or(&[], |s| s.as_encoded_bytes()), a.url.ext().map_or(&[], |s| s.as_encoded_bytes()),
b.url.extension().map_or(&[], |s| s.as_encoded_bytes()), b.url.ext().map_or(&[], |s| s.as_encoded_bytes()),
self.promote(a, b), 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 { methods.add_async_method("send", |lua, me, value: Value| async move {
match me.0.send(value).await { match me.0.send(value).await {
Ok(()) => true.into_lua_multi(&lua), 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) { fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("send", |lua, me, value: Value| match me.0.send(value) { methods.add_method("send", |lua, me, value: Value| match me.0.send(value) {
Ok(()) => true.into_lua_multi(lua), 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) { match tx.send(value) {
Ok(()) => true.into_lua_multi(lua), 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 { match rx.await {
Ok(value) => value.into_lua_multi(&lua), 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> { pub(super) fn file_cache(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, t: Table| { lua.create_function(|_, t: Table| {
let file: FileRef = t.raw_get("file")?; 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); return Ok(None);
} }

View file

@ -8,27 +8,36 @@ use crate::bindings::ImageInfo;
impl Utils { impl Utils {
pub(super) fn image_info(lua: &Lua) -> mlua::Result<Function> { pub(super) fn image_info(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, url: UrlRef| async move { 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), 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> { pub(super) fn image_show(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move { 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), 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> { pub(super) fn image_precache(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move { 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), 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 area: Area = t.raw_get("area")?;
let mut lock = PreviewLock::try_from(t)?; 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, Ok(text) => text,
Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua), Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua),
Err(e @ PeekError::Unexpected(_)) => { Err(e @ PeekError::Unexpected(_)) => {

View file

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

View file

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

View file

@ -188,7 +188,7 @@ impl File {
continue; 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.prog.send(TaskProg::New(task.id, cha.len))?;
if cha.is_orphan() || (cha.is_link() && !task.follow) { if cha.is_orphan() || (cha.is_link() && !task.follow) {
@ -259,7 +259,7 @@ impl File {
continue; 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.prog.send(TaskProg::New(task.id, cha.len))?;
self.queue(FileIn::Hardlink(task.spawn(from, to, cha)), NORMAL).await?; 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(); let parent = buf[0].0.parent_url().unwrap();
FilesOp::Size( FilesOp::Size(
parent.into(), 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(); .emit();
}); });

View file

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

View file

@ -2,7 +2,7 @@ use std::{ffi::OsStr, hash::{Hash, Hasher}, ops::Deref, path::Path};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use crate::{loc::LocBuf, url::{Uri, Urn, UrnBuf}}; use crate::{loc::LocBuf, url::{Uri, Urn}};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct Loc<'a> { pub struct Loc<'a> {
@ -151,9 +151,6 @@ impl<'a> Loc<'a> {
}) })
} }
#[inline]
pub fn urn_owned(self) -> UrnBuf { self.urn().to_owned() }
#[inline] #[inline]
pub fn base(self) -> &'a Urn { pub fn base(self) -> &'a Urn {
Urn::new(unsafe { Urn::new(unsafe {
@ -179,7 +176,13 @@ impl<'a> Loc<'a> {
pub fn has_trail(self) -> bool { self.bytes().len() != self.urn } pub fn has_trail(self) -> bool { self.bytes().len() != self.urn }
#[inline] #[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] #[inline]
pub fn parent(self) -> Option<&'a Path> { self.inner.parent() } 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 anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::UrnBuf; 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)] #[derive(Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)]
pub struct UrlBuf { pub struct UrlBuf {
@ -12,12 +12,6 @@ pub struct UrlBuf {
pub scheme: Scheme, pub scheme: Scheme,
} }
impl Deref for UrlBuf {
type Target = LocBuf;
fn deref(&self) -> &Self::Target { &self.loc }
}
impl From<LocBuf> for UrlBuf { impl From<LocBuf> for UrlBuf {
fn from(loc: LocBuf) -> Self { Self { loc, scheme: Scheme::Regular } } fn from(loc: LocBuf) -> Self { Self { loc, scheme: Scheme::Regular } }
} }
@ -146,9 +140,7 @@ impl UrlBuf {
} }
#[inline] #[inline]
pub fn as_path(&self) -> Option<&Path> { pub fn as_path(&self) -> Option<&Path> { self.as_url().as_path() }
Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual())
}
#[inline] #[inline]
pub fn into_path(self) -> Option<PathBuf> { pub fn into_path(self) -> Option<PathBuf> {
@ -163,11 +155,8 @@ impl UrlBuf {
Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() } Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() }
} }
// TODO: use Urn instead of UrlBuf
#[inline] #[inline]
pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> { pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> { self.as_url().pair() }
Some((self.parent_url()?, self.loc.urn_owned()))
}
#[inline] #[inline]
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
@ -232,6 +221,30 @@ impl UrlBuf {
// FIXME: remove // FIXME: remove
#[inline] #[inline]
pub fn into_path2(self) -> PathBuf { self.loc.into_path() } 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 { 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 hashbrown::Equivalent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -50,12 +50,16 @@ impl Deref for UrlBufCov {
fn deref(&self) -> &Self::Target { &self.0 } fn deref(&self) -> &Self::Target { &self.0 }
} }
impl From<UrlBufCov> for UrlBuf {
fn from(value: UrlBufCov) -> Self { value.0 }
}
impl From<UrlBuf> for UrlBufCov { impl From<UrlBuf> for UrlBufCov {
fn from(value: UrlBuf) -> Self { Self(value) } fn from(value: UrlBuf) -> Self { Self(value) }
} }
impl From<UrlBufCov> for UrlBuf { impl From<PathBuf> for UrlBufCov {
fn from(value: UrlBufCov) -> Self { value.0 } fn from(value: PathBuf) -> Self { Self(UrlBuf::from(value)) }
} }
impl From<UrlCow<'_>> for UrlBufCov { 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 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)] #[derive(Clone, Eq, Hash, PartialEq)]
pub struct Url<'a> { pub struct Url<'a> {
@ -10,12 +10,6 @@ pub struct Url<'a> {
pub scheme: Scheme, pub scheme: Scheme,
} }
impl<'a> Deref for Url<'a> {
type Target = Loc<'a>;
fn deref(&self) -> &Self::Target { &self.loc }
}
// TODO: remove // TODO: remove
impl<'a> From<&'a Url<'a>> for Url<'a> { 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() } } 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] #[inline]
pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) } 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] #[inline]
pub fn as_url(&'a self) -> Url<'a> { Self::from(self) } 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() } 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> { pub fn base(&self) -> Option<Self> {
use Scheme as S; use Scheme as S;
@ -163,9 +178,17 @@ impl<'a> Url<'a> {
#[inline] #[inline]
pub fn pair(&self) -> Option<(Url<'a>, UrnBuf)> { 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] #[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);
}
}
}