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=fd", desc = "Search files by name via fd" },
{ on = "S", run = "search --via=rg", desc = "Search files by content via ripgrep" }, { 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 = "<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 # Linemode
{ on = [ "m", "s" ], run = "linemode size", desc = "Linemode: size" }, { on = [ "m", "s" ], run = "linemode size", desc = "Linemode: size" },

View file

@ -19,7 +19,7 @@ impl Mgr {
if self.yanked.cut { if self.yanked.cut {
tasks.file_cut(&src, dest, opt.force); 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(()); self.unyank(());
} else { } else {
tasks.file_copy(&src, dest, opt.force, opt.follow); tasks.file_copy(&src, dest, opt.force, opt.follow);

View file

@ -57,7 +57,7 @@ impl Mgr {
#[yazi_codegen::command] #[yazi_codegen::command]
pub fn remove_do(&mut self, opt: Opt, tasks: &Tasks) { pub fn remove_do(&mut self, opt: Opt, tasks: &Tasks) {
self.tabs.iter_mut().for_each(|t| { 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 { for u in &opt.targets {

View file

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

View file

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

View file

@ -20,11 +20,7 @@ impl Selected {
#[inline] #[inline]
pub fn add(&mut self, url: &Url) -> bool { self.add_same(&[url]) == 1 } 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 { pub fn add_many(&mut self, urls: &[impl AsRef<Url>]) -> usize {
if same {
return self.add_same(urls);
}
let mut grouped: HashMap<_, Vec<_>> = Default::default(); let mut grouped: HashMap<_, Vec<_>> = Default::default();
for u in urls { for u in urls {
if let Some(p) = u.as_ref().parent_url() { if let Some(p) = u.as_ref().parent_url() {
@ -66,22 +62,19 @@ impl Selected {
#[inline] #[inline]
pub fn remove(&mut self, url: &Url) -> bool { self.remove_same(&[url]) == 1 } 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 { pub fn remove_many(&mut self, urls: &[impl AsRef<Url>]) -> usize {
let affected = if same { let mut grouped: HashMap<_, Vec<_>> = Default::default();
self.remove_same(urls) for u in urls {
} else { if let Some(p) = u.as_ref().parent_url() {
let mut grouped: HashMap<_, Vec<_>> = Default::default(); grouped.entry(p).or_default().push(u);
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 { if affected > 0 {
self.inner.sort_unstable_by(|_, a, _, b| a.cmp(b)); self.inner.sort_unstable_by(|_, a, _, b| a.cmp(b));
} }
affected affected
} }
@ -113,10 +106,10 @@ impl Selected {
pub fn apply_op(&mut self, op: &FilesOp) { pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = op.diff_recoverable(|u| self.contains_key(u)); let (removal, addition) = op.diff_recoverable(|u| self.contains_key(u));
if !removal.is_empty() { if !removal.is_empty() {
self.remove_many(&removal, !op.cwd().is_search()); self.remove_many(&removal);
} }
if !addition.is_empty() { 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!(); render!();
} }
}; };
($left:expr, > $right:expr) => {{
let val = $left;
if val > $right {
render!();
}
val
}};
} }
#[macro_export] #[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 _permit = ya.hide()
local cwd = tostring(state()) local cwd, selected = state()
local child, err = local output, err = M.run_with(cwd, selected)
Command("fzf"):cwd(cwd):stdin(Command.INHERIT):stdout(Command.PIPED):stderr(Command.INHERIT):spawn() 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 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 end
local output, err = child:wait_with_output() local output, err = child:wait_with_output()
if not output then 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 elseif not output.status.success and output.status.code ~= 130 then
return fail("`fzf` exited with error code %s", output.status.code) return nil, Err("`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 })
end end
return output.stdout, nil
end 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 me
} }
#[inline]
pub fn len(&self) -> usize { self.args.len() }
#[inline]
pub fn is_empty(&self) -> bool { self.args.is_empty() }
// --- With // --- With
#[inline] #[inline]
pub fn with(mut self, name: impl Into<DataKey>, value: impl ToString) -> Self { pub fn with(mut self, name: impl Into<DataKey>, value: impl ToString) -> Self {