mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
128 lines
3.4 KiB
Rust
128 lines
3.4 KiB
Rust
use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR};
|
|
|
|
use anyhow::Result;
|
|
use yazi_fs::{CWD, expand_url, services::Local};
|
|
use yazi_macro::{act, render, succ};
|
|
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
|
|
use yazi_proxy::CmpProxy;
|
|
use yazi_shared::{OsStrSplit, event::Data, natsort, url::{Url, UrnBuf}};
|
|
|
|
use crate::{Actor, Ctx};
|
|
|
|
pub struct Trigger;
|
|
|
|
impl Actor for Trigger {
|
|
type Options = TriggerOpt;
|
|
|
|
const NAME: &str = "trigger";
|
|
|
|
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
|
|
let cmp = &mut cx.cmp;
|
|
if let Some(t) = opt.ticket {
|
|
if t < cmp.ticket {
|
|
succ!();
|
|
}
|
|
cmp.ticket = t;
|
|
}
|
|
|
|
let Some((parent, word)) = Self::split_url(&opt.word) else {
|
|
return act!(cmp:close, cx, false);
|
|
};
|
|
|
|
if cmp.caches.contains_key(&parent) {
|
|
let ticket = cmp.ticket;
|
|
return act!(cmp:show, cx, ShowOpt { cache_name: parent, word, ticket, ..Default::default() });
|
|
}
|
|
|
|
let ticket = cmp.ticket;
|
|
tokio::spawn(async move {
|
|
// TODO: support VFS
|
|
let mut dir = Local::read_dir(&parent).await?;
|
|
let mut cache = vec![];
|
|
|
|
// "/" is both a directory separator and the root directory per se
|
|
// As there's no parent directory for the FS root, it is a special case
|
|
if parent.loc.as_os_str() == "/" {
|
|
cache.push(CmpItem { name: OsString::new(), is_dir: true });
|
|
}
|
|
|
|
while let Ok(Some(ent)) = dir.next_entry().await {
|
|
if let Ok(ft) = ent.file_type().await {
|
|
cache.push(CmpItem { name: ent.file_name(), is_dir: ft.is_dir() });
|
|
}
|
|
}
|
|
|
|
if !cache.is_empty() {
|
|
cache.sort_unstable_by(|a, b| {
|
|
natsort(a.name.as_encoded_bytes(), b.name.as_encoded_bytes(), false)
|
|
});
|
|
CmpProxy::show(ShowOpt { cache, cache_name: parent, word, ticket });
|
|
}
|
|
|
|
Ok::<_, anyhow::Error>(())
|
|
});
|
|
|
|
succ!(render!(mem::replace(&mut cmp.visible, false)));
|
|
}
|
|
}
|
|
|
|
impl Trigger {
|
|
fn split_url(s: &str) -> Option<(Url, UrnBuf)> {
|
|
let (scheme, path) = Url::parse(s.as_bytes()).ok()?;
|
|
|
|
if !scheme.is_virtual() && path.as_os_str() == "~" {
|
|
return None; // We don't autocomplete a `~`, but `~/`
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
const SEP: &[char] = &['/', '\\'];
|
|
#[cfg(not(windows))]
|
|
const SEP: char = std::path::MAIN_SEPARATOR;
|
|
|
|
Some(match path.as_os_str().rsplit_once(SEP) {
|
|
Some((p, c)) if p.is_empty() => (Url { loc: MAIN_SEPARATOR_STR.into(), scheme }, c.into()),
|
|
Some((p, c)) => (expand_url(Url { loc: p.into(), scheme }).into_owned(), c.into()),
|
|
None => (CWD.load().as_ref().clone(), path.into()),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use yazi_shared::url::Urn;
|
|
|
|
use super::*;
|
|
|
|
fn compare(s: &str, parent: &str, child: &str) {
|
|
let (p, c) = Trigger::split_url(s).unwrap();
|
|
let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(p);
|
|
assert_eq!((p, c.as_urn()), (parent.try_into().unwrap(), Urn::new(child)));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn test_split() {
|
|
yazi_fs::init();
|
|
compare("", "", "");
|
|
compare(" ", "", " ");
|
|
compare("/", "/", "");
|
|
compare("//", "//", "");
|
|
compare("/foo", "/", "foo");
|
|
compare("/foo/", "/foo/", "");
|
|
compare("/foo/bar", "/foo/", "bar");
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn test_split() {
|
|
yazi_fs::init();
|
|
compare("foo", "", "foo");
|
|
compare(r"foo\", r"foo\", "");
|
|
compare(r"foo\bar", r"foo\", "bar");
|
|
compare(r"foo\bar\", r"foo\bar\", "");
|
|
compare(r"C:\", r"C:\", "");
|
|
compare(r"C:\foo", r"C:\", "foo");
|
|
compare(r"C:\foo\", r"C:\foo\", "");
|
|
compare(r"C:\foo\bar", r"C:\foo\", "bar");
|
|
}
|
|
}
|