This commit is contained in:
sxyazi 2023-09-29 10:45:42 +08:00
parent a9074c1447
commit f3b82274b6
No known key found for this signature in database
35 changed files with 946 additions and 830 deletions

8
Cargo.lock generated
View file

@ -220,9 +220,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.7.0"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019"
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
dependencies = [
"memchr",
"serde",
@ -1252,9 +1252,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-float"
version = "2.10.1"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
dependencies = [
"num-traits",
]

View file

@ -49,9 +49,9 @@ impl<'a> Widget for Tabs<'a> {
}
if i == tabs.idx() {
Span::styled(format!(" {text} "), THEME.tab.active.get())
Span::styled(format!(" {text} "), THEME.tab.active.into())
} else {
Span::styled(format!(" {text} "), THEME.tab.inactive.get())
Span::styled(format!(" {text} "), THEME.tab.inactive.into())
}
})
.collect::<Vec<_>>(),

View file

@ -7,7 +7,6 @@ mod help;
mod input;
mod logs;
mod manager;
mod parser;
mod root;
mod select;
mod signals;

View file

@ -1,39 +1,22 @@
use core::{files::File, Ctx};
use config::{MANAGER, THEME};
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}};
use shared::short_path;
use config::THEME;
use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
use tracing::info;
pub(super) struct Folder<'a> {
cx: &'a Ctx,
folder: &'a core::manager::Folder,
is_preview: bool,
is_selection: bool,
is_find: bool,
cx: &'a Ctx,
kind: FolderKind,
}
pub(super) enum FolderKind {
Parent = 0,
Current = 1,
Preview = 2,
}
impl<'a> Folder<'a> {
pub(super) fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self {
Self { cx, folder, is_preview: false, is_selection: false, is_find: false }
}
#[inline]
pub(super) fn with_preview(mut self, state: bool) -> Self {
self.is_preview = state;
self
}
#[inline]
pub(super) fn with_selection(mut self, state: bool) -> Self {
self.is_selection = state;
self
}
#[inline]
pub(super) fn with_find(mut self, state: bool) -> Self {
self.is_find = state;
self
}
pub(super) fn new(cx: &'a Ctx, kind: FolderKind) -> Self { Self { cx, kind } }
}
impl<'a> Folder<'a> {
@ -49,12 +32,12 @@ impl<'a> Folder<'a> {
#[inline]
fn item_style(&self, file: &File) -> Style {
let mimetype = &self.cx.manager.mimetype;
let mime = self.cx.manager.mimetype.get(file.url());
THEME
.filetypes
.iter()
.find(|x| x.matches(file.url(), mimetype.get(file.url()), file.is_dir()))
.map(|x| x.style.get())
.find(|x| x.matches(file.url(), mime, file.is_dir()))
.map(|x| x.style.into())
.unwrap_or_else(Style::new)
}
@ -87,73 +70,74 @@ impl<'a> Folder<'a> {
impl<'a> Widget for Folder<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let active = self.cx.manager.active();
let mode = active.mode();
let x = plugin::Folder { kind: self.kind as u8 }.render(self.cx, area);
if x.is_err() {
info!("{:?}", x);
return;
}
let window = if self.is_preview {
self.folder.window_for(active.preview().skip())
} else {
self.folder.window()
};
for x in x.unwrap() {
x.render(buf);
}
let items: Vec<_> = window
.iter()
.enumerate()
.map(|(i, f)| {
let is_selected = self.folder.files.is_selected(f.url());
if (!self.is_selection && is_selected)
|| (self.is_selection && mode.pending(self.folder.offset() + i, is_selected))
{
buf.set_style(
Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1 },
if self.is_selection {
THEME.marker.selecting.get()
} else {
THEME.marker.selected.get()
},
);
}
// let items: Vec<_> = window
// .iter()
// .enumerate()
// .map(|(i, f)| {
// let is_selected = self.folder.files.is_selected(f.url());
// if (!self.is_selection && is_selected)
// || (self.is_selection && mode.pending(self.folder.offset() + i, is_selected))
// {
// buf.set_style(
// Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1
// }, if self.is_selection {
// THEME.marker.selecting.get()
// } else {
// THEME.marker.selected.get()
// },
// );
// }
//
// let hovered = matches!(self.folder.hovered, Some(ref h) if h.url() ==
// f.url()); let style = if self.is_preview && hovered {
// THEME.preview.hovered.get()
// } else if hovered {
// THEME.selection.hovered.get()
// } else {
// self.item_style(f)
// };
//
// let mut spans = Vec::with_capacity(10);
// spans.push(Span::raw(format!(" {} ", Self::icon(f))));
// spans.extend(self.highlighted_item(f));
//
// if let Some(link_to) = f.link_to() {
// if MANAGER.show_symlink {
// spans.push(Span::raw(format!(" -> {}", link_to.display())));
// }
// }
//
// if let Some(idx) = active
// .finder()
// .filter(|&f| hovered && self.is_find && f.has_matched())
// .and_then(|finder| finder.matched_idx(f.url()))
// {
// let len = active.finder().unwrap().matched().len();
// spans.push(Span::styled(
// format!(
// " [{}/{}]",
// if idx > 99 { ">99".to_string() } else { (idx + 1).to_string() },
// if len > 99 { ">99".to_string() } else { len.to_string() }
// ),
// // TODO: to be configured by THEME?
// Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC),
// ));
// }
//
// ListItem::new(Line::from(spans)).style(style)
// })
// .collect();
let hovered = matches!(self.folder.hovered, Some(ref h) if h.url() == f.url());
let style = if self.is_preview && hovered {
THEME.preview.hovered.get()
} else if hovered {
THEME.selection.hovered.get()
} else {
self.item_style(f)
};
let mut spans = Vec::with_capacity(10);
spans.push(Span::raw(format!(" {} ", Self::icon(f))));
spans.extend(self.highlighted_item(f));
if let Some(link_to) = f.link_to() {
if MANAGER.show_symlink {
spans.push(Span::raw(format!(" -> {}", link_to.display())));
}
}
if let Some(idx) = active
.finder()
.filter(|&f| hovered && self.is_find && f.has_matched())
.and_then(|finder| finder.matched_idx(f.url()))
{
let len = active.finder().unwrap().matched().len();
spans.push(Span::styled(
format!(
" [{}/{}]",
if idx > 99 { ">99".to_string() } else { (idx + 1).to_string() },
if len > 99 { ">99".to_string() } else { len.to_string() }
),
// TODO: to be configured by THEME?
Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC),
));
}
ListItem::new(Line::from(spans)).style(style)
})
.collect();
List::new(items).render(area, buf);
// List::new(items).render(area, buf);
}
}

View file

