diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 557f29b2..261c8988 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -16,7 +16,7 @@ jobs: pull-requests: write discussions: write steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: issue-inactive-days: "30" issue-comment: > diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index eb363ac0..c4d4d6b5 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -81,7 +81,7 @@ keymap = [ { 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 = [ "" ], 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" }, # Linemode diff --git a/yazi-core/src/tab/commands/jump.rs b/yazi-core/src/tab/commands/jump.rs index cd0c73ea..ccbae89b 100644 --- a/yazi-core/src/tab/commands/jump.rs +++ b/yazi-core/src/tab/commands/jump.rs @@ -1,6 +1,8 @@ -use yazi_plugin::external::{self, FzfOpt, ZoxideOpt}; -use yazi_proxy::{AppProxy, TabProxy, HIDER}; -use yazi_shared::{event::Cmd, fs::ends_with_slash, Defer}; +use std::time::Duration; + +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; @@ -34,6 +36,21 @@ impl Tab { 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(); tokio::spawn(async move { let _permit = HIDER.acquire().await.unwrap(); @@ -43,7 +60,7 @@ impl Tab { let result = if opt.type_ == OptType::Fzf { external::fzf(FzfOpt { cwd }).await } else { - external::zoxide(ZoxideOpt { cwd }).await + unreachable!() }; let Ok(url) = result else { diff --git a/yazi-fm/src/notify/layout.rs b/yazi-fm/src/notify/layout.rs index 33783ef5..a3563e91 100644 --- a/yazi-fm/src/notify/layout.rs +++ b/yazi-fm/src/notify/layout.rs @@ -16,7 +16,7 @@ impl<'a> Layout<'a> { pub(crate) fn available(area: Rect) -> Rect { 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); let chunks = diff --git a/yazi-plugin/preset/plugins/zoxide.lua b/yazi-plugin/preset/plugins/zoxide.lua new file mode 100644 index 00000000..a0b32dfc --- /dev/null +++ b/yazi-plugin/preset/plugins/zoxide.lua @@ -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 } diff --git a/yazi-plugin/src/external/mod.rs b/yazi-plugin/src/external/mod.rs index 9400cf83..42041c66 100644 --- a/yazi-plugin/src/external/mod.rs +++ b/yazi-plugin/src/external/mod.rs @@ -3,11 +3,9 @@ mod fzf; mod highlighter; mod lsar; mod rg; -mod zoxide; pub use fd::*; pub use fzf::*; pub use highlighter::*; pub use lsar::*; pub use rg::*; -pub use zoxide::*; diff --git a/yazi-plugin/src/external/zoxide.rs b/yazi-plugin/src/external/zoxide.rs deleted file mode 100644 index 23d4ff2c..00000000 --- a/yazi-plugin/src/external/zoxide.rs +++ /dev/null @@ -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 { - 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") -} diff --git a/yazi-plugin/src/lib.rs b/yazi-plugin/src/lib.rs index 9cac546d..069f6649 100644 --- a/yazi-plugin/src/lib.rs +++ b/yazi-plugin/src/lib.rs @@ -23,6 +23,6 @@ pub use opt::*; pub use runtime::*; pub fn init() { - crate::init_lua(); crate::loader::init(); + crate::init_lua(); } diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index a18e394b..728fdb43 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -35,6 +35,7 @@ impl Loader { "noop" => include_bytes!("../../preset/plugins/noop.lua"), "pdf" => include_bytes!("../../preset/plugins/pdf.lua"), "video" => include_bytes!("../../preset/plugins/video.lua"), + "zoxide" => include_bytes!("../../preset/plugins/zoxide.lua"), _ => bail!("plugin not found: {name}"), })) })?; diff --git a/yazi-plugin/src/loader/require.rs b/yazi-plugin/src/loader/require.rs index 4c26f122..cde162e6 100644 --- a/yazi-plugin/src/loader/require.rs +++ b/yazi-plugin/src/loader/require.rs @@ -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; pub(super) struct Require; @@ -8,15 +9,16 @@ impl Require { pub(super) fn install(lua: &'static Lua) -> mlua::Result<()> { let globals = lua.globals(); - let require = globals.raw_get::<_, Function>("require")?; globals.raw_set( "require", - lua.create_function(move |lua, name: mlua::String| { - lua.named_registry_value::("rt")?.swap(name.to_str()?); - let mod_: Table = require.call(&name)?; + lua.create_function(|lua, name: mlua::String| { + let s = name.to_str()?; + futures::executor::block_on(LOADER.ensure(s)).into_lua_err()?; + + lua.named_registry_value::("rt")?.swap(s); + let mod_ = LOADER.load(s)?; lua.named_registry_value::("rt")?.reset(); - mod_.raw_set("_name", &name)?; Self::create_mt(lua, name, mod_) })?, )?; diff --git a/yazi-plugin/src/process/child.rs b/yazi-plugin/src/process/child.rs index 76242ae0..268726ab 100644 --- a/yazi-plugin/src/process/child.rs +++ b/yazi-plugin/src/process/child.rs @@ -1,9 +1,11 @@ use std::time::Duration; -use mlua::{IntoLuaMulti, Table, UserData, Value}; -use tokio::{io::{AsyncBufReadExt, AsyncReadExt, BufReader}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; +use futures::future::try_join3; +use mlua::{AnyUserData, IntoLuaMulti, Table, UserData, Value}; +use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, BufReader}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; use super::Status; +use crate::process::Output; pub struct Child { inner: tokio::process::Child, @@ -26,8 +28,8 @@ impl UserData for Child { #[inline] // TODO: return mlua::String instead of String async fn read_line(me: &mut Child) -> (String, u8) { - async fn read(t: Option) -> Option { - let mut r = t?; + async fn read(r: Option) -> Option { + let mut r = r?; let mut buf = String::new(); match r.read_line(&mut buf).await { Ok(0) | Err(_) => None, @@ -43,8 +45,8 @@ impl UserData for Child { } methods.add_async_method_mut("read", |_, me, len: usize| async move { - async fn read(t: Option, len: usize) -> Option> { - let mut r = t?; + async fn read(r: Option, len: usize) -> Option> { + let mut r = r?; let mut buf = vec![0; len]; match r.read(&mut buf).await { 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), } }); + methods.add_async_function("wait_with_output", |lua, ud: AnyUserData| async move { + async fn read_to_end(r: &mut Option) -> io::Result> { + 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::()?; + 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() { Ok(_) => (true, Value::Nil).into_lua_multi(lua), Err(e) => (false, e.raw_os_error()).into_lua_multi(lua),