feat: re-implement zoxide as a built-in plugin (#881)

This commit is contained in:
sxyazi 2024-04-07 09:00:58 +08:00
parent 0650affb76
commit cd2e7ff945
No known key found for this signature in database
11 changed files with 149 additions and 48 deletions

View File

@ -16,7 +16,7 @@ jobs:
pull-requests: write pull-requests: write
discussions: write discussions: write
steps: steps:
- uses: dessant/lock-threads@v4 - uses: dessant/lock-threads@v5
with: with:
issue-inactive-days: "30" issue-inactive-days: "30"
issue-comment: > issue-comment: >

View File

@ -81,7 +81,7 @@ keymap = [
{ on = [ "s" ], run = "search fd", desc = "Search files by name using fd" }, { on = [ "s" ], run = "search fd", desc = "Search files by name using fd" },
{ on = [ "S" ], run = "search rg", desc = "Search files by content using ripgrep" }, { on = [ "S" ], run = "search rg", desc = "Search files by content using ripgrep" },
{ on = [ "<C-s>" ], run = "search none", desc = "Cancel the ongoing search" }, { on = [ "<C-s>" ], run = "search none", desc = "Cancel the ongoing search" },
{ on = [ "z" ], run = "jump zoxide", desc = "Jump to a directory using zoxide" }, { on = [ "z" ], run = "plugin zoxide", desc = "Jump to a directory using zoxide" },
{ on = [ "Z" ], run = "jump fzf", desc = "Jump to a directory, or reveal a file using fzf" }, { on = [ "Z" ], run = "jump fzf", desc = "Jump to a directory, or reveal a file using fzf" },
# Linemode # Linemode

View File

@ -1,6 +1,8 @@
use yazi_plugin::external::{self, FzfOpt, ZoxideOpt}; use std::time::Duration;
use yazi_proxy::{AppProxy, TabProxy, HIDER};
use yazi_shared::{event::Cmd, fs::ends_with_slash, Defer}; use yazi_plugin::external::{self, FzfOpt};
use yazi_proxy::{options::{NotifyLevel, NotifyOpt}, AppProxy, TabProxy, HIDER};
use yazi_shared::{emit, event::Cmd, fs::ends_with_slash, Defer, Layer};
use crate::tab::Tab; use crate::tab::Tab;
@ -34,6 +36,21 @@ impl Tab {
return; return;
} }
// TODO: Remove this once Yazi v0.2.7 is released
if opt.type_ == OptType::Zoxide {
AppProxy::notify(NotifyOpt {
title: "Jump".to_owned(),
content: r#"The `jump zoxide` command has been deprecated in Yazi v0.2.5. Please replace it with `plugin zoxide` in your `keymap.toml`.
See https://github.com/sxyazi/yazi/issues/865 for more details."#.to_owned(),
level: NotifyLevel::Warn,
timeout: Duration::from_secs(15),
});
emit!(Call(Cmd::args("plugin", vec!["zoxide".to_owned()]), Layer::App));
return;
}
let cwd = self.current.cwd.clone(); let cwd = self.current.cwd.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _permit = HIDER.acquire().await.unwrap(); let _permit = HIDER.acquire().await.unwrap();
@ -43,7 +60,7 @@ impl Tab {
let result = if opt.type_ == OptType::Fzf { let result = if opt.type_ == OptType::Fzf {
external::fzf(FzfOpt { cwd }).await external::fzf(FzfOpt { cwd }).await
} else { } else {
external::zoxide(ZoxideOpt { cwd }).await unreachable!()
}; };
let Ok(url) = result else { let Ok(url) = result else {

View File

@ -16,7 +16,7 @@ impl<'a> Layout<'a> {
pub(crate) fn available(area: Rect) -> Rect { pub(crate) fn available(area: Rect) -> Rect {
let chunks = let chunks =
layout::Layout::horizontal([Constraint::Fill(1), Constraint::Length(40), Constraint::Max(1)]) layout::Layout::horizontal([Constraint::Fill(1), Constraint::Length(80), Constraint::Max(1)])
.split(area); .split(area);
let chunks = let chunks =

View File

@ -0,0 +1,79 @@
local state = ya.sync(function(st)
return {
cwd = tostring(cx.active.current.cwd),
empty = st.empty,
}
end)
local set_state = ya.sync(function(st, empty) st.empty = empty end)
local function notify(s, ...)
ya.notify { title = "Zoxide", content = string.format(s, ...), timeout = 5, level = "error" }
end
local function head(cwd)
local child = Command("zoxide"):args({ "query", "-l" }):stdout(Command.PIPED):spawn()
if not child then
return 0
end
local n = 0
repeat
local next, event = child:read_line()
if event ~= 0 then
break
elseif cwd ~= next:gsub("\n$", "") then
n = n + 1
end
until n >= 2
child:start_kill()
return n
end
local function setup(_, opts)
opts = opts or {}
if opts.update_db then
ps.sub(
"cd",
function()
ya.manager_emit("shell", {
confirm = true,
"zoxide add " .. ya.quote(tostring(cx.active.current.cwd)),
})
end
)
end
end
local function entry()
local st = state()
if st.empty == true then
return notify("No directory history in the database, check out the `zoxide` docs to set it up.")
elseif st.empty == nil and head(st.cwd) < 2 then
set_state(true)
return notify("No directory history in the database, check out the `zoxide` docs to set it up.")
end
local _permit = ya.hide()
local child, err = Command("zoxide")
:args({ "query", "-i", "--exclude" })
:arg(st.cwd)
:stdin(Command.INHERIT)
:stdout(Command.PIPED)
:stderr(Command.INHERIT)
:spawn()
if not child then
return notify("Spawn `zoxide` failed with error code %s. Do you have it installed?", err)
end
local output, err = child:wait_with_output()
if not output then
return notify("`zoxide` exited with error code %s", err)
end
ya.manager_emit("cd", { output.stdout:gsub("\n$", "") })
end
return { setup = setup, entry = entry }

View File

@ -3,11 +3,9 @@ mod fzf;
mod highlighter; mod highlighter;
mod lsar; mod lsar;
mod rg; mod rg;
mod zoxide;
pub use fd::*; pub use fd::*;
pub use fzf::*; pub use fzf::*;
pub use highlighter::*; pub use highlighter::*;
pub use lsar::*; pub use lsar::*;
pub use rg::*; pub use rg::*;
pub use zoxide::*;

View File

@ -1,26 +0,0 @@
use std::process::Stdio;
use anyhow::{bail, Result};
use tokio::process::Command;
use yazi_shared::fs::Url;
pub struct ZoxideOpt {
pub cwd: Url,
}
pub async fn zoxide(opt: ZoxideOpt) -> Result<Url> {
let child = Command::new("zoxide")
.args(["query", "-i", "--exclude"])
.arg(&opt.cwd)
.kill_on_drop(true)
.stdout(Stdio::piped())
.spawn()?;
let output = child.wait_with_output().await?;
let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !selected.is_empty() {
return Ok(Url::from(selected));
}
bail!("No match")
}

View File

@ -23,6 +23,6 @@ pub use opt::*;
pub use runtime::*; pub use runtime::*;
pub fn init() { pub fn init() {
crate::init_lua();
crate::loader::init(); crate::loader::init();
crate::init_lua();
} }

View File

@ -35,6 +35,7 @@ impl Loader {
"noop" => include_bytes!("../../preset/plugins/noop.lua"), "noop" => include_bytes!("../../preset/plugins/noop.lua"),
"pdf" => include_bytes!("../../preset/plugins/pdf.lua"), "pdf" => include_bytes!("../../preset/plugins/pdf.lua"),
"video" => include_bytes!("../../preset/plugins/video.lua"), "video" => include_bytes!("../../preset/plugins/video.lua"),
"zoxide" => include_bytes!("../../preset/plugins/zoxide.lua"),
_ => bail!("plugin not found: {name}"), _ => bail!("plugin not found: {name}"),
})) }))
})?; })?;

