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

View file

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

View file

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

View file

@ -1,39 +1,22 @@
use core::{files::File, Ctx}; use core::{files::File, Ctx};
use config::{MANAGER, THEME}; use config::THEME;
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
use shared::short_path; use tracing::info;
pub(super) struct Folder<'a> { pub(super) struct Folder<'a> {
cx: &'a Ctx, cx: &'a Ctx,
folder: &'a core::manager::Folder, kind: FolderKind,
is_preview: bool, }
is_selection: bool,
is_find: bool, pub(super) enum FolderKind {
Parent = 0,
Current = 1,
Preview = 2,
} }
impl<'a> Folder<'a> { impl<'a> Folder<'a> {
pub(super) fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self { pub(super) fn new(cx: &'a Ctx, kind: FolderKind) -> Self { Self { cx, kind } }
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
}
} }
impl<'a> Folder<'a> { impl<'a> Folder<'a> {
@ -49,12 +32,12 @@ impl<'a> Folder<'a> {
#[inline] #[inline]
fn item_style(&self, file: &File) -> Style { fn item_style(&self, file: &File) -> Style {
let mimetype = &self.cx.manager.mimetype; let mime = self.cx.manager.mimetype.get(file.url());
THEME THEME
.filetypes .filetypes
.iter() .iter()
.find(|x| x.matches(file.url(), mimetype.get(file.url()), file.is_dir())) .find(|x| x.matches(file.url(), mime, file.is_dir()))
.map(|x| x.style.get()) .map(|x| x.style.into())
.unwrap_or_else(Style::new) .unwrap_or_else(Style::new)
} }
@ -87,73 +70,74 @@ impl<'a> Folder<'a> {
impl<'a> Widget for Folder<'a> { impl<'a> Widget for Folder<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let active = self.cx.manager.active(); let x = plugin::Folder { kind: self.kind as u8 }.render(self.cx, area);
let mode = active.mode(); if x.is_err() {
info!("{:?}", x);
return;
}
let window = if self.is_preview { for x in x.unwrap() {
self.folder.window_for(active.preview().skip()) x.render(buf);
} else { }
self.folder.window()
};
let items: Vec<_> = window // let items: Vec<_> = window
.iter() // .iter()
.enumerate() // .enumerate()
.map(|(i, f)| { // .map(|(i, f)| {
let is_selected = self.folder.files.is_selected(f.url()); // let is_selected = self.folder.files.is_selected(f.url());
if (!self.is_selection && is_selected) // if (!self.is_selection && is_selected)
|| (self.is_selection && mode.pending(self.folder.offset() + i, is_selected)) // || (self.is_selection && mode.pending(self.folder.offset() + i, is_selected))
{ // {
buf.set_style( // buf.set_style(
Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1 }, // Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1
if self.is_selection { // }, if self.is_selection {
THEME.marker.selecting.get() // THEME.marker.selecting.get()
} else { // } else {
THEME.marker.selected.get() // 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()); // List::new(items).render(area, buf);
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);
} }
} }

View file

@ -3,7 +3,7 @@ use core::Ctx;
use config::MANAGER; use config::MANAGER;
use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::{Block, Borders, Padding, Widget}}; 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> { pub(crate) struct Layout<'a> {
cx: &'a Ctx, cx: &'a Ctx,
@ -32,16 +32,13 @@ impl<'a> Widget for Layout<'a> {
// Parent // Parent
let block = Block::new().borders(Borders::RIGHT).padding(Padding::new(1, 0, 0, 0)); let block = Block::new().borders(Borders::RIGHT).padding(Padding::new(1, 0, 0, 0));
if let Some(parent) = manager.parent() { if manager.parent().is_some() {
Folder::new(self.cx, parent).render(block.inner(chunks[0]), buf); Folder::new(self.cx, FolderKind::Parent).render(block.inner(chunks[0]), buf);
} }
block.render(chunks[0], buf); block.render(chunks[0], buf);
// Current // Current
Folder::new(self.cx, manager.current()) Folder::new(self.cx, FolderKind::Current).render(chunks[1], buf);
.with_selection(manager.active().mode().is_visual())
.with_find(manager.active().finder().is_some())
.render(chunks[1], buf);
// Preview // Preview
let block = Block::new().borders(Borders::LEFT).padding(Padding::new(0, 1, 0, 0)); 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 ansi_to_tui::IntoText;
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}};
use super::Folder; use super::{folder::FolderKind, Folder};
pub(super) struct Preview<'a> { pub(super) struct Preview<'a> {
cx: &'a Ctx, cx: &'a Ctx,
@ -27,9 +27,7 @@ impl<'a> Widget for Preview<'a> {
match &preview.lock.as_ref().unwrap().data { match &preview.lock.as_ref().unwrap().data {
PreviewData::Folder => { PreviewData::Folder => {
if let Some(folder) = manager.active().history(hovered) { Folder::new(self.cx, FolderKind::Preview).render(area, buf);
Folder::new(self.cx, folder).with_preview(true).render(area, buf);
}
} }
PreviewData::Text(s) => { PreviewData::Text(s) => {
let p = Paragraph::new(s.as_bytes().into_text().unwrap()); 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 ratatui::{buffer::Buffer, prelude::Rect, widgets::Widget};
use tracing::info; use tracing::info;
use crate::parser::Parser;
pub(crate) struct Layout<'a> { pub(crate) struct Layout<'a> {
cx: &'a Ctx, cx: &'a Ctx,
} }
@ -21,8 +19,8 @@ impl<'a> Widget for Layout<'a> {
return; return;
} }
if let Ok(s) = x { for x in x.unwrap() {
Parser::render(&s, buf); x.render(buf);
} }
} }
} }

View file

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

View file

@ -1,53 +1,35 @@
use anyhow::{bail, Result}; use std::str::FromStr;
use ratatui::style;
use serde::{Deserialize, Serialize, Serializer};
#[derive(Deserialize)] use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Deserialize)]
#[serde(try_from = "String")] #[serde(try_from = "String")]
pub struct Color([u8; 3]); pub struct Color(ratatui::style::Color);
impl TryFrom<&str> for Color { impl FromStr for Color {
type Error = anyhow::Error; type Err = anyhow::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != 7 { ratatui::style::Color::from_str(s).map(Self).map_err(|_| anyhow::anyhow!("invalid color"))
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)?,
]))
} }
} }
impl TryFrom<String> for Color { impl TryFrom<String> for Color {
type Error = anyhow::Error; 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 { impl From<Color> for ratatui::style::Color {
fn from(&Color(rgb): &Color) -> Self { style::Color::Rgb(rgb[0], rgb[1], rgb[2]) } fn from(value: Color) -> Self { value.0 }
}
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 Serialize for Color { impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where 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 serde::{Deserialize, Deserializer};
use super::Style; use super::Style;
use crate::{theme::Color, Pattern}; use crate::{theme::{Color, StyleShadow}, Pattern};
pub struct Filetype { pub struct Filetype {
pub name: Option<Pattern>, pub name: Option<Pattern>,
@ -30,18 +30,31 @@ impl Filetype {
{ {
#[derive(Deserialize)] #[derive(Deserialize)]
struct FiletypeOuter { struct FiletypeOuter {
rules: Vec<FiletypeOuterStyle>, rules: Vec<FiletypeRule>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct FiletypeOuterStyle { struct FiletypeRule {
name: Option<Pattern>, name: Option<Pattern>,
mime: Option<Pattern>, mime: Option<Pattern>,
fg: Option<Color>,
bg: Option<Color>, fg: Option<Color>,
bg: Option<Color>,
#[serde(default)] #[serde(default)]
bold: bool, bold: bool,
#[serde(default)] #[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( Ok(
@ -51,12 +64,19 @@ impl Filetype {
.map(|r| Filetype { .map(|r| Filetype {
name: r.name, name: r.name,
mime: r.mime, mime: r.mime,
style: Style { style: StyleShadow {
fg: r.fg, fg: r.fg,
bg: r.bg, bg: r.bg,
bold: r.bold, bold: r.bold,
underline: r.underline, 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<_>>(), .collect::<Vec<_>>(),
) )

View file

@ -1,34 +1,93 @@
use ratatui::style::{self, Modifier}; use ratatui::style::Modifier;
use serde::{Deserialize, Serialize}; use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
use super::Color; use super::Color;
#[derive(Deserialize, Serialize)] #[derive(Clone, Copy, Deserialize)]
#[serde(from = "StyleShadow")]
pub struct Style { pub struct Style {
pub fg: Option<Color>, pub fg: Option<Color>,
pub bg: Option<Color>, pub bg: Option<Color>,
#[serde(default)] pub modifier: Modifier,
pub bold: bool,
#[serde(default)]
pub underline: bool,
} }
impl Style { impl Serialize for Style {
pub fn get(&self) -> style::Style { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
let mut style = style::Style::new(); where
S: Serializer,
if let Some(fg) = &self.fg { {
style = style.fg(fg.into()); let mut map = serializer.serialize_map(Some(3))?;
} map.serialize_entry("fg", &self.fg)?;
if let Some(bg) = &self.bg { map.serialize_entry("bg", &self.bg)?;
style = style.bg(bg.into()); map.serialize_entry("modifier", &self.modifier.bits())?;
} map.end()
if self.bold { }
style = style.add_modifier(Modifier::BOLD); }
}
if self.underline { impl From<Style> for ratatui::style::Style {
style = style.add_modifier(Modifier::UNDERLINED); fn from(value: Style) -> Self {
} ratatui::style::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 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 { pub fn hover(&mut self, url: &Url) -> bool {
let new = self.files.position(url).unwrap_or(self.cursor); let new = self.files.position(url).unwrap_or(self.cursor);
if new > self.cursor { if new > self.cursor {

View file

@ -170,9 +170,6 @@ impl Preview {
} }
impl Preview { impl Preview {
#[inline]
pub fn lock(&self) -> &Option<PreviewLock> { &self.lock }
#[inline] #[inline]
pub fn skip(&self) -> usize { self.skip } 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 = {} 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() local mode = cx.manager.mode:upper()
if mode == "UNSET" then if mode == "UNSET" then
mode = "UN-SET" mode = "UN-SET"
end end
return ui.Line( local style = self.style()
ui.Span(THEME.status.separator.opening):fg(THEME.status.mode_normal.bg), return ui.Line {
ui.Span(" " .. mode .. " "):style(THEME.status.mode_normal) ui.Span(THEME.status.separator.opening):fg(style.bg),
) ui.Span(" " .. mode .. " "):style(style),
}
end end
function Status.size() function Status:size()
local h = cx.manager.current.hovered local h = cx.manager.current.hovered
if h == nil then if h == nil then
return ui.Span("") return ui.Span("")
end end
return ui.Line( local style = self.style()
ui.Span(" " .. utils.readable_size(h.length) .. " "):fg(THEME.status.mode_normal.bg):bg(THEME.status.fancy.bg), return ui.Line {
ui.Span(THEME.status.separator.closing):fg(THEME.status.fancy.bg) 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 end
function Status.name() function Status:name()
local h = cx.manager.current.hovered local h = cx.manager.current.hovered
if h == nil then if h == nil then
return ui.Span("") return ui.Span("")
end end
return ui.Span(" " .. utils.basename(tostring(h.url))) return ui.Span(" " .. h.name)
end end
function Status.permissions() function Status:permissions()
local h = cx.manager.current.hovered local h = cx.manager.current.hovered
if h == nil then if h == nil or h.permissions == nil then
return ui.Span("")
end
if h.permissions == nil then
return ui.Span("") return ui.Span("")
end end
@ -56,10 +65,10 @@ function Status.permissions()
end end
spans[i] = ui.Span(c):style(style) spans[i] = ui.Span(c):style(style)
end end
return ui.Line:from(spans) return ui.Line(spans)
end end
function Status.percentage() function Status:percentage()
local percent = 0 local percent = 0
local cursor = cx.manager.current.cursor local cursor = cx.manager.current.cursor
local length = #cx.manager.current.files local length = #cx.manager.current.files
@ -73,20 +82,22 @@ function Status.percentage()
percent = string.format(" %3d%% ", percent) percent = string.format(" %3d%% ", percent)
end end
return ui.Line( local style = self.style()
return ui.Line {
ui.Span(" " .. THEME.status.separator.opening):fg(THEME.status.fancy.bg), 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 end
function Status.position() function Status:position()
local cursor = cx.manager.current.cursor local cursor = cx.manager.current.cursor
local length = #cx.manager.current.files local length = #cx.manager.current.files
return ui.Line( local style = self.style()
ui.Span(string.format(" %2d/%-2d ", cursor + 1, length)):style(THEME.status.mode_normal), return ui.Line {
ui.Span(THEME.status.separator.closing):fg(THEME.status.mode_normal.bg) ui.Span(string.format(" %2d/%-2d ", cursor + 1, length)):style(style),
) ui.Span(THEME.status.separator.closing):fg(style.bg),
}
end end
function Status:render(area) function Status:render(area)
@ -95,11 +106,7 @@ function Status:render(area)
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) }) :constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
:split(area) :split(area)
local left = ui.Line(self.mode(), self.size(), self.name()) local left = ui.Line { self:mode(), self:size(), self:name() }
local right = ui.Line(self.permissions(), self.percentage(), self.position()) 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) }
return ui.Paragraph.render(
ui.Paragraph(left):area(chunks[1]),
ui.Paragraph(right):align(ui.Alignment.RIGHT):area(chunks[2])
)
end 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<()> { pub(crate) fn init() -> mlua::Result<()> {
LUA.register_userdata_type::<core::manager::Manager>(|reg| { LUA.register_userdata_type::<core::manager::Manager>(|reg| {
reg.add_field_method_get("mode", |_, me| Ok(me.active().mode().to_string())); 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("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| { 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("files", |_, me| me.named_user_value::<AnyUserData>("files"));
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<AnyUserData>("hovered")); 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| { LUA.register_userdata_type::<core::files::Files>(|reg| {
@ -46,6 +43,9 @@ impl Manager {
})?; })?;
LUA.register_userdata_type::<core::files::File>(|reg| { 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("url", |_, me| Ok(Url::from(me.url())));
reg.add_field_method_get("length", |_, me| Ok(me.length())); reg.add_field_method_get("length", |_, me| Ok(me.length()));
reg.add_field_method_get("link_to", |_, me| { 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(()) Ok(())
} }
@ -68,13 +72,14 @@ impl Manager {
inner: &'a core::manager::Manager, inner: &'a core::manager::Manager,
) -> mlua::Result<AnyUserData<'a>> { ) -> mlua::Result<AnyUserData<'a>> {
let ud = scope.create_any_userdata_ref(inner)?; 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("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) Ok(ud)
} }
fn folder<'a>( pub(crate) fn folder<'a>(
scope: &mlua::Scope<'a, 'a>, scope: &mlua::Scope<'a, 'a>,
inner: &'a core::manager::Folder, inner: &'a core::manager::Folder,
) -> mlua::Result<AnyUserData<'a>> { ) -> mlua::Result<AnyUserData<'a>> {
@ -107,4 +112,24 @@ impl Manager {
) -> mlua::Result<AnyUserData<'a>> { ) -> mlua::Result<AnyUserData<'a>> {
scope.create_any_userdata_ref(inner) 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); pub struct Url(shared::Url);
@ -8,6 +8,11 @@ impl From<&shared::Url> for Url {
impl UserData for Url { impl UserData for Url {
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(
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())); 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)] #![allow(clippy::unit_arg)]
mod bindings; mod bindings;
mod components;
mod layout; mod layout;
mod plugin; mod plugin;
mod status;
pub use layout::*; pub use components::*;
pub use plugin::*; pub use plugin::*;
pub use status::*;

View file

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