@ -3,7 +3,7 @@ use core::Ctx;
use config::MANAGER;
use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::{Block, Borders, Padding, Widget}};
use super::{Folder, Preview};
use super::{folder::FolderKind, Folder, Preview};
pub(crate) struct Layout<'a> {
cx: &'a Ctx,
@ -32,16 +32,13 @@ impl<'a> Widget for Layout<'a> {
// Parent
let block = Block::new().borders(Borders::RIGHT).padding(Padding::new(1, 0, 0, 0));
if let Some(parent) = manager.parent() {
Folder::new(self.cx, parent).render(block.inner(chunks[0]), buf);
if manager.parent().is_some() {
Folder::new(self.cx, FolderKind::Parent).render(block.inner(chunks[0]), buf);
}
block.render(chunks[0], buf);
// Current
Folder::new(self.cx, manager.current())
.with_selection(manager.active().mode().is_visual())
.with_find(manager.active().finder().is_some())
.render(chunks[1], buf);
Folder::new(self.cx, FolderKind::Current).render(chunks[1], buf);
// Preview
let block = Block::new().borders(Borders::LEFT).padding(Padding::new(0, 1, 0, 0));

View file

@ -3,7 +3,7 @@ use core::{manager::PreviewData, Ctx};
use ansi_to_tui::IntoText;
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}};
use super::Folder;
use super::{folder::FolderKind, Folder};
pub(super) struct Preview<'a> {
cx: &'a Ctx,
@ -27,9 +27,7 @@ impl<'a> Widget for Preview<'a> {
match &preview.lock.as_ref().unwrap().data {
PreviewData::Folder => {
if let Some(folder) = manager.active().history(hovered) {
Folder::new(self.cx, folder).with_preview(true).render(area, buf);
}
Folder::new(self.cx, FolderKind::Preview).render(area, buf);
}
PreviewData::Text(s) => {
let p = Paragraph::new(s.as_bytes().into_text().unwrap());

View file

@ -1,128 +0,0 @@
use anyhow::Result;
use config::theme::Color;
use ratatui::{prelude::{Alignment, Buffer, Rect}, style::{Modifier, Style}, text::{Line, Span}, widgets::{Paragraph, Widget}};
pub struct Parser;
impl Parser {
fn span(s: &str) -> Span<'static> {
let Some((args, content)) = s.split_once(';') else {
return Span::raw(s.to_string());
};
let args: Vec<_> = args.split(',').collect();
if args.len() != 4 {
return Span::raw(s.to_string());
}
let mut style = Style::new();
if let Ok(fg) = Color::try_from(args[0]) {
style = style.fg(fg.into());
}
if let Ok(bg) = Color::try_from(args[1]) {
style = style.bg(bg.into());
}
if let Ok(uc) = Color::try_from(args[2]) {
style = style.underline_color(uc.into());
}
if let Ok(modifier) = args[3].parse::<u16>() {
style = style.add_modifier(Modifier::from_bits_truncate(modifier));
}
Span::styled(content.to_string(), style)
}
fn line(s: &str) -> Line<'static> {
let mut last = '\0';
let mut spans: Vec<String> = vec![String::new()];
for c in s.chars() {
if c == '\n' && last == '\\' {
let last = spans.last_mut().unwrap();
last.pop();
last.push(c);
} else if c == '\n' {
spans.push(String::new());
} else {
spans.last_mut().unwrap().push(c);
}
last = c;
}
Line::from(spans.into_iter().map(|s| Self::span(&s)).collect::<Vec<_>>())
}
fn paragraph(s: &str) -> Paragraph {
let mut last = '\0';
let mut lines: Vec<String> = vec![String::new()];
for c in s.chars() {
if c == '\r' && last == '\\' {
let last = lines.last_mut().unwrap();
last.pop();
last.push(c);
} else if c == '\r' {
lines.push(String::new());
} else {
lines.last_mut().unwrap().push(c);
}
last = c;
}
Paragraph::new(lines.into_iter().map(|s| Self::line(&s)).collect::<Vec<_>>())
}
fn area(args: &[&str]) -> Result<Rect> {
Ok(Rect {
x: args[0].parse()?,
y: args[1].parse()?,
width: args[2].parse()?,
height: args[3].parse()?,
})
}
pub fn render(s: &str, buf: &mut Buffer) {
let Some(s) = s.strip_prefix('R') else {
return;
};
let mut last = '\0';
let mut paragraphs: Vec<String> = vec![String::new()];
for c in s.chars() {
if c == '\0' && last == '\\' {
let last = paragraphs.last_mut().unwrap();
last.pop();
last.push(c);
} else if c == '\0' {
paragraphs.push(String::new());
} else {
paragraphs.last_mut().unwrap().push(c);
}
last = c;
}
for paragraph in paragraphs {
let Some((args, content)) = paragraph.split_once(';') else {
continue;
};
let args: Vec<_> = args.split(',').collect();
if args.len() != 5 {
continue;
}
let Ok(area) = Self::area(&args) else {
continue;
};
let mut paragraph = Self::paragraph(content);
if let Ok(align) = args[4].parse::<u8>() {
paragraph = paragraph.alignment(match align {
1 => Alignment::Center,
2 => Alignment::Right,
_ => Alignment::Left,
});
}
paragraph.render(area, buf);
}
}
}

View file

@ -3,8 +3,6 @@ use core::Ctx;
use ratatui::{buffer::Buffer, prelude::Rect, widgets::Widget};
use tracing::info;
use crate::parser::Parser;
pub(crate) struct Layout<'a> {
cx: &'a Ctx,
}
@ -21,8 +19,8 @@ impl<'a> Widget for Layout<'a> {
return;
}
if let Ok(s) = x {
Parser::render(&s, buf);
for x in x.unwrap() {
x.render(buf);
}
}
}

View file

@ -19,11 +19,11 @@ impl<'a> Widget for Progress<'a> {
}
Gauge::default()
.gauge_style(THEME.status.progress_gauge.get())
.gauge_style(THEME.status.progress_gauge.into())
.percent(progress.0 as u16)
.label(Span::styled(
format!("{:>3}%, {} left", progress.0, progress.1),
THEME.status.progress_label.get(),
THEME.status.progress_label.into(),
))
.render(area, buf);
}

View file