View File

@ -1,5 +1,6 @@
use mlua::{ExternalResult, Function, IntoLua, Lua, MetaMethod, Table, TableExt, UserData, Value, Variadic}; use mlua::{ExternalResult, IntoLua, Lua, MetaMethod, Table, TableExt, UserData, Value, Variadic};
use super::LOADER;
use crate::RtRef; use crate::RtRef;
pub(super) struct Require; pub(super) struct Require;
@ -8,15 +9,16 @@ impl Require {
pub(super) fn install(lua: &'static Lua) -> mlua::Result<()> { pub(super) fn install(lua: &'static Lua) -> mlua::Result<()> {
let globals = lua.globals(); let globals = lua.globals();
let require = globals.raw_get::<_, Function>("require")?;
globals.raw_set( globals.raw_set(
"require", "require",
lua.create_function(move |lua, name: mlua::String| { lua.create_function(|lua, name: mlua::String| {
lua.named_registry_value::<RtRef>("rt")?.swap(name.to_str()?); let s = name.to_str()?;
let mod_: Table = require.call(&name)?; futures::executor::block_on(LOADER.ensure(s)).into_lua_err()?;
lua.named_registry_value::<RtRef>("rt")?.swap(s);
let mod_ = LOADER.load(s)?;
lua.named_registry_value::<RtRef>("rt")?.reset(); lua.named_registry_value::<RtRef>("rt")?.reset();
mod_.raw_set("_name", &name)?;
Self::create_mt(lua, name, mod_) Self::create_mt(lua, name, mod_)
})?, })?,
)?; )?;

View File

@ -1,9 +1,11 @@
use std::time::Duration; use std::time::Duration;
use mlua::{IntoLuaMulti, Table, UserData, Value}; use futures::future::try_join3;
use tokio::{io::{AsyncBufReadExt, AsyncReadExt, BufReader}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; use mlua::{AnyUserData, IntoLuaMulti, Table, UserData, Value};
use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, BufReader}, process::{ChildStderr, ChildStdin, ChildStdout}, select};
use super::Status; use super::Status;
use crate::process::Output;
pub struct Child { pub struct Child {
inner: tokio::process::Child, inner: tokio::process::Child,
@ -26,8 +28,8 @@ impl UserData for Child {
#[inline] #[inline]
// TODO: return mlua::String instead of String // TODO: return mlua::String instead of String
async fn read_line(me: &mut Child) -> (String, u8) { async fn read_line(me: &mut Child) -> (String, u8) {
async fn read(t: Option<impl AsyncBufReadExt + Unpin>) -> Option<String> { async fn read(r: Option<impl AsyncBufReadExt + Unpin>) -> Option<String> {
let mut r = t?; let mut r = r?;
let mut buf = String::new(); let mut buf = String::new();
match r.read_line(&mut buf).await { match r.read_line(&mut buf).await {
Ok(0) | Err(_) => None, Ok(0) | Err(_) => None,
@ -43,8 +45,8 @@ impl UserData for Child {
} }
methods.add_async_method_mut("read", |_, me, len: usize| async move { methods.add_async_method_mut("read", |_, me, len: usize| async move {
async fn read(t: Option<impl AsyncBufReadExt + Unpin>, len: usize) -> Option<Vec<u8>> { async fn read(r: Option<impl AsyncBufReadExt + Unpin>, len: usize) -> Option<Vec<u8>> {
let mut r = t?; let mut r = r?;
let mut buf = vec![0; len]; let mut buf = vec![0; len];
match r.read(&mut buf).await { match r.read(&mut buf).await {
Ok(0) | Err(_) => return None, Ok(0) | Err(_) => return None,
@ -73,6 +75,34 @@ impl UserData for Child {
Err(e) => (Value::Nil, e.raw_os_error()).into_lua_multi(lua), Err(e) => (Value::Nil, e.raw_os_error()).into_lua_multi(lua),
} }
}); });
methods.add_async_function("wait_with_output", |lua, ud: AnyUserData| async move {
async fn read_to_end(r: &mut Option<impl AsyncBufReadExt + Unpin>) -> io::Result<Vec<u8>> {
let mut vec = Vec::new();
if let Some(r) = r.as_mut() {
r.read_to_end(&mut vec).await?;
}
Ok(vec)
}
let mut me = ud.take::<Self>()?;
let mut stdout_pipe = me.stdout.take();
let mut stderr_pipe = me.stderr.take();
let stdout_fut = read_to_end(&mut stdout_pipe);
let stderr_fut = read_to_end(&mut stderr_pipe);
let result = try_join3(me.inner.wait(), stdout_fut, stderr_fut).await;
drop(stdout_pipe);
drop(stderr_pipe);
match result {
Ok((status, stdout, stderr)) => {
(Output::new(std::process::Output { status, stdout, stderr }), Value::Nil)
.into_lua_multi(lua)
}
Err(e) => (Value::Nil, e.raw_os_error()).into_lua_multi(lua),
}
});
methods.add_method_mut("start_kill", |lua, me, ()| match me.inner.start_kill() { methods.add_method_mut("start_kill", |lua, me, ()| match me.inner.start_kill() {
Ok(_) => (true, Value::Nil).into_lua_multi(lua), Ok(_) => (true, Value::Nil).into_lua_multi(lua),
Err(e) => (false, e.raw_os_error()).into_lua_multi(lua), Err(e) => (false, e.raw_os_error()).into_lua_multi(lua),