diff --git a/Cargo.lock b/Cargo.lock index e9ef0bd8..ed5798b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3485,6 +3485,7 @@ dependencies = [ "yazi-proxy", "yazi-shared", "yazi-term", + "yazi-watcher", "yazi-widgets", ] @@ -3617,10 +3618,8 @@ version = "25.6.11" dependencies = [ "anyhow", "crossterm 0.29.0", - "futures", "hashbrown", "indexmap", - "notify", "parking_lot", "ratatui", "tokio", @@ -3638,6 +3637,7 @@ dependencies = [ "yazi-proxy", "yazi-scheduler", "yazi-shared", + "yazi-watcher", "yazi-widgets", ] @@ -3714,6 +3714,7 @@ dependencies = [ "yazi-proxy", "yazi-shared", "yazi-term", + "yazi-watcher", "yazi-widgets", ] @@ -3886,6 +3887,24 @@ dependencies = [ "yazi-shared", ] +[[package]] +name = "yazi-watcher" +version = "25.6.11" +dependencies = [ + "anyhow", + "hashbrown", + "notify", + "parking_lot", + "tokio", + "tokio-stream", + "tracing", + "yazi-adapter", + "yazi-dds", + "yazi-fs", + "yazi-macro", + "yazi-shared", +] + [[package]] name = "yazi-widgets" version = "25.6.11" diff --git a/cspell.json b/cspell.json index 903b61ef..f18b3e65 100644 --- a/cspell.json +++ b/cspell.json @@ -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"]} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index b1e4389c..489dc72f 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -25,6 +25,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" } yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" } yazi-term = { path = "../yazi-term", version = "25.6.11" } +yazi-watcher = { path = "../yazi-watcher", version = "25.6.11" } yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" } # External dependencies diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index 643774b0..f29476f8 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -145,7 +145,7 @@ impl UserData for File { return None; } - let h = finder.filter.highlighted(me.url.file_name()?)?; + let h = finder.filter.highlighted(me.url.name()?)?; Some(h.into_iter().map(Range::from).collect::>()) }) }); diff --git a/yazi-actor/src/lives/tab.rs b/yazi-actor/src/lives/tab.rs index bcdef269..26c32d4d 100644 --- a/yazi-actor/src/lives/tab.rs +++ b/yazi-actor/src/lives/tab.rs @@ -46,7 +46,8 @@ impl UserData for Tab { fn add_fields>(fields: &mut F) { fields.add_field_method_get("id", |_, me| Ok(Id(me.id))); cached_field!(fields, name, |lua, me| { - lua.create_string(me.current.url.name().as_encoded_bytes()) + let url = &me.current.url; + lua.create_string(url.name().unwrap_or(url.loc.as_os_str()).as_encoded_bytes()) }); cached_field!(fields, mode, |_, me| Mode::make(&me.mode)); diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index acc534d1..0bbec3fe 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -10,9 +10,10 @@ use yazi_dds::Pubsub; use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, provider::{self, local::{Gate, Local}}}; use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; -use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER}; +use yazi_proxy::{AppProxy, HIDER, TasksProxy}; use yazi_shared::{OsStrJoin, event::Data, terminal_clear, url::{Component, UrlBuf}}; use yazi_term::tty::TTY; +use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -119,7 +120,7 @@ impl BulkRename { selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(), ); - if maybe_exists(&new).await && !provider::same(&old, &new).await.unwrap_or(false) { + if maybe_exists(&new).await && !provider::must_identical(&old, &new).await { failed.push((o, n, anyhow!("Destination already exists"))); } else if let Err(e) = provider::rename(&old, &new).await { failed.push((o, n, e.into())); diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index b008a929..1caaa625 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -84,7 +84,7 @@ impl Cd { } if let Some(p) = url.parent_url() { - FilesOp::Upserting(p.into(), [(url.urn_owned(), file)].into()).emit(); + FilesOp::Upserting(p.into(), [(url.urn().to_owned(), file)].into()).emit(); } MgrProxy::reveal(&url); } diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index 573aa94f..5a845e0c 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -38,10 +38,10 @@ impl Actor for Copy { } } "filename" => { - s.push(opt.separator.transform(u.name())); + s.push(opt.separator.transform(u.name().unwrap_or_default())); } "name_without_ext" => { - s.push(opt.separator.transform(u.file_stem().unwrap_or_default())); + s.push(opt.separator.transform(u.stem().unwrap_or_default())); } _ => bail!("Unknown copy type: {}", opt.r#type), }; diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 038fb362..f6f43fde 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -3,8 +3,9 @@ use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname}; use yazi_macro::succ; use yazi_parser::mgr::CreateOpt; -use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER}; +use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; use yazi_shared::{event::Data, url::{UrlBuf, UrnBuf}}; +use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -57,7 +58,7 @@ impl Create { } if let Ok(f) = File::new(&new).await { - FilesOp::Upserting(parent.into(), [(f.urn_owned(), f)].into()).emit(); + FilesOp::Upserting(parent.into(), [(f.urn().to_owned(), f)].into()).emit(); MgrProxy::reveal(&new) } Ok(()) diff --git a/yazi-actor/src/mgr/filter_do.rs b/yazi-actor/src/mgr/filter_do.rs index 5ac1886b..c68fc4da 100644 --- a/yazi-actor/src/mgr/filter_do.rs +++ b/yazi-actor/src/mgr/filter_do.rs @@ -16,7 +16,7 @@ impl Actor for FilterDo { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; - let hovered = cx.hovered().map(|f| f.urn_owned()); + let hovered = cx.hovered().map(|f| f.urn().to_owned()); cx.current_mut().files.set_filter(filter); if cx.hovered().map(|f| f.urn()) != hovered.as_deref() { diff --git a/yazi-actor/src/mgr/hidden.rs b/yazi-actor/src/mgr/hidden.rs index 427e1293..fc44e3d5 100644 --- a/yazi-actor/src/mgr/hidden.rs +++ b/yazi-actor/src/mgr/hidden.rs @@ -18,7 +18,7 @@ impl Actor for Hidden { let state = opt.state.bool(cx.tab().pref.show_hidden); cx.tab_mut().pref.show_hidden = state; - let hovered = cx.hovered().map(|f| f.urn_owned()); + let hovered = cx.hovered().map(|f| f.urn().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index 69b79196..d62657d0 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -1,10 +1,11 @@ use anyhow::Result; use crossterm::{execute, terminal::SetTitle}; use yazi_config::YAZI; -use yazi_fs::CWD; +use yazi_core::tab::Folder; +use yazi_fs::{CWD, Files, FilesOp, cha::Cha}; use yazi_macro::{act, succ}; use yazi_parser::VoidOpt; -use yazi_shared::event::Data; +use yazi_shared::{event::Data, url::UrlBuf}; use yazi_term::tty::TTY; use crate::{Actor, Ctx}; @@ -22,9 +23,9 @@ impl Actor for Refresh { } if let Some(p) = cx.parent() { - cx.mgr.watcher.trigger_dirs(&[cx.current(), p]); + Self::trigger_dirs(&[cx.current(), p]); } else { - cx.mgr.watcher.trigger_dirs(&[cx.current()]); + Self::trigger_dirs(&[cx.current()]); } act!(mgr:peek, cx)?; @@ -35,3 +36,27 @@ impl Actor for Refresh { succ!(); } } + +impl Refresh { + // TODO: performance improvement + pub fn trigger_dirs(folders: &[&Folder]) { + async fn go(cwd: UrlBuf, cha: Cha) { + let Some(cha) = Files::assert_stale(&cwd, cha).await else { return }; + + match Files::from_dir_bulk(&cwd).await { + Ok(files) => FilesOp::Full(cwd, files, cha).emit(), + Err(e) => FilesOp::issue_error(&cwd, e.kind()).await, + } + } + + let futs: Vec<_> = folders + .iter() + .filter(|&f| f.url.is_internal()) + .map(|&f| go(f.url.to_owned(), f.cha)) + .collect(); + + if !futs.is_empty() { + tokio::spawn(futures::future::join_all(futs)); + } + } +} diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index bcb4636f..b85da41d 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -4,8 +4,9 @@ use yazi_dds::Pubsub; use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname}; use yazi_macro::{act, err, succ}; use yazi_parser::mgr::RenameOpt; -use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER}; +use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; use yazi_shared::{Id, event::Data, url::{UrlBuf, UrnBuf}}; +use yazi_watcher::WATCHER; use crate::{Actor, Ctx}; @@ -47,9 +48,8 @@ impl Actor for Rename { return; } - let new = UrlBuf::from(old.parent().unwrap().join(name)); - if opt.force || !maybe_exists(&new).await || provider::same(&old, &new).await.unwrap_or(false) - { + let new = old.parent_url().unwrap().join(name); + if opt.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await { Self::r#do(tab, old, new).await.ok(); } else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { Self::r#do(tab, old, new).await.ok(); @@ -91,12 +91,12 @@ impl Rename { return String::new(); } - let ext = url.extension(); + let ext = url.ext(); match by { "stem" => ext.map_or_else(String::new, |s| format!(".{}", s.to_string_lossy())), - "ext" if ext.is_some() => format!("{}.", url.file_stem().unwrap().to_string_lossy()), - "dot_ext" if ext.is_some() => url.file_stem().unwrap().to_string_lossy().into_owned(), - _ => url.name().to_string_lossy().into_owned(), + "ext" if ext.is_some() => format!("{}.", url.stem().unwrap().to_string_lossy()), + "dot_ext" if ext.is_some() => url.stem().unwrap().to_string_lossy().into_owned(), + _ => url.name().unwrap_or_default().to_string_lossy().into_owned(), } } } diff --git a/yazi-actor/src/mgr/sort.rs b/yazi-actor/src/mgr/sort.rs index 7bc46638..b5f55f8a 100644 --- a/yazi-actor/src/mgr/sort.rs +++ b/yazi-actor/src/mgr/sort.rs @@ -23,7 +23,7 @@ impl Actor for Sort { pref.sort_translit = opt.translit.unwrap_or(pref.sort_translit); let sorter = FilesSorter::from(&*pref); - let hovered = cx.hovered().map(|f| f.urn_owned()); + let hovered = cx.hovered().map(|f| f.urn().to_owned()); let apply = |f: &mut Folder| { if f.stage == FolderStage::Loading { render!(); diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index d26f5594..92b46451 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -1,9 +1,10 @@ use anyhow::Result; -use yazi_core::{mgr::LINKED, tab::Folder}; +use yazi_core::tab::Folder; use yazi_fs::FilesOp; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::UpdateFilesOpt; use yazi_shared::event::Data; +use yazi_watcher::LINKED; use crate::{Actor, Ctx}; diff --git a/yazi-actor/src/mgr/update_mimes.rs b/yazi-actor/src/mgr/update_mimes.rs index b2fbb560..aa44454f 100644 --- a/yazi-actor/src/mgr/update_mimes.rs +++ b/yazi-actor/src/mgr/update_mimes.rs @@ -1,9 +1,9 @@ use anyhow::Result; use hashbrown::HashMap; -use yazi_core::mgr::LINKED; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::UpdateMimesOpt; use yazi_shared::{event::Data, pool::InternStr, url::UrlCov}; +use yazi_watcher::LINKED; use crate::{Actor, Ctx}; diff --git a/yazi-binding/src/error.rs b/yazi-binding/src/error.rs index 1c094ce5..7ffc40aa 100644 --- a/yazi-binding/src/error.rs +++ b/yazi-binding/src/error.rs @@ -14,11 +14,14 @@ pub enum Error { impl Error { pub fn install(lua: &Lua) -> mlua::Result<()> { - let new = lua.create_function(|_, msg: String| Ok(Error::Custom(msg.into())))?; + let new = lua.create_function(|_, msg: String| Ok(Error::custom(msg)))?; lua.globals().raw_set("Error", lua.create_table_from([("custom", new)])?) } + #[inline] + pub fn custom(msg: impl Into) -> Self { Self::Custom(msg.into()) } + pub fn into_string(self) -> SStr { match self { Error::Io(e) => Cow::Owned(e.to_string()), diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index a3b8b9f7..509462cc 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -154,10 +154,7 @@ macro_rules! impl_file_fields { $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to.clone().map($crate::Url::new))); $crate::cached_field!($fields, name, |lua, me| { - Some(me.name()) - .filter(|s| !s.is_empty()) - .map(|s| lua.create_string(s.as_encoded_bytes())) - .transpose() + me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); }; } diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 0202af53..bc3b3643 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -94,16 +94,16 @@ impl FromLua for Url { impl UserData for Url { fn add_fields>(fields: &mut F) { cached_field!(fields, name, |lua, me| { - me.file_name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); cached_field!(fields, stem, |lua, me| { - me.file_stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); cached_field!(fields, ext, |lua, me| { - me.extension().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.ext().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); cached_field!(fields, parent, |_, me| Ok(me.parent_url().map(Self::new))); - cached_field!(fields, urn, |_, me| Ok(Urn::new(me.urn_owned()))); + cached_field!(fields, urn, |_, me| Ok(Urn::new(me.urn()))); cached_field!(fields, base, |_, me| Ok(me.base().map(Self::new))); cached_field!(fields, domain, |lua, me| { me.scheme.domain().map(|s| lua.create_string(s)).transpose() diff --git a/yazi-binding/src/urn.rs b/yazi-binding/src/urn.rs index d30fcfa9..aa321494 100644 --- a/yazi-binding/src/urn.rs +++ b/yazi-binding/src/urn.rs @@ -17,7 +17,7 @@ impl From for yazi_shared::url::UrnBuf { } impl Urn { - pub fn new(urn: yazi_shared::url::UrnBuf) -> Self { Self { inner: urn } } + pub fn new(urn: impl Into) -> Self { Self { inner: urn.into() } } } impl FromLua for Urn { diff --git a/yazi-config/src/theme/icon.rs b/yazi-config/src/theme/icon.rs index ffd9ba20..ebe35821 100644 --- a/yazi-config/src/theme/icon.rs +++ b/yazi-config/src/theme/icon.rs @@ -76,7 +76,7 @@ impl Icon { #[inline] fn match_by_name(&self, file: &File) -> Option<&I> { - let name = file.name().to_str()?; + let name = file.name()?.to_str()?; if file.is_dir() { self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase())) } else { @@ -90,7 +90,7 @@ impl Icon { #[inline] fn match_by_ext(&self, file: &File) -> Option<&I> { - let ext = file.url.extension()?.to_str()?; + let ext = file.url.ext()?.to_str()?; self.exts.get(ext).or_else(|| self.exts.get(&ext.to_ascii_lowercase())) } } diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 5c2dc03f..0cf91439 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -19,15 +19,14 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" } yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" } yazi-scheduler = { path = "../yazi-scheduler", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" } +yazi-watcher = { path = "../yazi-watcher", version = "25.6.11" } yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" } # External dependencies anyhow = { workspace = true } crossterm = { workspace = true } -futures = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } -notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] } parking_lot = { workspace = true } ratatui = { workspace = true } tokio = { workspace = true } diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 6badf67e..77af3b42 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -9,8 +9,3 @@ yazi_macro::mod_pub!(cmp confirm help input mgr notify pick spot tab tasks which); yazi_macro::mod_flat!(core); - -pub fn init() { - mgr::WATCHED.with(<_>::default); - mgr::LINKED.with(<_>::default); -} diff --git a/yazi-core/src/mgr/linked.rs b/yazi-core/src/mgr/linked.rs deleted file mode 100644 index ea7a5830..00000000 --- a/yazi-core/src/mgr/linked.rs +++ /dev/null @@ -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 /* from ==> to */); - -impl Deref for Linked { - type Target = HashMap; - - 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 + 'b> - where - 'a: 'b, - T: Into>, - { - 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 { - 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![] - } - } -} diff --git a/yazi-core/src/mgr/mgr.rs b/yazi-core/src/mgr/mgr.rs index 15761fbd..ea095c53 100644 --- a/yazi-core/src/mgr/mgr.rs +++ b/yazi-core/src/mgr/mgr.rs @@ -2,8 +2,9 @@ use ratatui::layout::Rect; use yazi_adapter::Dimension; use yazi_config::popup::{Origin, Position}; use yazi_shared::url::UrlBuf; +use yazi_watcher::Watcher; -use super::{Mimetype, Tabs, Watcher, Yanked}; +use super::{Mimetype, Tabs, Yanked}; use crate::tab::{Folder, Tab}; pub struct Mgr { diff --git a/yazi-core/src/mgr/mod.rs b/yazi-core/src/mgr/mod.rs index 26bc8a47..fee53c44 100644 --- a/yazi-core/src/mgr/mod.rs +++ b/yazi-core/src/mgr/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(linked mgr mimetype tabs watcher yanked); +yazi_macro::mod_flat!(mgr mimetype tabs yanked); diff --git a/yazi-core/src/mgr/watcher.rs b/yazi-core/src/mgr/watcher.rs deleted file mode 100644 index 55ac0055..00000000 --- a/yazi-core/src/mgr/watcher.rs +++ /dev/null @@ -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>> = RoCell::new(); -pub static LINKED: RoCell> = RoCell::new(); - -pub struct Watcher { - in_tx: watch::Sender>, - out_tx: mpsc::UnboundedSender, -} - -// 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| { - 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) { - self.in_tx.send(it.filter(|u| u.is_regular()).cloned().collect()).ok(); - } - - pub fn push_files(&self, urls: Vec) { - Self::push_files_impl(&self.out_tx, urls.into_iter()); - } - - fn push_files_impl(out_tx: &mpsc::UnboundedSender, urls: impl Iterator) { - 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>, - 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) { - // 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( - mut watcher: W, - to_unwatch: HashSet, - to_watch: HashSet, - ) -> 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) { - 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; - } -} diff --git a/yazi-core/src/tab/finder.rs b/yazi-core/src/tab/finder.rs index 7c02d51d..ca61b217 100644 --- a/yazi-core/src/tab/finder.rs +++ b/yazi-core/src/tab/finder.rs @@ -29,7 +29,9 @@ impl Finder { pub fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option { for i in !include as usize..files.len() { let idx = (cursor + files.len() - i) % files.len(); - if self.filter.matches(files[idx].name()) { + if let Some(s) = files[idx].name() + && self.filter.matches(s) + { return Some(idx as isize - cursor as isize); } } @@ -39,7 +41,9 @@ impl Finder { pub fn next(&self, files: &Files, cursor: usize, include: bool) -> Option { for i in !include as usize..files.len() { let idx = (cursor + i) % files.len(); - if self.filter.matches(files[idx].name()) { + if let Some(s) = files[idx].name() + && self.filter.matches(s) + { return Some(idx as isize - cursor as isize); } } @@ -54,11 +58,11 @@ impl Finder { let mut i = 0u8; for file in folder.files.iter() { - if !self.filter.matches(file.name()) { + if file.name().is_none_or(|s| !self.filter.matches(s)) { continue; } - self.matched.insert(file.urn_owned(), i); + self.matched.insert(file.urn().to_owned(), i); if self.matched.len() > 99 { break; } diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 5187bbc1..bb663085 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -103,7 +103,7 @@ impl Folder { self.scroll(step) }; - self.trace = self.hovered().filter(|_| b).map(|h| h.urn_owned()).or(self.trace.take()); + self.trace = self.hovered().filter(|_| b).map(|h| h.urn().to_owned()).or(self.trace.take()); b |= self.squeeze_offset(); self.sync_page(false); diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index 7b8b38eb..04566676 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -8,7 +8,7 @@ use crate::mgr::Yanked; impl Tasks { pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) { for u in src.iter() { - let to = dest.join(u.file_name().unwrap()); + let to = dest.join(u.name().unwrap()); if force && *u == to { debug!("file_cut: same file, skipping {:?}", to); } else { @@ -19,7 +19,7 @@ impl Tasks { pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { for u in src.iter() { - let to = dest.join(u.file_name().unwrap()); + let to = dest.join(u.name().unwrap()); if force && *u == to { debug!("file_copy: same file, skipping {:?}", to); } else { @@ -30,7 +30,7 @@ impl Tasks { pub fn file_link(&self, src: &HashSet, dest: &UrlBuf, relative: bool, force: bool) { for u in src { - let to = dest.join(u.file_name().unwrap()); + let to = dest.join(u.name().unwrap()); if force && *u == to { debug!("file_link: same file, skipping {:?}", to); } else { @@ -41,7 +41,7 @@ impl Tasks { pub fn file_hardlink(&self, src: &HashSet, dest: &UrlBuf, force: bool, follow: bool) { for u in src { - let to = dest.join(u.file_name().unwrap()); + let to = dest.join(u.name().unwrap()); if force && *u == to { debug!("file_hardlink: same file, skipping {:?}", to); } else { diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index f1badf77..fe25c533 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -37,6 +37,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" } yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" } yazi-shared = { path = "../yazi-shared", version = "25.6.11" } yazi-term = { path = "../yazi-term", version = "25.6.11" } +yazi-watcher = { path = "../yazi-watcher", version = "25.6.11" } yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" } # External dependencies diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index 62089e61..2c321ed2 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -32,9 +32,9 @@ async fn main() -> anyhow::Result<()> { yazi_widgets::init(); - yazi_plugin::init()?; + yazi_watcher::init(); - yazi_core::init(); + yazi_plugin::init()?; yazi_dds::serve(); app::App::serve().await diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index e6610c02..d662925c 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -1,7 +1,7 @@ -use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::PathBuf}; +use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; use anyhow::Result; -use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn, UrnBuf}; +use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn}; use crate::{cha::Cha, provider}; @@ -47,7 +47,7 @@ impl File { pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } #[inline] - pub fn chdir(&self, wd: &UrlBuf) -> Self { + pub fn chdir(&self, wd: &Path) -> Self { Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() } } } @@ -64,13 +64,10 @@ impl File { pub fn urn(&self) -> &Urn { self.url.urn() } #[inline] - pub fn urn_owned(&self) -> UrnBuf { self.url.urn_owned() } + pub fn name(&self) -> Option<&OsStr> { self.url.name() } #[inline] - pub fn name(&self) -> &OsStr { self.url.name() } - - #[inline] - pub fn stem(&self) -> Option<&OsStr> { self.url.file_stem() } + pub fn stem(&self) -> Option<&OsStr> { self.url.stem() } } impl Hash for File { diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 84a58efa..4082bad0 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -162,7 +162,7 @@ impl Files { macro_rules! go { ($dist:expr, $src:expr, $inc:literal) => { - let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn_owned(), f)).collect(); + let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn().to_owned(), f)).collect(); for f in &$dist { if todo.remove(f.urn()).is_some() && todo.is_empty() { break; @@ -293,7 +293,7 @@ impl Files { } self.update_deleting( - files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn_owned()).collect(), + files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn().to_owned()).collect(), ); let (hidden, items) = self.update_updating(files); diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index bdafc13a..3280a7fc 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -7,7 +7,7 @@ use hashbrown::{HashMap, HashSet}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; use yazi_shared::url::{Component, Url, UrlBuf}; -use crate::{cha::Cha, provider}; +use crate::{cha::Cha, provider::{self, local::Local}}; #[inline] pub async fn maybe_exists<'a>(url: impl Into>) -> bool { @@ -31,13 +31,13 @@ pub fn ok_or_not_found(result: io::Result) -> io::Result { } } -pub async fn realname(u: &UrlBuf) -> Option { - let name = u.file_name()?; - if *u == provider::canonicalize(u).await.ok()? { +pub async fn realname(url: &UrlBuf) -> Option { + let (path, name) = (url.as_path()?, url.name()?); + if path == Local::canonicalize(path).await.ok()? { return None; } - realname_unchecked(u, &mut HashMap::new()) + realname_unchecked(path, &mut HashMap::new()) .await .ok() .filter(|s| s != name) diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index b3f1303f..c0a8c6fd 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use hashbrown::{HashMap, HashSet}; use yazi_macro::relay; use yazi_shared::{Id, Ids, url::{UrlBuf, UrnBuf}}; @@ -55,9 +57,9 @@ impl FilesOp { let Some(o_p) = o.parent_url() else { continue }; let Some(n_p) = n.url.parent_url() else { continue }; if o_p != n_p { - parents.entry_ref(&o_p).or_default().0.insert(o.urn_owned()); + parents.entry_ref(&o_p).or_default().0.insert(o.urn().to_owned()); } - parents.entry_ref(&n_p).or_default().1.insert(n.urn_owned(), n); + parents.entry_ref(&n_p).or_default().1.insert(n.urn().to_owned(), n); } for (p, (o, n)) in parents { match (o.is_empty(), n.is_empty()) { @@ -94,7 +96,7 @@ impl FilesOp { } } - pub fn chdir(&self, wd: &UrlBuf) -> Self { + pub fn chdir(&self, wd: &Path) -> Self { macro_rules! files { ($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect() }}; } @@ -102,7 +104,7 @@ impl FilesOp { ($map:expr) => {{ $map.iter().map(|(urn, file)| (urn.clone(), file.chdir(wd))).collect() }}; } - let w = wd.clone(); + let w = UrlBuf::from(wd); match self { Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha), Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket), diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index 2519f042..be0dbb4b 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -30,9 +30,9 @@ fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> { .expect("Failed to create Loc from expanded path"); let url = UrlBuf { loc, scheme: url.scheme.clone() }; - match absolute_url(&url) { - Cow::Borrowed(_) => url.into(), - Cow::Owned(u) => u.into(), + match absolute_url(url.as_url()) { + UrlCow::Borrowed(_) => url.into(), + UrlCow::Owned(u) => u.into(), } } @@ -62,7 +62,7 @@ fn expand_variables(p: &Path) -> Cow<'_, Path> { } } -fn absolute_url(url: &UrlBuf) -> Cow<'_, UrlBuf> { +fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> { let b = url.loc.as_os_str().as_encoded_bytes(); let local = !url.scheme.is_virtual(); @@ -89,7 +89,7 @@ fn absolute_url(url: &UrlBuf) -> Cow<'_, UrlBuf> { UrlBuf { loc, scheme: url.scheme.clone() }.into() } else if !url.is_absolute() { let cwd = CWD.load(); - let loc = LocBuf::with(cwd.loc.join(&url.loc), url.uri().count(), url.urn().count()) + let loc = LocBuf::with(cwd.loc.join(url.loc), url.uri().count(), url.urn().count()) .expect("Failed to create Loc from relative path"); UrlBuf { loc, scheme: cwd.scheme.clone() }.into() } else { diff --git a/yazi-fs/src/path/path.rs b/yazi-fs/src/path/path.rs index b4cb7162..ab3036aa 100644 --- a/yazi-fs/src/path/path.rs +++ b/yazi-fs/src/path/path.rs @@ -26,11 +26,11 @@ where } async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result { - let Some(stem) = url.file_stem().map(|s| s.to_owned()) else { + let Some(stem) = url.stem().map(|s| s.to_owned()) else { return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem")); }; - let dot_ext = url.extension().map_or_else(OsString::new, |e| { + let dot_ext = url.ext().map_or_else(OsString::new, |e| { let mut s = OsString::with_capacity(e.len() + 1); s.push("."); s.push(e); diff --git a/yazi-fs/src/provider/local/casefold.rs b/yazi-fs/src/provider/local/casefold.rs new file mode 100644 index 00000000..a86da5d0 --- /dev/null +++ b/yazi-fs/src/provider/local/casefold.rs @@ -0,0 +1,91 @@ +use std::{io, path::{Path, PathBuf}}; + +pub async fn valid_name_case(path: impl AsRef) -> io::Result { + 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 { + 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 { + 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 { + 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)) + } +} diff --git a/yazi-fs/src/provider/local/identical.rs b/yazi-fs/src/provider/local/identical.rs new file mode 100644 index 00000000..c34ac70c --- /dev/null +++ b/yazi-fs/src/provider/local/identical.rs @@ -0,0 +1,55 @@ +use std::{io, path::{Path, PathBuf}}; + +#[inline] +pub async fn identical(a: P, b: Q) -> io::Result +where + P: AsRef, + Q: AsRef, +{ + 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 { + 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 { + 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::() 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)?) +} diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index 5ec5e5ba..45760647 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -202,63 +202,6 @@ impl Local { tokio::fs::rename(from, to).await } - #[inline] - pub async fn same(a: P, b: Q) -> io::Result - where - P: AsRef, - Q: AsRef, - { - Self::same_impl(a.as_ref(), b.as_ref()).await - } - - #[cfg(unix)] - async fn same_impl(a: &Path, b: &Path) -> io::Result { - 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 { - 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 { - let path = path.to_owned(); - tokio::task::spawn_blocking(move || { - let file = std::fs::OpenOptions::new() - .access_mode(0) - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) - .open(path)?; - - let mut buf = [0u16; MAX_PATH as usize]; - let len = unsafe { - GetFinalPathNameByHandleW( - file.as_raw_handle() as HANDLE, - buf.as_mut_ptr(), - buf.len() as u32, - VOLUME_NAME_DOS, - ) - }; - - if len == 0 { - Err(io::Error::last_os_error()) - } else { - Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize]))) - } - }) - .await? - } - - Ok(final_name(a).await? == final_name(b).await?) - } - #[inline] pub async fn symlink_dir(original: P, link: Q) -> io::Result<()> where diff --git a/yazi-fs/src/provider/local/mod.rs b/yazi-fs/src/provider/local/mod.rs index 6e6b0857..c8ddd193 100644 --- a/yazi-fs/src/provider/local/mod.rs +++ b/yazi-fs/src/provider/local/mod.rs @@ -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); diff --git a/yazi-fs/src/provider/provider.rs b/yazi-fs/src/provider/provider.rs index 2f53b188..9bbf0742 100644 --- a/yazi-fs/src/provider/provider.rs +++ b/yazi-fs/src/provider/provider.rs @@ -2,7 +2,7 @@ use std::{io, path::{Path, PathBuf}}; use yazi_shared::url::{Url, UrlBuf}; -use crate::{cha::Cha, provider::{ReadDir, ReadDirSync, RwFile, local::Local}}; +use crate::{cha::Cha, provider::{ReadDir, ReadDirSync, RwFile, local::{self, Local}}}; #[inline] pub fn cache<'a, U>(url: U) -> Option @@ -86,6 +86,19 @@ where } } +#[inline] +pub async fn identical<'a, U, V>(a: U, b: V) -> io::Result +where + U: Into>, + V: Into>, +{ + if let (Some(a), Some(b)) = (a.into().as_path(), b.into().as_path()) { + local::identical(a, b).await + } else { + Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) + } +} + #[inline] pub async fn metadata<'a, U>(url: U) -> io::Result where @@ -98,6 +111,15 @@ where } } +#[inline] +pub async fn must_identical<'a, U, V>(a: U, b: V) -> bool +where + U: Into>, + V: Into>, +{ + identical(a, b).await.unwrap_or(false) +} + #[inline] pub async fn open<'a, U>(url: U) -> io::Result where @@ -195,19 +217,6 @@ where } } -#[inline] -pub async fn same<'a, U, V>(a: U, b: V) -> io::Result -where - U: Into>, - V: Into>, -{ - if let (Some(a), Some(b)) = (a.into().as_path(), b.into().as_path()) { - Local::same(a, b).await - } else { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) - } -} - #[inline] pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()> where diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 2e3a8544..286c8f51 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -40,11 +40,11 @@ impl FilesSorter { }), SortBy::Extension => items.sort_unstable_by(|a, b| { let ord = if self.sensitive { - self.cmp(a.url.extension(), b.url.extension(), self.promote(a, b)) + self.cmp(a.url.ext(), b.url.ext(), self.promote(a, b)) } else { self.cmp_insensitive( - a.url.extension().map_or(&[], |s| s.as_encoded_bytes()), - b.url.extension().map_or(&[], |s| s.as_encoded_bytes()), + a.url.ext().map_or(&[], |s| s.as_encoded_bytes()), + b.url.ext().map_or(&[], |s| s.as_encoded_bytes()), self.promote(a, b), ) }; diff --git a/yazi-plugin/src/bindings/chan.rs b/yazi-plugin/src/bindings/chan.rs index 9e3436a1..9c95fcaa 100644 --- a/yazi-plugin/src/bindings/chan.rs +++ b/yazi-plugin/src/bindings/chan.rs @@ -9,7 +9,7 @@ impl UserData for MpscTx { methods.add_async_method("send", |lua, me, value: Value| async move { match me.0.send(value).await { Ok(()) => true.into_lua_multi(&lua), - Err(e) => (false, Error::Custom(e.to_string().into())).into_lua_multi(&lua), + Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua), } }); } @@ -33,7 +33,7 @@ impl UserData for MpscUnboundedTx { fn add_methods>(methods: &mut M) { methods.add_method("send", |lua, me, value: Value| match me.0.send(value) { Ok(()) => true.into_lua_multi(lua), - Err(e) => (false, Error::Custom(e.to_string().into())).into_lua_multi(lua), + Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(lua), }); } } @@ -60,7 +60,7 @@ impl UserData for OneshotTx { }; match tx.send(value) { Ok(()) => true.into_lua_multi(lua), - Err(_) => (false, Error::Custom("Oneshot receiver closed".into())).into_lua_multi(lua), + Err(_) => (false, Error::custom("Oneshot receiver closed")).into_lua_multi(lua), } }); } @@ -74,7 +74,7 @@ impl UserData for OneshotRx { }; match rx.await { Ok(value) => value.into_lua_multi(&lua), - Err(e) => (Value::Nil, Error::Custom(e.to_string().into())).into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), } }); } diff --git a/yazi-plugin/src/utils/cache.rs b/yazi-plugin/src/utils/cache.rs index c5ffbdc5..353cbe79 100644 --- a/yazi-plugin/src/utils/cache.rs +++ b/yazi-plugin/src/utils/cache.rs @@ -11,7 +11,7 @@ impl Utils { pub(super) fn file_cache(lua: &Lua) -> mlua::Result { lua.create_function(|_, t: Table| { let file: FileRef = t.raw_get("file")?; - if file.url.parent() == Some(&YAZI.preview.cache_dir) { + if file.url.parent_url() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) { return Ok(None); } diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index 5e840fa4..ff6bae3c 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -8,27 +8,36 @@ use crate::bindings::ImageInfo; impl Utils { pub(super) fn image_info(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { - match yazi_adapter::ImageInfo::new(&url).await { + let Some(path) = url.as_path() else { + return (Value::Nil, Error::custom("Only local files are supported")).into_lua_multi(&lua); + }; + match yazi_adapter::ImageInfo::new(path).await { Ok(info) => ImageInfo::from(info).into_lua_multi(&lua), - Err(e) => (Value::Nil, Error::Custom(e.to_string().into())).into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), } }) } pub(super) fn image_show(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move { - match ADAPTOR.get().image_show(&url, *rect).await { + let Some(path) = url.as_path() else { + return (Value::Nil, Error::custom("Only local files are supported")).into_lua_multi(&lua); + }; + match ADAPTOR.get().image_show(path, *rect).await { Ok(area) => Rect::from(area).into_lua_multi(&lua), - Err(e) => (Value::Nil, Error::Custom(e.to_string().into())).into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::custom(e.to_string())).into_lua_multi(&lua), } }) } pub(super) fn image_precache(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move { - match Image::precache(&src, &dist).await { + let Some((src, dist)) = src.as_path().zip(dist.as_path()) else { + return (Value::Nil, Error::custom("Only local files are supported")).into_lua_multi(&lua); + }; + match Image::precache(src, dist).await { Ok(()) => true.into_lua_multi(&lua), - Err(e) => (false, Error::Custom(e.to_string().into())).into_lua_multi(&lua), + Err(e) => (false, Error::custom(e.to_string())).into_lua_multi(&lua), } }) } diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index f2a6b0b9..51a453a4 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -14,7 +14,11 @@ impl Utils { let area: Area = t.raw_get("area")?; let mut lock = PreviewLock::try_from(t)?; - let inner = match Highlighter::new(&lock.url).highlight(lock.skip, area.size()).await { + let Some(path) = lock.url.as_path() else { + return "Only local files are supported".into_lua_multi(&lua); + }; + + let inner = match Highlighter::new(path).highlight(lock.skip, area.size()).await { Ok(text) => text, Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua), Err(e @ PeekError::Unexpected(_)) => { diff --git a/yazi-plugin/src/utils/spot.rs b/yazi-plugin/src/utils/spot.rs index a8b99bdd..e75b7a84 100644 --- a/yazi-plugin/src/utils/spot.rs +++ b/yazi-plugin/src/utils/spot.rs @@ -24,9 +24,7 @@ impl Utils { style: THEME.spot.border.into(), titles: vec![( ratatui::widgets::block::Position::Top, - ratatui::text::Line::raw(lock.url.name().to_string_lossy().into_owned()) - .centered() - .style(THEME.spot.title), + ratatui::text::Line::raw("Spot").centered().style(THEME.spot.title), )], }), Renderable::Table(Box::new(table)), diff --git a/yazi-proxy/src/semaphore.rs b/yazi-proxy/src/semaphore.rs index c98d689c..d2274e04 100644 --- a/yazi-proxy/src/semaphore.rs +++ b/yazi-proxy/src/semaphore.rs @@ -3,9 +3,4 @@ use yazi_shared::RoCell; pub static HIDER: RoCell = RoCell::new(); -pub static WATCHER: RoCell = RoCell::new(); - -pub(super) fn init_semaphore() { - HIDER.init(Semaphore::new(1)); - WATCHER.init(Semaphore::new(1)); -} +pub(super) fn init_semaphore() { HIDER.init(Semaphore::new(1)); } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 9ab415d9..f9050465 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -188,7 +188,7 @@ impl File { continue; } - let to = dest.join(from.file_name().unwrap()); + let to = dest.join(from.name().unwrap()); self.prog.send(TaskProg::New(task.id, cha.len))?; if cha.is_orphan() || (cha.is_link() && !task.follow) { @@ -259,7 +259,7 @@ impl File { continue; } - let to = dest.join(from.file_name().unwrap()); + let to = dest.join(from.name().unwrap()); self.prog.send(TaskProg::New(task.id, cha.len))?; self.queue(FileIn::Hardlink(task.spawn(from, to, cha)), NORMAL).await?; } diff --git a/yazi-scheduler/src/prework/prework.rs b/yazi-scheduler/src/prework/prework.rs index 2eb27257..3b481fb7 100644 --- a/yazi-scheduler/src/prework/prework.rs +++ b/yazi-scheduler/src/prework/prework.rs @@ -93,7 +93,7 @@ impl Prework { let parent = buf[0].0.parent_url().unwrap(); FilesOp::Size( parent.into(), - HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn_owned(), s))), + HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn().to_owned(), s))), ) .emit(); }); diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index f3095f7b..b4936923 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -2,7 +2,7 @@ use std::{cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Has use anyhow::Result; -use crate::{loc::Loc, url::{Uri, Urn, UrnBuf}}; +use crate::{loc::Loc, url::{Uri, Urn}}; #[derive(Clone, Default, Eq)] pub struct LocBuf { @@ -160,9 +160,6 @@ impl LocBuf { #[inline] pub fn urn(&self) -> &Urn { self.as_loc().urn() } - #[inline] - pub fn urn_owned(&self) -> UrnBuf { self.as_loc().urn_owned() } - #[inline] pub fn base(&self) -> &Urn { self.as_loc().base() } @@ -176,7 +173,13 @@ impl LocBuf { pub fn has_trail(&self) -> bool { self.as_loc().has_trail() } #[inline] - pub fn name(&self) -> &OsStr { self.as_loc().name() } + pub fn name(&self) -> Option<&OsStr> { self.as_loc().name() } + + #[inline] + pub fn stem(&self) -> Option<&OsStr> { self.as_loc().stem() } + + #[inline] + pub fn ext(&self) -> Option<&OsStr> { self.as_loc().ext() } } #[cfg(test)] @@ -189,21 +192,21 @@ mod tests { let loc: LocBuf = Path::new("/").into(); assert_eq!(loc.uri().as_os_str(), OsStr::new("/")); assert_eq!(loc.urn().as_os_str(), OsStr::new("")); - assert_eq!(loc.name(), OsStr::new("")); + assert_eq!(loc.name(), None); assert_eq!(loc.base().as_os_str(), OsStr::new("")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: LocBuf = Path::new("/root").into(); assert_eq!(loc.uri().as_os_str(), OsStr::new("root")); assert_eq!(loc.urn().as_os_str(), OsStr::new("root")); - assert_eq!(loc.name(), OsStr::new("root")); + assert_eq!(loc.name().unwrap(), OsStr::new("root")); assert_eq!(loc.base().as_os_str(), OsStr::new("/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: LocBuf = Path::new("/root/code/foo/").into(); assert_eq!(loc.uri().as_os_str(), OsStr::new("foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo")); - assert_eq!(loc.name(), OsStr::new("foo")); + assert_eq!(loc.name().unwrap(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); } @@ -213,42 +216,42 @@ mod tests { let loc = LocBuf::with("/".into(), 0, 0)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("")); assert_eq!(loc.urn().as_os_str(), OsStr::new("")); - assert_eq!(loc.name(), OsStr::new("")); + assert_eq!(loc.name(), None); assert_eq!(loc.base().as_os_str(), OsStr::new("/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc = LocBuf::with("/root/code/".into(), 1, 1)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("code")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code")); - assert_eq!(loc.name(), OsStr::new("code")); + assert_eq!(loc.name().unwrap(), OsStr::new("code")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/")); let loc = LocBuf::with("/root/code/foo//".into(), 2, 1)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo")); - assert_eq!(loc.name(), OsStr::new("foo")); + assert_eq!(loc.name().unwrap(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); let loc = LocBuf::with("/root/code/foo//".into(), 2, 2)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); - assert_eq!(loc.name(), OsStr::new("foo")); + assert_eq!(loc.name().unwrap(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/")); let loc = LocBuf::with("/root/code/foo//bar/".into(), 2, 2)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("foo//bar")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo//bar")); - assert_eq!(loc.name(), OsStr::new("bar")); + assert_eq!(loc.name().unwrap(), OsStr::new("bar")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); let loc = LocBuf::with("/root/code/foo//bar/".into(), 3, 2)?; assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo//bar")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo//bar")); - assert_eq!(loc.name(), OsStr::new("bar")); + assert_eq!(loc.name().unwrap(), OsStr::new("bar")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); Ok(()) diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 506d271b..7273aafd 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -2,7 +2,7 @@ use std::{ffi::OsStr, hash::{Hash, Hasher}, ops::Deref, path::Path}; use anyhow::{Result, bail}; -use crate::{loc::LocBuf, url::{Uri, Urn, UrnBuf}}; +use crate::{loc::LocBuf, url::{Uri, Urn}}; #[derive(Clone, Copy, Debug)] pub struct Loc<'a> { @@ -151,9 +151,6 @@ impl<'a> Loc<'a> { }) } - #[inline] - pub fn urn_owned(self) -> UrnBuf { self.urn().to_owned() } - #[inline] pub fn base(self) -> &'a Urn { Urn::new(unsafe { @@ -179,7 +176,13 @@ impl<'a> Loc<'a> { pub fn has_trail(self) -> bool { self.bytes().len() != self.urn } #[inline] - pub fn name(self) -> &'a OsStr { self.inner.file_name().unwrap_or(OsStr::new("")) } + pub fn name(self) -> Option<&'a OsStr> { self.inner.file_name() } + + #[inline] + pub fn stem(self) -> Option<&'a OsStr> { self.inner.file_stem() } + + #[inline] + pub fn ext(self) -> Option<&'a OsStr> { self.inner.extension() } #[inline] pub fn parent(self) -> Option<&'a Path> { self.inner.parent() } diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 4310b61f..32178b66 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -1,10 +1,10 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}, str::FromStr}; +use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, path::{Path, PathBuf}, str::FromStr}; use anyhow::Result; use serde::{Deserialize, Serialize}; use super::UrnBuf; -use crate::{loc::LocBuf, pool::Pool, url::{Components, Display, Encode, EncodeTilded, Scheme, Url, UrlCow, Urn}}; +use crate::{loc::LocBuf, pool::Pool, url::{Components, Display, Encode, EncodeTilded, Scheme, Uri, Url, UrlCow, Urn}}; #[derive(Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)] pub struct UrlBuf { @@ -12,12 +12,6 @@ pub struct UrlBuf { pub scheme: Scheme, } -impl Deref for UrlBuf { - type Target = LocBuf; - - fn deref(&self) -> &Self::Target { &self.loc } -} - impl From for UrlBuf { fn from(loc: LocBuf) -> Self { Self { loc, scheme: Scheme::Regular } } } @@ -146,9 +140,7 @@ impl UrlBuf { } #[inline] - pub fn as_path(&self) -> Option<&Path> { - Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual()) - } + pub fn as_path(&self) -> Option<&Path> { self.as_url().as_path() } #[inline] pub fn into_path(self) -> Option { @@ -163,11 +155,8 @@ impl UrlBuf { Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() } } - // TODO: use Urn instead of UrlBuf #[inline] - pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> { - Some((self.parent_url()?, self.loc.urn_owned())) - } + pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> { self.as_url().pair() } #[inline] pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } @@ -232,6 +221,30 @@ impl UrlBuf { // FIXME: remove #[inline] pub fn into_path2(self) -> PathBuf { self.loc.into_path() } + + #[inline] + pub fn name(&self) -> Option<&OsStr> { self.as_url().name() } + + #[inline] + pub fn stem(&self) -> Option<&OsStr> { self.as_url().stem() } + + #[inline] + pub fn ext(&self) -> Option<&OsStr> { self.as_url().ext() } + + #[inline] + pub fn uri(&self) -> &Uri { self.as_url().uri() } + + #[inline] + pub fn urn(&self) -> &Urn { self.as_url().urn() } + + #[inline] + pub fn is_absolute(&self) -> bool { self.as_url().is_absolute() } + + #[inline] + pub fn has_root(&self) -> bool { self.as_url().has_root() } + + #[inline] + pub fn has_trail(&self) -> bool { self.as_url().has_trail() } } impl Debug for UrlBuf { diff --git a/yazi-shared/src/url/cov.rs b/yazi-shared/src/url/cov.rs index 485c4034..d6f56ef3 100644 --- a/yazi-shared/src/url/cov.rs +++ b/yazi-shared/src/url/cov.rs @@ -1,4 +1,4 @@ -use std::{hash::{Hash, Hasher}, ops::Deref}; +use std::{hash::{Hash, Hasher}, ops::Deref, path::PathBuf}; use hashbrown::Equivalent; use serde::{Deserialize, Serialize}; @@ -50,12 +50,16 @@ impl Deref for UrlBufCov { fn deref(&self) -> &Self::Target { &self.0 } } +impl From for UrlBuf { + fn from(value: UrlBufCov) -> Self { value.0 } +} + impl From for UrlBufCov { fn from(value: UrlBuf) -> Self { Self(value) } } -impl From for UrlBuf { - fn from(value: UrlBufCov) -> Self { value.0 } +impl From for UrlBufCov { + fn from(value: PathBuf) -> Self { Self(UrlBuf::from(value)) } } impl From> for UrlBufCov { diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 3f0ecda0..2398e334 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -1,8 +1,8 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, ops::Deref, path::Path}; +use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::Path}; use hashbrown::Equivalent; -use crate::{loc::{Loc, LocBuf}, url::{Components, Encode, Scheme, UrlBuf, UrnBuf}}; +use crate::{loc::{Loc, LocBuf}, url::{Components, Encode, Scheme, Uri, UrlBuf, Urn, UrnBuf}}; #[derive(Clone, Eq, Hash, PartialEq)] pub struct Url<'a> { @@ -10,12 +10,6 @@ pub struct Url<'a> { pub scheme: Scheme, } -impl<'a> Deref for Url<'a> { - type Target = Loc<'a>; - - fn deref(&self) -> &Self::Target { &self.loc } -} - // TODO: remove impl<'a> From<&'a Url<'a>> for Url<'a> { fn from(value: &'a Url) -> Self { Self { loc: value.loc.as_loc(), scheme: value.scheme.clone() } } @@ -69,6 +63,12 @@ impl<'a> Url<'a> { #[inline] pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) } + #[inline] + pub fn is_absolute(&self) -> bool { self.loc.is_absolute() } + + #[inline] + pub fn has_root(&self) -> bool { self.loc.has_root() } + #[inline] pub fn as_url(&'a self) -> Url<'a> { Self::from(self) } @@ -90,6 +90,21 @@ impl<'a> Url<'a> { UrlBuf { loc, scheme: self.scheme.clone() } } + #[inline] + pub fn uri(&self) -> &'a Uri { self.loc.uri() } + + #[inline] + pub fn urn(&self) -> &'a Urn { self.loc.urn() } + + #[inline] + pub fn name(self) -> Option<&'a OsStr> { self.loc.name() } + + #[inline] + pub fn stem(&self) -> Option<&'a OsStr> { self.loc.stem() } + + #[inline] + pub fn ext(&self) -> Option<&'a OsStr> { self.loc.ext() } + pub fn base(&self) -> Option { use Scheme as S; @@ -163,9 +178,17 @@ impl<'a> Url<'a> { #[inline] pub fn pair(&self) -> Option<(Url<'a>, UrnBuf)> { - Some((self.parent_url()?, self.loc.urn_owned())) + Some((self.parent_url()?, self.loc.urn().to_owned())) } #[inline] - pub fn as_path(&self) -> Option<&Path> { Some(&*self.loc).filter(|_| !self.scheme.is_virtual()) } + pub fn as_path(&self) -> Option<&'a Path> { + Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual()) + } + + #[inline] + pub fn has_base(&self) -> bool { self.loc.has_base() } + + #[inline] + pub fn has_trail(&self) -> bool { self.loc.has_trail() } } diff --git a/yazi-watcher/Cargo.toml b/yazi-watcher/Cargo.toml new file mode 100644 index 00000000..eeb5bff3 --- /dev/null +++ b/yazi-watcher/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "yazi-watcher" +version = "25.6.11" +edition = "2024" +license = "MIT" +authors = [ "sxyazi " ] +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 } diff --git a/yazi-watcher/src/backend/backend.rs b/yazi-watcher/src/backend/backend.rs new file mode 100644 index 00000000..b43da5d3 --- /dev/null +++ b/yazi-watcher/src/backend/backend.rs @@ -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) -> 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, to_watch: Vec) -> 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(out_tx: &mpsc::UnboundedSender, urls: I) + where + I: IntoIterator, + T: Into, + { + 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>) -> Result<()> { + if let Some(path) = url.into().as_path() { self.local.watch(path) } else { Ok(()) } + } + + fn unwatch<'a>(&mut self, url: impl Into>) -> Result<()> { + if let Some(path) = url.into().as_path() { self.local.unwatch(path) } else { Ok(()) } + } +} diff --git a/yazi-watcher/src/backend/local.rs b/yazi-watcher/src/backend/local.rs new file mode 100644 index 00000000..621ae714 --- /dev/null +++ b/yazi-watcher/src/backend/local.rs @@ -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); + +impl Local { + pub(super) fn serve(out_tx: mpsc::UnboundedSender) -> Self { + let handler = move |res: Result| { + 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)?, + } + } +} diff --git a/yazi-watcher/src/backend/mod.rs b/yazi-watcher/src/backend/mod.rs new file mode 100644 index 00000000..e0f9955f --- /dev/null +++ b/yazi-watcher/src/backend/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(backend local); diff --git a/yazi-watcher/src/lib.rs b/yazi-watcher/src/lib.rs new file mode 100644 index 00000000..755c2954 --- /dev/null +++ b/yazi-watcher/src/lib.rs @@ -0,0 +1,13 @@ +yazi_macro::mod_pub!(backend); + +yazi_macro::mod_flat!(linked watched watcher); + +pub static LINKED: yazi_shared::RoCell> = yazi_shared::RoCell::new(); +pub static WATCHED: yazi_shared::RoCell> = yazi_shared::RoCell::new(); +pub static WATCHER: yazi_shared::RoCell = yazi_shared::RoCell::new(); + +pub fn init() { + LINKED.with(<_>::default); + WATCHED.with(<_>::default); + WATCHER.init(tokio::sync::Semaphore::new(1)); +} diff --git a/yazi-watcher/src/linked.rs b/yazi-watcher/src/linked.rs new file mode 100644 index 00000000..13f9890c --- /dev/null +++ b/yazi-watcher/src/linked.rs @@ -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 /* from ==> to */); + +impl Deref for Linked { + type Target = HashMap; + + 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 + 'b> + where + 'a: 'b, + T: Into>, + { + 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 { + 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, watched: &'static RwLock) { + 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(); + } +} diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs new file mode 100644 index 00000000..5fb6f206 --- /dev/null +++ b/yazi-watcher/src/watched.rs @@ -0,0 +1,30 @@ +use std::path::Path; + +use hashbrown::HashSet; +use yazi_shared::url::{Url, UrlBuf}; + +#[derive(Default)] +pub struct Watched(HashSet); + +impl Watched { + #[inline] + pub(crate) fn contains<'a>(&self, url: impl Into>) -> bool { + self.0.contains(&url.into()) + } + + #[inline] + pub(crate) fn diff(&self, new: &HashSet) -> (Vec, Vec) { + (self.0.difference(new).cloned().collect(), new.difference(&self.0).cloned().collect()) + } + + #[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_path()) + } + + #[inline] + pub(crate) fn remove<'a>(&mut self, url: impl Into>) { self.0.remove(&url.into()); } +} diff --git a/yazi-watcher/src/watcher.rs b/yazi-watcher/src/watcher.rs new file mode 100644 index 00000000..23524a81 --- /dev/null +++ b/yazi-watcher/src/watcher.rs @@ -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>, + out_tx: mpsc::UnboundedSender, +} + +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) { + self.in_tx.send(it.filter(|u| u.is_regular()).cloned().collect()).ok(); + } + + pub fn push_files(&self, urls: Vec) { Backend::push_files(&self.out_tx, urls); } + + async fn fan_in(mut rx: watch::Receiver>, 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) { + // 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); + } + } +}