@ -1,53 +1,35 @@
use anyhow::{bail, Result};
use ratatui::style;
use serde::{Deserialize, Serialize, Serializer};
use std::str::FromStr;
#[derive(Deserialize)]
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Deserialize)]
#[serde(try_from = "String")]
pub struct Color([u8; 3]);
pub struct Color(ratatui::style::Color);
impl TryFrom<&str> for Color {
type Error = anyhow::Error;
impl FromStr for Color {
type Err = anyhow::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
if s.len() != 7 {
bail!("Invalid color: {s}");
}
Ok(Self([
u8::from_str_radix(&s[1..3], 16)?,
u8::from_str_radix(&s[3..5], 16)?,
u8::from_str_radix(&s[5..7], 16)?,
]))
fn from_str(s: &str) -> Result<Self, Self::Err> {
ratatui::style::Color::from_str(s).map(Self).map_err(|_| anyhow::anyhow!("invalid color"))
}
}
impl TryFrom<String> for Color {
type Error = anyhow::Error;
fn try_from(s: String) -> Result<Self, Self::Error> { Self::try_from(s.as_str()) }
fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(s.as_str()) }
}
impl From<&Color> for style::Color {
fn from(&Color(rgb): &Color) -> Self { style::Color::Rgb(rgb[0], rgb[1], rgb[2]) }
}
impl From<Color> for style::Color {
fn from(Color(rgb): Color) -> Self { style::Color::Rgb(rgb[0], rgb[1], rgb[2]) }
}
impl Color {
#[inline]
pub fn fg(&self) -> style::Style { style::Style::new().fg(self.into()) }
#[inline]
pub fn bg(&self) -> style::Style { style::Style::new().bg(self.into()) }
impl From<Color> for ratatui::style::Color {
fn from(value: Color) -> Self { value.0 }
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
S: serde::Serializer,
{
serializer.serialize_str(&format!("#{:02X}{:02X}{:02X}", self.0[0], self.0[1], self.0[2]))
self.0.to_string().serialize(serializer)
}
}

View file

@ -3,7 +3,7 @@ use std::path::Path;
use serde::{Deserialize, Deserializer};
use super::Style;
use crate::{theme::Color, Pattern};
use crate::{theme::{Color, StyleShadow}, Pattern};
pub struct Filetype {
pub name: Option<Pattern>,
@ -30,18 +30,31 @@ impl Filetype {
{
#[derive(Deserialize)]
struct FiletypeOuter {
rules: Vec<FiletypeOuterStyle>,
rules: Vec<FiletypeRule>,
}
#[derive(Deserialize)]
struct FiletypeOuterStyle {
name: Option<Pattern>,
mime: Option<Pattern>,
fg: Option<Color>,
bg: Option<Color>,
struct FiletypeRule {
name: Option<Pattern>,
mime: Option<Pattern>,
fg: Option<Color>,
bg: Option<Color>,
#[serde(default)]
bold: bool,
bold: bool,
#[serde(default)]
underline: bool,
dim: bool,
#[serde(default)]
italic: bool,
#[serde(default)]
underline: bool,
#[serde(default)]
blink: bool,
#[serde(default)]
blink_rapid: bool,
#[serde(default)]
hidden: bool,
#[serde(default)]
crossed: bool,
}
Ok(
@ -51,12 +64,19 @@ impl Filetype {
.map(|r| Filetype {
name: r.name,
mime: r.mime,
style: Style {
fg: r.fg,
bg: r.bg,
bold: r.bold,
underline: r.underline,
},
style: StyleShadow {
fg: r.fg,
bg: r.bg,
bold: r.bold,
dim: r.dim,
italic: r.italic,
underline: r.underline,
blink: r.blink,
blink_rapid: r.blink_rapid,
hidden: r.hidden,
crossed: r.crossed,
}
.into(),
})
.collect::<Vec<_>>(),
)

View file

@ -1,34 +1,93 @@
use ratatui::style::{self, Modifier};
use serde::{Deserialize, Serialize};
use ratatui::style::Modifier;
use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
use super::Color;
#[derive(Deserialize, Serialize)]
#[derive(Clone, Copy, Deserialize)]
#[serde(from = "StyleShadow")]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
#[serde(default)]
pub bold: bool,
#[serde(default)]
pub underline: bool,
pub fg: Option<Color>,
pub bg: Option<Color>,
pub modifier: Modifier,
}
impl Style {
pub fn get(&self) -> style::Style {
let mut style = style::Style::new();
if let Some(fg) = &self.fg {
style = style.fg(fg.into());
}
if let Some(bg) = &self.bg {
style = style.bg(bg.into());
}
if self.bold {
style = style.add_modifier(Modifier::BOLD);
}
if self.underline {
style = style.add_modifier(Modifier::UNDERLINED);
}
style
impl Serialize for Style {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("fg", &self.fg)?;
map.serialize_entry("bg", &self.bg)?;
map.serialize_entry("modifier", &self.modifier.bits())?;
map.end()
}
}
impl From<Style> for ratatui::style::Style {
fn from(value: Style) -> Self {
ratatui::style::Style {
fg: value.fg.map(Into::into),
bg: value.bg.map(Into::into),
underline_color: None,
add_modifier: value.modifier,
sub_modifier: Modifier::empty(),
}
}
}
#[derive(Deserialize)]
pub(super) struct StyleShadow {
#[serde(default)]
pub(super) fg: Option<Color>,
#[serde(default)]
pub(super) bg: Option<Color>,
#[serde(default)]
pub(super) bold: bool,
#[serde(default)]
pub(super) dim: bool,
#[serde(default)]
pub(super) italic: bool,
#[serde(default)]
pub(super) underline: bool,
#[serde(default)]
pub(super) blink: bool,
#[serde(default)]
pub(super) blink_rapid: bool,
#[serde(default)]
pub(super) hidden: bool,
#[serde(default)]
pub(super) crossed: bool,
}
impl From<StyleShadow> for Style {
fn from(value: StyleShadow) -> Self {
let mut modifier = Modifier::empty();
if value.bold {
modifier |= Modifier::BOLD;
}
if value.dim {
modifier |= Modifier::DIM;
}
if value.italic {
modifier |= Modifier::ITALIC;
}
if value.underline {
modifier |= Modifier::UNDERLINED;
}
if value.blink {
modifier |= Modifier::SLOW_BLINK;
}
if value.blink_rapid {
modifier |= Modifier::RAPID_BLINK;
}
if value.hidden {
modifier |= Modifier::HIDDEN;
}
if value.crossed {
modifier |= Modifier::CROSSED_OUT;
}
Self { fg: value.fg, bg: value.bg, modifier }
}
}

View file

@ -90,19 +90,6 @@ impl Folder {
old != self.cursor
}
#[inline]
pub fn window(&self) -> &[File] {
let end = (self.offset + MANAGER.layout.folder_height()).min(self.files.len());
&self.files[self.offset..end]
}
#[inline]
pub fn window_for(&self, offset: usize) -> &[File] {
let start = offset.min(self.files.len().saturating_sub(1));
let end = (offset + MANAGER.layout.folder_height()).min(self.files.len());
&self.files[start..end]
}
pub fn hover(&mut self, url: &Url) -> bool {
let new = self.files.position(url).unwrap_or(self.cursor);
if new > self.cursor {

View file

@ -170,9 +170,6 @@ impl Preview {
}
impl Preview {
#[inline]
pub fn lock(&self) -> &Option<PreviewLock> { &self.lock }
#[inline]
pub fn skip(&self) -> usize { self.skip }

View file

@ -0,0 +1,57 @@
Folder = {}
function Folder:parent(area)
local parent = cx.manager.parent
if parent == nil then
return ui.Paragraph(area, ui.Line {})
end
local lines = {}
for _, f in pairs(parent.files) do
lines[#lines + 1] = ui.Line { ui.Span(f.name) }
end
return { ui.Paragraph(area, lines) }
end
function Folder:current(area)
local hovered = nil
if cx.manager.current.hovered ~= nil then
hovered = cx.manager.current.hovered.url
end
local lines = {}
for _, f in pairs(cx.manager.current.files) do
local line = ui.Line { ui.Span(f.name) }
if f.url == hovered then
line = line:style(THEME.selection.hovered)
end
lines[#lines + 1] = line
end
return { ui.Paragraph(area, lines) }
end
function Folder:preview(area)
local target = cx.manager.preview.folder
if target == nil then
return ui.Paragraph(area, ui.Line {})
end
local lines = {}
for _, f in pairs(target.files) do
lines[#lines + 1] = ui.Line { ui.Span(f.name) }
end
return { ui.Paragraph(area, lines) }
end
function Folder:render(area, args)
if args.kind == 0 then
return self:parent(area)
elseif args.kind == 1 then
return self:current(area)
elseif args.kind == 2 then
return self:preview(area)
end
end

View file

@ -1,45 +1,54 @@
Status = {}
function Status.mode()
function Status.style()
local mode = cx.manager.mode:upper()
if mode == "SELECT" then
return THEME.status.mode_select
elseif mode == "UNSET" then
return THEME.status.mode_unset
else
return THEME.status.mode_normal
end
end
function Status:mode()
local mode = cx.manager.mode:upper()
if mode == "UNSET" then
mode = "UN-SET"
end
return ui.Line(
ui.Span(THEME.status.separator.opening):fg(THEME.status.mode_normal.bg),
ui.Span(" " .. mode .. " "):style(THEME.status.mode_normal)
)
local style = self.style()
return ui.Line {
ui.Span(THEME.status.separator.opening):fg(style.bg),
ui.Span(" " .. mode .. " "):style(style),
}
end
function Status.size()
function Status:size()
local h = cx.manager.current.hovered
if h == nil then
return ui.Span("")
end
return ui.Line(
ui.Span(" " .. utils.readable_size(h.length) .. " "):fg(THEME.status.mode_normal.bg):bg(THEME.status.fancy.bg),
ui.Span(THEME.status.separator.closing):fg(THEME.status.fancy.bg)
)
local style = self.style()
return ui.Line {
ui.Span(" " .. utils.readable_size(h.length) .. " "):fg(style.bg):bg(THEME.status.fancy.bg),
ui.Span(THEME.status.separator.closing):fg(THEME.status.fancy.bg),
}
end
function Status.name()
function Status:name()
local h = cx.manager.current.hovered
if h == nil then
return ui.Span("")
end
return ui.Span(" " .. utils.basename(tostring(h.url)))
return ui.Span(" " .. h.name)
end
function Status.permissions()
function Status:permissions()
local h = cx.manager.current.hovered
if h == nil then
return ui.Span("")
end
if h.permissions == nil then
if h == nil or h.permissions == nil then
return ui.Span("")
end
@ -56,10 +65,10 @@ function Status.permissions()
end
spans[i] = ui.Span(c):style(style)
end
return ui.Line:from(spans)
return ui.Line(spans)
end
function Status.percentage()
function Status:percentage()
local percent = 0
local cursor = cx.manager.current.cursor
local length = #cx.manager.current.files
@ -73,20 +82,22 @@ function Status.percentage()
percent = string.format(" %3d%% ", percent)
end
return ui.Line(
local style = self.style()
return ui.Line {
ui.Span(" " .. THEME.status.separator.opening):fg(THEME.status.fancy.bg),
ui.Span(percent):fg(THEME.status.mode_normal.bg):bg(THEME.status.fancy.bg)
)
ui.Span(percent):fg(style.bg):bg(THEME.status.fancy.bg),
}
end
function Status.position()
function Status:position()
local cursor = cx.manager.current.cursor
local length = #cx.manager.current.files
return ui.Line(
ui.Span(string.format(" %2d/%-2d ", cursor + 1, length)):style(THEME.status.mode_normal),
ui.Span(THEME.status.separator.closing):fg(THEME.status.mode_normal.bg)
)
local style = self.style()
return ui.Line {
ui.Span(string.format(" %2d/%-2d ", cursor + 1, length)):style(style),
ui.Span(THEME.status.separator.closing):fg(style.bg),
}
end
function Status:render(area)
@ -95,11 +106,7 @@ function Status:render(area)
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
:split(area)
local left = ui.Line(self.mode(), self.size(), self.name())
local right = ui.Line(self.permissions(), self.percentage(), self.position())
return ui.Paragraph.render(
ui.Paragraph(left):area(chunks[1]),
ui.Paragraph(right):align(ui.Alignment.RIGHT):area(chunks[2])
)
local left = ui.Line { self:mode(), self:size(), self:name() }
local right = ui.Line { self:permissions(), self:percentage(), self:position() }
return { ui.Paragraph(chunks[1], { left }), ui.Paragraph(chunks[2], { right }):align(ui.Alignment.RIGHT) }
end

View file

@ -1,35 +0,0 @@
local Line = {}
function Line:new(...)
local o = {
spans = { ... },
}
setmetatable(o, self)
self.__index = self
return o
end
function Line:from(spans) return self:new(table.unpack(spans)) end
function Line:to_string()
local s = ""
for _, el in ipairs(self.spans) do
local mt = getmetatable(el)
if mt == ui.Line then
for _, span in ipairs(el.spans) do
s = s .. span:to_string():gsub("\n", "\\\n") .. "\n"
end
else
s = s .. el:to_string():gsub("\n", "\\\n") .. "\n"
end
end
return s.sub(s, 1, -2)
end
setmetatable(Line, {
__call = function(self, ...) return self:new(...) end,
__tostring = function(self) return self:to_string() end,
})
ui = ui or {}
ui.Line = Line

View file

@ -1,66 +0,0 @@
local Paragraph = {}
local Alignment = {
LEFT = 0,
CENTER = 1,
RIGHT = 2,
}
function Paragraph:new(...)
local o = {
alignment = 0,
position = nil,
lines = { ... },
}
setmetatable(o, self)
self.__index = self
return o
end
function Paragraph:from(lines) return self:new(table.unpack(lines)) end
function Paragraph:align(align)
self.alignment = align
return self
end
function Paragraph:area(rect)
self.position = rect
return self
end
function Paragraph:to_string()
local s = ""
for _, line in ipairs(self.lines) do
s = s .. line:to_string():gsub("\r", "\\\r") .. "\r"
end
return s.sub(s, 1, -2)
end
function Paragraph.render(...)
local s = "R"
for _, paragraph in ipairs { ... } do
s = s
.. paragraph.position.x
.. ","
.. paragraph.position.y
.. ","
.. paragraph.position.width
.. ","
.. paragraph.position.height
.. ","
.. paragraph.alignment
.. ";"
.. paragraph:to_string():gsub("\0", "\\\0")
.. "\0"
end
return s.sub(s, 1, -2)
end
setmetatable(Paragraph, {
__call = function(self, ...) return self:new(...) end,
__tostring = function(self) return self:to_string() end,
})
ui = ui or {}
ui.Paragraph = Paragraph
ui.Alignment = Alignment

View file

@ -1,124 +0,0 @@
local Span = {}
function Span:new(content)
local o = {
content = content,
foreground = "",
background = "",
modifier = 0,
}
setmetatable(o, self)
self.__index = self
return o
end
function Span:fg(color)
self.foreground = color
return self
end
function Span:bg(color)
self.background = color
return self
end
function Span:bold()
self.modifier = self.modifier | 1
return self
end
function Span:dim()
self.modifier = self.modifier | 2
return self
end
function Span:italic()
self.modifier = self.modifier | 4
return self
end
function Span:underline()
self.modifier = self.modifier | 8
return self
end
function Span:blink()
self.modifier = self.modifier | 16
return self
end
function Span:blink_rapid()
self.modifier = self.modifier | 32
return self
end
function Span:reverse()
self.modifier = self.modifier | 64
return self
end
function Span:hidden()
self.modifier = self.modifier | 128
return self
end
function Span:crossed()
self.modifier = self.modifier | 256
return self
end
function Span:reset()
self.foreground = ""
self.background = ""
self.modifier = 0
return self
end
function Span:style(style)
if style.fg then
self:fg(style.fg)
end
if style.bg then
self:bg(style.bg)
end
if style.bold then
self:bold()
end
if style.dim then
self:dim()
end
if style.italic then
self:italic()
end
if style.underline then
self:underline()
end
if style.blink then
self:blink()
end
if style.blink_rapid then
self:blink_rapid()
end
if style.reverse then
self:reverse()
end
if style.hidden then
self:hidden()
end
if style.crossed then
self:crossed()
end
return self
end
function Span:to_string()
return string.format("%s,%s,,%s;%s", self.foreground, self.background, self.modifier, self.content)
end
setmetatable(Span, {
__call = function(self, content) return self:new(content) end,
__tostring = function(self) return self:to_string() end,
})
ui = ui or {}
ui.Span = Span

11
plugin/preset/ui.lua Normal file
View file

@ -0,0 +1,11 @@
ui = {
Alignment = {
LEFT = 0,
CENTER = 1,
RIGHT = 2,
},
Direction = {
HORIZONTAL = false,
VERTICAL = true,
},
}

View file

@ -9,8 +9,9 @@ impl Manager {
pub(crate) fn init() -> mlua::Result<()> {
LUA.register_userdata_type::<core::manager::Manager>(|reg| {
reg.add_field_method_get("mode", |_, me| Ok(me.active().mode().to_string()));
reg.add_field_function_get("current", |_, me| me.named_user_value::<AnyUserData>("current"));
reg.add_field_function_get("parent", |_, me| me.named_user_value::<AnyUserData>("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::Folder>(|reg| {
@ -20,10 +21,6 @@ impl Manager {
reg.add_field_function_get("files", |_, me| me.named_user_value::<AnyUserData>("files"));
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<AnyUserData>("hovered"));
// reg.add_field_method_get("window", |_, me| {
// LUA.scope(|scope| scope.create_nonstatic_userdata(Files { files:
// &me.0.files }))
// });
})?;
LUA.register_userdata_type::<core::files::Files>(|reg| {
@ -46,6 +43,9 @@ impl Manager {
})?;
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_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| {
@ -60,6 +60,10 @@ impl Manager {
});
})?;
LUA.register_userdata_type::<core::manager::Preview>(|reg| {
reg.add_field_function_get("folder", |_, me| me.named_user_value::<AnyUserData>("folder"));
})?;
Ok(())
}
@ -68,13 +72,14 @@ impl Manager {
inner: &'a core::manager::Manager,
) -> mlua::Result<AnyUserData<'a>> {
let ud = scope.create_any_userdata_ref(inner)?;
ud.set_named_user_value("current", Self::folder(scope, inner.current())?)?;
ud.set_named_user_value("parent", inner.parent().and_then(|p| Self::folder(scope, p).ok()))?;
ud.set_named_user_value("current", Self::folder(scope, inner.current())?)?;
ud.set_named_user_value("preview", Self::preview(scope, inner.active())?)?;
Ok(ud)
}
fn folder<'a>(
pub(crate) fn folder<'a>(
scope: &mlua::Scope<'a, 'a>,
inner: &'a core::manager::Folder,
) -> mlua::Result<AnyUserData<'a>> {
@ -107,4 +112,24 @@ impl Manager {
) -> mlua::Result<AnyUserData<'a>> {
scope.create_any_userdata_ref(inner)
}
fn preview<'a>(
scope: &mlua::Scope<'a, 'a>,
tab: &'a core::manager::Tab,
) -> mlua::Result<AnyUserData<'a>> {
let inner = tab.preview();
let ud = 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(scope, f).ok()),
)?;
Ok(ud)
}
}

View file

@ -1,4 +1,4 @@
use mlua::{MetaMethod, UserData};
use mlua::{MetaMethod, UserData, UserDataRef};
pub struct Url(shared::Url);
@ -8,6 +8,11 @@ impl From<&shared::Url> for Url {
impl UserData for Url {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_function(
MetaMethod::Eq,
|_, (lhs, rhs): (UserDataRef<Self>, UserDataRef<Self>)| Ok(lhs.0 == rhs.0),
);
methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.0.display().to_string()));
}
}

45
plugin/src/components.rs Normal file
View file

@ -0,0 +1,45 @@
use core::Ctx;
use mlua::{Result, Table, TableExt};
use ratatui::layout;
use crate::{bindings, layout::{Paragraph, Rect}, GLOBALS, LUA};
pub struct Status;
impl Status {
pub fn render(cx: &Ctx, area: layout::Rect) -> Result<Vec<Paragraph>> {
LUA.scope(|scope| {
let tbl = LUA.create_table()?;
tbl.set("manager", bindings::Manager::make(scope, &cx.manager)?)?;
tbl.set("tasks", bindings::Tasks::make(scope, &cx.tasks)?)?;
GLOBALS.set("cx", tbl)?;
let comp: Table = GLOBALS.get("Status")?;
comp.call_method::<_, _>("render", Rect(area))
})
}
}
pub struct Folder {
pub kind: u8,
}
impl Folder {
fn args(&self) -> Result<Table> {
let tbl = LUA.create_table()?;
tbl.set("kind", self.kind)?;
Ok(tbl)
}
pub fn render(self, cx: &Ctx, area: layout::Rect) -> Result<Vec<Paragraph>> {
LUA.scope(|scope| {
let tbl = LUA.create_table()?;
tbl.set("manager", bindings::Manager::make(scope, &cx.manager)?)?;
GLOBALS.set("cx", tbl)?;
let comp: Table = GLOBALS.get("Folder")?;
comp.call_method::<_, _>("render", (Rect(area), self.args()?))
})
}
}

View file

@ -1,180 +0,0 @@
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, UserDataMethods, Value};
use ratatui::layout;
use crate::{GLOBALS, LUA};
// --- Rect
#[derive(Clone, Copy)]
pub struct Rect(layout::Rect);
impl From<layout::Rect> for Rect {
fn from(value: layout::Rect) -> Self { Self(value) }
}
impl<'lua> FromLua<'lua> for Rect {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Rect",
message: Some("expected a Rect".to_string()),
}),
}
}
}
impl UserData for Rect {
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("x", |_, me| Ok(me.0.x));
fields.add_field_method_get("y", |_, me| Ok(me.0.y));
fields.add_field_method_get("width", |_, me| Ok(me.0.width));
fields.add_field_method_get("height", |_, me| Ok(me.0.height));
}
}
// --- Constraint
#[derive(Clone, Copy)]
pub struct Constraint(layout::Constraint);
impl Constraint {
pub(super) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
let constraint = LUA.create_table()?;
constraint.set(
"Percentage",
LUA.create_function(|_, n: u16| Ok(Constraint(layout::Constraint::Percentage(n))))?,
)?;
constraint.set(
"Ratio",
LUA
.create_function(|_, (a, b): (u32, u32)| Ok(Constraint(layout::Constraint::Ratio(a, b))))?,
)?;
constraint.set(
"Length",
LUA.create_function(|_, n: u16| Ok(Constraint(layout::Constraint::Length(n))))?,
)?;
constraint
.set("Max", LUA.create_function(|_, n: u16| Ok(Constraint(layout::Constraint::Max(n))))?)?;
constraint
.set("Min", LUA.create_function(|_, n: u16| Ok(Constraint(layout::Constraint::Min(n))))?)?;
ui.set("Constraint", constraint)
}
}
impl<'lua> FromLua<'lua> for Constraint {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Constraint",
message: Some("expected a Constraint".to_string()),
}),
}
}
}
impl UserData for Constraint {}
// --- Layout
#[derive(Clone, Default)]
pub struct Layout {
direction: bool,
margin: Option<layout::Margin>,
constraints: Vec<layout::Constraint>,
}
impl Layout {
pub(super) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set("Layout", LUA.create_function(|_, ()| Ok(Self::default()))?)?;
let direction = LUA.create_table()?;
direction.set("HORIZONTAL", false)?;
direction.set("VERTICAL", true)?;
ui.set("Direction", direction)
}
}
impl<'lua> FromLua<'lua> for Layout {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(ud.borrow::<Self>()?.clone()),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Layout",
message: Some("expected a Layout".to_string()),
}),
}
}
}
impl UserData for Layout {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("direction", |_, (ud, value): (AnyUserData, bool)| {
{
let mut me = ud.borrow_mut::<Self>()?;
me.direction = value;
}
Ok(ud)
});
methods.add_function("margin", |_, (ud, value): (AnyUserData, u16)| {
{
let mut me = ud.borrow_mut::<Self>()?;
me.margin = Some(layout::Margin::new(value, value));
}
Ok(ud)
});
methods.add_function("margin_h", |_, (ud, value): (AnyUserData, u16)| {
{
let mut me = ud.borrow_mut::<Self>()?;
if let Some(margin) = &mut me.margin {
margin.horizontal = value;
} else {
me.margin = Some(layout::Margin::new(value, 0));
}
}
Ok(ud)
});
methods.add_function("margin_v", |_, (ud, value): (AnyUserData, u16)| {
{
let mut me = ud.borrow_mut::<Self>()?;
if let Some(margin) = &mut me.margin {
margin.vertical = value;
} else {
me.margin = Some(layout::Margin::new(0, value));
}
}
Ok(ud)
});
methods.add_function("constraints", |_, (ud, value): (AnyUserData, Vec<Constraint>)| {
{
let mut me = ud.borrow_mut::<Self>()?;
me.constraints = value.into_iter().map(|c| c.0).collect();
}
Ok(ud)
});
methods.add_function("split", |_, (ud, value): (AnyUserData, Rect)| {
let me = ud.borrow::<Self>()?;
let mut layout = layout::Layout::new()
.direction(if me.direction {
layout::Direction::Vertical
} else {
layout::Direction::Horizontal
})
.constraints(me.constraints.as_slice());
if let Some(margin) = me.margin {
layout = layout.horizontal_margin(margin.horizontal);
layout = layout.vertical_margin(margin.vertical);
}
let chunks: Vec<Rect> = layout.split(value.0).iter().copied().map(Rect).collect();
Ok(chunks)
});
}
}

View file

@ -0,0 +1,54 @@
use mlua::{FromLua, Lua, Table, UserData, Value};
use crate::{GLOBALS, LUA};
#[derive(Clone, Copy)]
pub(crate) struct Constraint(pub(super) ratatui::layout::Constraint);
impl Constraint {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
let constraint = LUA.create_table()?;
constraint.set(
"Percentage",
LUA
.create_function(|_, n: u16| Ok(Constraint(ratatui::layout::Constraint::Percentage(n))))?,
)?;
constraint.set(
"Ratio",
LUA.create_function(|_, (a, b): (u32, u32)| {
Ok(Constraint(ratatui::layout::Constraint::Ratio(a, b)))
})?,
)?;
constraint.set(
"Length",
LUA.create_function(|_, n: u16| Ok(Constraint(ratatui::layout::Constraint::Length(n))))?,
)?;
constraint.set(
"Max",
LUA.create_function(|_, n: u16| Ok(Constraint(ratatui::layout::Constraint::Max(n))))?,
)?;
constraint.set(
"Min",
LUA.create_function(|_, n: u16| Ok(Constraint(ratatui::layout::Constraint::Min(n))))?,
)?;
ui.set("Constraint", constraint)
}
}
impl<'lua> FromLua<'lua> for Constraint {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Constraint",
message: Some("expected a Constraint".to_string()),
}),
}
}
}
impl UserData for Constraint {}

View file

@ -0,0 +1,89 @@
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, UserDataMethods, Value};
use super::{Constraint, Rect};
use crate::{GLOBALS, LUA};
#[derive(Clone, Default)]
pub(crate) struct Layout {
direction: bool,
margin: Option<ratatui::layout::Margin>,
constraints: Vec<ratatui::layout::Constraint>,
}
impl Layout {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set("Layout", LUA.create_function(|_, ()| Ok(Self::default()))?)
}
}
impl<'lua> FromLua<'lua> for Layout {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(ud.borrow::<Self>()?.clone()),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Layout",
message: Some("expected a Layout".to_string()),
}),
}
}
}
impl UserData for Layout {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("direction", |_, (ud, value): (AnyUserData, bool)| {
ud.borrow_mut::<Self>()?.direction = value;
Ok(ud)
});
methods.add_function("margin", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.margin = Some(ratatui::layout::Margin::new(value, value));
Ok(ud)
});
methods.add_function("margin_h", |_, (ud, value): (AnyUserData, u16)| {
{
let mut me = ud.borrow_mut::<Self>()?;
if let Some(margin) = &mut me.margin {
margin.horizontal = value;
} else {
me.margin = Some(ratatui::layout::Margin::new(value, 0));
}
}
Ok(ud)
});
methods.add_function("margin_v", |_, (ud, value): (AnyUserData, u16)| {
{
let mut me = ud.borrow_mut::<Self>()?;
if let Some(margin) = &mut me.margin {
margin.vertical = value;
} else {
me.margin = Some(ratatui::layout::Margin::new(0, value));
}
}
Ok(ud)
});
methods.add_function("constraints", |_, (ud, value): (AnyUserData, Vec<Constraint>)| {
ud.borrow_mut::<Self>()?.constraints = value.into_iter().map(|c| c.0).collect();
Ok(ud)
});
methods.add_function("split", |_, (ud, value): (AnyUserData, Rect)| {
let me = ud.borrow::<Self>()?;
let mut layout = ratatui::layout::Layout::new()
.direction(if me.direction {
ratatui::layout::Direction::Vertical
} else {
ratatui::layout::Direction::Horizontal
})
.constraints(me.constraints.as_slice());
if let Some(margin) = me.margin {
layout = layout.horizontal_margin(margin.horizontal);
layout = layout.vertical_margin(margin.vertical);
}
let chunks: Vec<Rect> = layout.split(value.0).iter().copied().map(Rect).collect();
Ok(chunks)
});
}
}

70
plugin/src/layout/line.rs Normal file
View file

@ -0,0 +1,70 @@
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, UserDataMethods, Value};
use super::{Span, Style};
use crate::{GLOBALS, LUA};
#[derive(Clone)]
pub(crate) struct Line(pub(super) ratatui::text::Line<'static>);
impl Line {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set(
"Line",
LUA.create_function(|_, value: Value| {
if let Value::Table(tbl) = value {
let seq: Vec<_> = tbl.sequence_values().filter_map(|v| v.ok()).collect();
let mut spans = Vec::with_capacity(seq.len());
for value in seq {
if let Value::UserData(ud) = value {
if let Ok(span) = ud.take::<Span>() {
spans.push(span.0);
} else if let Ok(line) = ud.take::<Line>() {
spans.extend(line.0.spans.into_iter().collect::<Vec<_>>());
} else {
return Err(mlua::Error::external("expected a table of Spans or Lines"));
}
}
}
return Ok(Self(ratatui::text::Line::from(spans)));
}
Err(mlua::Error::external("expected a table of Spans or Lines"))
})?,
)
}
}
impl<'lua> FromLua<'lua> for Line {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(ud.borrow::<Self>()?.clone()),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Line",
message: Some("expected a Line".to_string()),
}),
}
}
}
impl UserData for Line {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("style", |_, (ud, style): (AnyUserData, Style)| {
{
let mut me = ud.borrow_mut::<Self>()?;
me.0.reset_style();
me.0.patch_style(style.0);
}
Ok(ud)
});
methods.add_function("align", |_, (ud, align): (AnyUserData, u8)| {
ud.borrow_mut::<Self>()?.0.alignment = Some(match align {
1 => ratatui::prelude::Alignment::Center,
2 => ratatui::prelude::Alignment::Right,
_ => ratatui::prelude::Alignment::Left,
});
Ok(ud)
});
}
}

17
plugin/src/layout/mod.rs Normal file
View file

@ -0,0 +1,17 @@
#![allow(clippy::module_inception)]
mod constraint;
mod layout;
mod line;
mod paragraph;
mod rect;
mod span;
mod style;
pub(super) use constraint::*;
pub(super) use layout::*;
pub(super) use line::*;
pub(super) use paragraph::*;
pub(super) use rect::*;
pub(super) use span::*;
pub(super) use style::*;

View file

@ -0,0 +1,72 @@
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, Value};
use ratatui::widgets::Widget;
use super::{Rect, Style};
use crate::{layout::Line, GLOBALS, LUA};
#[derive(Clone, Debug)]
pub struct Paragraph {
area: ratatui::layout::Rect,
text: ratatui::text::Text<'static>,
style: Option<ratatui::style::Style>,
alignment: ratatui::prelude::Alignment,
}
impl Paragraph {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set(
"Paragraph",
LUA.create_function(|_, (area, lines): (Rect, Vec<Line>)| {
Ok(Self {
area: area.0,
text: lines.into_iter().map(|s| s.0).collect::<Vec<_>>().into(),
style: None,
alignment: Default::default(),
})
})?,
)
}
pub fn render(self, buf: &mut ratatui::buffer::Buffer) {
let mut p = ratatui::widgets::Paragraph::new(self.text);
if let Some(style) = self.style {
p = p.style(style);
}
p = p.alignment(self.alignment);
p.render(self.area, buf)
}
}
impl<'lua> FromLua<'lua> for Paragraph {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(ud.borrow::<Self>()?.clone()),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Paragraph",
message: Some("expected a Paragraph".to_string()),
}),
}
}
}
impl UserData for Paragraph {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("style", |_, (ud, style): (AnyUserData, Style)| {
ud.borrow_mut::<Self>()?.style = Some(style.0);
Ok(ud)
});
methods.add_function("align", |_, (ud, align): (AnyUserData, u8)| {
ud.borrow_mut::<Self>()?.alignment = match align {
1 => ratatui::prelude::Alignment::Center,
2 => ratatui::prelude::Alignment::Right,
_ => ratatui::prelude::Alignment::Left,
};
Ok(ud)
});
}
}

26
plugin/src/layout/rect.rs Normal file
View file

@ -0,0 +1,26 @@
use mlua::{FromLua, Lua, UserData, Value};
#[derive(Clone, Copy)]
pub(crate) struct Rect(pub(crate) ratatui::layout::Rect);
impl<'lua> FromLua<'lua> for Rect {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Rect",
message: Some("expected a Rect".to_string()),
}),
}
}
}
impl UserData for Rect {
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("x", |_, me| Ok(me.0.x));
fields.add_field_method_get("y", |_, me| Ok(me.0.y));
fields.add_field_method_get("width", |_, me| Ok(me.0.width));
fields.add_field_method_get("height", |_, me| Ok(me.0.height));
}
}

90
plugin/src/layout/span.rs Normal file
View file

@ -0,0 +1,90 @@
use config::theme::Color;
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, UserDataMethods, Value};
use super::Style;
use crate::{GLOBALS, LUA};
#[derive(Clone)]
pub(crate) struct Span(pub(super) ratatui::text::Span<'static>);
impl Span {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set(
"Span",
LUA.create_function(|_, content: String| Ok(Self(ratatui::text::Span::raw(content))))?,
)
}
}
impl<'lua> FromLua<'lua> for Span {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(ud.borrow::<Self>()?.clone()),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Span",
message: Some("expected a Span".to_string()),
}),
}
}
}
impl UserData for Span {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("fg", |_, (ud, color): (AnyUserData, String)| {
ud.borrow_mut::<Self>()?.0.style.fg = Color::try_from(color).ok().map(Into::into);
Ok(ud)
});
methods.add_function("bg", |_, (ud, color): (AnyUserData, String)| {
ud.borrow_mut::<Self>()?.0.style.bg = Color::try_from(color).ok().map(Into::into);
Ok(ud)
});
methods.add_function("bold", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier |= ratatui::style::Modifier::BOLD;
Ok(ud)
});
methods.add_function("dim", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier |= ratatui::style::Modifier::DIM;
Ok(ud)
});
methods.add_function("italic", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier |= ratatui::style::Modifier::ITALIC;
Ok(ud)
});
methods.add_function("underline", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier |= ratatui::style::Modifier::UNDERLINED;
Ok(ud)
});
methods.add_function("blink", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier |= ratatui::style::Modifier::SLOW_BLINK;
Ok(ud)
});
methods.add_function("blink_rapid", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier |= ratatui::style::Modifier::RAPID_BLINK;
Ok(ud)
});
methods.add_function("hidden", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier |= ratatui::style::Modifier::HIDDEN;
Ok(ud)
});
methods.add_function("crossed", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier |= ratatui::style::Modifier::CROSSED_OUT;
Ok(ud)
});
methods.add_function("reset", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.style.add_modifier = ratatui::style::Modifier::empty();
Ok(ud)
});
methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.0.style = match value {
Value::Nil => ratatui::style::Style::default(),
Value::Table(tbl) => Style::from(tbl).0,
Value::UserData(ud) => ud.borrow::<Style>()?.0,
_ => return Err(mlua::Error::external("expected a Style or Table or nil")),
};
Ok(ud)
});
}
}

View file

@ -0,0 +1,94 @@
use config::theme::Color;
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, UserDataMethods, Value};
use tracing::info;
use crate::{GLOBALS, LUA};
#[derive(Clone, Copy, Default)]
pub(crate) struct Style(pub(super) ratatui::style::Style);
impl Style {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set("Style", LUA.create_function(|_, ()| Ok(Self::default()))?)
}
}
impl<'a> From<Table<'a>> for Style {
fn from(value: Table) -> Self {
let mut style = ratatui::style::Style::default();
if let Ok(fg) = value.get::<_, String>("fg") {
style.fg = Color::try_from(fg).ok().map(Into::into);
}
if let Ok(bg) = value.get::<_, String>("bg") {
style.bg = Color::try_from(bg).ok().map(Into::into);
}
style.add_modifier = ratatui::style::Modifier::from_bits_truncate(
value.get::<_, u16>("modifier").unwrap_or_default(),
);
Self(style)
}
}
impl<'lua> FromLua<'lua> for Style {
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Style",
message: Some("expected a Style".to_string()),
}),
}
}
}
impl UserData for Style {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("fg", |_, (ud, color): (AnyUserData, String)| {
ud.borrow_mut::<Self>()?.0.fg = Color::try_from(color).ok().map(Into::into);
info!("fg: {:?}", ud.borrow::<Self>()?.0.fg);
Ok(ud)
});
methods.add_function("bg", |_, (ud, color): (AnyUserData, String)| {
ud.borrow_mut::<Self>()?.0.bg = Color::try_from(color).ok().map(Into::into);
Ok(ud)
});
methods.add_function("bold", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier |= ratatui::style::Modifier::BOLD;
Ok(ud)
});
methods.add_function("dim", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier |= ratatui::style::Modifier::DIM;
Ok(ud)
});
methods.add_function("italic", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier |= ratatui::style::Modifier::ITALIC;
Ok(ud)
});
methods.add_function("underline", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier |= ratatui::style::Modifier::UNDERLINED;
Ok(ud)
});
methods.add_function("blink", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier |= ratatui::style::Modifier::SLOW_BLINK;
Ok(ud)
});
methods.add_function("blink_rapid", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier |= ratatui::style::Modifier::RAPID_BLINK;
Ok(ud)
});
methods.add_function("hidden", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier |= ratatui::style::Modifier::HIDDEN;
Ok(ud)
});
methods.add_function("crossed", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier |= ratatui::style::Modifier::CROSSED_OUT;
Ok(ud)
});
methods.add_function("reset", |_, ud: AnyUserData| {
ud.borrow_mut::<Self>()?.0.add_modifier = ratatui::style::Modifier::empty();
Ok(ud)
});
}
}

View file

@ -1,10 +1,9 @@
#![allow(clippy::unit_arg)]
mod bindings;
mod components;
mod layout;
mod plugin;
mod status;
pub use layout::*;
pub use components::*;
pub use plugin::*;
pub use status::*;

View file

@ -13,16 +13,13 @@ pub fn init() {
let lua = Lua::new();
// Base
lua.load(include_str!("../preset/ui.lua")).exec()?;
lua.load(include_str!("../preset/utils.lua")).exec()?;
lua.load(include_str!("../preset/inspect/inspect.lua")).exec()?;
// Elements
lua.load(include_str!("../preset/elements/span.lua")).exec()?;
lua.load(include_str!("../preset/elements/line.lua")).exec()?;
lua.load(include_str!("../preset/elements/paragraph.lua")).exec()?;
// Components
lua.load(include_str!("../preset/components/status.lua")).exec()?;
lua.load(include_str!("../preset/components/folder.lua")).exec()?;
// Initialize
LUA.init(lua);
@ -30,8 +27,12 @@ pub fn init() {
bindings::init()?;
// Install
layout::Layout::install()?;
layout::Constraint::install()?;
layout::Layout::install()?;
layout::Line::install()?;
layout::Paragraph::install()?;
layout::Span::install()?;
layout::Style::install()?;
let options =
SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false);

View file

@ -1,34 +0,0 @@
use core::Ctx;
use mlua::{Result, Table, TableExt};
use ratatui::layout;
use crate::{bindings, Rect, GLOBALS, LUA};
pub struct Status;
impl Status {
fn scope<T, F: FnOnce() -> Result<T>>(cx: &Ctx, f: F) -> Result<T> {
// crate::Manager::register()?;
LUA.scope(|scope| {
let manager = bindings::Manager::make(scope, &cx.manager)?;
let tasks = bindings::Tasks::make(scope, &cx.tasks)?;
let cx = LUA.create_table()?;
cx.set("manager", manager)?;
cx.set("tasks", tasks)?;
GLOBALS.set("cx", cx)?;
f()
})
}
pub fn render(cx: &Ctx, area: layout::Rect) -> Result<String> {
Self::scope(cx, || {
let status: Table = GLOBALS.get("Status")?;
status.call_method::<_, String>("render", Rect::from(area))
})
}
}