mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
..
This commit is contained in:
parent
71c86a50c9
commit
e2dfa9813a
26 changed files with 390 additions and 228 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1364,6 +1364,7 @@ dependencies = [
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"shared",
|
"shared",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,13 @@
|
||||||
use core::Ctx;
|
use ratatui::{prelude::{Buffer, Rect}, widgets::Widget};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Paragraph, Widget}};
|
pub(crate) struct Layout;
|
||||||
use shared::readable_path;
|
|
||||||
|
|
||||||
use super::Tabs;
|
impl Widget for Layout {
|
||||||
|
|
||||||
pub(crate) struct Layout<'a> {
|
|
||||||
cx: &'a Ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Layout<'a> {
|
|
||||||
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for Layout<'a> {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let chunks = layout::Layout::new()
|
let x = plugin::Header.render(area, buf);
|
||||||
.direction(Direction::Horizontal)
|
if x.is_err() {
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
info!("{:?}", x);
|
||||||
.split(area);
|
}
|
||||||
|
|
||||||
let cwd = &self.cx.manager.current().cwd;
|
|
||||||
let location = if cwd.is_search() {
|
|
||||||
format!("{} (search: {})", readable_path(cwd), cwd.frag().unwrap())
|
|
||||||
} else {
|
|
||||||
readable_path(cwd)
|
|
||||||
};
|
|
||||||
|
|
||||||
Paragraph::new(location).style(Style::new().fg(Color::Cyan)).render(chunks[0], buf);
|
|
||||||
|
|
||||||
Tabs::new(self.cx).render(chunks[1], buf);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
mod layout;
|
mod layout;
|
||||||
mod tabs;
|
|
||||||
|
|
||||||
pub(super) use layout::*;
|
pub(super) use layout::*;
|
||||||
use tabs::*;
|
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
use core::Ctx;
|
|
||||||
use std::ops::ControlFlow;
|
|
||||||
|
|
||||||
use config::THEME;
|
|
||||||
use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, text::{Line, Span}, widgets::{Paragraph, Widget}};
|
|
||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
|
||||||
|
|
||||||
pub(super) struct Tabs<'a> {
|
|
||||||
cx: &'a Ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Tabs<'a> {
|
|
||||||
pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
|
||||||
|
|
||||||
fn truncate(&self, name: &str) -> String {
|
|
||||||
let mut width = 0;
|
|
||||||
let flow =
|
|
||||||
name.chars().try_fold(String::with_capacity(THEME.tab.max_width as usize), |mut s, c| {
|
|
||||||
width += c.width().unwrap_or(0);
|
|
||||||
if s.width() < THEME.tab.max_width as usize {
|
|
||||||
s.push(c);
|
|
||||||
ControlFlow::Continue(s)
|
|
||||||
} else {
|
|
||||||
ControlFlow::Break(s)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match flow {
|
|
||||||
ControlFlow::Break(s) => s,
|
|
||||||
ControlFlow::Continue(s) => s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for Tabs<'a> {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let tabs = self.cx.manager.tabs();
|
|
||||||
|
|
||||||
let line = Line::from(
|
|
||||||
tabs
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, tab)| {
|
|
||||||
let mut text = format!("{}", i + 1);
|
|
||||||
if THEME.tab.max_width >= 3 {
|
|
||||||
text.push(' ');
|
|
||||||
text.push_str(tab.name());
|
|
||||||
text = self.truncate(&text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == tabs.idx() {
|
|
||||||
Span::styled(format!(" {text} "), THEME.tab.active.into())
|
|
||||||
} else {
|
|
||||||
Span::styled(format!(" {text} "), THEME.tab.inactive.into())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Paragraph::new(line).alignment(Alignment::Right).render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,7 @@ impl<'a> Widget for Root<'a> {
|
||||||
.constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)])
|
.constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
header::Layout::new(self.cx).render(chunks[0], buf);
|
header::Layout.render(chunks[0], buf);
|
||||||
manager::Layout::new(self.cx).render(chunks[1], buf);
|
manager::Layout::new(self.cx).render(chunks[1], buf);
|
||||||
status::Layout.render(chunks[2], buf);
|
status::Layout.render(chunks[2], buf);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[tab]
|
[tabs]
|
||||||
active = { fg = "#1E2031", bg = "#80AEFA" }
|
active = { fg = "#1E2031", bg = "#80AEFA" }
|
||||||
inactive = { fg = "#C8D3F8", bg = "#484D66" }
|
inactive = { fg = "#C8D3F8", bg = "#484D66" }
|
||||||
max_width = 1
|
max_width = 1
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use super::{Files, Filetype, Icon, Marker, Status, Style};
|
||||||
use crate::{validation::check_validation, MERGED_THEME};
|
use crate::{validation::check_validation, MERGED_THEME};
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Validate)]
|
#[derive(Deserialize, Serialize, Validate)]
|
||||||
pub struct Tab {
|
pub struct Tabs {
|
||||||
pub active: Style,
|
pub active: Style,
|
||||||
pub inactive: Style,
|
pub inactive: Style,
|
||||||
#[validate(range(min = 1, message = "Must be greater than 0"))]
|
#[validate(range(min = 1, message = "Must be greater than 0"))]
|
||||||
|
|
@ -23,7 +23,7 @@ pub struct Preview {
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub tab: Tab,
|
pub tabs: Tabs,
|
||||||
pub status: Status,
|
pub status: Status,
|
||||||
pub files: Files,
|
pub files: Files,
|
||||||
pub marker: Marker,
|
pub marker: Marker,
|
||||||
|
|
@ -38,7 +38,7 @@ impl Default for Theme {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap();
|
let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap();
|
||||||
|
|
||||||
check_validation(theme.tab.validate());
|
check_validation(theme.tabs.validate());
|
||||||
|
|
||||||
theme.preview.syntect_theme = expand_path(&theme.preview.syntect_theme);
|
theme.preview.syntect_theme = expand_path(&theme.preview.syntect_theme);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ impl Manager {
|
||||||
pub fn yank(&mut self, cut: bool) -> bool {
|
pub fn yank(&mut self, cut: bool) -> bool {
|
||||||
self.yanked.0 = cut;
|
self.yanked.0 = cut;
|
||||||
self.yanked.1 = self.selected().into_iter().map(|f| f.url_owned()).collect();
|
self.yanked.1 = self.selected().into_iter().map(|f| f.url_owned()).collect();
|
||||||
false
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn quit(&self, tasks: &Tasks, no_cwd_file: bool) -> bool {
|
pub fn quit(&self, tasks: &Tasks, no_cwd_file: bool) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -458,18 +458,6 @@ impl Tab {
|
||||||
pub fn in_selecting(&self) -> bool { self.mode.is_visual() || self.current.files.has_selected() }
|
pub fn in_selecting(&self) -> bool { self.mode.is_visual() || self.current.files.has_selected() }
|
||||||
|
|
||||||
// --- Current
|
// --- Current
|
||||||
// TODO: remove this
|
|
||||||
#[inline]
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
self
|
|
||||||
.current
|
|
||||||
.cwd
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.or_else(|| self.current.cwd.to_str())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected(&self) -> Vec<&File> {
|
pub fn selected(&self) -> Vec<&File> {
|
||||||
let pending = self.mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default();
|
let pending = self.mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default();
|
||||||
let selected = self.current.files.selected(&pending, self.mode.is_unset());
|
let selected = self.current.files.selected(&pending, self.mode.is_unset());
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@ shared = { path = "../shared" }
|
||||||
# External dependencies
|
# External dependencies
|
||||||
anyhow = "^1"
|
anyhow = "^1"
|
||||||
mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize" ] }
|
mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize" ] }
|
||||||
tracing = "^0"
|
|
||||||
ratatui = "^0"
|
ratatui = "^0"
|
||||||
|
tracing = "^0"
|
||||||
|
unicode-width = "^0"
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,24 @@ function Folder:markers(area, markers)
|
||||||
|
|
||||||
local elements = {}
|
local elements = {}
|
||||||
local append = function(last)
|
local append = function(last)
|
||||||
local rect = ui.Rect {
|
local p = ui.Paragraph(
|
||||||
|
ui.Rect {
|
||||||
x = area.x - 1,
|
x = area.x - 1,
|
||||||
y = area.y + last[1] - 1,
|
y = area.y + last[1] - 1,
|
||||||
w = 1,
|
w = 1,
|
||||||
h = 1 + last[2] - last[1],
|
h = 1 + last[2] - last[1],
|
||||||
}
|
},
|
||||||
elements[#elements + 1] = ui.Paragraph(rect, {}):style(THEME.marker.selected)
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
if last[3] == 1 then
|
||||||
|
p = p:style(THEME.marker.copied)
|
||||||
|
elseif last[3] == 2 then
|
||||||
|
p = p:style(THEME.marker.cut)
|
||||||
|
elseif last[3] == 3 then
|
||||||
|
p = p:style(THEME.marker.selected)
|
||||||
|
end
|
||||||
|
elements[#elements + 1] = p
|
||||||
end
|
end
|
||||||
|
|
||||||
local last = { markers[1][1], markers[1][1], markers[1][2] } -- start, end, type
|
local last = { markers[1][1], markers[1][1], markers[1][2] } -- start, end, type
|
||||||
|
|
@ -89,9 +100,12 @@ function Folder:current(area)
|
||||||
end
|
end
|
||||||
items[#items + 1] = item
|
items[#items + 1] = item
|
||||||
|
|
||||||
-- Mark selected/yanked files
|
-- Mark yanked/selected files
|
||||||
if f:selected() then
|
local yanked = f:yanked()
|
||||||
markers[#markers + 1] = { i, 1 }
|
if yanked ~= 0 then
|
||||||
|
markers[#markers + 1] = { i, yanked }
|
||||||
|
elseif f:selected() then
|
||||||
|
markers[#markers + 1] = { i, 3 }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
43
plugin/preset/components/header.lua
Normal file
43
plugin/preset/components/header.lua
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
Header = {}
|
||||||
|
|
||||||
|
function Header:cwd()
|
||||||
|
local cwd = cx.active.current.cwd
|
||||||
|
|
||||||
|
local span
|
||||||
|
if not cwd.is_search then
|
||||||
|
span = ui.Span(utils.readable_path(tostring(cwd)))
|
||||||
|
else
|
||||||
|
span = ui.Span(string.format("%s (search: %s)", utils.readable_path(tostring(cwd)), cwd.frag))
|
||||||
|
end
|
||||||
|
return span:fg("cyan")
|
||||||
|
end
|
||||||
|
|
||||||
|
function Header:tabs()
|
||||||
|
local spans = {}
|
||||||
|
for i = 1, #cx.tabs do
|
||||||
|
local text = i
|
||||||
|
if THEME.tabs.max_width > 2 then
|
||||||
|
text = utils.truncate(text .. " " .. cx.tabs[i]:name(), THEME.tabs.max_width)
|
||||||
|
end
|
||||||
|
if i == cx.tabs.idx + 1 then
|
||||||
|
spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.tabs.active)
|
||||||
|
else
|
||||||
|
spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.tabs.inactive)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ui.Line(spans)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Header:render(area)
|
||||||
|
local chunks = ui.Layout()
|
||||||
|
:direction(ui.Direction.HORIZONTAL)
|
||||||
|
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
|
||||||
|
:split(area)
|
||||||
|
|
||||||
|
local left = ui.Line { self:cwd() }
|
||||||
|
local right = ui.Line { self:tabs() }
|
||||||
|
return {
|
||||||
|
ui.Paragraph(chunks[1], { left }),
|
||||||
|
ui.Paragraph(chunks[2], { right }):align(ui.Alignment.RIGHT),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
@ -113,14 +113,19 @@ function Status:progress(area, offset)
|
||||||
h = 1,
|
h = 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
local percent = 0
|
if progress.fail == 0 then
|
||||||
if progress.processed ~= 0 then
|
gauge = gauge:gauge_style(THEME.status.progress_normal)
|
||||||
percent = math.floor(progress.processed * 100 / progress.found)
|
else
|
||||||
|
gauge = gauge:gauge_style(THEME.status.progress_error)
|
||||||
|
end
|
||||||
|
|
||||||
|
local percent = 99
|
||||||
|
if progress.found ~= 0 then
|
||||||
|
percent = math.min(99, progress.processed * 100 / progress.found)
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gauge
|
gauge
|
||||||
:gauge_style(THEME.status.progress_normal)
|
|
||||||
:percent(percent)
|
:percent(percent)
|
||||||
:label(ui.Span(string.format("%3d%%, %d left", percent, left)):style(THEME.status.progress_label)),
|
:label(ui.Span(string.format("%3d%%, %d left", percent, left)):style(THEME.status.progress_label)),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,14 @@ function utils.readable_size(size)
|
||||||
end
|
end
|
||||||
return string.format("%.1f %s", size, units[i])
|
return string.format("%.1f %s", size, units[i])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function utils.readable_path(path)
|
||||||
|
local home = os.getenv("HOME")
|
||||||
|
if home == nil then
|
||||||
|
return path
|
||||||
|
elseif string.sub(path, 1, #home) == home then
|
||||||
|
return "~" .. string.sub(path, #home + 1)
|
||||||
|
else
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,20 @@
|
||||||
use core::Ctx;
|
use core::Ctx;
|
||||||
|
|
||||||
use config::{MANAGER, THEME};
|
use config::{MANAGER, THEME};
|
||||||
use mlua::{AnyUserData, Function, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value};
|
use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value};
|
||||||
|
|
||||||
use super::{Range, Url};
|
use super::{Range, Url};
|
||||||
use crate::{layout::Style, LUA};
|
use crate::{layout::Style, LUA};
|
||||||
|
|
||||||
struct File(core::files::File);
|
pub struct Active<'a, 'b> {
|
||||||
|
|
||||||
impl From<&core::files::File> for File {
|
|
||||||
fn from(value: &core::files::File) -> Self { Self(value.clone()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserData for File {
|
|
||||||
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("url", |_, me| Ok(Url::from(me.0.url())));
|
|
||||||
fields.add_field_method_get("length", |_, me| Ok(me.0.length()));
|
|
||||||
fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from)));
|
|
||||||
fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link()));
|
|
||||||
fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Tab<'a, 'b> {
|
|
||||||
scope: &'b mlua::Scope<'a, 'a>,
|
scope: &'b mlua::Scope<'a, 'a>,
|
||||||
|
|
||||||
cx: &'a core::Ctx,
|
cx: &'a core::Ctx,
|
||||||
inner: &'a core::manager::Tab,
|
inner: &'a core::manager::Tab,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> Tab<'a, 'b> {
|
impl<'a, 'b> Active<'a, 'b> {
|
||||||
pub(crate) fn init() -> mlua::Result<()> {
|
pub(crate) fn init() -> mlua::Result<()> {
|
||||||
LUA.register_userdata_type::<core::manager::Tab>(|reg| {
|
|
||||||
reg.add_field_function_get("mode", |_, me| me.named_user_value::<AnyUserData>("mode"));
|
|
||||||
reg.add_field_function_get("parent", |_, me| me.named_user_value::<Value>("parent"));
|
|
||||||
reg.add_field_function_get("current", |_, me| me.named_user_value::<AnyUserData>("current"));
|
|
||||||
reg.add_field_function_get("preview", |_, me| me.named_user_value::<AnyUserData>("preview"));
|
|
||||||
})?;
|
|
||||||
|
|
||||||
LUA.register_userdata_type::<core::manager::Mode>(|reg| {
|
LUA.register_userdata_type::<core::manager::Mode>(|reg| {
|
||||||
reg.add_field_method_get("is_select", |_, me| Ok(me.is_select()));
|
reg.add_field_method_get("is_select", |_, me| Ok(me.is_select()));
|
||||||
reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset()));
|
reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset()));
|
||||||
|
|
@ -57,58 +34,6 @@ impl<'a, 'b> Tab<'a, 'b> {
|
||||||
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<Value>("hovered"));
|
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<Value>("hovered"));
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
LUA.register_userdata_type::<core::files::Files>(|reg| {
|
|
||||||
reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len()));
|
|
||||||
|
|
||||||
reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| {
|
|
||||||
let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| {
|
|
||||||
let files = me.borrow::<core::files::Files>()?;
|
|
||||||
let i = i + 1;
|
|
||||||
Ok(if i > files.len() {
|
|
||||||
mlua::Variadic::new()
|
|
||||||
} else {
|
|
||||||
mlua::Variadic::from_iter([i.into_lua(lua)?, File::from(&files[i - 1]).into_lua(lua)?])
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
Ok((iter, me, 0))
|
|
||||||
});
|
|
||||||
|
|
||||||
reg.add_function("slice", |_, (me, skip, take): (AnyUserData, usize, usize)| {
|
|
||||||
let files = me.borrow::<core::files::Files>()?;
|
|
||||||
Ok(files.iter().skip(skip).take(take).map(File::from).collect::<Vec<_>>())
|
|
||||||
});
|
|
||||||
})?;
|
|
||||||
|
|
||||||
LUA.register_userdata_type::<core::files::File>(|reg| {
|
|
||||||
reg.add_field_method_get("name", |_, me| {
|
|
||||||
Ok(me.url().file_name().map(|n| n.to_string_lossy().to_string()))
|
|
||||||
});
|
|
||||||
reg.add_function("icon", |_, me: AnyUserData| {
|
|
||||||
me.named_user_value::<Function>("icon")?.call::<_, String>(())
|
|
||||||
});
|
|
||||||
reg.add_function("style", |_, me: AnyUserData| {
|
|
||||||
me.named_user_value::<Function>("style")?.call::<_, Style>(())
|
|
||||||
});
|
|
||||||
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<bool>("hovered"));
|
|
||||||
reg.add_function("selected", |_, me: AnyUserData| {
|
|
||||||
me.named_user_value::<Function>("selected")?.call::<_, bool>(me)
|
|
||||||
});
|
|
||||||
reg.add_function("highlights", |_, me: AnyUserData| {
|
|
||||||
me.named_user_value::<Function>("highlights")?.call::<_, Value>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
reg.add_field_method_get("url", |_, me| Ok(Url::from(me.url())));
|
|
||||||
reg.add_field_method_get("length", |_, me| Ok(me.length()));
|
|
||||||
reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from)));
|
|
||||||
reg.add_field_method_get("is_link", |_, me| Ok(me.is_link()));
|
|
||||||
reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden()));
|
|
||||||
|
|
||||||
// Meta
|
|
||||||
reg.add_field_method_get("permissions", |_, me| {
|
|
||||||
Ok(shared::permissions(me.meta().permissions()))
|
|
||||||
});
|
|
||||||
})?;
|
|
||||||
|
|
||||||
LUA.register_userdata_type::<core::manager::Preview>(|reg| {
|
LUA.register_userdata_type::<core::manager::Preview>(|reg| {
|
||||||
reg.add_field_function_get("folder", |_, me| me.named_user_value::<Value>("folder"));
|
reg.add_field_function_get("folder", |_, me| me.named_user_value::<Value>("folder"));
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -116,13 +41,8 @@ impl<'a, 'b> Tab<'a, 'b> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, cx: &'a Ctx) -> Self {
|
||||||
scope: &'b mlua::Scope<'a, 'a>,
|
Self { scope, cx, inner: cx.manager.active() }
|
||||||
|
|
||||||
cx: &'a Ctx,
|
|
||||||
inner: &'a core::manager::Tab,
|
|
||||||
) -> Self {
|
|
||||||
Self { scope, cx, inner }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn make(&self) -> mlua::Result<AnyUserData<'a>> {
|
pub(crate) fn make(&self) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
|
@ -212,6 +132,20 @@ impl<'a, 'b> Tab<'a, 'b> {
|
||||||
matches!(&folder.hovered, Some(f) if f.url() == inner.url()),
|
matches!(&folder.hovered, Some(f) if f.url() == inner.url()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
ud.set_named_user_value(
|
||||||
|
"yanked",
|
||||||
|
self.scope.create_function(|_, ()| {
|
||||||
|
let (cut, urls) = self.cx.manager.yanked();
|
||||||
|
Ok(if !urls.contains(inner.url()) {
|
||||||
|
0u8
|
||||||
|
} else if *cut {
|
||||||
|
2u8
|
||||||
|
} else {
|
||||||
|
1u8
|
||||||
|
})
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
|
||||||
ud.set_named_user_value(
|
ud.set_named_user_value(
|
||||||
"selected",
|
"selected",
|
||||||
self.scope.create_function(|_, me: AnyUserData| {
|
self.scope.create_function(|_, me: AnyUserData| {
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
pub fn init() -> mlua::Result<()> {
|
pub fn init() -> mlua::Result<()> {
|
||||||
super::tab::Tab::init()?;
|
super::active::Active::init()?;
|
||||||
|
super::files::Files::init()?;
|
||||||
|
super::tabs::Tabs::init()?;
|
||||||
super::tasks::Tasks::init()?;
|
super::tasks::Tasks::init()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
83
plugin/src/bindings/files.rs
Normal file
83
plugin/src/bindings/files.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
use mlua::{AnyUserData, Function, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value};
|
||||||
|
|
||||||
|
use super::Url;
|
||||||
|
use crate::{layout::Style, LUA};
|
||||||
|
|
||||||
|
pub struct File(core::files::File);
|
||||||
|
|
||||||
|
impl From<&core::files::File> for File {
|
||||||
|
fn from(value: &core::files::File) -> Self { Self(value.clone()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserData for File {
|
||||||
|
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
|
||||||
|
fields.add_field_method_get("url", |_, me| Ok(Url::from(me.0.url())));
|
||||||
|
fields.add_field_method_get("length", |_, me| Ok(me.0.length()));
|
||||||
|
fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from)));
|
||||||
|
fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link()));
|
||||||
|
fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Files;
|
||||||
|
|
||||||
|
impl Files {
|
||||||
|
pub(crate) fn init() -> mlua::Result<()> {
|
||||||
|
LUA.register_userdata_type::<core::files::Files>(|reg| {
|
||||||
|
reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len()));
|
||||||
|
|
||||||
|
reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| {
|
||||||
|
let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| {
|
||||||
|
let files = me.borrow::<core::files::Files>()?;
|
||||||
|
let i = i + 1;
|
||||||
|
Ok(if i > files.len() {
|
||||||
|
mlua::Variadic::new()
|
||||||
|
} else {
|
||||||
|
mlua::Variadic::from_iter([i.into_lua(lua)?, File::from(&files[i - 1]).into_lua(lua)?])
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
Ok((iter, me, 0))
|
||||||
|
});
|
||||||
|
|
||||||
|
reg.add_function("slice", |_, (me, skip, take): (AnyUserData, usize, usize)| {
|
||||||
|
let files = me.borrow::<core::files::Files>()?;
|
||||||
|
Ok(files.iter().skip(skip).take(take).map(File::from).collect::<Vec<_>>())
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
LUA.register_userdata_type::<core::files::File>(|reg| {
|
||||||
|
reg.add_field_method_get("name", |_, me| {
|
||||||
|
Ok(me.url().file_name().map(|n| n.to_string_lossy().to_string()))
|
||||||
|
});
|
||||||
|
reg.add_function("icon", |_, me: AnyUserData| {
|
||||||
|
me.named_user_value::<Function>("icon")?.call::<_, String>(())
|
||||||
|
});
|
||||||
|
reg.add_function("style", |_, me: AnyUserData| {
|
||||||
|
me.named_user_value::<Function>("style")?.call::<_, Style>(())
|
||||||
|
});
|
||||||
|
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<bool>("hovered"));
|
||||||
|
reg.add_function("yanked", |_, me: AnyUserData| {
|
||||||
|
me.named_user_value::<Function>("yanked")?.call::<_, u8>(me)
|
||||||
|
});
|
||||||
|
reg.add_function("selected", |_, me: AnyUserData| {
|
||||||
|
me.named_user_value::<Function>("selected")?.call::<_, bool>(me)
|
||||||
|
});
|
||||||
|
reg.add_function("highlights", |_, me: AnyUserData| {
|
||||||
|
me.named_user_value::<Function>("highlights")?.call::<_, Value>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
reg.add_field_method_get("url", |_, me| Ok(Url::from(me.url())));
|
||||||
|
reg.add_field_method_get("length", |_, me| Ok(me.length()));
|
||||||
|
reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from)));
|
||||||
|
reg.add_field_method_get("is_link", |_, me| Ok(me.is_link()));
|
||||||
|
reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden()));
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
reg.add_field_method_get("permissions", |_, me| {
|
||||||
|
Ok(shared::permissions(me.meta().permissions()))
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
#![allow(clippy::module_inception)]
|
#![allow(clippy::module_inception)]
|
||||||
|
|
||||||
|
mod active;
|
||||||
mod bindings;
|
mod bindings;
|
||||||
|
mod files;
|
||||||
mod shared;
|
mod shared;
|
||||||
mod tab;
|
mod tabs;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
|
|
||||||
|
pub use active::*;
|
||||||
pub use bindings::*;
|
pub use bindings::*;
|
||||||
|
pub use files::*;
|
||||||
pub use shared::*;
|
pub use shared::*;
|
||||||
pub use tab::*;
|
pub use tabs::*;
|
||||||
pub use tasks::*;
|
pub use tasks::*;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ impl From<&shared::Url> for Url {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserData for Url {
|
impl UserData for Url {
|
||||||
|
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
|
||||||
|
fields.add_field_method_get("frag", |_, me| Ok(me.0.frag().map(ToOwned::to_owned)));
|
||||||
|
fields.add_field_method_get("is_regular", |_, me| Ok(me.0.is_regular()));
|
||||||
|
fields.add_field_method_get("is_search", |_, me| Ok(me.0.is_search()));
|
||||||
|
fields.add_field_method_get("is_archive", |_, me| Ok(me.0.is_archive()));
|
||||||
|
}
|
||||||
|
|
||||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
methods.add_meta_function(
|
methods.add_meta_function(
|
||||||
MetaMethod::Eq,
|
MetaMethod::Eq,
|
||||||
|
|
|
||||||
96
plugin/src/bindings/tabs.rs
Normal file
96
plugin/src/bindings/tabs.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value};
|
||||||
|
|
||||||
|
use crate::LUA;
|
||||||
|
|
||||||
|
pub struct Tabs<'a, 'b> {
|
||||||
|
scope: &'b mlua::Scope<'a, 'a>,
|
||||||
|
|
||||||
|
inner: &'a core::manager::Tabs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Tabs<'a, 'b> {
|
||||||
|
pub(crate) fn init() -> mlua::Result<()> {
|
||||||
|
LUA.register_userdata_type::<core::manager::Tabs>(|reg| {
|
||||||
|
reg.add_field_method_get("idx", |_, me| Ok(me.idx()));
|
||||||
|
reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len()));
|
||||||
|
reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| {
|
||||||
|
let items = me.named_user_value::<Vec<AnyUserData>>("items")?;
|
||||||
|
Ok(items.get(index - 1).cloned())
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
LUA.register_userdata_type::<core::manager::Tab>(|reg| {
|
||||||
|
reg.add_method("name", |_, me, ()| {
|
||||||
|
Ok(
|
||||||
|
me.current
|
||||||
|
.cwd
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy())
|
||||||
|
.or_else(|| Some(me.current.cwd.to_string_lossy()))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_owned(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
reg.add_field_function_get("mode", |_, me| me.named_user_value::<AnyUserData>("mode"));
|
||||||
|
reg.add_field_function_get("parent", |_, me| me.named_user_value::<Value>("parent"));
|
||||||
|
reg.add_field_function_get("current", |_, me| me.named_user_value::<AnyUserData>("current"));
|
||||||
|
reg.add_field_function_get("preview", |_, me| me.named_user_value::<AnyUserData>("preview"));
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a core::manager::Tabs) -> Self {
|
||||||
|
Self { scope, inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn make(&self) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
let ud = self.scope.create_any_userdata_ref(self.inner)?;
|
||||||
|
|
||||||
|
ud.set_named_user_value(
|
||||||
|
"items",
|
||||||
|
self.inner.iter().filter_map(|t| self.tab(t).ok()).collect::<Vec<_>>(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(ud)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab(&self, inner: &'a core::manager::Tab) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
let ud = self.scope.create_any_userdata_ref(inner)?;
|
||||||
|
|
||||||
|
ud.set_named_user_value("parent", inner.parent.as_ref().and_then(|p| self.folder(p).ok()))?;
|
||||||
|
ud.set_named_user_value("current", self.folder(&inner.current)?)?;
|
||||||
|
ud.set_named_user_value("preview", self.preview(inner)?)?;
|
||||||
|
|
||||||
|
Ok(ud)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn folder(&self, inner: &'a core::manager::Folder) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
let ud = self.scope.create_any_userdata_ref(inner)?;
|
||||||
|
ud.set_named_user_value("files", self.files(&inner.files)?)?;
|
||||||
|
|
||||||
|
Ok(ud)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn files(&self, inner: &'a core::files::Files) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
self.scope.create_any_userdata_ref(inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(&self, tab: &'a core::manager::Tab) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
let inner = tab.preview();
|
||||||
|
|
||||||
|
let ud = self.scope.create_any_userdata_ref(inner)?;
|
||||||
|
ud.set_named_user_value(
|
||||||
|
"folder",
|
||||||
|
inner
|
||||||
|
.lock
|
||||||
|
.as_ref()
|
||||||
|
.filter(|l| l.is_folder())
|
||||||
|
.and_then(|l| tab.history(&l.url))
|
||||||
|
.and_then(|f| self.folder(f).ok()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(ud)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,13 @@ use mlua::{AnyUserData, LuaSerdeExt, UserDataFields};
|
||||||
|
|
||||||
use crate::LUA;
|
use crate::LUA;
|
||||||
|
|
||||||
pub struct Tasks;
|
pub struct Tasks<'a, 'b> {
|
||||||
|
scope: &'b mlua::Scope<'a, 'a>,
|
||||||
|
|
||||||
impl Tasks {
|
inner: &'a core::tasks::Tasks,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Tasks<'a, 'b> {
|
||||||
pub(crate) fn init() -> mlua::Result<()> {
|
pub(crate) fn init() -> mlua::Result<()> {
|
||||||
LUA.register_userdata_type::<core::tasks::Tasks>(|reg| {
|
LUA.register_userdata_type::<core::tasks::Tasks>(|reg| {
|
||||||
reg.add_field_method_get("progress", |lua, me| lua.to_value(&me.progress))
|
reg.add_field_method_get("progress", |lua, me| lua.to_value(&me.progress))
|
||||||
|
|
@ -13,10 +17,11 @@ impl Tasks {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn make<'a>(
|
pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a core::tasks::Tasks) -> Self {
|
||||||
scope: &mlua::Scope<'a, 'a>,
|
Self { scope, inner }
|
||||||
inner: &'a core::tasks::Tasks,
|
}
|
||||||
) -> mlua::Result<AnyUserData<'a>> {
|
|
||||||
scope.create_any_userdata_ref(inner)
|
pub(crate) fn make(&self) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
self.scope.create_any_userdata_ref(self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,22 @@ fn layout(values: Vec<AnyUserData>, buf: &mut ratatui::prelude::Buffer) -> mlua:
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Status
|
||||||
|
pub struct Header;
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
pub fn render(
|
||||||
|
self,
|
||||||
|
area: ratatui::layout::Rect,
|
||||||
|
buf: &mut ratatui::prelude::Buffer,
|
||||||
|
) -> mlua::Result<()> {
|
||||||
|
let comp: Table = GLOBALS.get("Header")?;
|
||||||
|
let values: Vec<AnyUserData> = comp.call_method::<_, _>("render", Rect(area))?;
|
||||||
|
|
||||||
|
layout(values, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Status
|
// --- Status
|
||||||
pub struct Status;
|
pub struct Status;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ mod config;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
mod scope;
|
mod scope;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
pub use components::*;
|
pub use components::*;
|
||||||
use config::*;
|
use config::*;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
||||||
use mlua::{Lua, Table};
|
use mlua::{Lua, Table};
|
||||||
use shared::RoCell;
|
use shared::RoCell;
|
||||||
|
|
||||||
use crate::{bindings, layout};
|
use crate::{bindings, layout, utils};
|
||||||
|
|
||||||
pub(crate) static LUA: RoCell<Lua> = RoCell::new();
|
pub(crate) static LUA: RoCell<Lua> = RoCell::new();
|
||||||
pub(crate) static GLOBALS: RoCell<Table> = RoCell::new();
|
pub(crate) static GLOBALS: RoCell<Table> = RoCell::new();
|
||||||
|
|
@ -17,12 +17,14 @@ pub fn init() {
|
||||||
lua.load(include_str!("../preset/inspect/inspect.lua")).exec()?;
|
lua.load(include_str!("../preset/inspect/inspect.lua")).exec()?;
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
lua.load(include_str!("../preset/components/status.lua")).exec()?;
|
|
||||||
lua.load(include_str!("../preset/components/folder.lua")).exec()?;
|
lua.load(include_str!("../preset/components/folder.lua")).exec()?;
|
||||||
|
lua.load(include_str!("../preset/components/header.lua")).exec()?;
|
||||||
|
lua.load(include_str!("../preset/components/status.lua")).exec()?;
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
LUA.init(lua);
|
LUA.init(lua);
|
||||||
GLOBALS.init(LUA.globals());
|
GLOBALS.init(LUA.globals());
|
||||||
|
utils::init()?;
|
||||||
bindings::init()?;
|
bindings::init()?;
|
||||||
|
|
||||||
// Install
|
// Install
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ use crate::{bindings, GLOBALS, LUA};
|
||||||
pub fn scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) {
|
pub fn scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) {
|
||||||
let _ = LUA.scope(|scope| {
|
let _ = LUA.scope(|scope| {
|
||||||
let tbl = LUA.create_table()?;
|
let tbl = LUA.create_table()?;
|
||||||
tbl.set("active", bindings::Tab::new(scope, cx, cx.manager.active()).make()?)?;
|
tbl.set("active", bindings::Active::new(scope, cx).make()?)?;
|
||||||
tbl.set("tasks", bindings::Tasks::make(scope, &cx.tasks)?)?;
|
tbl.set("tabs", bindings::Tabs::new(scope, cx.manager.tabs()).make()?)?;
|
||||||
|
tbl.set("tasks", bindings::Tasks::new(scope, &cx.tasks).make()?)?;
|
||||||
GLOBALS.set("cx", tbl)?;
|
GLOBALS.set("cx", tbl)?;
|
||||||
|
|
||||||
Ok(f(scope))
|
Ok(f(scope))
|
||||||
|
|
|
||||||
33
plugin/src/utils.rs
Normal file
33
plugin/src/utils.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use std::ops::ControlFlow;
|
||||||
|
|
||||||
|
use mlua::Table;
|
||||||
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
|
use crate::{GLOBALS, LUA};
|
||||||
|
|
||||||
|
pub fn init() -> mlua::Result<()> {
|
||||||
|
let utils: Table = GLOBALS.get("utils")?;
|
||||||
|
|
||||||
|
utils.set(
|
||||||
|
"truncate",
|
||||||
|
LUA.create_function(|_, (text, max): (String, usize)| {
|
||||||
|
let mut width = 0;
|
||||||
|
let flow = text.chars().try_fold(String::with_capacity(max), |mut s, c| {
|
||||||
|
width += c.width().unwrap_or(0);
|
||||||
|
if s.width() < max {
|
||||||
|
s.push(c);
|
||||||
|
ControlFlow::Continue(s)
|
||||||
|
} else {
|
||||||
|
ControlFlow::Break(s)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(match flow {
|
||||||
|
ControlFlow::Break(s) => s,
|
||||||
|
ControlFlow::Continue(s) => s,
|
||||||
|
})
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue