mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-24 08:11:04 +00:00
feat: dual watcher for local files (#3713)
This commit is contained in:
parent
0bb6379a2a
commit
9eb4d03a44
15 changed files with 250 additions and 115 deletions
46
Cargo.lock
generated
46
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
@ -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:?}");
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
yazi-watcher/src/watchee.rs
Normal file
56
yazi-watcher/src/watchee.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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:?}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue