Skip to content

Commit f9abe88

Browse files
authored
feat: multi-path architecture (#3334)
1 parent 577065c commit f9abe88

File tree

153 files changed

+4180
-2333
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+4180
-2333
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ scopeguard = "1.2.0"
4848
serde = { version = "1.0.228", features = [ "derive" ] }
4949
serde_json = "1.0.145"
5050
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
51+
thiserror = "2.0.17"
5152
tokio = { version = "1.48.0", features = [ "full" ] }
5253
tokio-stream = "0.1.17"
5354
tokio-util = "0.7.17"
@@ -61,4 +62,5 @@ uzers = "0.12.1"
6162
format_push_string = "warn"
6263
implicit_clone = "warn"
6364
module_inception = "allow"
65+
unit_arg = "allow"
6466
use_self = "warn"

cspell.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"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","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied"],"version":"0.2"}
1+
{"version":"0.2","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","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool"]}

yazi-actor/src/cmp/show.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use std::{ffi::OsStr, mem, ops::ControlFlow};
1+
use std::{mem, ops::ControlFlow};
22

33
use anyhow::Result;
44
use yazi_macro::{render, succ};
55
use yazi_parser::cmp::{CmpItem, ShowOpt};
6-
use yazi_shared::{data::Data, osstr_contains, osstr_starts_with};
6+
use yazi_shared::{data::Data, path::{AsPathDyn, PathDyn, PathLike}, strand::{AsStrand, StrandLike}};
77

88
use crate::{Actor, Ctx};
99

@@ -30,7 +30,7 @@ impl Actor for Show {
3030
};
3131

3232
cmp.ticket = opt.ticket;
33-
cmp.cands = Self::match_candidates(opt.word.as_os_str(), cache);
33+
cmp.cands = Self::match_candidates(opt.word.as_path_dyn(), cache);
3434
if cmp.cands.is_empty() {
3535
succ!(render!(mem::replace(&mut cmp.visible, false)));
3636
}
@@ -43,16 +43,20 @@ impl Actor for Show {
4343
}
4444

4545
impl Show {
46-
fn match_candidates(word: &OsStr, cache: &[CmpItem]) -> Vec<CmpItem> {
47-
let smart = !word.as_encoded_bytes().iter().any(|c| c.is_ascii_uppercase());
46+
fn match_candidates(word: PathDyn, cache: &[CmpItem]) -> Vec<CmpItem> {
47+
let smart = !word.encoded_bytes().iter().any(|&b| b.is_ascii_uppercase());
4848

4949
let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| {
50-
if osstr_starts_with(&item.name, word, smart) {
50+
let name = item.name.as_strand();
51+
let starts_with =
52+
if smart { name.eq_ignore_ascii_case(word) } else { name.starts_with(word) };
53+
54+
if starts_with {
5155
exact.push(item);
5256
if exact.len() >= LIMIT {
5357
return ControlFlow::Break((exact, fuzzy));
5458
}
55-
} else if fuzzy.len() < LIMIT - exact.len() && osstr_contains(&item.name, word) {
59+
} else if fuzzy.len() < LIMIT - exact.len() && name.contains(word) {
5660
// Here we don't break the control flow, since we want more exact matching.
5761
fuzzy.push(item)
5862
}

yazi-actor/src/cmp/trigger.rs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use std::{ffi::OsString, mem, path::{MAIN_SEPARATOR_STR, PathBuf}};
1+
use std::{mem, path::MAIN_SEPARATOR_STR};
22

33
use anyhow::Result;
44
use yazi_fs::{CWD, path::expand_url, provider::{DirReader, FileHolder}};
55
use yazi_macro::{act, render, succ};
66
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
77
use yazi_proxy::CmpProxy;
8-
use yazi_shared::{OsStrSplit, data::Data, natsort, scheme::SchemeLike, url::{UrlBuf, UrlCow}};
8+
use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::StrandBufLike, url::{UrlBuf, UrlCow, UrlLike}};
99
use yazi_vfs::provider;
1010

1111
use crate::{Actor, Ctx};
@@ -32,7 +32,7 @@ impl Actor for Trigger {
3232

3333
if cmp.caches.contains_key(&parent) {
3434
let ticket = cmp.ticket;
35-
return act!(cmp:show, cx, ShowOpt { cache_name: parent, word, ticket, ..Default::default() });
35+
return act!(cmp:show, cx, ShowOpt { cache: vec![], cache_name: parent, word, ticket });
3636
}
3737

3838
let ticket = cmp.ticket;
@@ -42,8 +42,8 @@ impl Actor for Trigger {
4242

4343
// "/" is both a directory separator and the root directory per se
4444
// As there's no parent directory for the FS root, it is a special case
45-
if parent.loc.as_os_str() == "/" {
46-
cache.push(CmpItem { name: OsString::new(), is_dir: true });
45+
if parent.loc() == "/" {
46+
cache.push(CmpItem { name: Default::default(), is_dir: true });
4747
}
4848

4949
while let Ok(Some(ent)) = dir.next().await {
@@ -53,9 +53,8 @@ impl Actor for Trigger {
5353
}
5454

5555
if !cache.is_empty() {
56-
cache.sort_unstable_by(|a, b| {
57-
natsort(a.name.as_encoded_bytes(), b.name.as_encoded_bytes(), false)
58-
});
56+
cache
57+
.sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), false));
5958
CmpProxy::show(ShowOpt { cache, cache_name: parent, word, ticket });
6059
}
6160

@@ -67,37 +66,42 @@ impl Actor for Trigger {
6766
}
6867

6968
impl Trigger {
70-
fn split_url(s: &str) -> Option<(UrlBuf, PathBuf)> {
71-
let (scheme, path, ..) = UrlCow::parse(s.as_bytes()).ok()?;
69+
fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> {
70+
let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?;
7271

73-
if scheme.is_local() && path.as_os_str() == "~" {
72+
if scheme.is_local() && path == "~" {
7473
return None; // We don't autocomplete a `~`, but `~/`
7574
}
7675

77-
#[cfg(windows)]
78-
const SEP: &[char] = &['/', '\\'];
79-
#[cfg(not(windows))]
80-
const SEP: char = std::path::MAIN_SEPARATOR;
76+
let sep = if cfg!(windows) {
77+
AnyAsciiChar::new(&[b'/', b'\\']).unwrap()
78+
} else {
79+
AnyAsciiChar::new(&[b'/']).unwrap()
80+
};
8181

82-
Some(match path.as_os_str().rsplit_once(SEP) {
82+
Some(match path.as_path().rsplit_pred(sep) {
8383
Some((p, c)) if p.is_empty() => {
84-
(UrlBuf { loc: MAIN_SEPARATOR_STR.into(), scheme: scheme.into() }, c.into())
84+
let root = PathBufDyn::with(scheme.kind(), MAIN_SEPARATOR_STR).expect("valid root");
85+
(UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into())
86+
}
87+
Some((p, c)) => {
88+
let parent = PathBufDyn::with(scheme.kind(), p.as_dyn()).expect("valid parent");
89+
(expand_url(UrlCow::try_from((scheme, parent)).ok()?), c.into())
8590
}
86-
Some((p, c)) => (expand_url(UrlBuf { loc: p.into(), scheme: scheme.into() }), c.into()),
8791
None => (CWD.load().as_ref().clone(), path.into()),
8892
})
8993
}
9094
}
9195

9296
#[cfg(test)]
9397
mod tests {
94-
use yazi_shared::url::UrlLike;
98+
use yazi_shared::{path::PathBufLike, url::UrlLike};
9599

96100
use super::*;
97101

98102
fn compare(s: &str, parent: &str, child: &str) {
99103
let (mut p, c) = Trigger::split_url(s).unwrap();
100-
if let Some(u) = p.strip_prefix(yazi_fs::CWD.load().as_ref()) {
104+
if let Ok(u) = p.try_strip_prefix(yazi_fs::CWD.load().as_ref()) {
101105
p = UrlBuf::from(u);
102106
}
103107
assert_eq!((p, c.to_str().unwrap()), (parent.parse().unwrap(), child));

yazi-actor/src/lives/file.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Valu
44
use yazi_binding::Style;
55
use yazi_config::THEME;
66
use yazi_plugin::bindings::Range;
7-
use yazi_shared::url::UrlLike;
7+
use yazi_shared::{path::PathLike, url::UrlLike};
88

99
use super::Lives;
1010
use crate::lives::PtrCell;
@@ -88,11 +88,8 @@ impl UserData for File {
8888
if !me.url.has_trail() {
8989
return Ok(None);
9090
}
91-
let Some(path) = me.url.as_path() else {
92-
return Ok(None);
93-
};
9491

95-
let mut comp = path.strip_prefix(me.url.loc.trail()).unwrap_or(path).components();
92+
let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components();
9693
comp.next_back();
9794
Some(lua.create_string(comp.as_path().as_os_str().as_encoded_bytes())).transpose()
9895
});

yazi-actor/src/lives/tab.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::ops::Deref;
22

33
use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods, Value};
44
use yazi_binding::{Id, UrlRef, cached_field};
5-
use yazi_shared::url::UrlLike;
5+
use yazi_shared::{path::PathLike, strand::StrandLike, url::UrlLike};
66

77
use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected};
88

@@ -47,7 +47,7 @@ impl UserData for Tab {
4747
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
4848
cached_field!(fields, name, |lua, me| {
4949
let url = &me.current.url;
50-
lua.create_string(url.name().unwrap_or(url.loc.as_os_str()).as_encoded_bytes())
50+
lua.create_string(url.name().map_or_else(|| url.loc().encoded_bytes(), |n| n.encoded_bytes()))
5151
});
5252

5353
cached_field!(fields, mode, |_, me| Mode::make(&me.mode));

yazi-actor/src/mgr/bulk_rename.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider
1111
use yazi_macro::{err, succ};
1212
use yazi_parser::VoidOpt;
1313
use yazi_proxy::{AppProxy, HIDER, TasksProxy};
14-
use yazi_shared::{OsStrJoin, data::Data, terminal_clear, url::{AsUrl, Component, UrlBuf, UrlCow, UrlLike}};
14+
use yazi_shared::{OsStrJoin, data::Data, path::PathDyn, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
1515
use yazi_term::tty::TTY;
1616
use yazi_vfs::{VfsFile, maybe_exists, provider};
1717
use yazi_watcher::WATCHER;
@@ -50,7 +50,12 @@ impl Actor for BulkRename {
5050
.write_all(old.join(OsStr::new("\n")).as_encoded_bytes())
5151
.await?;
5252

53-
defer! { tokio::spawn(Local.remove_file(tmp.clone())); }
53+
defer! {
54+
let tmp = tmp.clone();
55+
tokio::spawn(async move {
56+
Local::regular(&tmp).remove_file().await
57+
});
58+
}
5459
TasksProxy::process_exec(
5560
cwd.into(),
5661
Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run),
@@ -64,8 +69,8 @@ impl Actor for BulkRename {
6469
defer!(AppProxy::resume());
6570
AppProxy::stop().await;
6671

67-
let new: Vec<_> = Local
68-
.read_to_string(&tmp)
72+
let new: Vec<_> = Local::regular(&tmp)
73+
.read_to_string()
6974
.await?
7075
.lines()
7176
.take(old.len())
@@ -120,10 +125,12 @@ impl BulkRename {
120125
let permit = WATCHER.acquire().await.unwrap();
121126
let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len()));
122127
for (o, n) in todo {
123-
let (old, new): (UrlBuf, UrlBuf) = (
124-
selected[o.0].components().take(root).chain([Component::Normal(&o)]).collect(),
125-
selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(),
126-
);
128+
let (Ok(old), Ok(new)) =
129+
(Self::replace_url(&selected[o.0], root, &o), Self::replace_url(&selected[n.0], root, &n))
130+
else {
131+
failed.push((o, n, anyhow!("Invalid new or old file name")));
132+
continue;
133+
};
127134

128135
if maybe_exists(&new).await && !provider::must_identical(&old, &new).await {
129136
failed.push((o, n, anyhow!("Destination already exists")));
@@ -137,7 +144,7 @@ impl BulkRename {
137144
}
138145

139146
if !succeeded.is_empty() {
140-
let it = succeeded.iter().map(|(o, n)| (o, &n.url));
147+
let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url()));
141148
err!(Pubsub::pub_after_bulk(it));
142149
FilesOp::rename(succeeded);
143150
}
@@ -153,6 +160,10 @@ impl BulkRename {
153160
YAZI.opener.block(YAZI.open.all(Path::new("bulk-rename.txt"), "text/plain"))
154161
}
155162

163+
fn replace_url(url: &UrlBuf, take: usize, rep: &OsStr) -> Result<UrlBuf> {
164+
Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned())
165+
}
166+
156167
async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
157168
let mut stdout = TTY.lockout();
158169
terminal_clear(&mut *stdout)?;

yazi-actor/src/mgr/copy.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::ffi::OsString;
2-
31
use anyhow::{Result, bail};
42
use yazi_macro::{act, succ};
53
use yazi_parser::mgr::CopyOpt;
@@ -18,7 +16,7 @@ impl Actor for Copy {
1816
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
1917
act!(mgr:escape_visual, cx)?;
2018

21-
let mut s = OsString::new();
19+
let mut s = Vec::<u8>::new();
2220
let mut it = if opt.hovered {
2321
Box::new(cx.hovered().map(|h| &h.url).into_iter())
2422
} else {
@@ -30,29 +28,29 @@ impl Actor for Copy {
3028
match opt.r#type.as_ref() {
3129
// TODO: rename to "url"
3230
"path" => {
33-
s.push(opt.separator.transform(&u.os_str()));
31+
s.extend_from_slice(&opt.separator.transform(&u.os_str()));
3432
}
3533
"dirname" => {
3634
if let Some(p) = u.parent() {
37-
s.push(opt.separator.transform(&p.os_str()));
35+
s.extend_from_slice(&opt.separator.transform(&p.os_str()));
3836
}
3937
}
4038
"filename" => {
41-
s.push(opt.separator.transform(u.name().unwrap_or_default()));
39+
s.extend_from_slice(&opt.separator.transform(&u.name().unwrap_or_default()));
4240
}
4341
"name_without_ext" => {
44-
s.push(opt.separator.transform(u.stem().unwrap_or_default()));
42+
s.extend_from_slice(&opt.separator.transform(&u.stem().unwrap_or_default()));
4543
}
4644
_ => bail!("Unknown copy type: {}", opt.r#type),
4745
};
4846
if it.peek().is_some() {
49-
s.push("\n");
47+
s.push(b'\n');
5048
}
5149
}
5250

5351
// Copy the CWD path regardless even if the directory is empty
5452
if s.is_empty() && opt.r#type == "dirname" {
55-
s.push(opt.separator.transform(&cx.cwd().os_str()));
53+
s.extend_from_slice(&opt.separator.transform(&cx.cwd().os_str()));
5654
}
5755

5856
futures::executor::block_on(CLIPBOARD.set(s));

yazi-actor/src/mgr/create.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ impl Actor for Create {
2727
return;
2828
}
2929

30-
let new = cwd.join(&name);
30+
let Ok(new) = cwd.try_join(&name) else {
31+
return;
32+
};
33+
3134
if !opt.force
3235
&& maybe_exists(&new).await
3336
&& !ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await

0 commit comments

Comments
 (0)