mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41: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-proxy",
|
||||||
"yazi-shared",
|
"yazi-shared",
|
||||||
"yazi-term",
|
"yazi-term",
|
||||||
|
"yazi-watcher",
|
||||||
"yazi-widgets",
|
"yazi-widgets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3617,10 +3618,8 @@ version = "25.6.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
"futures",
|
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"notify",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -3638,6 +3637,7 @@ dependencies = [
|
||||||
"yazi-proxy",
|
"yazi-proxy",
|
||||||
"yazi-scheduler",
|
"yazi-scheduler",
|
||||||
"yazi-shared",
|
"yazi-shared",
|
||||||
|
"yazi-watcher",
|
||||||
"yazi-widgets",
|
"yazi-widgets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3714,6 +3714,7 @@ dependencies = [
|
||||||
"yazi-proxy",
|
"yazi-proxy",
|
||||||
"yazi-shared",
|
"yazi-shared",
|
||||||
"yazi-term",
|
"yazi-term",
|
||||||
|
"yazi-watcher",
|
||||||
"yazi-widgets",
|
"yazi-widgets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3886,6 +3887,24 @@ dependencies = [
|
||||||
"yazi-shared",
|
"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]]
|
[[package]]
|
||||||
name = "yazi-widgets"
|
name = "yazi-widgets"
|
||||||
version = "25.6.11"
|
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-proxy = { path = "../yazi-proxy", version = "25.6.11" }
|
||||||
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
|
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
|
||||||
yazi-term = { path = "../yazi-term", 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" }
|
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
|
||||||
|
|
||||||
# External dependencies
|
# External dependencies
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ impl UserData for File {
|
||||||
return None;
|
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<_>>())
|
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) {
|
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||||
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
|
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
|
||||||
cached_field!(fields, name, |lua, me| {
|
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));
|
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_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, provider::{self, local::{Gate, Local}}};
|
||||||
use yazi_macro::{err, succ};
|
use yazi_macro::{err, succ};
|
||||||
use yazi_parser::VoidOpt;
|
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_shared::{OsStrJoin, event::Data, terminal_clear, url::{Component, UrlBuf}};
|
||||||
use yazi_term::tty::TTY;
|
use yazi_term::tty::TTY;
|
||||||
|
use yazi_watcher::WATCHER;
|
||||||
|
|
||||||
use crate::{Actor, Ctx};
|
use crate::{Actor, Ctx};
|
||||||
|
|
||||||
|
|
@ -119,7 +120,7 @@ impl BulkRename {
|
||||||
selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(),
|
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")));
|
failed.push((o, n, anyhow!("Destination already exists")));
|
||||||
} else if let Err(e) = provider::rename(&old, &new).await {
|
} else if let Err(e) = provider::rename(&old, &new).await {
|
||||||
failed.push((o, n, e.into()));
|
failed.push((o, n, e.into()));
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ impl Cd {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(p) = url.parent_url() {
|
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);
|
MgrProxy::reveal(&url);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ impl Actor for Copy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"filename" => {
|
"filename" => {
|
||||||
s.push(opt.separator.transform(u.name()));
|
s.push(opt.separator.transform(u.name().unwrap_or_default()));
|
||||||
}
|
}
|
||||||
"name_without_ext" => {
|
"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),
|
_ => 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_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
|
||||||
use yazi_macro::succ;
|
use yazi_macro::succ;
|
||||||
use yazi_parser::mgr::CreateOpt;
|
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_shared::{event::Data, url::{UrlBuf, UrnBuf}};
|
||||||
|
use yazi_watcher::WATCHER;
|
||||||
|
|
||||||
use crate::{Actor, Ctx};
|
use crate::{Actor, Ctx};
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ impl Create {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(f) = File::new(&new).await {
|
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)
|
MgrProxy::reveal(&new)
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ impl Actor for FilterDo {
|
||||||
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
|
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 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);
|
cx.current_mut().files.set_filter(filter);
|
||||||
|
|
||||||
if cx.hovered().map(|f| f.urn()) != hovered.as_deref() {
|
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);
|
let state = opt.state.bool(cx.tab().pref.show_hidden);
|
||||||
cx.tab_mut().pref.show_hidden = state;
|
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| {
|
let apply = |f: &mut Folder| {
|
||||||
if f.stage == FolderStage::Loading {
|
if f.stage == FolderStage::Loading {
|
||||||
render!();
|
render!();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{execute, terminal::SetTitle};
|
use crossterm::{execute, terminal::SetTitle};
|
||||||
use yazi_config::YAZI;
|
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_macro::{act, succ};
|
||||||
use yazi_parser::VoidOpt;
|
use yazi_parser::VoidOpt;
|
||||||
use yazi_shared::event::Data;
|
use yazi_shared::{event::Data, url::UrlBuf};
|
||||||
use yazi_term::tty::TTY;
|
use yazi_term::tty::TTY;
|
||||||
|
|
||||||
use crate::{Actor, Ctx};
|
use crate::{Actor, Ctx};
|
||||||
|
|
@ -22,9 +23,9 @@ impl Actor for Refresh {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(p) = cx.parent() {
|
if let Some(p) = cx.parent() {
|
||||||
cx.mgr.watcher.trigger_dirs(&[cx.current(), p]);
|
Self::trigger_dirs(&[cx.current(), p]);
|
||||||
} else {
|
} else {
|
||||||
cx.mgr.watcher.trigger_dirs(&[cx.current()]);
|
Self::trigger_dirs(&[cx.current()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
act!(mgr:peek, cx)?;
|
act!(mgr:peek, cx)?;
|
||||||
|
|
@ -35,3 +36,27 @@ impl Actor for Refresh {
|
||||||
succ!();
|
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_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
|
||||||
use yazi_macro::{act, err, succ};
|
use yazi_macro::{act, err, succ};
|
||||||
use yazi_parser::mgr::RenameOpt;
|
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_shared::{Id, event::Data, url::{UrlBuf, UrnBuf}};
|
||||||
|
use yazi_watcher::WATCHER;
|
||||||
|
|
||||||
use crate::{Actor, Ctx};
|
use crate::{Actor, Ctx};
|
||||||
|
|
||||||
|
|
@ -47,9 +48,8 @@ impl Actor for Rename {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new = UrlBuf::from(old.parent().unwrap().join(name));
|
let new = old.parent_url().unwrap().join(name);
|
||||||
if opt.force || !maybe_exists(&new).await || provider::same(&old, &new).await.unwrap_or(false)
|
if opt.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await {
|
||||||
{
|
|
||||||
Self::r#do(tab, old, new).await.ok();
|
Self::r#do(tab, old, new).await.ok();
|
||||||
} else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await {
|
} else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await {
|
||||||
Self::r#do(tab, old, new).await.ok();
|
Self::r#do(tab, old, new).await.ok();
|
||||||
|
|
@ -91,12 +91,12 @@ impl Rename {
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let ext = url.extension();
|
let ext = url.ext();
|
||||||
match by {
|
match by {
|
||||||
"stem" => ext.map_or_else(String::new, |s| format!(".{}", s.to_string_lossy())),
|
"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()),
|
"ext" if ext.is_some() => format!("{}.", url.stem().unwrap().to_string_lossy()),
|
||||||
"dot_ext" if ext.is_some() => url.file_stem().unwrap().to_string_lossy().into_owned(),
|
"dot_ext" if ext.is_some() => url.stem().unwrap().to_string_lossy().into_owned(),
|
||||||
_ => url.name().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);
|
pref.sort_translit = opt.translit.unwrap_or(pref.sort_translit);
|
||||||
|
|
||||||
let sorter = FilesSorter::from(&*pref);
|
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| {
|
let apply = |f: &mut Folder| {
|
||||||
if f.stage == FolderStage::Loading {
|
if f.stage == FolderStage::Loading {
|
||||||
render!();
|
render!();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use yazi_core::{mgr::LINKED, tab::Folder};
|
use yazi_core::tab::Folder;
|
||||||
use yazi_fs::FilesOp;
|
use yazi_fs::FilesOp;
|
||||||
use yazi_macro::{act, render, succ};
|
use yazi_macro::{act, render, succ};
|
||||||
use yazi_parser::mgr::UpdateFilesOpt;
|
use yazi_parser::mgr::UpdateFilesOpt;
|
||||||
use yazi_shared::event::Data;
|
use yazi_shared::event::Data;
|
||||||
|
use yazi_watcher::LINKED;
|
||||||
|
|
||||||
use crate::{Actor, Ctx};
|
use crate::{Actor, Ctx};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use yazi_core::mgr::LINKED;
|
|
||||||
use yazi_macro::{act, render, succ};
|
use yazi_macro::{act, render, succ};
|
||||||
use yazi_parser::mgr::UpdateMimesOpt;
|
use yazi_parser::mgr::UpdateMimesOpt;
|
||||||
use yazi_shared::{event::Data, pool::InternStr, url::UrlCov};
|
use yazi_shared::{event::Data, pool::InternStr, url::UrlCov};
|
||||||
|
use yazi_watcher::LINKED;
|
||||||
|
|
||||||
use crate::{Actor, Ctx};
|
use crate::{Actor, Ctx};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,14 @@ pub enum Error {
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
pub fn install(lua: &Lua) -> mlua::Result<()> {
|
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)])?)
|
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 {
|
pub fn into_string(self) -> SStr {
|
||||||
match self {
|
match self {
|
||||||
Error::Io(e) => Cow::Owned(e.to_string()),
|
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, link_to, |_, me| Ok(me.link_to.clone().map($crate::Url::new)));
|
||||||
|
|
||||||
$crate::cached_field!($fields, name, |lua, me| {
|
$crate::cached_field!($fields, name, |lua, me| {
|
||||||
Some(me.name())
|
me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose()
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.map(|s| lua.create_string(s.as_encoded_bytes()))
|
|
||||||
.transpose()
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,16 +94,16 @@ impl FromLua for Url {
|
||||||
impl UserData for Url {
|
impl UserData for Url {
|
||||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||||
cached_field!(fields, name, |lua, me| {
|
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| {
|
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| {
|
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, 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, base, |_, me| Ok(me.base().map(Self::new)));
|
||||||
cached_field!(fields, domain, |lua, me| {
|
cached_field!(fields, domain, |lua, me| {
|
||||||
me.scheme.domain().map(|s| lua.create_string(s)).transpose()
|
me.scheme.domain().map(|s| lua.create_string(s)).transpose()
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ impl From<Urn> for yazi_shared::url::UrnBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Urn {
|
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 {
|
impl FromLua for Urn {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ impl Icon {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn match_by_name(&self, file: &File) -> Option<&I> {
|
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() {
|
if file.is_dir() {
|
||||||
self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase()))
|
self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase()))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -90,7 +90,7 @@ impl Icon {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn match_by_ext(&self, file: &File) -> Option<&I> {
|
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()))
|
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-proxy = { path = "../yazi-proxy", version = "25.6.11" }
|
||||||
yazi-scheduler = { path = "../yazi-scheduler", version = "25.6.11" }
|
yazi-scheduler = { path = "../yazi-scheduler", version = "25.6.11" }
|
||||||
yazi-shared = { path = "../yazi-shared", 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" }
|
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
|
||||||
|
|
||||||
# External dependencies
|
# External dependencies
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
futures = { workspace = true }
|
|
||||||
hashbrown = { workspace = true }
|
hashbrown = { workspace = true }
|
||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] }
|
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
tokio = { 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_pub!(cmp confirm help input mgr notify pick spot tab tasks which);
|
||||||
|
|
||||||
yazi_macro::mod_flat!(core);
|
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_adapter::Dimension;
|
||||||
use yazi_config::popup::{Origin, Position};
|
use yazi_config::popup::{Origin, Position};
|
||||||
use yazi_shared::url::UrlBuf;
|
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};
|
use crate::tab::{Folder, Tab};
|
||||||
|
|
||||||
pub struct Mgr {
|
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> {
|
pub fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
|
||||||
for i in !include as usize..files.len() {
|
for i in !include as usize..files.len() {
|
||||||
let idx = (cursor + files.len() - i) % 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);
|
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> {
|
pub fn next(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
|
||||||
for i in !include as usize..files.len() {
|
for i in !include as usize..files.len() {
|
||||||
let idx = (cursor + i) % 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);
|
return Some(idx as isize - cursor as isize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,11 +58,11 @@ impl Finder {
|
||||||
|
|
||||||
let mut i = 0u8;
|
let mut i = 0u8;
|
||||||
for file in folder.files.iter() {
|
for file in folder.files.iter() {
|
||||||
if !self.filter.matches(file.name()) {
|
if file.name().is_none_or(|s| !self.filter.matches(s)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.matched.insert(file.urn_owned(), i);
|
self.matched.insert(file.urn().to_owned(), i);
|
||||||
if self.matched.len() > 99 {
|
if self.matched.len() > 99 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ impl Folder {
|
||||||
self.scroll(step)
|
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();
|
b |= self.squeeze_offset();
|
||||||
|
|
||||||
self.sync_page(false);
|
self.sync_page(false);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::mgr::Yanked;
|
||||||
impl Tasks {
|
impl Tasks {
|
||||||
pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) {
|
pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) {
|
||||||
for u in src.iter() {
|
for u in src.iter() {
|
||||||
let to = dest.join(u.file_name().unwrap());
|
let to = dest.join(u.name().unwrap());
|
||||||
if force && *u == to {
|
if force && *u == to {
|
||||||
debug!("file_cut: same file, skipping {:?}", to);
|
debug!("file_cut: same file, skipping {:?}", to);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -19,7 +19,7 @@ impl Tasks {
|
||||||
|
|
||||||
pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) {
|
pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) {
|
||||||
for u in src.iter() {
|
for u in src.iter() {
|
||||||
let to = dest.join(u.file_name().unwrap());
|
let to = dest.join(u.name().unwrap());
|
||||||
if force && *u == to {
|
if force && *u == to {
|
||||||
debug!("file_copy: same file, skipping {:?}", to);
|
debug!("file_copy: same file, skipping {:?}", to);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -30,7 +30,7 @@ impl Tasks {
|
||||||
|
|
||||||
pub fn file_link(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, relative: bool, force: bool) {
|
pub fn file_link(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, relative: bool, force: bool) {
|
||||||
for u in src {
|
for u in src {
|
||||||
let to = dest.join(u.file_name().unwrap());
|
let to = dest.join(u.name().unwrap());
|
||||||
if force && *u == to {
|
if force && *u == to {
|
||||||
debug!("file_link: same file, skipping {:?}", to);
|
debug!("file_link: same file, skipping {:?}", to);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -41,7 +41,7 @@ impl Tasks {
|
||||||
|
|
||||||
pub fn file_hardlink(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, force: bool, follow: bool) {
|
pub fn file_hardlink(&self, src: &HashSet<UrlBufCov>, dest: &UrlBuf, force: bool, follow: bool) {
|
||||||
for u in src {
|
for u in src {
|
||||||
let to = dest.join(u.file_name().unwrap());
|
let to = dest.join(u.name().unwrap());
|
||||||
if force && *u == to {
|
if force && *u == to {
|
||||||
debug!("file_hardlink: same file, skipping {:?}", to);
|
debug!("file_hardlink: same file, skipping {:?}", to);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" }
|
||||||
yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" }
|
yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" }
|
||||||
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
|
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
|
||||||
yazi-term = { path = "../yazi-term", 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" }
|
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
|
||||||
|
|
||||||
# External dependencies
|
# External dependencies
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
yazi_widgets::init();
|
yazi_widgets::init();
|
||||||
|
|
||||||
yazi_plugin::init()?;
|
yazi_watcher::init();
|
||||||
|
|
||||||
yazi_core::init();
|
yazi_plugin::init()?;
|
||||||
|
|
||||||
yazi_dds::serve();
|
yazi_dds::serve();
|
||||||
app::App::serve().await
|
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 anyhow::Result;
|
||||||
use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn, UrnBuf};
|
use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn};
|
||||||
|
|
||||||
use crate::{cha::Cha, provider};
|
use crate::{cha::Cha, provider};
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ impl File {
|
||||||
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
|
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
|
||||||
|
|
||||||
#[inline]
|
#[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() }
|
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() }
|
pub fn urn(&self) -> &Urn { self.url.urn() }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn urn_owned(&self) -> UrnBuf { self.url.urn_owned() }
|
pub fn name(&self) -> Option<&OsStr> { self.url.name() }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn name(&self) -> &OsStr { self.url.name() }
|
pub fn stem(&self) -> Option<&OsStr> { self.url.stem() }
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn stem(&self) -> Option<&OsStr> { self.url.file_stem() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for File {
|
impl Hash for File {
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ impl Files {
|
||||||
|
|
||||||
macro_rules! go {
|
macro_rules! go {
|
||||||
($dist:expr, $src:expr, $inc:literal) => {
|
($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 {
|
for f in &$dist {
|
||||||
if todo.remove(f.urn()).is_some() && todo.is_empty() {
|
if todo.remove(f.urn()).is_some() && todo.is_empty() {
|
||||||
break;
|
break;
|
||||||
|
|
@ -293,7 +293,7 @@ impl Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_deleting(
|
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);
|
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 tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
|
||||||
use yazi_shared::url::{Component, Url, UrlBuf};
|
use yazi_shared::url::{Component, Url, UrlBuf};
|
||||||
|
|
||||||
use crate::{cha::Cha, provider};
|
use crate::{cha::Cha, provider::{self, local::Local}};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub async fn maybe_exists<'a>(url: impl Into<Url<'a>>) -> bool {
|
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> {
|
pub async fn realname(url: &UrlBuf) -> Option<OsString> {
|
||||||
let name = u.file_name()?;
|
let (path, name) = (url.as_path()?, url.name()?);
|
||||||
if *u == provider::canonicalize(u).await.ok()? {
|
if path == Local::canonicalize(path).await.ok()? {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
realname_unchecked(u, &mut HashMap::new())
|
realname_unchecked(path, &mut HashMap::new())
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|s| s != name)
|
.filter(|s| s != name)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use yazi_macro::relay;
|
use yazi_macro::relay;
|
||||||
use yazi_shared::{Id, Ids, url::{UrlBuf, UrnBuf}};
|
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(o_p) = o.parent_url() else { continue };
|
||||||
let Some(n_p) = n.url.parent_url() else { continue };
|
let Some(n_p) = n.url.parent_url() else { continue };
|
||||||
if o_p != n_p {
|
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 {
|
for (p, (o, n)) in parents {
|
||||||
match (o.is_empty(), n.is_empty()) {
|
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 {
|
macro_rules! files {
|
||||||
($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect() }};
|
($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() }};
|
($map:expr) => {{ $map.iter().map(|(urn, file)| (urn.clone(), file.chdir(wd))).collect() }};
|
||||||
}
|
}
|
||||||
|
|
||||||
let w = wd.clone();
|
let w = UrlBuf::from(wd);
|
||||||
match self {
|
match self {
|
||||||
Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha),
|
Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha),
|
||||||
Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket),
|
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");
|
.expect("Failed to create Loc from expanded path");
|
||||||
|
|
||||||
let url = UrlBuf { loc, scheme: url.scheme.clone() };
|
let url = UrlBuf { loc, scheme: url.scheme.clone() };
|
||||||
match absolute_url(&url) {
|
match absolute_url(url.as_url()) {
|
||||||
Cow::Borrowed(_) => url.into(),
|
UrlCow::Borrowed(_) => url.into(),
|
||||||
Cow::Owned(u) => u.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 b = url.loc.as_os_str().as_encoded_bytes();
|
||||||
let local = !url.scheme.is_virtual();
|
let local = !url.scheme.is_virtual();
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@ fn absolute_url(url: &UrlBuf) -> Cow<'_, UrlBuf> {
|
||||||
UrlBuf { loc, scheme: url.scheme.clone() }.into()
|
UrlBuf { loc, scheme: url.scheme.clone() }.into()
|
||||||
} else if !url.is_absolute() {
|
} else if !url.is_absolute() {
|
||||||
let cwd = CWD.load();
|
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");
|
.expect("Failed to create Loc from relative path");
|
||||||
UrlBuf { loc, scheme: cwd.scheme.clone() }.into()
|
UrlBuf { loc, scheme: cwd.scheme.clone() }.into()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result<UrlBuf> {
|
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"));
|
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);
|
let mut s = OsString::with_capacity(e.len() + 1);
|
||||||
s.push(".");
|
s.push(".");
|
||||||
s.push(e);
|
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
|
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]
|
#[inline]
|
||||||
pub async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()>
|
pub async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()>
|
||||||
where
|
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 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]
|
#[inline]
|
||||||
pub fn cache<'a, U>(url: U) -> Option<PathBuf>
|
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]
|
#[inline]
|
||||||
pub async fn metadata<'a, U>(url: U) -> io::Result<std::fs::Metadata>
|
pub async fn metadata<'a, U>(url: U) -> io::Result<std::fs::Metadata>
|
||||||
where
|
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]
|
#[inline]
|
||||||
pub async fn open<'a, U>(url: U) -> io::Result<RwFile>
|
pub async fn open<'a, U>(url: U) -> io::Result<RwFile>
|
||||||
where
|
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]
|
#[inline]
|
||||||
pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()>
|
pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ impl FilesSorter {
|
||||||
}),
|
}),
|
||||||
SortBy::Extension => items.sort_unstable_by(|a, b| {
|
SortBy::Extension => items.sort_unstable_by(|a, b| {
|
||||||
let ord = if self.sensitive {
|
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 {
|
} else {
|
||||||
self.cmp_insensitive(
|
self.cmp_insensitive(
|
||||||
a.url.extension().map_or(&[], |s| s.as_encoded_bytes()),
|
a.url.ext().map_or(&[], |s| s.as_encoded_bytes()),
|
||||||
b.url.extension().map_or(&[], |s| s.as_encoded_bytes()),
|
b.url.ext().map_or(&[], |s| s.as_encoded_bytes()),
|
||||||
self.promote(a, b),
|
self.promote(a, b),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ impl UserData for MpscTx {
|
||||||
methods.add_async_method("send", |lua, me, value: Value| async move {
|
methods.add_async_method("send", |lua, me, value: Value| async move {
|
||||||
match me.0.send(value).await {
|
match me.0.send(value).await {
|
||||||
Ok(()) => true.into_lua_multi(&lua),
|
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) {
|
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
||||||
methods.add_method("send", |lua, me, value: Value| match me.0.send(value) {
|
methods.add_method("send", |lua, me, value: Value| match me.0.send(value) {
|
||||||
Ok(()) => true.into_lua_multi(lua),
|
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) {
|
match tx.send(value) {
|
||||||
Ok(()) => true.into_lua_multi(lua),
|
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 {
|
match rx.await {
|
||||||
Ok(value) => value.into_lua_multi(&lua),
|
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> {
|
pub(super) fn file_cache(lua: &Lua) -> mlua::Result<Function> {
|
||||||
lua.create_function(|_, t: Table| {
|
lua.create_function(|_, t: Table| {
|
||||||
let file: FileRef = t.raw_get("file")?;
|
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);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,27 +8,36 @@ use crate::bindings::ImageInfo;
|
||||||
impl Utils {
|
impl Utils {
|
||||||
pub(super) fn image_info(lua: &Lua) -> mlua::Result<Function> {
|
pub(super) fn image_info(lua: &Lua) -> mlua::Result<Function> {
|
||||||
lua.create_async_function(|lua, url: UrlRef| async move {
|
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),
|
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> {
|
pub(super) fn image_show(lua: &Lua) -> mlua::Result<Function> {
|
||||||
lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move {
|
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),
|
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> {
|
pub(super) fn image_precache(lua: &Lua) -> mlua::Result<Function> {
|
||||||
lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move {
|
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),
|
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 area: Area = t.raw_get("area")?;
|
||||||
let mut lock = PreviewLock::try_from(t)?;
|
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,
|
Ok(text) => text,
|
||||||
Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua),
|
Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua),
|
||||||
Err(e @ PeekError::Unexpected(_)) => {
|
Err(e @ PeekError::Unexpected(_)) => {
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,7 @@ impl Utils {
|
||||||
style: THEME.spot.border.into(),
|
style: THEME.spot.border.into(),
|
||||||
titles: vec![(
|
titles: vec![(
|
||||||
ratatui::widgets::block::Position::Top,
|
ratatui::widgets::block::Position::Top,
|
||||||
ratatui::text::Line::raw(lock.url.name().to_string_lossy().into_owned())
|
ratatui::text::Line::raw("Spot").centered().style(THEME.spot.title),
|
||||||
.centered()
|
|
||||||
.style(THEME.spot.title),
|
|
||||||
)],
|
)],
|
||||||
}),
|
}),
|
||||||
Renderable::Table(Box::new(table)),
|
Renderable::Table(Box::new(table)),
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,4 @@ use yazi_shared::RoCell;
|
||||||
|
|
||||||
pub static HIDER: RoCell<Semaphore> = RoCell::new();
|
pub static HIDER: RoCell<Semaphore> = RoCell::new();
|
||||||
|
|
||||||
pub static WATCHER: RoCell<Semaphore> = RoCell::new();
|
pub(super) fn init_semaphore() { HIDER.init(Semaphore::new(1)); }
|
||||||
|
|
||||||
pub(super) fn init_semaphore() {
|
|
||||||
HIDER.init(Semaphore::new(1));
|
|
||||||
WATCHER.init(Semaphore::new(1));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ impl File {
|
||||||
continue;
|
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.prog.send(TaskProg::New(task.id, cha.len))?;
|
||||||
|
|
||||||
if cha.is_orphan() || (cha.is_link() && !task.follow) {
|
if cha.is_orphan() || (cha.is_link() && !task.follow) {
|
||||||
|
|
@ -259,7 +259,7 @@ impl File {
|
||||||
continue;
|
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.prog.send(TaskProg::New(task.id, cha.len))?;
|
||||||
self.queue(FileIn::Hardlink(task.spawn(from, to, cha)), NORMAL).await?;
|
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();
|
let parent = buf[0].0.parent_url().unwrap();
|
||||||
FilesOp::Size(
|
FilesOp::Size(
|
||||||
parent.into(),
|
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();
|
.emit();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{cmp, ffi::{OsStr, OsString}, fmt::{self, Debug, Formatter}, hash::{Has
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::{loc::Loc, url::{Uri, Urn, UrnBuf}};
|
use crate::{loc::Loc, url::{Uri, Urn}};
|
||||||
|
|
||||||
#[derive(Clone, Default, Eq)]
|
#[derive(Clone, Default, Eq)]
|
||||||
pub struct LocBuf {
|
pub struct LocBuf {
|
||||||
|
|
@ -160,9 +160,6 @@ impl LocBuf {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn urn(&self) -> &Urn { self.as_loc().urn() }
|
pub fn urn(&self) -> &Urn { self.as_loc().urn() }
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn urn_owned(&self) -> UrnBuf { self.as_loc().urn_owned() }
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn base(&self) -> &Urn { self.as_loc().base() }
|
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() }
|
pub fn has_trail(&self) -> bool { self.as_loc().has_trail() }
|
||||||
|
|
||||||
#[inline]
|
#[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)]
|
#[cfg(test)]
|
||||||
|
|
@ -189,21 +192,21 @@ mod tests {
|
||||||
let loc: LocBuf = Path::new("/").into();
|
let loc: LocBuf = Path::new("/").into();
|
||||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("/"));
|
assert_eq!(loc.uri().as_os_str(), OsStr::new("/"));
|
||||||
assert_eq!(loc.urn().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.base().as_os_str(), OsStr::new(""));
|
||||||
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
|
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
|
||||||
|
|
||||||
let loc: LocBuf = Path::new("/root").into();
|
let loc: LocBuf = Path::new("/root").into();
|
||||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("root"));
|
assert_eq!(loc.uri().as_os_str(), OsStr::new("root"));
|
||||||
assert_eq!(loc.urn().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.base().as_os_str(), OsStr::new("/"));
|
||||||
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
|
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
|
||||||
|
|
||||||
let loc: LocBuf = Path::new("/root/code/foo/").into();
|
let loc: LocBuf = Path::new("/root/code/foo/").into();
|
||||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("foo"));
|
assert_eq!(loc.uri().as_os_str(), OsStr::new("foo"));
|
||||||
assert_eq!(loc.urn().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.base().as_os_str(), OsStr::new("/root/code/"));
|
||||||
assert_eq!(loc.trail().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)?;
|
let loc = LocBuf::with("/".into(), 0, 0)?;
|
||||||
assert_eq!(loc.uri().as_os_str(), OsStr::new(""));
|
assert_eq!(loc.uri().as_os_str(), OsStr::new(""));
|
||||||
assert_eq!(loc.urn().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.base().as_os_str(), OsStr::new("/"));
|
||||||
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
|
assert_eq!(loc.trail().as_os_str(), OsStr::new("/"));
|
||||||
|
|
||||||
let loc = LocBuf::with("/root/code/".into(), 1, 1)?;
|
let loc = LocBuf::with("/root/code/".into(), 1, 1)?;
|
||||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("code"));
|
assert_eq!(loc.uri().as_os_str(), OsStr::new("code"));
|
||||||
assert_eq!(loc.urn().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.base().as_os_str(), OsStr::new("/root/"));
|
||||||
assert_eq!(loc.trail().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)?;
|
let loc = LocBuf::with("/root/code/foo//".into(), 2, 1)?;
|
||||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo"));
|
assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo"));
|
||||||
assert_eq!(loc.urn().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/"));
|
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
|
||||||
assert_eq!(loc.trail().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//".into(), 2, 2)?;
|
let loc = LocBuf::with("/root/code/foo//".into(), 2, 2)?;
|
||||||
assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo"));
|
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.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.base().as_os_str(), OsStr::new("/root/"));
|
||||||
assert_eq!(loc.trail().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)?;
|
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.uri().as_os_str(), OsStr::new("foo//bar"));
|
||||||
assert_eq!(loc.urn().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.base().as_os_str(), OsStr::new("/root/code/"));
|
||||||
assert_eq!(loc.trail().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)?;
|
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.uri().as_os_str(), OsStr::new("code/foo//bar"));
|
||||||
assert_eq!(loc.urn().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/"));
|
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
|
||||||
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/"));
|
assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/"));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{ffi::OsStr, hash::{Hash, Hasher}, ops::Deref, path::Path};
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
use crate::{loc::LocBuf, url::{Uri, Urn, UrnBuf}};
|
use crate::{loc::LocBuf, url::{Uri, Urn}};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Loc<'a> {
|
pub struct Loc<'a> {
|
||||||
|
|
@ -151,9 +151,6 @@ impl<'a> Loc<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn urn_owned(self) -> UrnBuf { self.urn().to_owned() }
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn base(self) -> &'a Urn {
|
pub fn base(self) -> &'a Urn {
|
||||||
Urn::new(unsafe {
|
Urn::new(unsafe {
|
||||||
|
|
@ -179,7 +176,13 @@ impl<'a> Loc<'a> {
|
||||||
pub fn has_trail(self) -> bool { self.bytes().len() != self.urn }
|
pub fn has_trail(self) -> bool { self.bytes().len() != self.urn }
|
||||||
|
|
||||||
#[inline]
|
#[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]
|
#[inline]
|
||||||
pub fn parent(self) -> Option<&'a Path> { self.inner.parent() }
|
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 anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::UrnBuf;
|
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)]
|
#[derive(Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)]
|
||||||
pub struct UrlBuf {
|
pub struct UrlBuf {
|
||||||
|
|
@ -12,12 +12,6 @@ pub struct UrlBuf {
|
||||||
pub scheme: Scheme,
|
pub scheme: Scheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for UrlBuf {
|
|
||||||
type Target = LocBuf;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.loc }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LocBuf> for UrlBuf {
|
impl From<LocBuf> for UrlBuf {
|
||||||
fn from(loc: LocBuf) -> Self { Self { loc, scheme: Scheme::Regular } }
|
fn from(loc: LocBuf) -> Self { Self { loc, scheme: Scheme::Regular } }
|
||||||
}
|
}
|
||||||
|
|
@ -146,9 +140,7 @@ impl UrlBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_path(&self) -> Option<&Path> {
|
pub fn as_path(&self) -> Option<&Path> { self.as_url().as_path() }
|
||||||
Some(self.loc.as_path()).filter(|_| !self.scheme.is_virtual())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn into_path(self) -> Option<PathBuf> {
|
pub fn into_path(self) -> Option<PathBuf> {
|
||||||
|
|
@ -163,11 +155,8 @@ impl UrlBuf {
|
||||||
Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() }
|
Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use Urn instead of UrlBuf
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> {
|
pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> { self.as_url().pair() }
|
||||||
Some((self.parent_url()?, self.loc.urn_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
|
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
|
||||||
|
|
@ -232,6 +221,30 @@ impl UrlBuf {
|
||||||
// FIXME: remove
|
// FIXME: remove
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn into_path2(self) -> PathBuf { self.loc.into_path() }
|
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 {
|
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 hashbrown::Equivalent;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -50,12 +50,16 @@ impl Deref for UrlBufCov {
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
fn deref(&self) -> &Self::Target { &self.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<UrlBufCov> for UrlBuf {
|
||||||
|
fn from(value: UrlBufCov) -> Self { value.0 }
|
||||||
|
}
|
||||||
|
|
||||||
impl From<UrlBuf> for UrlBufCov {
|
impl From<UrlBuf> for UrlBufCov {
|
||||||
fn from(value: UrlBuf) -> Self { Self(value) }
|
fn from(value: UrlBuf) -> Self { Self(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UrlBufCov> for UrlBuf {
|
impl From<PathBuf> for UrlBufCov {
|
||||||
fn from(value: UrlBufCov) -> Self { value.0 }
|
fn from(value: PathBuf) -> Self { Self(UrlBuf::from(value)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UrlCow<'_>> for UrlBufCov {
|
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 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)]
|
#[derive(Clone, Eq, Hash, PartialEq)]
|
||||||
pub struct Url<'a> {
|
pub struct Url<'a> {
|
||||||
|
|
@ -10,12 +10,6 @@ pub struct Url<'a> {
|
||||||
pub scheme: Scheme,
|
pub scheme: Scheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for Url<'a> {
|
|
||||||
type Target = Loc<'a>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.loc }
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: remove
|
// TODO: remove
|
||||||
impl<'a> From<&'a Url<'a>> for Url<'a> {
|
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() } }
|
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]
|
#[inline]
|
||||||
pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) }
|
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]
|
#[inline]
|
||||||
pub fn as_url(&'a self) -> Url<'a> { Self::from(self) }
|
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() }
|
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> {
|
pub fn base(&self) -> Option<Self> {
|
||||||
use Scheme as S;
|
use Scheme as S;
|
||||||
|
|
||||||
|
|
@ -163,9 +178,17 @@ impl<'a> Url<'a> {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pair(&self) -> Option<(Url<'a>, UrnBuf)> {
|
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]
|
#[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