From 1765aba68440f73c590cedac14ece6778fe88ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Sun, 30 Mar 2025 22:17:07 +0800 Subject: [PATCH] feat!: enhance `fzf` integration (#2553) --- yazi-config/preset/keymap-default.toml | 4 +- yazi-core/src/mgr/commands/paste.rs | 2 +- yazi-core/src/mgr/commands/remove.rs | 2 +- yazi-core/src/tab/commands/escape.rs | 5 +- yazi-core/src/tab/commands/toggle_all.rs | 49 +++++++++++----- yazi-core/src/tab/selected.rs | 29 ++++------ yazi-macro/src/event.rs | 7 +++ yazi-plugin/preset/plugins/fzf.lua | 74 +++++++++++++++++++----- yazi-shared/src/event/cmd.rs | 6 ++ 9 files changed, 124 insertions(+), 54 deletions(-) diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index f4cda809..14fc54e7 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -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 = "", 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" }, diff --git a/yazi-core/src/mgr/commands/paste.rs b/yazi-core/src/mgr/commands/paste.rs index fcc814cb..39c05838 100644 --- a/yazi-core/src/mgr/commands/paste.rs +++ b/yazi-core/src/mgr/commands/paste.rs @@ -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); diff --git a/yazi-core/src/mgr/commands/remove.rs b/yazi-core/src/mgr/commands/remove.rs index 35f373c9..9f9ce19b 100644 --- a/yazi-core/src/mgr/commands/remove.rs +++ b/yazi-core/src/mgr/commands/remove.rs @@ -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 { diff --git a/yazi-core/src/tab/commands/escape.rs b/yazi-core/src/tab/commands/escape.rs index e8b30569..bd1234d2 100644 --- a/yazi-core/src/tab/commands/escape.rs +++ b/yazi-core/src/tab/commands/escape.rs @@ -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.", diff --git a/yazi-core/src/tab/commands/toggle_all.rs b/yazi-core/src/tab/commands/toggle_all.rs index 3bf004d3..4228bf6b 100644 --- a/yazi-core/src/tab/commands/toggle_all.rs +++ b/yazi-core/src/tab/commands/toggle_all.rs @@ -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, state: Option, } impl From 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 for Opt { } } } + impl From> for Opt { - fn from(state: Option) -> Self { Self { state } } + fn from(state: Option) -> 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.", diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 9586c1c2..364f5c10 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -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], same: bool) -> usize { - if same { - return self.add_same(urls); - } - + pub fn add_many(&mut self, urls: &[impl AsRef]) -> 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], 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]) -> 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); } } } diff --git a/yazi-macro/src/event.rs b/yazi-macro/src/event.rs index c2f5c48b..745cdf42 100644 --- a/yazi-macro/src/event.rs +++ b/yazi-macro/src/event.rs @@ -24,6 +24,13 @@ macro_rules! render { render!(); } }; + ($left:expr, > $right:expr) => {{ + let val = $left; + if val > $right { + render!(); + } + val + }}; } #[macro_export] diff --git a/yazi-plugin/preset/plugins/fzf.lua b/yazi-plugin/preset/plugins/fzf.lua index f655a217..645675b0 100644 --- a/yazi-plugin/preset/plugins/fzf.lua +++ b/yazi-plugin/preset/plugins/fzf.lua @@ -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 diff --git a/yazi-shared/src/event/cmd.rs b/yazi-shared/src/event/cmd.rs index 1fc84615..340528e5 100644 --- a/yazi-shared/src/event/cmd.rs +++ b/yazi-shared/src/event/cmd.rs @@ -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, value: impl ToString) -> Self {