feat: plugin-specific state persistence (#590)

This commit is contained in:
三咲雅 · Misaki Masa 2024-01-28 00:30:41 +08:00 committed by GitHub
parent c325c332de
commit 68ffd82c0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 130 additions and 89 deletions

View file

@ -23,7 +23,7 @@ impl App {
Lives::register()?; Lives::register()?;
let mut app = Self { cx: Ctx::make(), term: Some(term), signals }; let mut app = Self { cx: Ctx::make(), term: Some(term), signals };
app.render()?; app.render();
let mut times = 0; let mut times = 0;
let mut events = Vec::with_capacity(200); let mut events = Vec::with_capacity(200);
@ -39,13 +39,13 @@ impl App {
if times >= 50 { if times >= 50 {
times = 0; times = 0;
app.render()?; app.render();
} else if let Ok(event) = app.signals.rx.try_recv() { } else if let Ok(event) = app.signals.rx.try_recv() {
events.push(event); events.push(event);
emit!(Render); emit!(Render);
} else { } else {
times = 0; times = 0;
app.render()?; app.render();
} }
} }
Ok(()) Ok(())
@ -58,7 +58,7 @@ impl App {
Event::Seq(execs, layer) => self.dispatch_seq(execs, layer), Event::Seq(execs, layer) => self.dispatch_seq(execs, layer),
Event::Render => self.dispatch_render(), Event::Render => self.dispatch_render(),
Event::Key(key) => self.dispatch_key(key), Event::Key(key) => self.dispatch_key(key),
Event::Resize => self.resize()?, Event::Resize => self.resize(()),
Event::Paste(str) => self.dispatch_paste(str), Event::Paste(str) => self.dispatch_paste(str),
Event::Quit(opt) => self.quit(opt), Event::Quit(opt) => self.quit(opt),
} }

View file

@ -1,7 +1,7 @@
use std::fmt::Display; use std::fmt::Display;
use mlua::{ExternalError, ExternalResult, IntoLua, Table, TableExt, Value, Variadic}; use mlua::{ExternalError, ExternalResult, IntoLua, Table, TableExt, Variadic};
use tracing::{error, warn}; use tracing::warn;
use yazi_plugin::{LOADED, LUA}; use yazi_plugin::{LOADED, LUA};
use yazi_shared::{emit, event::Exec, Layer}; use yazi_shared::{emit, event::Exec, Layer};
@ -36,31 +36,26 @@ impl App {
}; };
let args = Variadic::from_iter(opt.data.args.into_iter().filter_map(|v| v.into_lua(&LUA).ok())); let args = Variadic::from_iter(opt.data.args.into_iter().filter_map(|v| v.into_lua(&LUA).ok()));
let mut ret: mlua::Result<Value> = Err("uninitialized plugin".into_lua_err()); let result = Lives::scope(&self.cx, |_| {
LUA.globals().set("YAZI_PLUGIN_NAME", LUA.create_string(&opt.name)?)?;
Lives::scope(&self.cx, |_| {
let mut plugin: Option<Table> = None; let mut plugin: Option<Table> = None;
if let Some(b) = LOADED.read().get(&opt.name) { if let Some(b) = LOADED.read().get(&opt.name) {
match LUA.load(b).call(args) { plugin = LUA.load(b).call(args)?;
Ok(t) => plugin = Some(t),
Err(e) => ret = Err(e),
}
} }
if let Some(plugin) = plugin {
ret = if let Some(cb) = opt.data.cb { cb(plugin) } else { plugin.call_method("entry", ()) };
}
});
if let Err(e) = ret { let Some(plugin) = plugin else {
error!("{e}"); return Err("plugin not found".into_lua_err());
return; };
}
if let Some(cb) = opt.data.cb { cb(plugin) } else { plugin.call_method("entry", ()) }
});
let Some(tx) = opt.data.tx else { let Some(tx) = opt.data.tx else {
return; return;
}; };
if let Ok(v) = ret.and_then(|v| v.try_into().into_lua_err()) { if let Ok(v) = result.and_then(|v| v.try_into().into_lua_err()) {
tx.send(v).ok(); tx.send(v).ok();
} }
} }

View file

@ -1,32 +1,31 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use anyhow::Result;
use ratatui::backend::Backend; use ratatui::backend::Backend;
use crate::{app::App, lives::Lives, root::{Root, COLLISION}}; use crate::{app::App, lives::Lives, root::{Root, COLLISION}};
impl App { impl App {
pub(crate) fn render(&mut self) -> Result<()> { pub(crate) fn render(&mut self) {
let Some(term) = &mut self.term else { let Some(term) = &mut self.term else {
return Ok(()); return;
}; };
let collision = COLLISION.swap(false, Ordering::Relaxed); let collision = COLLISION.swap(false, Ordering::Relaxed);
let frame = term.draw(|f| { let frame = term
Lives::scope(&self.cx, |_| { .draw(|f| {
f.render_widget(Root::new(&self.cx), f.size()); _ = Lives::scope(&self.cx, |_| Ok(f.render_widget(Root::new(&self.cx), f.size())));
}); if let Some((x, y)) = self.cx.cursor() {
f.set_cursor(x, y);
}
})
.unwrap();
if let Some((x, y)) = self.cx.cursor() {
f.set_cursor(x, y);
}
})?;
if !COLLISION.load(Ordering::Relaxed) { if !COLLISION.load(Ordering::Relaxed) {
if collision { if collision {
// Reload preview if collision is resolved // Reload preview if collision is resolved
self.cx.manager.peek(true); self.cx.manager.peek(true);
} }
return Ok(()); return;
} }
let mut patch = vec![]; let mut patch = vec![];
@ -39,12 +38,11 @@ impl App {
} }
} }
term.backend_mut().draw(patch.iter().map(|(x, y, cell)| (*x, *y, cell)))?; term.backend_mut().draw(patch.iter().map(|(x, y, cell)| (*x, *y, cell))).ok();
if let Some((x, y)) = self.cx.cursor() { if let Some((x, y)) = self.cx.cursor() {
term.show_cursor()?; term.show_cursor().ok();
term.set_cursor(x, y)?; term.set_cursor(x, y).ok();
} }
term.backend_mut().flush()?; term.backend_mut().flush().ok();
Ok(())
} }
} }

View file

@ -1,14 +1,23 @@
use anyhow::Result; use yazi_shared::event::Exec;
use crate::app::App; use crate::app::App;
pub struct Opt;
impl From<Exec> for Opt {
fn from(_: Exec) -> Self { Self }
}
impl From<()> for Opt {
fn from(_: ()) -> Self { Self }
}
impl App { impl App {
pub(crate) fn resize(&mut self) -> Result<()> { pub(crate) fn resize(&mut self, _: impl Into<Opt>) {
self.cx.manager.active_mut().preview.reset(); self.cx.manager.active_mut().preview.reset();
self.render()?; self.render();
self.cx.manager.current_mut().sync_page(true); self.cx.manager.current_mut().sync_page(true);
self.cx.manager.hover(None); self.cx.manager.hover(None);
Ok(())
} }
} }

View file

@ -9,7 +9,7 @@ impl App {
// While the app resumes, it's possible that the terminal size has changed. // While the app resumes, it's possible that the terminal size has changed.
// We need to trigger a resize, and render the UI based on the resized area. // We need to trigger a resize, and render the UI based on the resized area.
self.resize().unwrap(); self.resize(());
self.signals.resume(); self.signals.resume();
} }

View file

@ -37,6 +37,7 @@ impl<'a> Executor<'a> {
on!(plugin); on!(plugin);
on!(plugin_do); on!(plugin_do);
on!(update_progress); on!(update_progress);
on!(resize);
on!(stop); on!(stop);
on!(resume); on!(resume);
} }

View file

@ -23,7 +23,10 @@ impl Lives {
Ok(()) Ok(())
} }
pub(crate) fn scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) { pub(crate) fn scope<'a, T>(
cx: &'a Ctx,
f: impl FnOnce(&Scope<'a, 'a>) -> mlua::Result<T>,
) -> mlua::Result<T> {
let result = LUA.scope(|scope| { let result = LUA.scope(|scope| {
LUA.set_named_registry_value("cx", scope.create_any_userdata_ref(cx)?)?; LUA.set_named_registry_value("cx", scope.create_any_userdata_ref(cx)?)?;
@ -37,7 +40,7 @@ impl Lives {
])?, ])?,
)?; )?;
f(scope); let ret = f(scope)?;
LAYOUT.store(Arc::new(yazi_config::Layout { LAYOUT.store(Arc::new(yazi_config::Layout {
header: *global.get::<_, Table>("Header")?.get::<_, RectRef>("area")?, header: *global.get::<_, Table>("Header")?.get::<_, RectRef>("area")?,
@ -47,12 +50,13 @@ impl Lives {
status: *global.get::<_, Table>("Status")?.get::<_, RectRef>("area")?, status: *global.get::<_, Table>("Status")?.get::<_, RectRef>("area")?,
})); }));
Ok(()) Ok(ret)
}); });
if let Err(e) = result { if let Err(ref e) = result {
error!("{e}"); error!("{e}");
} }
result
} }
pub(crate) fn partial_scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) { pub(crate) fn partial_scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) {

View file

@ -1,4 +1,5 @@
#![allow(clippy::module_inception)] #![allow(clippy::module_inception)]
#![allow(clippy::unit_arg)]
mod app; mod app;
mod completion; mod completion;

View file

@ -19,6 +19,26 @@ function Folder:icon(file)
return icon and ui.Span(" " .. icon.text .. " "):style(icon.style) or ui.Span("") return icon and ui.Span(" " .. icon.text .. " "):style(icon.style) or ui.Span("")
end end
function Folder:highlight_ranges(s, ranges)
if ranges == nil or #ranges == 0 then
return { ui.Span(s) }
end
local spans = {}
local last = 0
for _, r in ipairs(ranges) do
if r[1] > last then
spans[#spans + 1] = ui.Span(s:sub(last + 1, r[1]))
end
spans[#spans + 1] = ui.Span(s:sub(r[1] + 1, r[2])):style(THEME.manager.find_keyword)
last = r[2]
end
if last < #s then
spans[#spans + 1] = ui.Span(s:sub(last + 1))
end
return spans
end
function Folder:highlighted_name(file) function Folder:highlighted_name(file)
-- Complete prefix when searching across directories -- Complete prefix when searching across directories
local prefix = file:prefix() or "" local prefix = file:prefix() or ""
@ -28,7 +48,7 @@ function Folder:highlighted_name(file)
-- Range highlighting for filenames -- Range highlighting for filenames
local highlights = file:highlights() local highlights = file:highlights()
local spans = ui.highlight_ranges(prefix .. file.name, highlights) local spans = self:highlight_ranges(prefix .. file.name, highlights)
-- Show symlink target -- Show symlink target
if MANAGER.show_symlink and file.link_to ~= nil then if MANAGER.show_symlink and file.link_to ~= nil then

View file

@ -30,13 +30,17 @@ function Header:tabs()
return ui.Line(spans) return ui.Line(spans)
end end
function Header:render(area) function Header:layout(area)
self.area = area self.area = area
local chunks = ui.Layout() return ui.Layout()
:direction(ui.Layout.HORIZONTAL) :direction(ui.Layout.HORIZONTAL)
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) }) :constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
:split(area) :split(area)
end
function Header:render(area)
local chunks = self:layout(area)
local left = ui.Line { self:cwd() } local left = ui.Line { self:cwd() }
local right = ui.Line { self:tabs() } local right = ui.Line { self:tabs() }

View file

@ -2,10 +2,10 @@ Manager = {
area = ui.Rect.default, area = ui.Rect.default,
} }
function Manager:render(area) function Manager:layout(area)
self.area = area self.area = area
local chunks = ui.Layout() return ui.Layout()
:direction(ui.Layout.HORIZONTAL) :direction(ui.Layout.HORIZONTAL)
:constraints({ :constraints({
ui.Constraint.Ratio(MANAGER.ratio.parent, MANAGER.ratio.all), ui.Constraint.Ratio(MANAGER.ratio.parent, MANAGER.ratio.all),
@ -13,6 +13,10 @@ function Manager:render(area)
ui.Constraint.Ratio(MANAGER.ratio.preview, MANAGER.ratio.all), ui.Constraint.Ratio(MANAGER.ratio.preview, MANAGER.ratio.all),
}) })
:split(area) :split(area)
end
function Manager:render(area)
local chunks = self:layout(area)
return ya.flat { return ya.flat {
-- Borders -- Borders

View file

@ -0,0 +1,16 @@
local cache = {}
state = setmetatable({
clear = function() cache[YAZI_PLUGIN_NAME] = nil end,
}, {
__index = function(_, k)
local bucket = YAZI_PLUGIN_NAME
return cache[bucket] and cache[bucket][k]
end,
__newindex = function(_, k, v)
local bucket = YAZI_PLUGIN_NAME
cache[bucket] = cache[bucket] or {}
cache[bucket][k] = v
end,
})

View file

@ -1,21 +0,0 @@
ui = {}
function ui.highlight_ranges(s, ranges)
if ranges == nil or #ranges == 0 then
return { ui.Span(s) }
end
local spans = {}
local last = 0
for _, r in ipairs(ranges) do
if r[1] > last then
spans[#spans + 1] = ui.Span(s:sub(last + 1, r[1]))
end
spans[#spans + 1] = ui.Span(s:sub(r[1] + 1, r[2])):style(THEME.manager.find_keyword)
last = r[2]
end
if last < #s then
spans[#spans + 1] = ui.Span(s:sub(last + 1))
end
return spans
end

View file

@ -39,7 +39,7 @@ function ya.sync(f)
end end
local calls = ya.SYNC_CALLS local calls = ya.SYNC_CALLS
return function(...) return plugin_retrieve(ya.PLUGIN_NAME, calls, ...) end return function(...) return plugin_retrieve(YAZI_PLUGIN_NAME, calls, ...) end
end end
function ya.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end function ya.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end

View file

@ -3,8 +3,7 @@ use mlua::{AnyUserData, Lua, Table};
use crate::cast_to_renderable; use crate::cast_to_renderable;
pub fn init(lua: &Lua) -> mlua::Result<()> { pub fn init(lua: &Lua) -> mlua::Result<()> {
let globals = lua.globals(); let ui: Table = lua.create_table()?;
let ui: Table = globals.get("ui").or_else(|_| lua.create_table())?;
// Register // Register
super::Padding::register(lua)?; super::Padding::register(lua)?;

View file

@ -64,9 +64,7 @@ impl UserData for Layout {
ud.borrow_mut::<Self>()?.constraints = value.into_iter().map(|c| c.0).collect(); ud.borrow_mut::<Self>()?.constraints = value.into_iter().map(|c| c.0).collect();
Ok(ud) Ok(ud)
}); });
methods.add_function("split", |lua, (ud, value): (AnyUserData, RectRef)| { methods.add_method("split", |lua, me, value: RectRef| {
let me = ud.borrow::<Self>()?;
let mut layout = ratatui::layout::Layout::new( let mut layout = ratatui::layout::Layout::new(
if me.direction == VERTICAL { if me.direction == VERTICAL {
ratatui::layout::Direction::Vertical ratatui::layout::Direction::Vertical

View file

@ -9,8 +9,9 @@ pub async fn entry(name: String, args: Vec<ValueSendable>) -> mlua::Result<()> {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let lua = slim_lua()?; let lua = slim_lua()?;
let args = Variadic::from_iter(args.into_iter().filter_map(|v| v.into_lua(&lua).ok())); lua.globals().set("YAZI_PLUGIN_NAME", lua.create_string(&name)?)?;
let args = Variadic::from_iter(args.into_iter().filter_map(|v| v.into_lua(&lua).ok()));
let plugin: Table = if let Some(b) = LOADED.read().get(&name) { let plugin: Table = if let Some(b) = LOADED.read().get(&name) {
lua.load(b).call(args)? lua.load(b).call(args)?
} else { } else {

View file

@ -14,7 +14,7 @@ pub fn init() {
// Base // Base
lua.load(include_str!("../preset/inspect/inspect.lua")).exec()?; lua.load(include_str!("../preset/inspect/inspect.lua")).exec()?;
lua.load(include_str!("../preset/ui.lua")).exec()?; lua.load(include_str!("../preset/state.lua")).exec()?;
lua.load(include_str!("../preset/ya.lua")).exec()?; lua.load(include_str!("../preset/ya.lua")).exec()?;
crate::elements::init(lua)?; crate::elements::init(lua)?;

View file

@ -28,6 +28,17 @@ impl Utils {
Ok((args, named)) Ok((args, named))
} }
#[inline]
fn create_exec(cmd: String, table: Table, data: Option<Value>) -> mlua::Result<Exec> {
let (args, named) = Self::parse_args(table)?;
let mut exec = Exec { cmd, args, named, ..Default::default() };
if let Some(data) = data.and_then(|v| ValueSendable::try_from(v).ok()) {
exec = exec.with_data(data);
}
Ok(exec)
}
pub(super) fn call(lua: &Lua, ya: &Table) -> mlua::Result<()> { pub(super) fn call(lua: &Lua, ya: &Table) -> mlua::Result<()> {
ya.set( ya.set(
"render", "render",
@ -37,17 +48,18 @@ impl Utils {
})?, })?,
)?; )?;
ya.set(
"app_emit",
lua.create_function(|_, (cmd, table, data): (String, Table, Option<Value>)| {
emit!(Call(Self::create_exec(cmd, table, data)?, Layer::App));
Ok(())
})?,
)?;
ya.set( ya.set(
"manager_emit", "manager_emit",
lua.create_function(|_, (cmd, table, data): (String, Table, Option<Value>)| { lua.create_function(|_, (cmd, table, data): (String, Table, Option<Value>)| {
let (args, named) = Self::parse_args(table)?; emit!(Call(Self::create_exec(cmd, table, data)?, Layer::Manager));
let mut exec = Exec { cmd, args, named, ..Default::default() };
if let Some(data) = data.and_then(|v| ValueSendable::try_from(v).ok()) {
exec = exec.with_data(data);
}
emit!(Call(exec, Layer::Manager));
Ok(()) Ok(())
})?, })?,
)?; )?;