feat: dual watcher for local files (#3713)

This commit is contained in:
三咲雅 misaki masa 2026-02-26 08:16:58 +08:00 committed by GitHub
parent 0bb6379a2a
commit 9eb4d03a44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 250 additions and 115 deletions

46
Cargo.lock generated
View file

@ -576,9 +576,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.43"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
@ -861,7 +861,7 @@ dependencies = [
"libc",
"mio",
"parking_lot",
"rustix 1.1.3",
"rustix 1.1.4",
"serde",
"signal-hook 0.3.18",
"signal-hook-mio",
@ -2163,9 +2163,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.88"
version = "0.3.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d"
checksum = "f4eacb0641a310445a4c513f2a5e23e19952e269c6a38887254d5f837a305506"
dependencies = [
"once_cell",
"wasm-bindgen",
@ -2344,9 +2344,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litrs"
@ -3866,14 +3866,14 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.11.0",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
]
@ -5029,9 +5029,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.111"
version = "0.2.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac"
checksum = "05d7d0fce354c88b7982aec4400b3e7fcf723c32737cef571bd165f7613557ee"
dependencies = [
"cfg-if",
"once_cell",
@ -5042,9 +5042,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.61"
version = "0.4.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b"
checksum = "ee85afca410ac4abba5b584b12e77ea225db6ee5471d0aebaae0861166f9378a"
dependencies = [
"cfg-if",
"futures-util",
@ -5056,9 +5056,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.111"
version = "0.2.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1"
checksum = "55839b71ba921e4f75b674cb16f843f4b1f3b26ddfcb3454de1cf65cc021ec0f"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -5066,9 +5066,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.111"
version = "0.2.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af"
checksum = "caf2e969c2d60ff52e7e98b7392ff1588bffdd1ccd4769eba27222fd3d621571"
dependencies = [
"bumpalo",
"proc-macro2",
@ -5079,9 +5079,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.111"
version = "0.2.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41"
checksum = "0861f0dcdf46ea819407495634953cdcc8a8c7215ab799a7a7ce366be71c7b30"
dependencies = [
"unicode-ident",
]
@ -5122,9 +5122,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.88"
version = "0.3.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a"
checksum = "10053fbf9a374174094915bbce141e87a6bf32ecd9a002980db4b638405e8962"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -5217,7 +5217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
dependencies = [
"env_home",
"rustix 1.1.3",
"rustix 1.1.4",
"winsafe",
]

View file

@ -1 +1 @@
{"flagWords":[],"language":"en","version":"0.2","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","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU"]}
{"version":"0.2","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","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee"],"language":"en"}

View file

@ -16,7 +16,8 @@ impl Partitions {
where
F: Fn() + Copy + Send + 'static,
{
tokio::task::spawn_blocking(move || {
let rt = tokio::runtime::Handle::current();
std::thread::spawn(move || {
let session = unsafe { DASessionCreate(kCFAllocatorDefault) };
if session.is_null() {
return error!("Cannot create a disk arbitration session");
@ -39,11 +40,12 @@ impl Partitions {
}
let create_context = || {
let rt = rt.clone();
let boxed: Box<dyn Fn()> = Box::new(move || {
if mem::replace(&mut me.write().need_update, true) {
return;
}
Self::update(me, cb);
Self::update(me, cb, &rt);
});
Box::into_raw(Box::new(boxed)) as *mut c_void
};
@ -69,8 +71,11 @@ impl Partitions {
});
}
fn update(me: &'static Locked, cb: impl Fn() + Send + 'static) {
_ = tokio::task::spawn_blocking(move || {
fn update<F>(me: &'static Locked, cb: F, rt: &tokio::runtime::Handle)
where
F: Fn() + Send + 'static,
{
_ = rt.spawn_blocking(move || {
let result = Self::all_names().and_then(Self::all_partitions);
if let Err(ref e) = result {
error!("Error encountered while updating mount points: {e:?}");

View file

@ -14,11 +14,18 @@ pub struct Partition {
}
impl Partition {
// Match mount types that do not reliably emit change notifications, or do not
// update directory metadata on changes, and should be refreshed frequently.
pub fn heuristic(&self) -> bool {
// Match mount types that do not update directory mtime on changes,
// and should be refreshed frequently.
pub fn timeless(&self) -> bool {
let b: &[u8] = self.fstype.as_ref().map_or(b"", |s| s.as_encoded_bytes());
matches!(b, b"exfat" | b"fuse.rclone")
matches!(b, b"exfat")
}
// Match mount types that do not reliably emit change notifications,
// and should be polled for changes.
pub fn soundless(&self) -> bool {
let b: &[u8] = self.fstype.as_ref().map_or(b"", |s| s.as_encoded_bytes());
matches!(b, b"fuse.rclone")
}
#[rustfmt::skip]

View file

@ -31,14 +31,26 @@ impl Partitions {
self.inner.iter().find(|p| p.rdev == Some(dev))
}
pub fn heuristic(&self, _cha: Cha) -> bool {
pub fn timeless(&self, _cha: Cha) -> bool {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
self.by_dev(_cha.dev).is_some_and(|p| p.heuristic())
self.by_dev(_cha.dev).is_some_and(|p| p.timeless())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
// For now, assume other targets update directory stat data correctly
// For now, assume other targets update directory mtime correctly
false
}
}
pub fn soundless(&self, _cha: Cha) -> bool {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
self.by_dev(_cha.dev).is_some_and(|p| p.soundless())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
// For now, assume other targets emit change notifications correctly
false
}
}

View file

@ -70,7 +70,7 @@ impl VfsFiles for Files {
use std::io::ErrorKind;
match Cha::from_url(dir).await {
Ok(c) if !c.is_dir() => FilesOp::issue_error(dir, ErrorKind::NotADirectory).await,
Ok(c) if c.hits(cha) && !PARTITIONS.read().heuristic(cha) => {}
Ok(c) if c.hits(cha) && !PARTITIONS.read().timeless(cha) => {}
Ok(c) => return Some(c),
Err(e) => FilesOp::issue_error(dir, e).await,
}

View file

@ -1,8 +1,7 @@
use anyhow::Result;
use notify::Result;
use tokio::sync::mpsc;
use yazi_shared::url::AsUrl;
use crate::{Reporter, WATCHED, local::{self, LINKED, Linked}, remote};
use crate::{Reporter, WATCHED, Watchee, local::{self, LINKED, Linked}, remote};
pub(crate) struct Backend {
local: local::Local,
@ -14,7 +13,7 @@ impl Backend {
pub(crate) fn serve() -> Self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
yazi_fs::mounts::Partitions::monitor(&yazi_fs::mounts::PARTITIONS, || {
yazi_proxy::MgrProxy::refresh();
yazi_proxy::MgrProxy::watch();
yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount())
});
@ -29,26 +28,18 @@ impl Backend {
}
}
pub(super) fn watch(&mut self, url: impl AsUrl) -> Result<()> {
let url = url.as_url();
if let Some(path) = url.as_local() {
self.local.watch(path)?;
} else {
self.remote.watch(url)?;
pub(super) fn watch(&mut self, watchee: &mut Watchee) -> Result<()> {
match watchee {
Watchee::Local(..) => self.local.watch(watchee),
Watchee::Remote(_) => self.remote.watch(watchee),
}
Ok(())
}
pub(super) fn unwatch(&mut self, url: impl AsUrl) -> Result<()> {
let url = url.as_url();
if let Some(path) = url.as_local() {
self.local.unwatch(path)?;
} else {
self.remote.unwatch(url)?;
pub(super) fn unwatch(&mut self, watchee: &Watchee) -> Result<()> {
match watchee {
Watchee::Local(..) => self.local.unwatch(watchee),
Watchee::Remote(_) => self.remote.unwatch(watchee),
}
Ok(())
}
pub(super) async fn sync(self) -> Self {

View file

@ -1,6 +1,6 @@
yazi_macro::mod_pub!(local remote);
yazi_macro::mod_flat!(backend reporter watched watcher);
yazi_macro::mod_flat!(backend reporter watched watchee watcher);
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();

View file

@ -51,7 +51,7 @@ impl Linked {
let watched = watched.read();
// Remove entries that are no longer watched
linked.write().retain(|from, _| watched.contains(from));
linked.write().retain(|from, _| watched.contains_path(from));
// Update existing entries and remove broken links
for from in watched.paths() {

View file

@ -1,25 +1,27 @@
use std::{path::Path, time::Duration};
use anyhow::Result;
use hashbrown::HashSet;
use notify::{PollWatcher, RecommendedWatcher, RecursiveMode, Watcher};
use notify::{PollWatcher, RecommendedWatcher, RecursiveMode, Result, Watcher};
use tokio::{pin, sync::mpsc::{self, UnboundedReceiver}};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use tracing::error;
use yazi_fs::{File, FilesOp, provider};
use yazi_fs::{File, FilesOp, cha::Cha, mounts::PARTITIONS, provider};
use yazi_shared::url::{UrlBuf, UrlLike};
use yazi_vfs::VfsFile;
use crate::{Reporter, WATCHER};
use crate::{Reporter, WATCHER, Watchee};
pub(crate) struct Local(Box<dyn notify::Watcher + Send>);
pub(crate) struct Local {
primary: Option<RecommendedWatcher>,
alternative: PollWatcher,
}
impl Local {
pub(crate) fn serve(rx: mpsc::UnboundedReceiver<UrlBuf>, reporter: Reporter) -> Self {
tokio::spawn(Self::changed(rx));
let config = notify::Config::default().with_poll_interval(Duration::from_millis(500));
let handler = move |res: Result<notify::Event, notify::Error>| {
let handler = move |res: Result<notify::Event>| {
if let Ok(event) = res
&& !event.kind.is_access()
{
@ -27,35 +29,60 @@ impl Local {
}
};
if cfg!(target_os = "netbsd") || yazi_adapter::WSL.get() {
return Self(Box::new(PollWatcher::new(handler, config).unwrap()));
let primary = RecommendedWatcher::new(handler.clone(), config);
let alternative = PollWatcher::new(handler, config).unwrap();
if let Err(e) = &primary {
error!("Failed to initialize primary watcher: {e:?}");
}
Self(match RecommendedWatcher::new(handler.clone(), config) {
Ok(watcher) => Box::new(watcher),
Err(e) => {
error!("Falling back to PollWatcher due to RecommendedWatcher init failure: {e:?}");
Box::new(PollWatcher::new(handler, config).unwrap())
Self { primary: primary.ok(), alternative }
}
pub(crate) fn watch(&mut self, watchee: &mut Watchee) -> Result<()> {
let (path, alt) =
watchee.as_local_mut().ok_or_else(|| notify::Error::generic("Not a local watchee"))?;
if let Some(primary) = self.primary.as_mut().filter(|_| !*alt) {
match primary.watch(path, RecursiveMode::NonRecursive) {
Ok(()) => return Ok(()),
Err(e) => tracing::warn!("Failed to watch {path:?} with primary watcher: {e:?}"),
}
})
}
pub(crate) fn watch(&mut self, path: &Path) -> Result<()> {
match self.0.watch(path, RecursiveMode::NonRecursive) {
Ok(()) => Ok(()),
Err(e) if matches!(e.kind, notify::ErrorKind::PathNotFound) => Ok(()),
Err(e) => Err(e)?,
}
tracing::debug!("Watching {path:?} with alternative watcher");
*alt = true;
self.alternative.watch(path, RecursiveMode::NonRecursive)
}
pub(crate) fn unwatch(&mut self, path: &Path) -> Result<()> {
match self.0.unwatch(path) {
pub(crate) fn unwatch(&mut self, watchee: &Watchee) -> Result<()> {
let (path, alt) =
watchee.as_local().ok_or_else(|| notify::Error::generic("Not a local watchee"))?;
let result = if alt {
self.alternative.unwatch(path)
} else if let Some(primary) = &mut self.primary {
primary.unwatch(path)
} else {
Ok(())
};
match result {
Ok(()) => Ok(()),
Err(e) if matches!(e.kind, notify::ErrorKind::WatchNotFound) => Ok(()),
Err(e) => Err(e)?,
}
}
pub(crate) fn soundless(path: &Path) -> bool {
if cfg!(target_os = "netbsd") || yazi_adapter::WSL.get() {
return true;
}
let Ok(meta) = path.metadata() else { return true };
PARTITIONS.read().soundless(Cha::new(path.file_name().unwrap_or_default(), meta))
}
async fn changed(rx: UnboundedReceiver<UrlBuf>) {
// TODO: revert this once a new notification is implemented
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));

View file

@ -1,15 +1,15 @@
use std::time::{Duration, SystemTime};
use anyhow::Result;
use hashbrown::HashMap;
use notify::Result;
use tokio::{pin, sync::mpsc::UnboundedReceiver};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_fs::{File, FilesOp};
use yazi_proxy::MgrProxy;
use yazi_shared::url::{Url, UrlBuf, UrlLike};
use yazi_shared::url::{UrlBuf, UrlLike};
use yazi_vfs::VfsFile;
use crate::{Reporter, WATCHER};
use crate::{Reporter, WATCHER, Watchee};
pub(crate) struct Remote;
@ -20,9 +20,9 @@ impl Remote {
Self
}
pub(crate) fn watch(&mut self, _url: Url) -> Result<()> { Ok(()) }
pub(crate) fn watch(&mut self, _watchee: &mut Watchee) -> Result<()> { Ok(()) }
pub(crate) fn unwatch(&mut self, _url: Url) -> Result<()> { Ok(()) }
pub(crate) fn unwatch(&mut self, _watchee: &Watchee) -> Result<()> { Ok(()) }
async fn changed(rx: UnboundedReceiver<(UrlBuf, bool)>) {
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));

View file

@ -36,7 +36,7 @@ impl Reporter {
let watched = WATCHED.read();
for parent in [parent].into_iter().chain(linked) {
if watched.contains(parent) {
if watched.contains_url(parent) {
self.local_tx.send(url.to_owned()).ok();
self.local_tx.send(parent.to_owned()).ok();
}
@ -57,7 +57,7 @@ impl Reporter {
fn report_remote(&self, url: UrlCow) {
let Some(parent) = url.parent() else { return };
if !WATCHED.read().contains(parent) {
if !WATCHED.read().contains_url(parent) {
return;
}

View file

@ -1,32 +1,43 @@
use std::path::Path;
use std::{ops::{Deref, DerefMut}, path::Path};
use hashbrown::HashSet;
use percent_encoding::percent_decode_str;
use yazi_fs::{Xdg, path::PercentEncoding};
use yazi_shared::{path::{Component, PathBufDyn, PathDyn, PathLike}, pool::InternStr, scheme::SchemeKind, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_shared::{path::{Component, PathBufDyn, PathDyn, PathLike}, pool::InternStr, scheme::SchemeKind, url::{AsUrl, UrlBuf}};
use crate::Watchee;
#[derive(Debug, Default)]
pub struct Watched(HashSet<UrlBuf>);
pub struct Watched(HashSet<Watchee<'static>>);
impl Deref for Watched {
type Target = HashSet<Watchee<'static>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl DerefMut for Watched {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl Watched {
#[inline]
pub(crate) fn contains(&self, url: impl AsUrl) -> bool { self.0.contains(&url.as_url()) }
#[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())
pub(super) fn contains_url(&self, url: impl AsUrl) -> bool {
let url = url.as_url();
if url.as_local().is_some() {
self.0.contains(&Watchee::Local(url.into(), false))
|| self.0.contains(&Watchee::Local(url.into(), true))
} else {
self.0.contains(&Watchee::Remote(url.into()))
}
}
#[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_local())
pub(super) fn contains_path(&self, path: &Path) -> bool {
self.0.iter().any(|watchee| watchee.as_url().as_local() == Some(path))
}
#[inline]
pub(crate) fn remove(&mut self, url: impl AsUrl) { self.0.remove(&url.as_url()); }
pub(super) fn paths(&self) -> impl Iterator<Item = &Path> {
self.0.iter().filter_map(|watchee| watchee.as_url().as_local())
}
pub(super) fn find_by_cache(&self, cache: PathDyn) -> Option<UrlBuf> {
let mut it = cache.try_strip_prefix(Xdg::cache_dir()).ok()?.components();
@ -46,6 +57,6 @@ impl Watched {
.ok()?;
let url = UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), domain };
if self.contains(&url) { Some(url) } else { None }
if self.contains_url(&url) { Some(url) } else { None }
}
}

View file

@ -0,0 +1,56 @@
use std::{hash::Hash, path::Path};
use yazi_shared::url::{AsUrl, Url, UrlCow, UrlLike};
use crate::local::Local;
#[derive(Debug, Eq, Hash, PartialEq)]
pub enum Watchee<'a> {
Local(UrlCow<'a>, bool),
Remote(UrlCow<'a>),
}
impl AsUrl for Watchee<'_> {
fn as_url(&self) -> Url<'_> {
match self {
Self::Local(url, _) => url.as_url(),
Self::Remote(url) => url.as_url(),
}
}
}
impl<'a> Watchee<'a> {
pub(super) fn as_local(&self) -> Option<(&Path, bool)> {
Some(match self {
Self::Local(url, alt) => (url.as_local()?, *alt),
Self::Remote(_) => None?,
})
}
pub(super) fn as_local_mut(&mut self) -> Option<(&Path, &mut bool)> {
Some(match self {
Self::Local(url, alt) => (url.as_local()?, alt),
Self::Remote(_) => None?,
})
}
pub(super) fn new<U>(url: U) -> Self
where
U: Into<UrlCow<'a>>,
{
let url = url.into();
if let Some(path) = url.as_local() {
let b = Local::soundless(path);
Self::Local(url, b)
} else {
Self::Remote(url)
}
}
pub(super) fn to_static(&self) -> Watchee<'static> {
match self {
Self::Local(url, alt) => Watchee::Local(url.to_owned().into(), *alt),
Self::Remote(url) => Watchee::Remote(url.to_owned().into()),
}
}
}

View file

@ -4,7 +4,7 @@ use tracing::error;
use yazi_fs::FsUrl;
use yazi_shared::url::{UrlBuf, UrlCow, UrlLike};
use crate::{Reporter, WATCHED, backend::Backend};
use crate::{Reporter, WATCHED, Watchee, backend::Backend};
pub struct Watcher {
tx: watch::Sender<HashSet<UrlBuf>>,
@ -52,7 +52,8 @@ impl Watcher {
async fn watched(mut rx: watch::Receiver<HashSet<UrlBuf>>, mut backend: Backend) {
loop {
let (to_unwatch, to_watch) = WATCHED.read().diff(&rx.borrow_and_update());
let (rx_, to_unwatch, to_watch) = Self::diff(rx).await;
rx = rx_;
if !to_unwatch.is_empty() || !to_watch.is_empty() {
backend = Self::sync(backend, to_unwatch, to_watch).await;
@ -65,17 +66,42 @@ impl Watcher {
}
}
async fn sync(mut backend: Backend, to_unwatch: Vec<UrlBuf>, to_watch: Vec<UrlBuf>) -> Backend {
async fn diff(
mut rx: watch::Receiver<HashSet<UrlBuf>>,
) -> (watch::Receiver<HashSet<UrlBuf>>, Vec<Watchee<'static>>, Vec<Watchee<'static>>) {
tokio::task::spawn_blocking(move || {
for u in to_unwatch {
match backend.unwatch(&u) {
Ok(()) => WATCHED.write().remove(&u),
let new = rx.borrow_and_update();
let new_: HashSet<_> = new.iter().map(Watchee::new).collect();
let old = WATCHED.read();
let to_unwatch = old.difference(&new_).map(Watchee::to_static).collect();
let to_watch = new_.difference(&old).map(Watchee::to_static).collect();
drop(new_);
drop(new);
(rx, to_unwatch, to_watch)
})
.await
.unwrap()
}
async fn sync(
mut backend: Backend,
to_unwatch: Vec<Watchee<'static>>,
to_watch: Vec<Watchee<'static>>,
) -> Backend {
tokio::task::spawn_blocking(move || {
for watchee in to_unwatch {
match backend.unwatch(&watchee) {
Ok(()) => _ = WATCHED.write().remove(&watchee),
Err(e) => error!("Unwatch failed: {e:?}"),
}
}
for u in to_watch {
match backend.watch(&u) {
Ok(()) => WATCHED.write().insert(u),
for mut watchee in to_watch {
match backend.watch(&mut watchee) {
Ok(()) => _ = WATCHED.write().insert(watchee),
Err(e) if matches!(e.kind, notify::ErrorKind::PathNotFound) => {}
Err(e) => error!("Watch failed: {e:?}"),
}
}