feat!: new @sync peek annotation for sync previewers (#2487)

This commit is contained in:
三咲雅 · Misaki Masa 2025-03-16 17:41:27 +08:00 committed by GitHub
parent 324a439c73
commit 8b3ce1838a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 125 additions and 126 deletions

4
Cargo.lock generated
View file

@ -836,9 +836,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"

View file

@ -22,7 +22,7 @@ clap = { version = "4.5.32", features = [ "derive" ] }
core-foundation-sys = "0.8.7"
crossterm = { version = "0.28.1", features = [ "event-stream" ] }
dirs = "6.0.0"
foldhash = "0.1.4"
foldhash = "0.1.5"
futures = "0.3.31"
globset = "0.4.16"
indexmap = { version = "2.8.0", features = [ "serde" ] }

View file

@ -30,13 +30,13 @@ keymap = [
{ on = "<PageDown>", run = "arrow 100%", desc = "Move cursor down one page" },
{ on = [ "g", "g" ], run = "arrow top", desc = "Move cursor to the top" },
{ on = "G", run = "arrow bot", desc = "Move cursor to the bottom" },
{ on = "G", run = "arrow bot", desc = "Move cursor to the bottom" },
# Navigation
{ on = "h", run = "leave", desc = "Go back to the parent directory" },
{ on = "h", run = "leave", desc = "Back to the parent directory" },
{ on = "l", run = "enter", desc = "Enter the child directory" },
{ on = "<Left>", run = "leave", desc = "Go back to the parent directory" },
{ on = "<Left>", run = "leave", desc = "Back to the parent directory" },
{ on = "<Right>", run = "enter", desc = "Enter the child directory" },
{ on = "H", run = "back", desc = "Back to previous directory" },

View file

@ -118,7 +118,7 @@ preloaders = [
{ mime = "application/ms-opentype", run = "font" },
]
previewers = [
{ name = "*/", run = "folder", sync = true },
{ name = "*/", run = "folder" },
# Code
{ mime = "text/*", run = "code" },
{ mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" },

View file

@ -10,8 +10,6 @@ pub struct Previewer {
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Cmd,
#[serde(default)]
pub sync: bool,
}
impl Previewer {

View file

@ -32,11 +32,7 @@ impl Preview {
};
self.abort();
if previewer.sync {
isolate::peek_sync(&previewer.run, file, mime, self.skip);
} else {
self.previewer_ct = Some(isolate::peek(&previewer.run, file, mime, self.skip));
}
self.previewer_ct = isolate::peek(&previewer.run, file, mime, self.skip);
}
pub fn go_folder(&mut self, file: File, dir: Option<Cha>, force: bool) {

View file

@ -29,7 +29,7 @@ impl App {
}
tokio::spawn(async move {
match LOADER.ensure(&opt.id).await {
match LOADER.ensure(&opt.id, |_| ()).await {
Ok(()) => AppProxy::plugin_do(opt),
Err(e) => AppProxy::notify_error("Plugin load failed", e),
}

View file

@ -35,10 +35,10 @@ impl Term {
execute!(
TTY.writer(),
screen::SetScreen(true),
Print(Mux::csi("\x1bP$q q\x1b\\")), // Request cursor shape (DECRQSS query for DECSCUSR)
Print(Mux::csi("\x1b[?12$p")), // Request cursor blink status (DECSET)
Print("\x1b[?u"), // Request keyboard enhancement flags (CSI u)
Print(Mux::csi("\x1b[0c")), // Request device attributes
Print("\x1bP$q q\x1b\\"), // Request cursor shape (DECRQSS query for DECSCUSR)
Print(Mux::csi("\x1b[?12$p")), // Request cursor blink status (DECSET)
Print("\x1b[?u"), // Request keyboard enhancement flags (CSI u)
Print(Mux::csi("\x1b[0c")), // Request device attributes
screen::SetScreen(false),
EnableBracketedPaste,
mouse::SetMouse(true),

View file

@ -1,3 +1,5 @@
--- @sync peek
local M = {}
function M:peek(job)

View file

@ -29,16 +29,11 @@ impl<'a> Runtime<'a> {
}
fn args(lua: &Lua) -> mlua::Result<Value> {
Composer::make(lua, 5, |lua, key| {
match key {
b"entries" => {
lua.create_sequence_from(ARGS.entries.iter().map(Url::from))?.into_lua(lua)?
}
b"cwd_file" => ARGS.cwd_file.as_ref().map(Url::from).into_lua(lua)?,
b"chooser_file" => ARGS.chooser_file.as_ref().map(Url::from).into_lua(lua)?,
_ => return Ok(Value::Nil),
}
.into_lua(lua)
Composer::make(lua, 5, |lua, key| match key {
b"entries" => lua.create_sequence_from(ARGS.entries.iter().map(Url::from))?.into_lua(lua),
b"cwd_file" => ARGS.cwd_file.as_ref().map(Url::from).into_lua(lua),
b"chooser_file" => ARGS.chooser_file.as_ref().map(Url::from).into_lua(lua),
_ => Ok(Value::Nil),
})
}

View file

@ -7,12 +7,12 @@ use super::slim_lua;
use crate::loader::LOADER;
pub async fn entry(opt: PluginOpt) -> mlua::Result<()> {
LOADER.ensure(&opt.id).await.into_lua_err()?;
LOADER.ensure(&opt.id, |_| ()).await.into_lua_err()?;
tokio::task::spawn_blocking(move || {
let lua = slim_lua(&opt.id)?;
let plugin: Table = if let Some(b) = LOADER.read().get(opt.id.as_ref()) {
lua.load(b.as_bytes()).set_name(opt.id.as_ref()).call(())?
let plugin: Table = if let Some(c) = LOADER.read().get(opt.id.as_ref()) {
lua.load(c.as_bytes()).set_name(opt.id.as_ref()).call(())?
} else {
return Err("unloaded plugin".into_lua_err());
};

View file

@ -13,12 +13,12 @@ pub async fn fetch(
if files.is_empty() {
return Ok((FetchState::Bool(true), None));
}
LOADER.ensure(&cmd.name).await.into_lua_err()?;
LOADER.ensure(&cmd.name, |_| ()).await.into_lua_err()?;
tokio::task::spawn_blocking(move || {
let lua = slim_lua(&cmd.name)?;
let plugin: Table = if let Some(b) = LOADER.read().get(&cmd.name) {
lua.load(b.as_bytes()).set_name(&cmd.name).call(())?
let plugin: Table = if let Some(c) = LOADER.read().get(&cmd.name) {
lua.load(c.as_bytes()).set_name(&cmd.name).call(())?
} else {
return Err("unloaded plugin".into_lua_err());
};

View file

@ -1,6 +1,6 @@
use std::borrow::Cow;
use mlua::{ExternalError, ExternalResult, HookTriggers, IntoLua, ObjectLike, Table, VmState};
use mlua::{ExternalError, HookTriggers, IntoLua, ObjectLike, Table, VmState};
use tokio::{runtime::Handle, select};
use tokio_util::sync::CancellationToken;
use tracing::error;
@ -17,61 +17,36 @@ pub fn peek(
file: yazi_fs::File,
mime: Cow<'static, str>,
skip: usize,
) -> CancellationToken {
) -> Option<CancellationToken> {
let ct = CancellationToken::new();
let (ct1, ct2) = (ct.clone(), ct.clone());
if let Some(c) = LOADER.read().get(&cmd.name) {
if c.sync_peek {
peek_sync(cmd, file, mime, skip);
} else {
peek_async(cmd, file, mime, skip, ct.clone());
}
return Some(ct).filter(|_| !c.sync_peek);
}
tokio::task::spawn_blocking(move || {
let future = async {
LOADER.ensure(&cmd.name).await.into_lua_err()?;
let lua = slim_lua(&cmd.name)?;
lua.set_hook(
HookTriggers::new().on_calls().on_returns().every_nth_instruction(2000),
move |_, dbg| {
if ct1.is_cancelled() && dbg.source().what != "C" {
Err("Peek task cancelled".into_lua_err())
} else {
Ok(VmState::Continue)
}
},
);
let plugin: Table = if let Some(b) = LOADER.read().get(&cmd.name) {
lua.load(b.as_bytes()).set_name(&cmd.name).call(())?
} else {
return Err("unloaded plugin".into_lua_err());
};
let job = lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?),
("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?),
("file", File(file).into_lua(&lua)?),
("mime", mime.into_lua(&lua)?),
("skip", skip.into_lua(&lua)?),
])?;
if ct2.is_cancelled() { Ok(()) } else { plugin.call_async_method("peek", job).await }
};
let result = Handle::current().block_on(async {
select! {
_ = ct2.cancelled() => Ok(()),
r = future => r,
}
});
if let Err(e) = result {
if !e.to_string().contains("Peek task cancelled") {
error!("{e}");
}
let ct_ = ct.clone();
tokio::spawn(async move {
select! {
_ = ct_.cancelled() => {},
Ok(b) = LOADER.ensure(&cmd.name, |c| c.sync_peek) => {
if b {
peek_sync(cmd, file, mime, skip);
} else {
peek_async(cmd, file, mime, skip, ct_);
}
},
else => {}
}
});
ct
Some(ct)
}
pub fn peek_sync(cmd: &'static Cmd, file: yazi_fs::File, mime: Cow<'static, str>, skip: usize) {
fn peek_sync(cmd: &'static Cmd, file: yazi_fs::File, mime: Cow<'static, str>, skip: usize) {
let cb: PluginCallback = Box::new(move |lua, plugin| {
let job = lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(lua)?),
@ -86,3 +61,57 @@ pub fn peek_sync(cmd: &'static Cmd, file: yazi_fs::File, mime: Cow<'static, str>
AppProxy::plugin(PluginOpt::new_callback(&cmd.name, cb));
}
fn peek_async(
cmd: &'static Cmd,
file: yazi_fs::File,
mime: Cow<'static, str>,
skip: usize,
ct: CancellationToken,
) {
let ct_ = ct.clone();
tokio::task::spawn_blocking(move || {
let future = async {
let lua = slim_lua(&cmd.name)?;
lua.set_hook(
HookTriggers::new().on_calls().on_returns().every_nth_instruction(2000),
move |_, dbg| {
if ct.is_cancelled() && dbg.source().what != "C" {
Err("Peek task cancelled".into_lua_err())
} else {
Ok(VmState::Continue)
}
},
);
let plugin: Table = if let Some(c) = LOADER.read().get(&cmd.name) {
lua.load(c.as_bytes()).set_name(&cmd.name).call(())?
} else {
return Err("unloaded plugin".into_lua_err());
};
let job = lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?),
("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?),
("file", File(file).into_lua(&lua)?),
("mime", mime.into_lua(&lua)?),
("skip", skip.into_lua(&lua)?),
])?;
if ct_.is_cancelled() { Ok(()) } else { plugin.call_async_method("peek", job).await }
};
let result = Handle::current().block_on(async {
select! {
_ = ct_.cancelled() => Ok(()),
r = future => r,
}
});
if let Err(e) = result {
if !e.to_string().contains("Peek task cancelled") {
error!("{e}");
}
}
});
}

View file

@ -11,7 +11,7 @@ pub async fn preload(
cmd: &'static Cmd,
file: yazi_fs::File,
) -> mlua::Result<(bool, Option<Error>)> {
LOADER.ensure(&cmd.name).await.into_lua_err()?;
LOADER.ensure(&cmd.name, |_| ()).await.into_lua_err()?;
tokio::task::spawn_blocking(move || {
let lua = slim_lua(&cmd.name)?;

View file

@ -21,7 +21,7 @@ pub fn spot(
tokio::task::spawn_blocking(move || {
let future = async {
LOADER.ensure(&cmd.name).await.into_lua_err()?;
LOADER.ensure(&cmd.name, |_| ()).await.into_lua_err()?;
let lua = slim_lua(&cmd.name)?;
lua.set_hook(

View file

@ -5,6 +5,7 @@ use yazi_shared::natsort;
pub struct Chunk {
pub bytes: Cow<'static, [u8]>,
pub since: String,
pub sync_peek: bool,
pub sync_entry: bool,
}
@ -29,6 +30,7 @@ impl Chunk {
let Some(i) = rest.iter().position(|&b| b == b' ' || b == b'\t') else { break };
match (rest[..i].trim_ascii(), rest[i..].trim_ascii()) {
(b"@sync", b"peek") => self.sync_peek = true,
(b"@sync", b"entry") => self.sync_entry = true,
(b"@since", b"") => continue,
@ -44,7 +46,8 @@ impl Chunk {
impl From<Cow<'static, [u8]>> for Chunk {
fn from(b: Cow<'static, [u8]>) -> Self {
let mut chunk = Self { bytes: b, since: String::new(), sync_entry: false };
let mut chunk =
Self { bytes: b, since: String::new(), sync_entry: false, sync_peek: false };
chunk.analyze();
chunk
}

View file

@ -50,47 +50,23 @@ impl Default for Loader {
}
impl Loader {
pub async fn ensure(&self, name: &str) -> Result<()> {
if let Some(chunk) = self.cache.read().get(name) {
return Self::compatible_or_error(name, chunk);
}
// TODO: remove this
fn warn(name: &str) {
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
yazi_proxy::AppProxy::notify(yazi_proxy::options::NotifyOpt {
title: "Deprecated entry file".to_owned(),
content: format!(
"The plugin's entry file `init.lua` has been deprecated in favor of the new `main.lua` (user's own `init.lua` remains unchanged).
Please run `ya pack -m` to automatically migrate all plugins, or manually rename your `{name}.yazi/init.lua` to `{name}.yazi/main.lua`."
),
level: yazi_proxy::options::NotifyLevel::Warn,
timeout: std::time::Duration::from_secs(25),
});
}
pub async fn ensure<F, T>(&self, name: &str, f: F) -> Result<T>
where
F: FnOnce(&Chunk) -> T,
{
if let Some(c) = self.cache.read().get(name) {
return Self::compatible_or_error(name, c).map(|_| f(c));
}
let p = BOOT.plugin_dir.join(format!("{name}.yazi/main.lua"));
let chunk = match fs::read(&p).await {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let p = BOOT.plugin_dir.join(format!("{name}.yazi/init.lua"));
match fs::read(&p).await {
Ok(b) => {
warn(name);
b
}
Err(e) => Err(e).with_context(|| format!("Failed to load plugin from {p:?}"))?,
}
}
Err(e) => Err(e).with_context(|| format!("Failed to load plugin from {p:?}"))?,
};
let chunk =
fs::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into();
let mut cache = self.cache.write();
cache.insert(name.to_owned(), chunk.into());
Self::compatible_or_error(name, cache.get(name).unwrap())
let result = Self::compatible_or_error(name, &chunk);
let inspect = f(&chunk);
self.cache.write().insert(name.to_owned(), chunk);
result.map(|_| inspect)
}
pub fn load(&self, lua: &Lua, id: &str) -> mlua::Result<Table> {

View file

@ -13,7 +13,7 @@ impl Require {
"require",
lua.create_function(|lua, id: mlua::String| {
let s = &id.to_str()?;
futures::executor::block_on(LOADER.ensure(s)).into_lua_err()?;
futures::executor::block_on(LOADER.ensure(s, |_| ())).into_lua_err()?;
lua.named_registry_value::<RtRefMut>("ir")?.push(s);
let mod_ = LOADER.load(lua, s);
@ -29,7 +29,7 @@ impl Require {
"require",
lua.create_async_function(|lua, id: mlua::String| async move {
let s = &id.to_str()?;
LOADER.ensure(s).await.into_lua_err()?;
LOADER.ensure(s, |_| ()).await.into_lua_err()?;
lua.named_registry_value::<RtRefMut>("ir")?.push(s);
let mod_ = LOADER.load(&lua, s);