diff --git a/Cargo.lock b/Cargo.lock index 70366b42..2834affd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/cspell.json b/cspell.json index 9d6fcbca..f1b5f8f0 100644 --- a/cspell.json +++ b/cspell.json @@ -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"]} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/yazi-fs/src/mounts/macos.rs b/yazi-fs/src/mounts/macos.rs index f4bcc5d4..74e9c292 100644 --- a/yazi-fs/src/mounts/macos.rs +++ b/yazi-fs/src/mounts/macos.rs @@ -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 = 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(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:?}"); diff --git a/yazi-fs/src/mounts/partition.rs b/yazi-fs/src/mounts/partition.rs index e7721f69..ccbab9da 100644 --- a/yazi-fs/src/mounts/partition.rs +++ b/yazi-fs/src/mounts/partition.rs @@ -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] diff --git a/yazi-fs/src/mounts/partitions.rs b/yazi-fs/src/mounts/partitions.rs index 032ef220..dde393f3 100644 --- a/yazi-fs/src/mounts/partitions.rs +++ b/yazi-fs/src/mounts/partitions.rs @@ -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 } } diff --git a/yazi-vfs/src/files.rs b/yazi-vfs/src/files.rs index 7440888d..726cb90a 100644 --- a/yazi-vfs/src/files.rs +++ b/yazi-vfs/src/files.rs @@ -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, } diff --git a/yazi-watcher/src/backend.rs b/yazi-watcher/src/backend.rs index 646e12be..70a7858a 100644 --- a/yazi-watcher/src/backend.rs +++ b/yazi-watcher/src/backend.rs @@ -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 { diff --git a/yazi-watcher/src/lib.rs b/yazi-watcher/src/lib.rs index 614d0274..c82a22fc 100644 --- a/yazi-watcher/src/lib.rs +++ b/yazi-watcher/src/lib.rs @@ -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> = yazi_shared::RoCell::new(); pub static WATCHER: yazi_shared::RoCell = yazi_shared::RoCell::new(); diff --git a/yazi-watcher/src/local/linked.rs b/yazi-watcher/src/local/linked.rs index 41d5b4c1..f27a50ca 100644 --- a/yazi-watcher/src/local/linked.rs +++ b/yazi-watcher/src/local/linked.rs @@ -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() { diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index 53ca671e..56a08c1f 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -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); +pub(crate) struct Local { + primary: Option, + alternative: PollWatcher, +} impl Local { pub(crate) fn serve(rx: mpsc::UnboundedReceiver, 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| { + let handler = move |res: Result| { 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) { // TODO: revert this once a new notification is implemented let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250)); diff --git a/yazi-watcher/src/remote/remote.rs b/yazi-watcher/src/remote/remote.rs index 1b804f9e..fa0f8270 100644 --- a/yazi-watcher/src/remote/remote.rs +++ b/yazi-watcher/src/remote/remote.rs @@ -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)); diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index eee9a2f1..7d74b1a3 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -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; } diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index cdb69a34..1334a4a3 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -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); +pub struct Watched(HashSet>); + +impl Deref for Watched { + type Target = HashSet>; + + 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) -> (Vec, Vec) { - (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) { self.0.insert(url.into()); } - - #[inline] - pub(crate) fn paths(&self) -> impl Iterator { - 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 { + self.0.iter().filter_map(|watchee| watchee.as_url().as_local()) + } pub(super) fn find_by_cache(&self, cache: PathDyn) -> Option { 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 } } } diff --git a/yazi-watcher/src/watchee.rs b/yazi-watcher/src/watchee.rs new file mode 100644 index 00000000..bcb14f9b --- /dev/null +++ b/yazi-watcher/src/watchee.rs @@ -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(url: U) -> Self + where + U: Into>, + { + 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()), + } + } +} diff --git a/yazi-watcher/src/watcher.rs b/yazi-watcher/src/watcher.rs index a8ce1dcd..23dffdd6 100644 --- a/yazi-watcher/src/watcher.rs +++ b/yazi-watcher/src/watcher.rs @@ -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>, @@ -52,7 +52,8 @@ impl Watcher { async fn watched(mut rx: watch::Receiver>, 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, to_watch: Vec) -> Backend { + async fn diff( + mut rx: watch::Receiver>, + ) -> (watch::Receiver>, Vec>, Vec>) { 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>, + to_watch: Vec>, + ) -> 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:?}"), } }