mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: support VFS for the file watcher (#3108)
This commit is contained in:
parent
c27ef58116
commit
12172b7301
65 changed files with 770 additions and 493 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":"0.2","language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS"]}
|
||||
{"flagWords":[],"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold"],"version":"0.2"}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>())
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ impl UserData for Tab {
|
|||
fn add_fields<F: UserDataFields<Self>>(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));
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ impl Actor for FilterDo {
|
|||
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
|
||||
let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) };
|
||||
|
||||
let 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() {
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SStr>) -> Self { Self::Custom(msg.into()) }
|
||||
|
||||
pub fn into_string(self) -> SStr {
|
||||
match self {
|
||||
Error::Io(e) => Cow::Owned(e.to_string()),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,16 +94,16 @@ impl FromLua for Url {
|
|||
impl UserData for Url {
|
||||
fn add_fields<F: UserDataFields<Self>>(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()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ impl From<Urn> 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<yazi_shared::url::UrnBuf>) -> Self { Self { inner: urn.into() } }
|
||||
}
|
||||
|
||||
impl FromLua for Urn {
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
use std::{iter, ops::{Deref, DerefMut}};
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use yazi_shared::url::{Url, UrlBuf};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Linked(HashMap<UrlBuf, UrlBuf> /* from ==> to */);
|
||||
|
||||
impl Deref for Linked {
|
||||
type Target = HashMap<UrlBuf, UrlBuf>;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
impl DerefMut for Linked {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
||||
}
|
||||
|
||||
impl Linked {
|
||||
pub fn from_dir<'a, 'b, T>(&'a self, url: T) -> Box<dyn Iterator<Item = &'a UrlBuf> + 'b>
|
||||
where
|
||||
'a: 'b,
|
||||
T: Into<Url<'b>>,
|
||||
{
|
||||
let url = url.into();
|
||||
if url.scheme.is_virtual() {
|
||||
Box::new(iter::empty())
|
||||
} else if let Some(to) = self.get(&url) {
|
||||
Box::new(self.iter().filter(move |(k, v)| *v == to && *k != url).map(|(k, _)| k))
|
||||
} else {
|
||||
Box::new(self.iter().filter(move |(_, v)| *v == url).map(|(k, _)| k))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(&self, url: Url) -> Vec<UrlBuf> {
|
||||
if url.scheme.is_virtual() {
|
||||
vec![]
|
||||
} else if let Some((parent, urn)) = url.pair() {
|
||||
self.from_dir(parent).map(|u| u.join(&urn)).collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(linked mgr mimetype tabs watcher yanked);
|
||||
yazi_macro::mod_flat!(mgr mimetype tabs yanked);
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use notify::{PollWatcher, RecommendedWatcher, RecursiveMode, Watcher as _Watcher};
|
||||
use parking_lot::RwLock;
|
||||
use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
|
||||
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
||||
use tracing::error;
|
||||
use yazi_fs::{File, Files, FilesOp, cha::Cha, provider, realname_unchecked};
|
||||
use yazi_proxy::WATCHER;
|
||||
use yazi_shared::{RoCell, url::UrlBuf};
|
||||
|
||||
use super::Linked;
|
||||
use crate::tab::Folder;
|
||||
|
||||
pub(crate) static WATCHED: RoCell<RwLock<HashSet<UrlBuf>>> = RoCell::new();
|
||||
pub static LINKED: RoCell<RwLock<Linked>> = RoCell::new();
|
||||
|
||||
pub struct Watcher {
|
||||
in_tx: watch::Sender<HashSet<UrlBuf>>,
|
||||
out_tx: mpsc::UnboundedSender<UrlBuf>,
|
||||
}
|
||||
|
||||
// FIXME: VFS
|
||||
impl Watcher {
|
||||
pub(super) fn serve() -> Self {
|
||||
let (in_tx, in_rx) = watch::channel(Default::default());
|
||||
let (out_tx, out_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let out_tx_ = out_tx.clone();
|
||||
let handler = move |res: Result<notify::Event, notify::Error>| {
|
||||
let Ok(event) = res else { return };
|
||||
if event.kind.is_access() {
|
||||
return;
|
||||
}
|
||||
Self::push_files_impl(&out_tx_, event.paths.into_iter().map(UrlBuf::from));
|
||||
};
|
||||
|
||||
let config = notify::Config::default().with_poll_interval(Duration::from_millis(500));
|
||||
if yazi_adapter::WSL.get() || cfg!(target_os = "netbsd") {
|
||||
tokio::spawn(Self::fan_in(in_rx, PollWatcher::new(handler, config).unwrap()));
|
||||
} else {
|
||||
tokio::spawn(Self::fan_in(in_rx, RecommendedWatcher::new(handler, config).unwrap()));
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
yazi_fs::mounts::Partitions::monitor(yazi_fs::mounts::PARTITIONS.clone(), || {
|
||||
yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount())
|
||||
});
|
||||
|
||||
tokio::spawn(Self::fan_out(out_rx));
|
||||
Self { in_tx, out_tx }
|
||||
}
|
||||
|
||||
pub fn watch<'a>(&mut self, it: impl Iterator<Item = &'a UrlBuf>) {
|
||||
self.in_tx.send(it.filter(|u| u.is_regular()).cloned().collect()).ok();
|
||||
}
|
||||
|
||||
pub fn push_files(&self, urls: Vec<UrlBuf>) {
|
||||
Self::push_files_impl(&self.out_tx, urls.into_iter());
|
||||
}
|
||||
|
||||
fn push_files_impl(out_tx: &mpsc::UnboundedSender<UrlBuf>, urls: impl Iterator<Item = UrlBuf>) {
|
||||
let (mut parents, watched) = (HashSet::new(), WATCHED.read());
|
||||
for u in urls {
|
||||
let Some(p) = u.parent_url() else { continue };
|
||||
if !watched.contains(&p) && !LINKED.read().from_dir(&p).any(|u| watched.contains(u)) {
|
||||
continue;
|
||||
}
|
||||
if parents.contains(&p) {
|
||||
out_tx.send(u).ok();
|
||||
} else {
|
||||
let p = p.to_owned();
|
||||
out_tx.send(u).ok();
|
||||
out_tx.send(p.to_owned()).ok();
|
||||
parents.insert(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: performance improvement
|
||||
pub fn trigger_dirs(&self, folders: &[&Folder]) {
|
||||
async fn go(cwd: UrlBuf, cha: Cha) {
|
||||
let Some(cha) = Files::assert_stale(&cwd, cha).await else { return };
|
||||
|
||||
match Files::from_dir_bulk(&cwd).await {
|
||||
Ok(files) => FilesOp::Full(cwd, files, cha).emit(),
|
||||
Err(e) => FilesOp::issue_error(&cwd, e.kind()).await,
|
||||
}
|
||||
}
|
||||
|
||||
let futs: Vec<_> = folders
|
||||
.iter()
|
||||
.filter(|&f| f.url.is_internal())
|
||||
.map(|&f| go(f.url.to_owned(), f.cha))
|
||||
.collect();
|
||||
|
||||
if !futs.is_empty() {
|
||||
tokio::spawn(futures::future::join_all(futs));
|
||||
}
|
||||
}
|
||||
|
||||
async fn fan_in(
|
||||
mut rx: watch::Receiver<HashSet<UrlBuf>>,
|
||||
mut watcher: impl notify::Watcher + Send + 'static,
|
||||
) {
|
||||
loop {
|
||||
let (to_unwatch, to_watch): (HashSet<_>, HashSet<_>) = {
|
||||
let (new, old) = (&*rx.borrow_and_update(), &*WATCHED.read());
|
||||
(old.difference(new).cloned().collect(), new.difference(old).cloned().collect())
|
||||
};
|
||||
|
||||
watcher = Self::sync_watched(watcher, to_unwatch, to_watch).await;
|
||||
|
||||
if !rx.has_changed().unwrap_or(false) {
|
||||
Self::sync_linked().await;
|
||||
}
|
||||
|
||||
if rx.changed().await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fan_out(rx: UnboundedReceiver<UrlBuf>) {
|
||||
// TODO: revert this once a new notification is implemented
|
||||
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));
|
||||
pin!(rx);
|
||||
|
||||
while let Some(chunk) = rx.next().await {
|
||||
let urls: HashSet<_> = chunk.into_iter().collect();
|
||||
let mut cached: HashMap<_, _> = HashMap::new();
|
||||
|
||||
let _permit = WATCHER.acquire().await.unwrap();
|
||||
let mut ops = Vec::with_capacity(urls.len());
|
||||
|
||||
for u in urls {
|
||||
let Some((parent, urn)) = u.pair() else { continue };
|
||||
let Ok(file) = File::new(&u).await else {
|
||||
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
|
||||
continue;
|
||||
};
|
||||
|
||||
let u = &file.url;
|
||||
let eq = (!file.is_link() && provider::canonicalize(u).await.is_ok_and(|c| c == *u))
|
||||
|| realname_unchecked(u, &mut cached).await.is_ok_and(|s| urn.as_urn() == s);
|
||||
|
||||
if !eq {
|
||||
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
|
||||
continue;
|
||||
}
|
||||
|
||||
ops.push(FilesOp::Upserting(parent.into(), [(urn, file)].into()));
|
||||
}
|
||||
|
||||
FilesOp::mutate(ops);
|
||||
}
|
||||
}
|
||||
|
||||
async fn sync_watched<W>(
|
||||
mut watcher: W,
|
||||
to_unwatch: HashSet<UrlBuf>,
|
||||
to_watch: HashSet<UrlBuf>,
|
||||
) -> W
|
||||
where
|
||||
W: notify::Watcher + Send + 'static,
|
||||
{
|
||||
use notify::ErrorKind::WatchNotFound;
|
||||
|
||||
if to_unwatch.is_empty() && to_watch.is_empty() {
|
||||
return watcher;
|
||||
}
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
for u in to_unwatch {
|
||||
match watcher.unwatch(&u) {
|
||||
Ok(()) => _ = WATCHED.write().remove(&u),
|
||||
Err(e) if matches!(e.kind, WatchNotFound) => _ = WATCHED.write().remove(&u),
|
||||
Err(e) => error!("Unwatch failed: {e:?}"),
|
||||
}
|
||||
}
|
||||
for u in to_watch {
|
||||
if watcher.watch(&u, RecursiveMode::NonRecursive).is_ok() {
|
||||
WATCHED.write().insert(u);
|
||||
}
|
||||
}
|
||||
watcher
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn sync_linked() {
|
||||
let mut new = WATCHED.read().clone();
|
||||
|
||||
let old = {
|
||||
let mut linked = LINKED.write();
|
||||
linked.retain(|k, _| new.remove(k));
|
||||
linked.keys().cloned().collect()
|
||||
};
|
||||
|
||||
async fn go(todo: HashSet<UrlBuf>) {
|
||||
for from in todo {
|
||||
let Ok(to) = provider::canonicalize(&from).await else { continue };
|
||||
|
||||
if to != from && WATCHED.read().contains(&from) {
|
||||
LINKED.write().insert(from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go(new).await;
|
||||
go(old).await;
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,9 @@ impl Finder {
|
|||
pub fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
|
||||
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<isize> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<UrlBufCov>, 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<UrlBufCov>, 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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<Url<'a>>) -> bool {
|
||||
|
|
@ -31,13 +31,13 @@ pub fn ok_or_not_found<T: Default>(result: io::Result<T>) -> io::Result<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn realname(u: &UrlBuf) -> Option<OsString> {
|
||||
let name = u.file_name()?;
|
||||
if *u == provider::canonicalize(u).await.ok()? {
|
||||
pub async fn realname(url: &UrlBuf) -> Option<OsString> {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ where
|
|||
}
|
||||
|
||||
async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result<UrlBuf> {
|
||||
let Some(stem) = url.file_stem().map(|s| s.to_owned()) else {
|
||||
let Some(stem) = url.stem().map(|s| s.to_owned()) else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem"));
|
||||
};
|
||||
|
||||
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);
|
||||
|
|
|
|||
91
yazi-fs/src/provider/local/casefold.rs
Normal file
91
yazi-fs/src/provider/local/casefold.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
use std::{io, path::{Path, PathBuf}};
|
||||
|
||||
pub async fn valid_name_case(path: impl AsRef<Path>) -> io::Result<bool> {
|
||||
let path = path.as_ref().to_owned();
|
||||
tokio::task::spawn_blocking(move || valid_name_case_impl(path)).await?
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "macos",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "freebsd"
|
||||
))]
|
||||
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
|
||||
use std::{ffi::{CStr, CString, OsStr}, os::{fd::{AsRawFd, FromRawFd, OwnedFd}, unix::ffi::OsStrExt}};
|
||||
|
||||
use libc::{F_GETPATH, O_RDONLY, O_SYMLINK, PATH_MAX};
|
||||
|
||||
let cstr = CString::new(path.into_os_string().into_encoded_bytes())?;
|
||||
let Some(name) = Path::new(OsStr::from_bytes(cstr.as_bytes())).file_name() else {
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
let fd = match unsafe { libc::open(cstr.as_ptr(), O_RDONLY | O_SYMLINK) } {
|
||||
ret if ret < 0 => return Err(io::Error::last_os_error()),
|
||||
ret => unsafe { OwnedFd::from_raw_fd(ret) },
|
||||
};
|
||||
|
||||
let mut buf = [0u8; PATH_MAX as usize];
|
||||
if unsafe { libc::fcntl(fd.as_raw_fd(), F_GETPATH, buf.as_mut_ptr()) } < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(
|
||||
unsafe { CStr::from_ptr(buf.as_ptr() as *const i8) }
|
||||
.to_bytes()
|
||||
.ends_with(name.as_encoded_bytes()),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
|
||||
use std::{ffi::{CString, OsStr}, fs::File, os::{fd::{AsRawFd, FromRawFd}, unix::ffi::OsStrExt}};
|
||||
|
||||
use libc::{O_NOFOLLOW, O_PATH};
|
||||
|
||||
let cstr = CString::new(path.into_os_string().into_encoded_bytes())?;
|
||||
let path = Path::new(OsStr::from_bytes(cstr.as_bytes()));
|
||||
let Some(name) = path.file_name() else { return Ok(true) };
|
||||
|
||||
let file = match unsafe { libc::open(cstr.as_ptr(), O_PATH | O_NOFOLLOW) } {
|
||||
ret if ret < 0 => return Err(io::Error::last_os_error()),
|
||||
ret => unsafe { File::from_raw_fd(ret) },
|
||||
};
|
||||
|
||||
Ok(if file.metadata()?.is_symlink() {
|
||||
std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))?.starts_with(path)
|
||||
} else {
|
||||
std::fs::canonicalize(path)?.file_name() == Some(name)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
|
||||
use std::{ffi::OsString, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}};
|
||||
|
||||
use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}};
|
||||
|
||||
let Some(name) = path.file_name() else { return Ok(true) };
|
||||
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.access_mode(0)
|
||||
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
|
||||
.open(&path)?;
|
||||
|
||||
let mut buf = [0u16; MAX_PATH as usize];
|
||||
let len = unsafe {
|
||||
GetFinalPathNameByHandleW(
|
||||
file.as_raw_handle() as HANDLE,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as u32,
|
||||
VOLUME_NAME_DOS,
|
||||
)
|
||||
};
|
||||
|
||||
if len == 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize])).file_name() == Some(name))
|
||||
}
|
||||
}
|
||||
55
yazi-fs/src/provider/local/identical.rs
Normal file
55
yazi-fs/src/provider/local/identical.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use std::{io, path::{Path, PathBuf}};
|
||||
|
||||
#[inline]
|
||||
pub async fn identical<P, Q>(a: P, b: Q) -> io::Result<bool>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let (a, b) = (a.as_ref().to_owned(), b.as_ref().to_owned());
|
||||
tokio::task::spawn_blocking(move || identical_impl(a, b)).await?
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn identical_impl(a: PathBuf, b: PathBuf) -> io::Result<bool> {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let (a_, b_) = (std::fs::symlink_metadata(&a)?, std::fs::symlink_metadata(&b)?);
|
||||
Ok(
|
||||
a_.ino() == b_.ino()
|
||||
&& a_.dev() == b_.dev()
|
||||
&& std::fs::canonicalize(a)? == std::fs::canonicalize(b)?,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn identical_impl(a: PathBuf, b: PathBuf) -> io::Result<bool> {
|
||||
use std::{fs::OpenOptions, mem, os::windows::{fs::OpenOptionsExt, io::AsRawHandle}};
|
||||
|
||||
use windows_sys::Win32::{Foundation::HANDLE, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_ID_INFO, FileIdInfo, GetFileInformationByHandleEx}};
|
||||
|
||||
fn file_id(path: PathBuf) -> io::Result<(u64, u128)> {
|
||||
let file = OpenOptions::new()
|
||||
.access_mode(0)
|
||||
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
|
||||
.open(path)?;
|
||||
|
||||
let mut info: FILE_ID_INFO = unsafe { mem::zeroed() };
|
||||
let ret = unsafe {
|
||||
GetFileInformationByHandleEx(
|
||||
file.as_raw_handle() as HANDLE,
|
||||
FileIdInfo,
|
||||
&mut info as *mut FILE_ID_INFO as _,
|
||||
mem::size_of::<FILE_ID_INFO>() as u32,
|
||||
)
|
||||
};
|
||||
|
||||
if ret == 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok((info.VolumeSerialNumber, u128::from_le_bytes(info.FileId.Identifier)))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(file_id(a)? == file_id(b)?)
|
||||
}
|
||||
|
|
@ -202,63 +202,6 @@ impl Local {
|
|||
tokio::fs::rename(from, to).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn same<P, Q>(a: P, b: Q) -> io::Result<bool>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
Self::same_impl(a.as_ref(), b.as_ref()).await
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn same_impl(a: &Path, b: &Path) -> io::Result<bool> {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let (a_, b_) = (tokio::fs::symlink_metadata(a).await?, tokio::fs::symlink_metadata(b).await?);
|
||||
Ok(
|
||||
a_.ino() == b_.ino()
|
||||
&& a_.dev() == b_.dev()
|
||||
&& tokio::fs::canonicalize(a).await? == tokio::fs::canonicalize(b).await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn same_impl(a: &Path, b: &Path) -> io::Result<bool> {
|
||||
use std::{ffi::OsString, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}};
|
||||
|
||||
use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}};
|
||||
|
||||
async fn final_name(path: &Path) -> io::Result<PathBuf> {
|
||||
let path = path.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.access_mode(0)
|
||||
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
|
||||
.open(path)?;
|
||||
|
||||
let mut buf = [0u16; MAX_PATH as usize];
|
||||
let len = unsafe {
|
||||
GetFinalPathNameByHandleW(
|
||||
file.as_raw_handle() as HANDLE,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as u32,
|
||||
VOLUME_NAME_DOS,
|
||||
)
|
||||
};
|
||||
|
||||
if len == 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize])))
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
Ok(final_name(a).await? == final_name(b).await?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>
|
||||
|
|
@ -86,6 +86,19 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn identical<'a, U, V>(a: U, b: V) -> io::Result<bool>
|
||||
where
|
||||
U: Into<Url<'a>>,
|
||||
V: Into<Url<'a>>,
|
||||
{
|
||||
if let (Some(a), Some(b)) = (a.into().as_path(), b.into().as_path()) {
|
||||
local::identical(a, b).await
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn metadata<'a, U>(url: U) -> io::Result<std::fs::Metadata>
|
||||
where
|
||||
|
|
@ -98,6 +111,15 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn must_identical<'a, U, V>(a: U, b: V) -> bool
|
||||
where
|
||||
U: Into<Url<'a>>,
|
||||
V: Into<Url<'a>>,
|
||||
{
|
||||
identical(a, b).await.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn open<'a, U>(url: U) -> io::Result<RwFile>
|
||||
where
|
||||
|
|
@ -195,19 +217,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn same<'a, U, V>(a: U, b: V) -> io::Result<bool>
|
||||
where
|
||||
U: Into<Url<'a>>,
|
||||
V: Into<Url<'a>>,
|
||||
{
|
||||
if let (Some(a), Some(b)) = (a.into().as_path(), b.into().as_path()) {
|
||||
Local::same(a, b).await
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<M: mlua::UserDataMethods<Self>>(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),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ impl Utils {
|
|||
pub(super) fn file_cache(lua: &Lua) -> mlua::Result<Function> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,27 +8,36 @@ use crate::bindings::ImageInfo;
|
|||
impl Utils {
|
||||
pub(super) fn image_info(lua: &Lua) -> mlua::Result<Function> {
|
||||
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<Function> {
|
||||
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<Function> {
|
||||
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),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(_)) => {
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -3,9 +3,4 @@ use yazi_shared::RoCell;
|
|||
|
||||
pub static HIDER: RoCell<Semaphore> = RoCell::new();
|
||||
|
||||
pub static WATCHER: RoCell<Semaphore> = 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)); }
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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<LocBuf> 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<PathBuf> {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<UrlBufCov> for UrlBuf {
|
||||
fn from(value: UrlBufCov) -> Self { value.0 }
|
||||
}
|
||||
|
||||
impl From<UrlBuf> for UrlBufCov {
|
||||
fn from(value: UrlBuf) -> Self { Self(value) }
|
||||
}
|
||||
|
||||
impl From<UrlBufCov> for UrlBuf {
|
||||
fn from(value: UrlBufCov) -> Self { value.0 }
|
||||
impl From<PathBuf> for UrlBufCov {
|
||||
fn from(value: PathBuf) -> Self { Self(UrlBuf::from(value)) }
|
||||
}
|
||||
|
||||
impl From<UrlCow<'_>> for UrlBufCov {
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
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() }
|
||||
}
|
||||
|
|
|
|||
27
yazi-watcher/Cargo.toml
Normal file
27
yazi-watcher/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "yazi-watcher"
|
||||
version = "25.6.11"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = [ "sxyazi <sxyazi@gmail.com>" ]
|
||||
description = "Yazi file watcher"
|
||||
homepage = "https://yazi-rs.github.io"
|
||||
repository = "https://github.com/sxyazi/yazi"
|
||||
|
||||
[dependencies]
|
||||
yazi-adapter = { path = "../yazi-adapter", version = "25.6.11" }
|
||||
yazi-dds = { path = "../yazi-dds", version = "25.6.11" }
|
||||
yazi-fs = { path = "../yazi-fs", version = "25.6.11" }
|
||||
yazi-macro = { path = "../yazi-macro", version = "25.6.11" }
|
||||
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
|
||||
|
||||
# Logging
|
||||
tracing = { workspace = true }
|
||||
|
||||
# External dependencies
|
||||
anyhow = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] }
|
||||
parking_lot = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
74
yazi-watcher/src/backend/backend.rs
Normal file
74
yazi-watcher/src/backend/backend.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use anyhow::Result;
|
||||
use hashbrown::HashSet;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
use yazi_shared::url::{Url, UrlBuf};
|
||||
|
||||
use crate::{LINKED, WATCHED, backend};
|
||||
|
||||
pub(crate) struct Backend {
|
||||
local: backend::Local,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub(crate) fn serve(out_tx: mpsc::UnboundedSender<UrlBuf>) -> Self {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
yazi_fs::mounts::Partitions::monitor(yazi_fs::mounts::PARTITIONS.clone(), || {
|
||||
yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount())
|
||||
});
|
||||
|
||||
Self { local: backend::Local::serve(out_tx) }
|
||||
}
|
||||
|
||||
pub(crate) async fn sync(mut self, to_unwatch: Vec<UrlBuf>, to_watch: Vec<UrlBuf>) -> Backend {
|
||||
if to_unwatch.is_empty() && to_watch.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
for u in to_unwatch {
|
||||
match self.unwatch(&u) {
|
||||
Ok(()) => WATCHED.write().remove(&u),
|
||||
Err(e) => error!("Unwatch failed: {e:?}"),
|
||||
}
|
||||
}
|
||||
for u in to_watch {
|
||||
match self.watch(&u) {
|
||||
Ok(()) => WATCHED.write().insert(u),
|
||||
Err(e) => error!("Watch failed: {e:?}"),
|
||||
}
|
||||
}
|
||||
self
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn push_files<I, T>(out_tx: &mpsc::UnboundedSender<UrlBuf>, urls: I)
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<UrlBuf>,
|
||||
{
|
||||
let (mut todo, watched) = (HashSet::new(), WATCHED.read());
|
||||
for url in urls.into_iter().map(Into::into) {
|
||||
let Some(parent) = url.parent_url() else { continue };
|
||||
if todo.contains(&parent) {
|
||||
todo.insert(url);
|
||||
} else if watched.contains(&parent)
|
||||
|| LINKED.read().from_dir(&parent).any(|p| watched.contains(Url::regular(p)))
|
||||
{
|
||||
todo.insert(parent.to_owned());
|
||||
todo.insert(url);
|
||||
}
|
||||
}
|
||||
todo.into_iter().for_each(|u| _ = out_tx.send(u));
|
||||
}
|
||||
|
||||
fn watch<'a>(&mut self, url: impl Into<Url<'a>>) -> Result<()> {
|
||||
if let Some(path) = url.into().as_path() { self.local.watch(path) } else { Ok(()) }
|
||||
}
|
||||
|
||||
fn unwatch<'a>(&mut self, url: impl Into<Url<'a>>) -> Result<()> {
|
||||
if let Some(path) = url.into().as_path() { self.local.unwatch(path) } else { Ok(()) }
|
||||
}
|
||||
}
|
||||
41
yazi-watcher/src/backend/local.rs
Normal file
41
yazi-watcher/src/backend/local.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use std::{path::Path, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use notify::{ErrorKind::WatchNotFound, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use tokio::sync::mpsc;
|
||||
use yazi_shared::url::UrlBuf;
|
||||
|
||||
use crate::backend::Backend;
|
||||
|
||||
pub(super) struct Local(Box<dyn notify::Watcher + Send>);
|
||||
|
||||
impl Local {
|
||||
pub(super) fn serve(out_tx: mpsc::UnboundedSender<UrlBuf>) -> Self {
|
||||
let handler = move |res: Result<notify::Event, notify::Error>| {
|
||||
let Ok(event) = res else { return };
|
||||
if event.kind.is_access() {
|
||||
return;
|
||||
}
|
||||
Backend::push_files(&out_tx, event.paths);
|
||||
};
|
||||
|
||||
let config = notify::Config::default().with_poll_interval(Duration::from_millis(500));
|
||||
Self(if yazi_adapter::WSL.get() || cfg!(target_os = "netbsd") {
|
||||
Box::new(PollWatcher::new(handler, config).unwrap())
|
||||
} else {
|
||||
Box::new(RecommendedWatcher::new(handler, config).unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn watch(&mut self, path: &Path) -> Result<()> {
|
||||
Ok(self.0.watch(path, RecursiveMode::NonRecursive)?)
|
||||
}
|
||||
|
||||
pub(super) fn unwatch(&mut self, path: &Path) -> Result<()> {
|
||||
match self.0.unwatch(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if matches!(e.kind, WatchNotFound) => Ok(()),
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
1
yazi-watcher/src/backend/mod.rs
Normal file
1
yazi-watcher/src/backend/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(backend local);
|
||||
13
yazi-watcher/src/lib.rs
Normal file
13
yazi-watcher/src/lib.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
yazi_macro::mod_pub!(backend);
|
||||
|
||||
yazi_macro::mod_flat!(linked watched watcher);
|
||||
|
||||
pub static LINKED: yazi_shared::RoCell<parking_lot::RwLock<Linked>> = yazi_shared::RoCell::new();
|
||||
pub static WATCHED: yazi_shared::RoCell<parking_lot::RwLock<Watched>> = yazi_shared::RoCell::new();
|
||||
pub static WATCHER: yazi_shared::RoCell<tokio::sync::Semaphore> = yazi_shared::RoCell::new();
|
||||
|
||||
pub fn init() {
|
||||
LINKED.with(<_>::default);
|
||||
WATCHED.with(<_>::default);
|
||||
WATCHER.init(tokio::sync::Semaphore::new(1));
|
||||
}
|
||||
72
yazi-watcher/src/linked.rs
Normal file
72
yazi-watcher/src/linked.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use std::{iter, ops::{Deref, DerefMut}, path::{Path, PathBuf}};
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use parking_lot::RwLock;
|
||||
use yazi_shared::url::Url;
|
||||
|
||||
use crate::Watched;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Linked(HashMap<PathBuf, PathBuf> /* from ==> to */);
|
||||
|
||||
impl Deref for Linked {
|
||||
type Target = HashMap<PathBuf, PathBuf>;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
impl DerefMut for Linked {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
||||
}
|
||||
|
||||
impl Linked {
|
||||
pub fn from_dir<'a, 'b, T>(&'a self, url: T) -> Box<dyn Iterator<Item = &'a Path> + 'b>
|
||||
where
|
||||
'a: 'b,
|
||||
T: Into<Url<'b>>,
|
||||
{
|
||||
let url: Url = url.into();
|
||||
let Some(path) = url.as_path() else {
|
||||
return Box::new(iter::empty());
|
||||
};
|
||||
|
||||
if let Some(to) = self.get(path) {
|
||||
Box::new(self.iter().filter(move |(k, v)| *v == to && *k != path).map(|(k, _)| k.as_path()))
|
||||
} else {
|
||||
Box::new(self.iter().filter(move |(_, v)| *v == path).map(|(k, _)| k.as_path()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(&self, url: Url) -> Vec<PathBuf> {
|
||||
let Some(path) = url.as_path() else { return vec![] };
|
||||
if let Some((parent, name)) = path.parent().zip(path.file_name()) {
|
||||
self.from_dir(Url::regular(parent)).map(|p| p.join(name)).collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn sync(linked: &'static RwLock<Self>, watched: &'static RwLock<Watched>) {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut new: HashSet<_> = watched.read().paths().map(ToOwned::to_owned).collect();
|
||||
let mut linked = linked.write();
|
||||
|
||||
linked.retain(|k, _| new.remove(k));
|
||||
for from in new {
|
||||
linked.insert(from, PathBuf::new());
|
||||
}
|
||||
|
||||
for (from, to) in linked.iter_mut() {
|
||||
match std::fs::canonicalize(from) {
|
||||
Ok(c) if c != *from && watched.read().contains(Url::regular(from)) => *to = c,
|
||||
Ok(_) => *to = PathBuf::new(),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => *to = PathBuf::new(),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
linked.retain(|_, v| !v.as_os_str().is_empty());
|
||||
})
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
30
yazi-watcher/src/watched.rs
Normal file
30
yazi-watcher/src/watched.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use std::path::Path;
|
||||
|
||||
use hashbrown::HashSet;
|
||||
use yazi_shared::url::{Url, UrlBuf};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Watched(HashSet<UrlBuf>);
|
||||
|
||||
impl Watched {
|
||||
#[inline]
|
||||
pub(crate) fn contains<'a>(&self, url: impl Into<Url<'a>>) -> bool {
|
||||
self.0.contains(&url.into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn diff(&self, new: &HashSet<UrlBuf>) -> (Vec<UrlBuf>, Vec<UrlBuf>) {
|
||||
(self.0.difference(new).cloned().collect(), new.difference(&self.0).cloned().collect())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn insert(&mut self, url: impl Into<UrlBuf>) { self.0.insert(url.into()); }
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn paths(&self) -> impl Iterator<Item = &Path> {
|
||||
self.0.iter().filter_map(|u| u.as_path())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn remove<'a>(&mut self, url: impl Into<Url<'a>>) { self.0.remove(&url.into()); }
|
||||
}
|
||||
81
yazi-watcher/src/watcher.rs
Normal file
81
yazi-watcher/src/watcher.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use hashbrown::HashSet;
|
||||
use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
|
||||
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
||||
use yazi_fs::{File, FilesOp, provider::local};
|
||||
use yazi_shared::url::UrlBuf;
|
||||
|
||||
use crate::{LINKED, Linked, WATCHED, WATCHER, backend::Backend};
|
||||
|
||||
pub struct Watcher {
|
||||
in_tx: watch::Sender<HashSet<UrlBuf>>,
|
||||
out_tx: mpsc::UnboundedSender<UrlBuf>,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub fn serve() -> Self {
|
||||
let (in_tx, in_rx) = watch::channel(Default::default());
|
||||
let (out_tx, out_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let backend = Backend::serve(out_tx.clone());
|
||||
|
||||
tokio::spawn(Self::fan_in(in_rx, backend));
|
||||
tokio::spawn(Self::fan_out(out_rx));
|
||||
|
||||
Self { in_tx, out_tx }
|
||||
}
|
||||
|
||||
pub fn watch<'a>(&mut self, it: impl Iterator<Item = &'a UrlBuf>) {
|
||||
self.in_tx.send(it.filter(|u| u.is_regular()).cloned().collect()).ok();
|
||||
}
|
||||
|
||||
pub fn push_files(&self, urls: Vec<UrlBuf>) { Backend::push_files(&self.out_tx, urls); }
|
||||
|
||||
async fn fan_in(mut rx: watch::Receiver<HashSet<UrlBuf>>, mut backend: Backend) {
|
||||
loop {
|
||||
let (to_unwatch, to_watch) = WATCHED.read().diff(&rx.borrow_and_update());
|
||||
backend = backend.sync(to_unwatch, to_watch).await;
|
||||
|
||||
if !rx.has_changed().unwrap_or(false) {
|
||||
Linked::sync(&LINKED, &WATCHED).await;
|
||||
}
|
||||
|
||||
if rx.changed().await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fan_out(rx: UnboundedReceiver<UrlBuf>) {
|
||||
// TODO: revert this once a new notification is implemented
|
||||
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));
|
||||
pin!(rx);
|
||||
|
||||
while let Some(chunk) = rx.next().await {
|
||||
let urls: HashSet<_> = chunk.into_iter().collect();
|
||||
|
||||
let _permit = WATCHER.acquire().await.unwrap();
|
||||
let mut ops = Vec::with_capacity(urls.len());
|
||||
|
||||
for u in urls {
|
||||
let Some((parent, urn)) = u.pair() else { continue };
|
||||
let Ok(file) = File::new(&u).await else {
|
||||
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(p) = file.url.as_path()
|
||||
&& !local::valid_name_case(p).await.unwrap_or(false)
|
||||
{
|
||||
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
|
||||
continue;
|
||||
}
|
||||
|
||||
ops.push(FilesOp::Upserting(parent.into(), [(urn, file)].into()));
|
||||
}
|
||||
|
||||
FilesOp::mutate(ops);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue