From e774044cad1d92323d47903c9a6675f3ab8fe1c1 Mon Sep 17 00:00:00 2001 From: Ahmed Hassan Date: Tue, 29 Jul 2025 22:18:59 +0400 Subject: [PATCH] Adding Everything (es) search integration for Windows Users Only * Integrated Everything CLI (es/esfind) as a new search backend in Yazi. Added Windows-only runtime check to ensure the feature is available only for Windows users. * Updated search actor, parser, plugin, and keymap to support the new "es" search option. * Improved error handling and user feedback for unsupported platforms and missing CLI. * Ensured correct path handling for search results to enable preview and open actions. * Updated documentation and spell-check word list for the new search tool. --- buildHere.bat | 1 + cspell.json | 2 +- yazi-actor/src/mgr/search.rs | 11 ++- yazi-boot/src/actions/debug.rs | 1 + yazi-config/preset/keymap-default.toml | 1 + yazi-fs/src/cha/cha.rs | 2 +- yazi-parser/src/mgr/search.rs | 13 ++- yazi-plugin/src/external/es.rs | 62 +++++++++++++ yazi-plugin/src/external/mod.rs | 2 +- yazi-shared/src/url/url.rs | 117 ++++++++++++++++++------- 10 files changed, 171 insertions(+), 41 deletions(-) create mode 100644 buildHere.bat create mode 100644 yazi-plugin/src/external/es.rs diff --git a/buildHere.bat b/buildHere.bat new file mode 100644 index 00000000..92565f1f --- /dev/null +++ b/buildHere.bat @@ -0,0 +1 @@ +cargo build --profile release-windows --locked \ No newline at end of file diff --git a/cspell.json b/cspell.json index 6cfa834e..d51bd8bd 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"version":"0.2","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"],"language":"en"} \ No newline at end of file +{"version":"0.2","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"],"language":"en"} diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index a377b7f0..afd4b2ac 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -6,7 +6,10 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_fs::{FilesOp, cha::Cha}; use yazi_macro::{act, succ}; -use yazi_parser::{VoidOpt, mgr::{SearchOpt, SearchOptVia}}; +use yazi_parser::{ + VoidOpt, + mgr::{SearchOpt, SearchOptVia}, +}; use yazi_plugin::external; use yazi_proxy::{InputProxy, MgrProxy}; use yazi_shared::event::Data; @@ -75,6 +78,12 @@ impl Actor for SearchDo { subject: opt.subject.into_owned(), args: opt.args, }), + SearchOptVia::Es => external::es(external::EsOpt { + cwd: cwd.clone(), + hidden, + subject: opt.subject.into_owned(), + args: opt.args, + }), }?; let rx = UnboundedReceiverStream::new(rx).chunks_timeout(5000, Duration::from_millis(500)); diff --git a/yazi-boot/src/actions/debug.rs b/yazi-boot/src/actions/debug.rs index 9ee15264..7adf1a17 100644 --- a/yazi-boot/src/actions/debug.rs +++ b/yazi-boot/src/actions/debug.rs @@ -88,6 +88,7 @@ impl Actions { writeln!(s, " fzf : {}", Self::process_output("fzf", "--version"))?; #[rustfmt::skip] writeln!(s, " fd/fdfind : {} / {}", Self::process_output("fd", "--version"), Self::process_output("fdfind", "--version"))?; + writeln!(s, " es : {}", Self::process_output("es", "--version"))?; writeln!(s, " rg : {}", Self::process_output("rg", "--version"))?; writeln!(s, " chafa : {}", Self::process_output("chafa", "--version"))?; writeln!(s, " zoxide : {}", Self::process_output("zoxide", "--version"))?; diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index c02666de..10985df2 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -81,6 +81,7 @@ keymap = [ { on = ".", run = "hidden toggle", desc = "Toggle the visibility of hidden files" }, { on = "s", run = "search --via=fd", desc = "Search files by name via fd" }, { on = "S", run = "search --via=rg", desc = "Search files by content via ripgrep" }, + { on = "E", run = "search --via=es", desc = "Search files by content via EveryThing" }, { on = "", run = "escape --search", desc = "Cancel the ongoing search" }, { on = "z", run = "plugin fzf", desc = "Jump to a file/directory via fzf" }, { on = "Z", run = "plugin zoxide", desc = "Jump to a directory via zoxide" }, diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 49a9d459..47a10fd2 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -77,7 +77,7 @@ impl Cha { #[inline] pub fn from_dummy(_url: &Url, ft: Option) -> Self { - let mut me = ft.map(Self::from_half_ft).unwrap_or_default(); + let me = ft.map(Self::from_half_ft).unwrap_or_default(); #[cfg(unix)] if _url.urn().is_hidden() { me.kind |= ChaKind::HIDDEN; diff --git a/yazi-parser/src/mgr/search.rs b/yazi-parser/src/mgr/search.rs index 193e76bd..9d22fae3 100644 --- a/yazi-parser/src/mgr/search.rs +++ b/yazi-parser/src/mgr/search.rs @@ -4,9 +4,9 @@ use yazi_shared::{SStr, event::CmdCow}; #[derive(Debug)] pub struct SearchOpt { - pub via: SearchOptVia, - pub subject: SStr, - pub args: Vec, + pub via: SearchOptVia, + pub subject: SStr, + pub args: Vec, pub args_raw: SStr, } @@ -36,7 +36,9 @@ impl TryFrom for SearchOpt { } impl IntoLua for &SearchOpt { - fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } + fn into_lua(self, _: &Lua) -> mlua::Result { + Err("unsupported".into_lua_err()) + } } // Via @@ -45,6 +47,7 @@ pub enum SearchOptVia { Rg, Rga, Fd, + Es, } impl From<&str> for SearchOptVia { @@ -52,6 +55,7 @@ impl From<&str> for SearchOptVia { match value { "rg" => Self::Rg, "rga" => Self::Rga, + "es" => Self::Es, _ => Self::Fd, } } @@ -63,6 +67,7 @@ impl SearchOptVia { Self::Rg => "rg", Self::Rga => "rga", Self::Fd => "fd", + Self::Es => "es", } } } diff --git a/yazi-plugin/src/external/es.rs b/yazi-plugin/src/external/es.rs new file mode 100644 index 00000000..ba509268 --- /dev/null +++ b/yazi-plugin/src/external/es.rs @@ -0,0 +1,62 @@ +use std::path::Path; +use std::process::Stdio; + +use anyhow::Result; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + process::{Child, Command}, + sync::mpsc::{self, UnboundedReceiver}, +}; +use yazi_fs::File; +use yazi_shared::url::Url; + +pub struct EsOpt { + pub cwd: Url, + pub hidden: bool, + pub subject: String, + pub args: Vec, +} + +pub fn es(opt: EsOpt) -> Result> { + let mut child = spawn("es", &opt)?; + + let mut it = BufReader::new(child.stdout.take().unwrap()).lines(); + let (tx, rx) = mpsc::unbounded_channel(); + + tokio::spawn(async move { + while let Ok(Some(line)) = it.next_line().await { + if let Ok(file) = File::new(Path::new(&line).into()).await { + tx.send(file).ok(); + } + } + child.wait().await.ok(); + }); + Ok(rx) +} + +fn spawn(program: &str, opt: &EsOpt) -> std::io::Result { + if !cfg!(windows) { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Sorry, EveryThing Search is only avaiable for Windows Users!", + )); + } + let Some(path) = opt.cwd.as_path() else { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Check if you have EveryThing CLI installed!", + )); + }; + + Command::new(program) + .arg("-path") + .arg(path) + // .arg(if opt.hidden { "/ah" } else { "/a-h" }) + // .args(&opt.args) + .arg("-regex") + .arg(&opt.subject) + .kill_on_drop(true) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() +} diff --git a/yazi-plugin/src/external/mod.rs b/yazi-plugin/src/external/mod.rs index f974108b..861dfab7 100644 --- a/yazi-plugin/src/external/mod.rs +++ b/yazi-plugin/src/external/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(fd highlighter rg rga); +yazi_macro::mod_flat!(fd highlighter rg rga es); diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 1540624f..4b3c79ba 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -1,37 +1,57 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, ops::Deref, path::{Path, PathBuf}}; +use std::{ + borrow::Cow, + ffi::OsStr, + fmt::{Debug, Formatter}, + hash::BuildHasher, + ops::Deref, + path::{Path, PathBuf}, +}; use percent_encoding::percent_decode; use serde::{Deserialize, Serialize}; use super::UrnBuf; -use crate::{IntoOsStr, url::{Components, Display, Loc, Scheme}}; +use crate::{ + IntoOsStr, + url::{Components, Display, Loc, Scheme}, +}; #[derive(Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)] pub struct Url { - pub loc: Loc, + pub loc: Loc, pub scheme: Scheme, } impl Deref for Url { type Target = Loc; - fn deref(&self) -> &Self::Target { &self.loc } + fn deref(&self) -> &Self::Target { + &self.loc + } } impl From for Url { - fn from(loc: Loc) -> Self { Self { loc, scheme: Scheme::Regular } } + fn from(loc: Loc) -> Self { + Self { loc, scheme: Scheme::Regular } + } } impl From for Url { - fn from(path: PathBuf) -> Self { Loc::from(path).into() } + fn from(path: PathBuf) -> Self { + Loc::from(path).into() + } } impl From<&PathBuf> for Url { - fn from(path: &PathBuf) -> Self { path.to_owned().into() } + fn from(path: &PathBuf) -> Self { + path.to_owned().into() + } } impl From<&Path> for Url { - fn from(path: &Path) -> Self { path.to_path_buf().into() } + fn from(path: &Path) -> Self { + path.to_path_buf().into() + } } impl TryFrom<&[u8]> for Url { @@ -53,34 +73,48 @@ impl TryFrom<&[u8]> for Url { impl TryFrom<&str> for Url { type Error = anyhow::Error; - fn try_from(value: &str) -> Result { value.as_bytes().try_into() } + fn try_from(value: &str) -> Result { + value.as_bytes().try_into() + } } impl TryFrom for Url { type Error = anyhow::Error; - fn try_from(value: String) -> Result { value.as_bytes().try_into() } + fn try_from(value: String) -> Result { + value.as_bytes().try_into() + } } impl AsRef for Url { - fn as_ref(&self) -> &Url { self } + fn as_ref(&self) -> &Url { + self + } } // FIXME: remove impl AsRef for Url { - fn as_ref(&self) -> &Path { &self.loc } + fn as_ref(&self) -> &Path { + &self.loc + } } impl<'a> From<&'a Url> for Cow<'a, Url> { - fn from(url: &'a Url) -> Self { Cow::Borrowed(url) } + fn from(url: &'a Url) -> Self { + Cow::Borrowed(url) + } } impl From for Cow<'_, Url> { - fn from(url: Url) -> Self { Cow::Owned(url) } + fn from(url: Url) -> Self { + Cow::Owned(url) + } } impl From> for Url { - fn from(url: Cow<'_, Url>) -> Self { url.into_owned() } + fn from(url: Cow<'_, Url>) -> Self { + url.into_owned() + } } impl Url { @@ -105,16 +139,16 @@ impl Url { Scheme::SearchItem => { Self { loc: Loc::with(self.loc.base(), self.loc.join(path)), scheme: Scheme::SearchItem } } - Scheme::Archive(_) => { - Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() } - } + Scheme::Archive(_) => Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() }, Scheme::Sftp(_) => Self { loc: self.loc.join(path).into(), scheme: self.scheme.clone() }, } } // FIXME: check usages #[inline] - pub fn components(&self) -> Components<'_> { Components::new(self) } + pub fn components(&self) -> Components<'_> { + Components::new(self) + } #[inline] pub fn covariant(&self, other: &Self) -> bool { @@ -122,10 +156,14 @@ impl Url { } #[inline] - pub fn display(&self) -> Display<'_> { Display::new(self) } + pub fn display(&self) -> Display<'_> { + Display::new(self) + } #[inline] - pub fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() } + pub fn os_str(&self) -> Cow<'_, OsStr> { + self.components().os_str() + } pub fn parent_url(&self) -> Option { let parent = self.loc.parent()?; @@ -151,10 +189,7 @@ impl Url { return None; } - Some(Self { - loc: self.loc.strip_prefix(&base.loc).ok()?.into(), - scheme: self.scheme.clone(), - }) + Some(Self { loc: self.loc.strip_prefix(&base.loc).ok()?.into(), scheme: self.scheme.clone() }) } #[inline] @@ -163,13 +198,19 @@ impl Url { } #[inline] - pub fn set_name(&mut self, name: impl AsRef) { self.loc.set_name(name); } + pub fn set_name(&mut self, name: impl AsRef) { + self.loc.set_name(name); + } #[inline] - pub fn pair(&self) -> Option<(Self, UrnBuf)> { Some((self.parent_url()?, self.loc.urn_owned())) } + pub fn pair(&self) -> Option<(Self, UrnBuf)> { + Some((self.parent_url()?, self.loc.urn_owned())) + } #[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) + } #[inline] pub fn rebase(&self, parent: &Path) -> Self { @@ -181,10 +222,14 @@ impl Url { impl Url { // --- Regular #[inline] - pub fn is_regular(&self) -> bool { self.scheme == Scheme::Regular } + pub fn is_regular(&self) -> bool { + self.scheme == Scheme::Regular + } #[inline] - pub fn to_regular(&self) -> Self { Self { loc: self.loc.clone(), scheme: Scheme::Regular } } + pub fn to_regular(&self) -> Self { + Self { loc: self.loc.clone(), scheme: Scheme::Regular } + } #[inline] pub fn into_regular(mut self) -> Self { @@ -194,7 +239,9 @@ impl Url { // --- Search #[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 to_search(&self, frag: impl AsRef) -> Self { @@ -209,11 +256,15 @@ impl Url { // --- Archive #[inline] - pub fn is_archive(&self) -> bool { matches!(self.scheme, Scheme::Archive(_)) } + pub fn is_archive(&self) -> bool { + matches!(self.scheme, Scheme::Archive(_)) + } // FIXME: remove #[inline] - pub fn into_path(self) -> PathBuf { self.loc.into_path() } + pub fn into_path(self) -> PathBuf { + self.loc.into_path() + } } impl Debug for Url {