feat!: enhance fzf integration (#2553)

This commit is contained in:
三咲雅 · Misaki Masa 2025-03-30 22:17:07 +08:00 committed by GitHub
parent 8488ae8394
commit 1765aba684
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 124 additions and 54 deletions

View file

@ -82,8 +82,8 @@ keymap = [
{ 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 = "<C-s>", run = "escape --search", desc = "Cancel the ongoing search" },
{ on = "z", run = "plugin zoxide", desc = "Jump to a directory via zoxide" },
{ on = "Z", run = "plugin fzf", desc = "Jump to a file/directory via fzf" },
{ 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" },
# Linemode
{ on = [ "m", "s" ], run = "linemode size", desc = "Linemode: size" },

View file

@ -19,7 +19,7 @@ impl Mgr {
if self.yanked.cut {
tasks.file_cut(&src, dest, opt.force);
self.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(&src, false));
self.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(&src));
self.unyank(());
} else {
tasks.file_copy(&src, dest, opt.force, opt.follow);

View file

@ -57,7 +57,7 @@ impl Mgr {
#[yazi_codegen::command]
pub fn remove_do(&mut self, opt: Opt, tasks: &Tasks) {
self.tabs.iter_mut().for_each(|t| {
t.selected.remove_many(&opt.targets, false);
t.selected.remove_many(&opt.targets);
});
for u in &opt.targets {

View file

@ -109,10 +109,9 @@ impl Tab {
let urls: Vec<_> =
indices.into_iter().filter_map(|i| self.current.files.get(i)).map(|f| &f.url).collect();
let same = !self.cwd().is_search();
if !select {
self.selected.remove_many(&urls, same);
} else if self.selected.add_many(&urls, same) != urls.len() {
self.selected.remove_many(&urls);
} else if self.selected.add_many(&urls) != urls.len() {
AppProxy::notify_warn(
"Escape visual mode",
"Some files cannot be selected, due to path nesting conflict.",

View file

@ -1,16 +1,26 @@
use yazi_macro::render;
use yazi_proxy::AppProxy;
use yazi_shared::event::CmdCow;
use yazi_shared::{event::CmdCow, url::Url};
use crate::tab::Tab;
struct Opt {
urls: Vec<Url>,
state: Option<bool>,
}
impl From<CmdCow> for Opt {
fn from(c: CmdCow) -> Self {
fn from(mut c: CmdCow) -> Self {
let mut urls = Vec::with_capacity(c.len());
for i in 0..c.len() {
match c.take_url(i) {
Some(url) => urls.push(url),
None => break,
}
}
Self {
urls,
state: match c.str("state") {
Some("on") => Some(true),
Some("off") => Some(false),
@ -19,27 +29,38 @@ impl From<CmdCow> for Opt {
}
}
}
impl From<Option<bool>> for Opt {
fn from(state: Option<bool>) -> Self { Self { state } }
fn from(state: Option<bool>) -> Self { Self { urls: vec![], state } }
}
impl Tab {
#[yazi_codegen::command]
pub fn toggle_all(&mut self, opt: Opt) {
let iter = self.current.files.iter().map(|f| &f.url);
let (removal, addition): (Vec<_>, Vec<_>) = match opt.state {
Some(true) => (vec![], iter.collect()),
Some(false) => (iter.collect(), vec![]),
None => iter.partition(|&u| self.selected.contains_key(u)),
use yazi_shared::Either::*;
let it = self.current.files.iter().map(|f| &f.url);
let either = match opt.state {
Some(true) if opt.urls.is_empty() => Left((vec![], it.collect())),
Some(true) => Right((vec![], opt.urls)),
Some(false) if opt.urls.is_empty() => Left((it.collect(), vec![])),
Some(false) => Right((opt.urls, vec![])),
None if opt.urls.is_empty() => Left(it.partition(|&u| self.selected.contains_key(u))),
None => Right(opt.urls.into_iter().partition(|u| self.selected.contains_key(u))),
};
let same = !self.cwd().is_search();
render!(self.selected.remove_many(&removal, same) > 0);
let warn = match either {
Left((removal, addition)) => {
render!(self.selected.remove_many(&removal) > 0);
render!(self.selected.add_many(&addition), > 0) != addition.len()
}
Right((removal, addition)) => {
render!(self.selected.remove_many(&removal) > 0);
render!(self.selected.add_many(&addition), > 0) != addition.len()
}
};
let added = self.selected.add_many(&addition, same);
render!(added > 0);
if added != addition.len() {
if warn {
AppProxy::notify_warn(
"Toggle all",
"Some files cannot be selected, due to path nesting conflict.",

View file

@ -20,11 +20,7 @@ impl Selected {
#[inline]
pub fn add(&mut self, url: &Url) -> bool { self.add_same(&[url]) == 1 }
pub fn add_many(&mut self, urls: &[impl AsRef<Url>], same: bool) -> usize {
if same {
return self.add_same(urls);
}
pub fn add_many(&mut self, urls: &[impl AsRef<Url>]) -> usize {
let mut grouped: HashMap<_, Vec<_>> = Default::default();
for u in urls {
if let Some(p) = u.as_ref().parent_url() {
@ -66,22 +62,19 @@ impl Selected {
#[inline]
pub fn remove(&mut self, url: &Url) -> bool { self.remove_same(&[url]) == 1 }
pub fn remove_many(&mut self, urls: &[impl AsRef<Url>], same: bool) -> usize {
let affected = if same {
self.remove_same(urls)
} else {
let mut grouped: HashMap<_, Vec<_>> = Default::default();
for u in urls {
if let Some(p) = u.as_ref().parent_url() {
grouped.entry(p).or_default().push(u);
}
pub fn remove_many(&mut self, urls: &[impl AsRef<Url>]) -> usize {
let mut grouped: HashMap<_, Vec<_>> = Default::default();
for u in urls {
if let Some(p) = u.as_ref().parent_url() {
grouped.entry(p).or_default().push(u);
}
grouped.into_values().map(|v| self.remove_same(&v)).sum()
};
}
let affected = grouped.into_values().map(|v| self.remove_same(&v)).sum();
if affected > 0 {
self.inner.sort_unstable_by(|_, a, _, b| a.cmp(b));
}
affected
}
@ -113,10 +106,10 @@ impl Selected {
pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = op.diff_recoverable(|u| self.contains_key(u));
if !removal.is_empty() {
self.remove_many(&removal, !op.cwd().is_search());
self.remove_many(&removal);
}
if !addition.is_empty() {
self.add_many(&addition, !op.cwd().is_search());
self.add_many(&addition);
}
}
}

View file

@ -24,6 +24,13 @@ macro_rules! render {
render!();
}
};
($left:expr, > $right:expr) => {{
let val = $left;
if val > $right {
render!();
}
val
}};
}
#[macro_export]

View file

@ -1,29 +1,73 @@
local state = ya.sync(function() return cx.active.current.cwd end)
local M = {}
local function fail(s, ...) ya.notify { title = "Fzf", content = string.format(s, ...), timeout = 5, level = "error" } end
local state = ya.sync(function()
local selected = {}
for _, url in pairs(cx.active.selected) do
selected[#selected + 1] = url
end
return cx.active.current.cwd, selected
end)
function M:entry()
ya.mgr_emit("escape", { visual = true })
local function entry()
local _permit = ya.hide()
local cwd = tostring(state())
local cwd, selected = state()
local child, err =
Command("fzf"):cwd(cwd):stdin(Command.INHERIT):stdout(Command.PIPED):stderr(Command.INHERIT):spawn()
local output, err = M.run_with(cwd, selected)
if not output then
return ya.notify { title = "Fzf", content = tostring(err), timeout = 5, level = "error" }
end
local urls = M.split_urls(cwd, output)
if #urls == 1 then
local cha = #selected == 0 and fs.cha(urls[1])
ya.mgr_emit(cha and cha.is_dir and "cd" or "reveal", { urls[1] })
elseif #urls > 1 then
urls.state = #selected > 0 and "off" or "on"
ya.mgr_emit("toggle_all", urls)
end
end
function M.run_with(cwd, selected)
local child, err = Command("fzf")
:arg("-m")
:cwd(tostring(cwd))
:stdin(#selected > 0 and Command.PIPED or Command.INHERIT)
:stdout(Command.PIPED)
:spawn()
if not child then
return fail("Failed to start `fzf`, error: " .. err)
return nil, Err("Failed to start `fzf`, error: %s", err)
end
for _, u in ipairs(selected) do
child:write_all(string.format("%s\n", u))
end
if #selected > 0 then
child:flush()
end
local output, err = child:wait_with_output()
if not output then
return fail("Cannot read `fzf` output, error: " .. err)
return nil, Err("Cannot read `fzf` output, error: %s", err)
elseif not output.status.success and output.status.code ~= 130 then
return fail("`fzf` exited with error code %s", output.status.code)
end
local target = output.stdout:gsub("\n$", "")
if target ~= "" then
ya.mgr_emit(target:find("[/\\]$") and "cd" or "reveal", { target })
return nil, Err("`fzf` exited with error code %s", output.status.code)
end
return output.stdout, nil
end
return { entry = entry }
function M.split_urls(cwd, output)
local t = {}
for line in output:gmatch("[^\r\n]+") do
local u = Url(line)
if u.is_absolute then
t[#t + 1] = u
else
t[#t + 1] = cwd:join(u)
end
end
return t
end
return M

View file

@ -33,6 +33,12 @@ impl Cmd {
me
}
#[inline]
pub fn len(&self) -> usize { self.args.len() }
#[inline]
pub fn is_empty(&self) -> bool { self.args.is_empty() }
// --- With
#[inline]
pub fn with(mut self, name: impl Into<DataKey>, value: impl ToString) -> Self {