feat: new theme system (#161)

This commit is contained in:
三咲雅 · Misaki Masa 2023-10-12 00:09:10 +08:00 committed by GitHub
parent 15c34fed5c
commit 1a2798eb15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 3014 additions and 1042 deletions

118
Cargo.lock generated
View File

@ -133,6 +133,7 @@ dependencies = [
"crossterm",
"futures",
"libc",
"plugin",
"ratatui",
"shared",
"signal-hook-tokio",
@ -217,6 +218,16 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.14.0"
@ -544,6 +555,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
dependencies = [
"serde",
]
[[package]]
name = "error-code"
version = "2.3.1"
@ -1011,6 +1031,25 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "lua-src"
version = "546.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c26d4af78361e025a3d03a2b964cd1592aff7495f4d4f7947218c084c6fdca8"
dependencies = [
"cc",
]
[[package]]
name = "luajit-src"
version = "210.4.8+resty107baaf"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05167e8b2a2185758d83ed23541e5bd8bce37072e4204e0ef2c9b322bc87c4e"
dependencies = [
"cc",
"which",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -1073,6 +1112,35 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "mlua"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c3a7a7ff4481ec91b951a733390211a8ace1caba57266ccb5f4d4966704e560"
dependencies = [
"bstr",
"erased-serde",
"mlua-sys",
"num-traits",
"once_cell",
"rustc-hash",
"serde",
"serde-value",
]
[[package]]
name = "mlua-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ec8b54eddb76093069cce9eeffb4c7b3a1a0fe66962d7bd44c4867928149ca3"
dependencies = [
"cc",
"cfg-if",
"lua-src",
"luajit-src",
"pkg-config",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -1203,6 +1271,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-float"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
dependencies = [
"num-traits",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -1276,6 +1353,20 @@ dependencies = [
"time",
]
[[package]]
name = "plugin"
version = "0.1.0"
dependencies = [
"anyhow",
"config",
"core",
"mlua",
"ratatui",
"shared",
"tracing",
"unicode-width",
]
[[package]]
name = "png"
version = "0.17.10"
@ -1450,6 +1541,12 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustversion"
version = "1.0.14"
@ -1492,6 +1589,16 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
@ -2142,6 +2249,17 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "which"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
dependencies = [
"either",
"libc",
"once_cell",
]
[[package]]
name = "winapi"
version = "0.3.9"

View File

@ -5,6 +5,7 @@ members = [
"app",
"config",
"core",
"plugin",
"shared",
]

View File

@ -7,6 +7,7 @@ edition = "2021"
adaptor = { path = "../adaptor" }
config = { path = "../config" }
core = { path = "../core" }
plugin = { path = "../plugin" }
shared = { path = "../shared" }
# External dependencies

View File

@ -1,4 +1,4 @@
use core::{emit, files::FilesOp, input::InputMode, Event};
use core::{emit, files::FilesOp, input::InputMode, Ctx, Event};
use std::ffi::OsString;
use anyhow::{Ok, Result};
@ -7,7 +7,7 @@ use crossterm::event::KeyEvent;
use shared::{expand_url, Term};
use tokio::sync::oneshot;
use crate::{Ctx, Executor, Logs, Root, Signals};
use crate::{Executor, Logs, Root, Signals};
pub(super) struct App {
cx: Ctx,
@ -21,7 +21,7 @@ impl App {
let term = Term::start()?;
let signals = Signals::start()?;
let mut app = Self { cx: Ctx::new(), term: Some(term), signals };
let mut app = Self { cx: Ctx::make(), term: Some(term), signals };
while let Some(event) = app.signals.recv().await {
match event {
@ -76,7 +76,9 @@ impl App {
fn dispatch_render(&mut self) {
if let Some(term) = &mut self.term {
let _ = term.draw(|f| {
f.render_widget(Root::new(&self.cx), f.size());
plugin::scope(&self.cx, |_| {
f.render_widget(Root::new(&self.cx), f.size());
});
if let Some((x, y)) = self.cx.cursor() {
f.set_cursor(x, y);
@ -209,8 +211,8 @@ impl App {
tasks.file_open(&targets);
}
}
Event::Progress(percent, left) => {
tasks.progress = (percent, left);
Event::Progress(progress) => {
tasks.progress = progress;
emit!(Render);
}

View File

@ -1,10 +1,8 @@
use core::{emit, files::FilesSorter, input::InputMode, manager::FinderCase};
use core::{emit, files::FilesSorter, input::InputMode, manager::FinderCase, Ctx};
use config::{keymap::{Control, Exec, Key, KeymapLayer}, manager::SortBy, KEYMAP};
use shared::{optional_bool, Url};
use super::Ctx;
pub(super) struct Executor;
impl Executor {

View File

@ -1,33 +0,0 @@
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Paragraph, Widget}};
use shared::readable_path;
use super::Tabs;
use crate::Ctx;
pub(crate) struct Layout<'a> {
cx: &'a Ctx,
}
impl<'a> Layout<'a> {
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
}
impl<'a> Widget for Layout<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let chunks = layout::Layout::new()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(area);
let cwd = &self.cx.manager.current().cwd;
let location = if cwd.is_search() {
format!("{} (search: {})", readable_path(cwd), cwd.frag().unwrap())
} else {
readable_path(cwd)
};
Paragraph::new(location).style(Style::new().fg(Color::Cyan)).render(chunks[0], buf);
Tabs::new(self.cx).render(chunks[1], buf);
}
}

View File

@ -1,5 +0,0 @@
mod layout;
mod tabs;
pub(super) use layout::*;
use tabs::*;

View File

@ -1,63 +0,0 @@
use std::ops::ControlFlow;
use config::THEME;
use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, text::{Line, Span}, widgets::{Paragraph, Widget}};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::Ctx;
pub(super) struct Tabs<'a> {
cx: &'a Ctx,
}
impl<'a> Tabs<'a> {
pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } }
fn truncate(&self, name: &str) -> String {
let mut width = 0;
let flow =
name.chars().try_fold(String::with_capacity(THEME.tab.max_width as usize), |mut s, c| {
width += c.width().unwrap_or(0);
if s.width() < THEME.tab.max_width as usize {
s.push(c);
ControlFlow::Continue(s)
} else {
ControlFlow::Break(s)
}
});
match flow {
ControlFlow::Break(s) => s,
ControlFlow::Continue(s) => s,
}
}
}
impl<'a> Widget for Tabs<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let tabs = self.cx.manager.tabs();
let line = Line::from(
tabs
.iter()
.enumerate()
.map(|(i, tab)| {
let mut text = format!("{}", i + 1);
if THEME.tab.max_width >= 3 {
text.push(' ');
text.push_str(tab.name());
text = self.truncate(&text);
}
if i == tabs.idx() {
Span::styled(format!(" {text} "), THEME.tab.active.get())
} else {
Span::styled(format!(" {text} "), THEME.tab.inactive.get())
}
})
.collect::<Vec<_>>(),
);
Paragraph::new(line).alignment(Alignment::Right).render(area, buf);
}
}

View File

@ -1,6 +1,6 @@
use ratatui::{layout::{self, Constraint}, prelude::{Buffer, Direction, Rect}, style::{Color, Style, Stylize}, widgets::{List, ListItem, Widget}};
use core::Ctx;
use crate::context::Ctx;
use ratatui::{layout::{self, Constraint}, prelude::{Buffer, Direction, Rect}, style::{Color, Style, Stylize}, widgets::{List, ListItem, Widget}};
pub(super) struct Bindings<'a> {
cx: &'a Ctx,

View File

@ -1,7 +1,8 @@
use core::Ctx;
use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, style::{Color, Style}, widgets::{Clear, Paragraph, Widget}};
use super::Bindings;
use crate::Ctx;
pub(crate) struct Layout<'a> {
cx: &'a Ctx,
@ -15,7 +16,7 @@ impl<'a> Widget for Layout<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let chunks = layout::Layout::new()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(1)].as_ref())
.constraints([Constraint::Min(0), Constraint::Length(1)])
.split(area);
Clear.render(area, buf);

View File

@ -1,12 +1,10 @@
use core::input::InputMode;
use core::{input::InputMode, Ctx};
use std::ops::Range;
use ansi_to_tui::IntoText;
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Style}, text::{Line, Text}, widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget}};
use shared::Term;
use crate::Ctx;
pub(crate) struct Input<'a> {
cx: &'a Ctx,
}

View File

@ -1,9 +1,7 @@
#![allow(clippy::module_inception)]
mod app;
mod context;
mod executor;
mod header;
mod help;
mod input;
mod logs;
@ -11,12 +9,10 @@ mod manager;
mod root;
mod select;
mod signals;
mod status;
mod tasks;
mod which;
use app::*;
use context::*;
use executor::*;
use logs::*;
use root::*;
@ -30,6 +26,8 @@ async fn main() -> anyhow::Result<()> {
core::init();
plugin::init();
adaptor::init();
App::run().await

View File

@ -1,161 +1,17 @@
use core::files::File;
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use tracing::error;
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 crate::Ctx;
pub(super) struct Folder<'a> {
cx: &'a Ctx,
folder: &'a core::manager::Folder,
is_preview: bool,
is_selection: bool,
is_find: bool,
pub(super) enum Folder {
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
}
}
impl<'a> Folder<'a> {
#[inline]
fn icon(file: &File) -> &'static str {
THEME
.icons
.iter()
.find(|x| x.name.match_path(file.url(), Some(file.is_dir())))
.map(|x| x.display.as_ref())
.unwrap_or("")
}
#[inline]
fn item_style(&self, file: &File) -> Style {
let mimetype = &self.cx.manager.mimetype;
THEME
.filetypes
.iter()
.find(|x| x.matches(file.url(), mimetype.get(file.url()), file.is_dir()))
.map(|x| x.style.get())
.unwrap_or_else(Style::new)
}
fn highlighted_item<'b>(&'b self, file: &'b File) -> Vec<Span> {
let short = short_path(file.url(), &self.folder.cwd);
let v = self.is_find.then_some(()).and_then(|_| {
let finder = self.cx.manager.active().finder()?;
#[cfg(target_os = "windows")]
let (head, body, tail) = finder.explode(short.name.to_string_lossy().as_bytes())?;
#[cfg(not(target_os = "windows"))]
let (head, body, tail) = {
use std::os::unix::ffi::OsStrExt;
finder.explode(short.name.as_bytes())?
};
// TODO: to be configured by THEME?
let style = Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC);
Some(vec![
Span::raw(short.prefix.join(head).display().to_string()),
Span::styled(body, style),
Span::raw(tail),
])
});
v.unwrap_or_else(|| vec![Span::raw(format!("{}", short))])
}
}
impl<'a> Widget for Folder<'a> {
impl Widget for Folder {
fn render(self, area: Rect, buf: &mut Buffer) {
let active = self.cx.manager.active();
let mode = active.mode();
let window = if self.is_preview {
self.folder.window_for(active.preview().skip())
} else {
self.folder.window()
};
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();
List::new(items).render(area, buf);
let folder = plugin::Folder { kind: self as u8 };
if let Err(e) = folder.render(area, buf) {
error!("{:?}", e);
}
}
}

View File

@ -1,8 +1,9 @@
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 crate::Ctx;
pub(crate) struct Layout<'a> {
cx: &'a Ctx,
@ -19,28 +20,22 @@ impl<'a> Widget for Layout<'a> {
let chunks = layout::Layout::new()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Ratio(layout.parent, layout.all),
Constraint::Ratio(layout.current, layout.all),
Constraint::Ratio(layout.preview, layout.all),
]
.as_ref(),
)
.constraints([
Constraint::Ratio(layout.parent, layout.all),
Constraint::Ratio(layout.current, layout.all),
Constraint::Ratio(layout.preview, layout.all),
])
.split(area);
// 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::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::Current.render(chunks[1], buf);
// Preview
let block = Block::new().borders(Borders::LEFT).padding(Padding::new(0, 1, 0, 0));

View File

@ -1,10 +1,9 @@
use core::manager::PreviewData;
use core::{manager::PreviewData, Ctx};
use ansi_to_tui::IntoText;
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}};
use super::Folder;
use crate::Ctx;
pub(super) struct Preview<'a> {
cx: &'a Ctx,
@ -28,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::Preview.render(area, buf);
}
PreviewData::Text(s) => {
let p = Paragraph::new(s.as_bytes().into_text().unwrap());

View File

@ -1,6 +1,9 @@
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget};
use core::Ctx;
use super::{header, input, manager, select, status, tasks, which, Ctx};
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget};
use tracing::error;
use super::{input, manager, select, tasks, which};
use crate::help;
pub(super) struct Root<'a> {
@ -15,12 +18,16 @@ impl<'a> Widget for Root<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let chunks = Layout::new()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)].as_ref())
.constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)])
.split(area);
header::Layout::new(self.cx).render(chunks[0], buf);
if let Err(e) = plugin::Header.render(chunks[0], buf) {
error!("{:?}", e);
}
manager::Layout::new(self.cx).render(chunks[1], buf);
status::Layout::new(self.cx).render(chunks[2], buf);
if let Err(e) = plugin::Status.render(chunks[2], buf) {
error!("{:?}", e);
}
if self.cx.tasks.visible {
tasks::Layout::new(self.cx).render(area, buf);

View File

@ -1,6 +1,6 @@
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Style}, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}};
use core::Ctx;
use crate::Ctx;
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Style}, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}};
pub(crate) struct Select<'a> {
cx: &'a Ctx,

View File

@ -1,24 +0,0 @@
use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::Widget};
use super::{Left, Right};
use crate::Ctx;
pub(crate) struct Layout<'a> {
cx: &'a Ctx,
}
impl<'a> Layout<'a> {
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
}
impl<'a> Widget for Layout<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let chunks = layout::Layout::new()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(area);
Left::new(self.cx).render(chunks[0], buf);
Right::new(self.cx).render(chunks[1], buf);
}
}

View File

@ -1,53 +0,0 @@
use config::THEME;
use ratatui::{buffer::Buffer, layout::Rect, style::Modifier, text::{Line, Span}, widgets::{Paragraph, Widget}};
use shared::readable_size;
use crate::Ctx;
pub(super) struct Left<'a> {
cx: &'a Ctx,
}
impl<'a> Left<'a> {
pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } }
}
impl<'a> Widget for Left<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let folder = self.cx.manager.current();
let mode = self.cx.manager.active().mode();
// Colors
let primary = mode.color(&THEME.status.primary);
let secondary = mode.color(&THEME.status.secondary);
let body = mode.color(&THEME.status.body);
// Separator
let separator = &THEME.status.separator;
// Mode
let mut spans = Vec::with_capacity(5);
spans.push(Span::styled(&separator.opening, primary.fg()));
spans.push(Span::styled(
format!(" {mode} "),
primary.bg().fg(**secondary).add_modifier(Modifier::BOLD),
));
if let Some(h) = &folder.hovered {
// Length
{
let size = if h.is_dir() { folder.files.size(h.url()) } else { None };
spans.push(Span::styled(
format!(" {} ", readable_size(size.unwrap_or(h.length()))),
body.bg().fg(**primary),
));
spans.push(Span::styled(&separator.closing, body.fg()));
}
// Filename
spans.push(Span::raw(format!(" {} ", h.name_display().unwrap())));
}
Paragraph::new(Line::from(spans)).render(area, buf);
}
}

View File

@ -1,9 +0,0 @@
mod layout;
mod left;
mod progress;
mod right;
pub(super) use layout::*;
use left::*;
use progress::*;
use right::*;

View File

@ -1,30 +0,0 @@
use config::THEME;
use ratatui::{buffer::Buffer, layout::Rect, text::Span, widgets::{Gauge, Widget}};
use crate::Ctx;
pub(super) struct Progress<'a> {
cx: &'a Ctx,
}
impl<'a> Progress<'a> {
pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } }
}
impl<'a> Widget for Progress<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let progress = &self.cx.tasks.progress;
if progress.0 >= 100 {
return;
}
Gauge::default()
.gauge_style(THEME.progress.gauge.get())
.percent(progress.0 as u16)
.label(Span::styled(
format!("{:>3}%, {} left", progress.0, progress.1),
THEME.progress.label.get(),
))
.render(area, buf);
}
}

View File

@ -1,94 +0,0 @@
use config::THEME;
use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, text::{Line, Span}, widgets::{Paragraph, Widget}};
use super::Progress;
use crate::Ctx;
pub(super) struct Right<'a> {
cx: &'a Ctx,
}
impl<'a> Right<'a> {
pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } }
#[cfg(not(target_os = "windows"))]
fn permissions(&self, s: &str) -> Vec<Span> {
// Colors
let mode = self.cx.manager.active().mode();
let tertiary = mode.color(&THEME.status.tertiary);
let info = mode.color(&THEME.status.info);
let success = mode.color(&THEME.status.success);
let warning = mode.color(&THEME.status.warning);
let danger = mode.color(&THEME.status.danger);
s.chars()
.map(|c| match c {
'-' => Span::styled("-", tertiary.fg()),
'r' => Span::styled("r", warning.fg()),
'w' => Span::styled("w", danger.fg()),
'x' | 's' | 'S' | 't' | 'T' => Span::styled(c.to_string(), info.fg()),
_ => Span::styled(c.to_string(), success.fg()),
})
.collect()
}
fn position(&self) -> Vec<Span> {
// Colors
let mode = self.cx.manager.active().mode();
let primary = mode.color(&THEME.status.primary);
let secondary = mode.color(&THEME.status.secondary);
let body = mode.color(&THEME.status.body);
// Separator
let separator = &THEME.status.separator;
let cursor = self.cx.manager.current().cursor();
let length = self.cx.manager.current().files.len();
let percent = if cursor == 0 || length == 0 { 0 } else { (cursor + 1) * 100 / length };
vec![
Span::raw(" "),
Span::styled(&separator.opening, body.fg()),
Span::styled(
if percent == 0 { " Top ".to_string() } else { format!(" {:>3}% ", percent) },
body.bg().fg(**primary),
),
Span::styled(
format!(" {:>2}/{:<2} ", (cursor + 1).min(length), length),
primary.bg().fg(**secondary),
),
Span::styled(&separator.closing, primary.fg()),
]
}
}
impl Widget for Right<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let manager = self.cx.manager.current();
let mut spans = Vec::with_capacity(20);
// Permissions
#[cfg(not(target_os = "windows"))]
if let Some(h) = &manager.hovered {
use std::os::unix::prelude::PermissionsExt;
spans.extend(self.permissions(&shared::file_mode(h.meta().permissions().mode())))
}
// Position
spans.extend(self.position());
// Progress
let line = Line::from(spans);
Progress::new(self.cx).render(
Rect {
x: area.x + area.width.saturating_sub(21 + line.width() as u16),
y: area.y,
width: 20.min(area.width),
height: 1,
},
buf,
);
Paragraph::new(line).alignment(Alignment::Right).render(area, buf);
}
}

View File

@ -1,9 +1,8 @@
use core::tasks::TASKS_PERCENT;
use core::{tasks::TASKS_PERCENT, Ctx};
use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Direction, Rect}, style::{Color, Modifier, Style}, widgets::{Block, BorderType, Borders, List, ListItem, Padding, Widget}};
use super::Clear;
use crate::Ctx;
pub(crate) struct Layout<'a> {
cx: &'a Ctx,
@ -15,29 +14,21 @@ impl<'a> Layout<'a> {
pub(super) fn area(area: Rect) -> Rect {
let chunk = layout::Layout::new()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
Constraint::Percentage(TASKS_PERCENT),
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
]
.as_ref(),
)
.constraints([
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
Constraint::Percentage(TASKS_PERCENT),
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
])
.split(area)[1];
let chunk = layout::Layout::new()
layout::Layout::new()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
Constraint::Percentage(TASKS_PERCENT),
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
]
.as_ref(),
)
.split(chunk)[1];
chunk
.constraints([
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
Constraint::Percentage(TASKS_PERCENT),
Constraint::Percentage((100 - TASKS_PERCENT) / 2),
])
.split(chunk)[1]
}
}

View File

@ -1,7 +1,8 @@
use core::Ctx;
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Block, Clear, Widget}};
use super::Side;
use crate::Ctx;
pub(crate) struct Which<'a> {
cx: &'a Ctx,
@ -34,9 +35,7 @@ impl Widget for Which<'_> {
let chunks = layout::Layout::new()
.direction(Direction::Horizontal)
.constraints(
[Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)].as_ref(),
)
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)])
.split(area);
Clear.render(area, buf);

View File

@ -1,30 +1,37 @@
[tab]
[tabs]
active = { fg = "#1E2031", bg = "#80AEFA" }
inactive = { fg = "#C8D3F8", bg = "#484D66" }
max_width = 1
[status]
primary = { normal = "#80AEFA", select = "#CD9EFC", unset = "#FFA577" }
secondary = { normal = "#1E2031", select = "#23273B", unset = "#23273B" }
tertiary = { normal = "#6D738F", select = "#6D738F", unset = "#6D738F" }
body = { normal = "#484D66", select = "#484D66", unset = "#484D66" }
emphasis = { normal = "#C8D3F8", select = "#C8D3F8", unset = "#C8D3F8" }
info = { normal = "#7AD9E5", select = "#7AD9E5", unset = "#7AD9E5" }
success = { normal = "#97DC8D", select = "#97DC8D", unset = "#97DC8D" }
warning = { normal = "#F3D398", select = "#F3D398", unset = "#F3D398" }
danger = { normal = "#FA7F94", select = "#FA7F94", unset = "#FA7F94" }
plain = { fg = "#FFFFFF" }
fancy = { bg = "#45475D" }
separator = { opening = "", closing = "" }
[progress]
gauge = { fg = "#FFA577", bg = "#484D66" }
label = { fg = "#FFFFFF", bold = true }
# Mode
mode_normal = { fg = "#181827", bg = "#7DB5FF", bold = true }
mode_select = { fg = "#1E1E30", bg = "#D2A4FE", bold = true }
mode_unset = { fg = "#1E1E30", bg = "#FFAF80", bold = true }
[selection]
# Progress
progress_label = { fg = "#FFFFFF", bold = true }
progress_normal = { fg = "#FFA577", bg = "#484D66" }
progress_error = { fg = "#FF84A9", bg = "#484D66" }
# Permissions
permissions_t = { fg = "#97DC8D" }
permissions_r = { fg = "#F3D398" }
permissions_w = { fg = "#FA7F94" }
permissions_x = { fg = "#7AD9E5" }
permissions_s = { fg = "#6D738F" }
[files]
hovered = { fg = "#1E2031", bg = "#80AEFA" }
[marker]
selecting = { fg = "#97DC8D", bg = "#97DC8D" }
selected = { fg = "#F3D398", bg = "#F3D398" }
selected = { fg = "#97DC8D", bg = "#97DC8D" }
copied = { fg = "#F3D398", bg = "#F3D398" }
cut = { fg = "#FF84A9", bg = "#FF84A9" }
[preview]
hovered = { underline = true }

View File

@ -70,5 +70,8 @@ micro_workers = 5
macro_workers = 10
bizarre_retry = 5
[plugins]
preload = []
[log]
enabled = false

View File

@ -8,9 +8,10 @@ mod log;
pub mod manager;
pub mod open;
mod pattern;
pub mod plugins;
mod preset;
pub mod preview;
pub mod tasks;
mod tasks;
pub mod theme;
mod validation;
mod xdg;
@ -27,6 +28,7 @@ pub static KEYMAP: RoCell<keymap::Keymap> = RoCell::new();
pub static LOG: RoCell<log::Log> = RoCell::new();
pub static MANAGER: RoCell<manager::Manager> = RoCell::new();
pub static OPEN: RoCell<open::Open> = RoCell::new();
pub static PLUGINS: RoCell<plugins::Plugins> = RoCell::new();
pub static PREVIEW: RoCell<preview::Preview> = RoCell::new();
pub static TASKS: RoCell<tasks::Tasks> = RoCell::new();
pub static THEME: RoCell<theme::Theme> = RoCell::new();
@ -42,6 +44,7 @@ pub fn init() {
LOG.with(Default::default);
MANAGER.with(Default::default);
OPEN.with(Default::default);
PLUGINS.with(Default::default);
PREVIEW.with(Default::default);
TASKS.with(Default::default);
THEME.with(Default::default);

View File

@ -1,12 +1,12 @@
use anyhow::bail;
use crossterm::terminal::WindowSize;
use ratatui::prelude::Rect;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use shared::Term;
use super::{FOLDER_MARGIN, PREVIEW_BORDER, PREVIEW_MARGIN};
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(try_from = "Vec<u32>")]
pub struct ManagerLayout {
pub parent: u32,

View File

@ -1,9 +1,9 @@
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use super::{ManagerLayout, SortBy};
use crate::MERGED_YAZI;
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
pub struct Manager {
pub layout: ManagerLayout,

View File

@ -1,7 +1,7 @@
use anyhow::bail;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(try_from = "String")]
pub enum SortBy {
#[default]

View File

@ -0,0 +1,3 @@
mod plugins;
pub use plugins::*;

View File

@ -0,0 +1,29 @@
use std::path::PathBuf;
use serde::Deserialize;
use shared::expand_path;
use validator::Validate;
use crate::MERGED_YAZI;
#[derive(Debug, Deserialize, Validate)]
pub struct Plugins {
pub preload: Vec<PathBuf>,
}
impl Default for Plugins {
fn default() -> Self {
#[derive(Deserialize)]
struct Outer {
plugins: Plugins,
}
let mut plugins = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().plugins;
plugins.preload.iter_mut().for_each(|p| {
*p = expand_path(&p);
});
plugins
}
}

View File

@ -1,47 +1,35 @@
use std::ops::Deref;
use std::str::FromStr;
use anyhow::{bail, Result};
use ratatui::style;
use serde::Deserialize;
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
#[derive(Clone, Copy, Deserialize)]
#[serde(try_from = "String")]
pub struct Color(pub(super) style::Color);
pub struct Color(ratatui::style::Color);
impl Default for Color {
fn default() -> Self { Self(style::Color::Reset) }
impl FromStr for Color {
type Err = anyhow::Error;
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> {
if s.len() < 7 {
bail!("Invalid color: {s}");
}
Ok(Self(style::Color::Rgb(
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 try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(s.as_str()) }
}
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: serde::Serializer,
{
self.0.to_string().serialize(serializer)
}
}
impl Deref for Color {
type Target = style::Color;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Color {
pub fn fg(&self) -> style::Style { style::Style::new().fg(self.0) }
pub fn bg(&self) -> style::Style { style::Style::new().bg(self.0) }
}
#[derive(Deserialize)]
pub struct ColorGroup {
pub normal: Color,
pub select: Color,
pub unset: Color,
}

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,16 +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>,
bold: Option<bool>,
underline: Option<bool>,
struct FiletypeRule {
name: Option<Pattern>,
mime: Option<Pattern>,
fg: Option<Color>,
bg: Option<Color>,
#[serde(default)]
bold: bool,
#[serde(default)]
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(
@ -49,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<_>>(),
)

15
config/src/theme/list.rs Normal file
View File

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use super::Style;
#[derive(Deserialize, Serialize)]
pub struct Files {
pub hovered: Style,
}
#[derive(Deserialize, Serialize)]
pub struct Marker {
pub selected: Style,
pub copied: Style,
pub cut: Style,
}

View File

@ -1,11 +1,15 @@
mod color;
mod filetype;
mod icon;
mod list;
mod status;
mod style;
mod theme;
pub use color::*;
pub use filetype::*;
pub use icon::*;
pub use list::*;
pub use status::*;
pub use style::*;
pub use theme::*;

View File

@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};
use super::Style;
#[derive(Deserialize, Serialize)]
pub struct Status {
pub plain: Style,
pub fancy: Style,
pub separator: StatusSeparator,
// Mode
pub mode_normal: Style,
pub mode_select: Style,
pub mode_unset: Style,
// Progress
pub progress_label: Style,
pub progress_normal: Style,
pub progress_error: Style,
// Permissions
pub permissions_t: Style,
pub permissions_r: Style,
pub permissions_w: Style,
pub permissions_x: Style,
pub permissions_s: Style,
}
#[derive(Deserialize, Serialize)]
pub struct StatusSeparator {
pub opening: String,
pub closing: String,
}

View File

@ -1,40 +1,93 @@
use ratatui::style::{self, Modifier};
use serde::Deserialize;
use ratatui::style::Modifier;
use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
use super::Color;
#[derive(Deserialize)]
#[derive(Clone, Copy, Deserialize)]
#[serde(from = "StyleShadow")]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: Option<bool>,
pub underline: Option<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.0);
}
if let Some(bg) = &self.bg {
style = style.bg(bg.0);
}
if let Some(bold) = self.bold {
if bold {
style = style.add_modifier(Modifier::BOLD);
} else {
style = style.remove_modifier(Modifier::BOLD);
}
}
if let Some(underline) = self.underline {
if underline {
style = style.add_modifier(Modifier::UNDERLINED);
} else {
style = style.remove_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

@ -1,74 +1,36 @@
use std::path::PathBuf;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use shared::expand_path;
use validator::Validate;
use super::{ColorGroup, Filetype, Icon, Style};
use super::{Files, Filetype, Icon, Marker, Status, Style};
use crate::{validation::check_validation, MERGED_THEME};
#[derive(Deserialize, Validate)]
pub struct Tab {
#[derive(Deserialize, Serialize, Validate)]
pub struct Tabs {
pub active: Style,
pub inactive: Style,
#[validate(range(min = 1, message = "Must be greater than 0"))]
pub max_width: u8,
}
#[derive(Deserialize)]
pub struct Status {
pub primary: ColorGroup,
pub secondary: ColorGroup,
pub tertiary: ColorGroup,
pub body: ColorGroup,
pub emphasis: ColorGroup,
pub info: ColorGroup,
pub success: ColorGroup,
pub warning: ColorGroup,
pub danger: ColorGroup,
pub separator: StatusSeparator,
}
#[derive(Deserialize)]
pub struct StatusSeparator {
pub opening: String,
pub closing: String,
}
#[derive(Deserialize)]
pub struct Progress {
pub gauge: Style,
pub label: Style,
}
#[derive(Deserialize)]
pub struct Selection {
pub hovered: Style,
}
#[derive(Deserialize)]
pub struct Marker {
pub selecting: Style,
pub selected: Style,
}
#[derive(Deserialize)]
#[derive(Deserialize, Serialize)]
pub struct Preview {
pub hovered: Style,
pub syntect_theme: PathBuf,
}
#[derive(Deserialize)]
#[derive(Deserialize, Serialize)]
pub struct Theme {
pub tab: Tab,
pub tabs: Tabs,
pub status: Status,
pub progress: Progress,
pub selection: Selection,
pub files: Files,
pub marker: Marker,
pub preview: Preview,
#[serde(rename = "filetype", deserialize_with = "Filetype::deserialize")]
#[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)]
pub filetypes: Vec<Filetype>,
#[serde(deserialize_with = "Icon::deserialize")]
#[serde(deserialize_with = "Icon::deserialize", skip_serializing)]
pub icons: Vec<Icon>,
}
@ -76,7 +38,7 @@ impl Default for Theme {
fn default() -> Self {
let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap();
check_validation(theme.tab.validate());
check_validation(theme.tabs.validate());
theme.preview.syntect_theme = expand_path(&theme.preview.syntect_theme);

View File

@ -1,10 +1,10 @@
use core::{help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position};
use config::keymap::KeymapLayer;
use crossterm::terminal::WindowSize;
use ratatui::prelude::Rect;
use shared::Term;
use crate::{help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position};
pub struct Ctx {
pub manager: Manager,
pub which: Which,
@ -15,7 +15,7 @@ pub struct Ctx {
}
impl Ctx {
pub(super) fn new() -> Self {
pub fn make() -> Self {
Self {
manager: Manager::make(),
which: Default::default(),
@ -26,7 +26,7 @@ impl Ctx {
}
}
pub(super) fn area(&self, pos: &Position) -> Rect {
pub fn area(&self, pos: &Position) -> Rect {
let WindowSize { columns, rows, .. } = Term::size();
let (x, y) = match pos {
@ -57,7 +57,7 @@ impl Ctx {
}
#[inline]
pub(super) fn cursor(&self) -> Option<(u16, u16)> {
pub fn cursor(&self) -> Option<(u16, u16)> {
if self.input.visible {
let Rect { x, y, .. } = self.area(&self.input.position);
return Some((x + 1 + self.input.cursor(), y + 1));
@ -69,7 +69,7 @@ impl Ctx {
}
#[inline]
pub(super) fn layer(&self) -> KeymapLayer {
pub fn layer(&self) -> KeymapLayer {
if self.which.visible {
KeymapLayer::Which
} else if self.help.visible() {
@ -86,7 +86,7 @@ impl Ctx {
}
#[inline]
pub(super) fn image_layer(&self) -> bool {
pub fn image_layer(&self) -> bool {
!matches!(self.layer(), KeymapLayer::Which | KeymapLayer::Help | KeymapLayer::Tasks)
}
}

View File

@ -7,7 +7,7 @@ use shared::{InputError, RoCell, Url};
use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot};
use super::{files::{File, FilesOp}, input::InputOpt, select::SelectOpt};
use crate::manager::PreviewLock;
use crate::{manager::PreviewLock, tasks::TasksProgress};
static TX: RoCell<UnboundedSender<Event>> = RoCell::new();
@ -36,7 +36,7 @@ pub enum Event {
// Tasks
Open(Vec<(OsString, String)>, Option<Opener>),
Progress(u8, u32),
Progress(TasksProgress),
}
impl Event {
@ -115,8 +115,8 @@ macro_rules! emit {
(Open($targets:expr, $opener:expr)) => {
$crate::Event::Open($targets, $opener).emit();
};
(Progress($percent:expr, $tasks:expr)) => {
$crate::Event::Progress($percent, $tasks).emit();
(Progress($progress:expr)) => {
$crate::Event::Progress($progress).emit();
};
($event:ident) => {

View File

@ -75,7 +75,14 @@ impl File {
#[inline]
pub fn length(&self) -> u64 { self.length }
// --- Link to
// --- Link to / Is link
#[inline]
pub fn link_to(&self) -> Option<&Url> { self.link_to.as_ref() }
#[inline]
pub fn is_link(&self) -> bool { self.is_link }
// -- Is hidden
#[inline]
pub fn is_hidden(&self) -> bool { self.is_hidden }
}

View File

@ -7,6 +7,7 @@
)]
mod blocker;
mod context;
mod event;
pub mod external;
pub mod files;
@ -21,6 +22,7 @@ pub mod tasks;
pub mod which;
pub use blocker::*;
pub use context::*;
pub use event::*;
pub use highlighter::*;
pub use position::*;

View File

@ -1,4 +1,4 @@
use std::{collections::BTreeMap, ffi::OsStr};
use std::{collections::BTreeMap, ffi::OsStr, ops::Range};
use anyhow::Result;
use regex::bytes::{Regex, RegexBuilder};
@ -91,13 +91,17 @@ impl Finder {
/// Explode the name into three parts: head, body, tail.
#[inline]
pub fn explode(&self, name: &[u8]) -> Option<(String, String, String)> {
let range = self.query.find(name).map(|m| m.range())?;
Some((
String::from_utf8_lossy(&name[..range.start]).to_string(),
String::from_utf8_lossy(&name[range.start..range.end]).to_string(),
String::from_utf8_lossy(&name[range.end..]).to_string(),
))
pub fn highlighted(&self, name: &OsStr) -> Option<Vec<Range<usize>>> {
#[cfg(windows)]
let found = self.query.find(name.to_string_lossy().as_bytes()).map(|m| m.range());
#[cfg(unix)]
let found = {
use std::os::unix::ffi::OsStrExt;
self.query.find(name.as_bytes()).map(|m| m.range())
};
found.map(|r| vec![r])
}
}
@ -105,17 +109,11 @@ impl Finder {
#[inline]
pub fn matched(&self) -> &BTreeMap<Url, u8> { &self.matched }
#[inline]
pub fn has_matched(&self) -> bool { !self.matched.is_empty() }
#[inline]
pub fn matched_idx(&self, url: &Url) -> Option<u8> {
if let Some((_, &idx)) = self.matched.iter().find(|(u, _)| *u == url) {
return Some(idx);
}
if url.file_name().map(|n| self.matches(n)) == Some(true) {
return Some(100);
}
None
}
}

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

@ -85,7 +85,7 @@ impl Manager {
pub fn yank(&mut self, cut: bool) -> bool {
self.yanked.0 = cut;
self.yanked.1 = self.selected().into_iter().map(|f| f.url_owned()).collect();
false
true
}
pub fn quit(&self, tasks: &Tasks, no_cwd_file: bool) -> bool {

View File

@ -1,8 +1,6 @@
use std::{collections::BTreeSet, fmt::Display};
use std::collections::BTreeSet;
use config::theme::{self, ColorGroup};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum Mode {
#[default]
Normal,
@ -11,15 +9,6 @@ pub enum Mode {
}
impl Mode {
#[inline]
pub fn color<'a>(&self, group: &'a ColorGroup) -> &'a theme::Color {
match *self {
Mode::Normal => &group.normal,
Mode::Select(..) => &group.select,
Mode::Unset(..) => &group.unset,
}
}
#[inline]
pub fn visual(&self) -> Option<(usize, &BTreeSet<usize>)> {
match self {
@ -59,12 +48,13 @@ impl Mode {
pub fn is_visual(&self) -> bool { matches!(self, Mode::Select(..) | Mode::Unset(..)) }
}
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Mode::Normal => write!(f, "NORMAL"),
Mode::Select(..) => write!(f, "SELECT"),
Mode::Unset(..) => write!(f, "UN-SET"),
impl ToString for Mode {
fn to_string(&self) -> String {
match self {
Mode::Normal => "normal",
Mode::Select(..) => "select",
Mode::Unset(..) => "unset",
}
.to_string()
}
}

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

@ -10,9 +10,9 @@ use super::{Backstack, Finder, FinderCase, Folder, Mode, Preview, PreviewLock};
use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, FilesOp, FilesSorter}, input::InputOpt, Event, Step, BLOCKER};
pub struct Tab {
pub(super) mode: Mode,
pub(super) current: Folder,
pub(super) parent: Option<Folder>,
pub mode: Mode,
pub current: Folder,
pub parent: Option<Folder>,
pub(super) backstack: Backstack<Url>,
pub(super) history: BTreeMap<Url, Folder>,
@ -455,30 +455,13 @@ impl Tab {
impl Tab {
// --- Mode
#[inline]
pub fn mode(&self) -> &Mode { &self.mode }
#[inline]
pub fn in_selecting(&self) -> bool {
self.mode().is_visual() || self.current.files.has_selected()
}
pub fn in_selecting(&self) -> bool { self.mode.is_visual() || self.current.files.has_selected() }
// --- Current
#[inline]
pub fn name(&self) -> &str {
self
.current
.cwd
.file_name()
.and_then(|n| n.to_str())
.or_else(|| self.current.cwd.to_str())
.unwrap_or_default()
}
pub fn selected(&self) -> Vec<&File> {
let mode = self.mode();
let pending = mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default();
let pending = self.mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default();
let selected = self.current.files.selected(&pending, self.mode.is_unset());
let selected = self.current.files.selected(&pending, mode.is_unset());
if selected.is_empty() {
self.current.hovered.as_ref().map(|h| vec![h]).unwrap_or_default()
} else {

View File

@ -54,7 +54,7 @@ impl Running {
match task.stage {
TaskStage::Pending => return None,
TaskStage::Dispatched => {
if task.processed < task.found {
if task.succ < task.total {
return None;
}
if let Some(hook) = self.hooks.remove(&id) {

View File

@ -1,14 +1,12 @@
use std::{ffi::OsStr, sync::Arc, time::Duration};
use async_channel::{Receiver, Sender};
use config::{open::Opener, TASKS};
use futures::{future::BoxFuture, FutureExt};
use parking_lot::RwLock;
use shared::{unique_path, Throttle, Url};
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep};
use tracing::{info, trace};
use super::{workers::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage};
use super::{workers::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage, TasksProgress};
use crate::emit;
pub struct Scheduler {
@ -16,7 +14,8 @@ pub struct Scheduler {
precache: Arc<Precache>,
process: Arc<Process>,
todo: Sender<BoxFuture<'static, ()>>,
todo: async_channel::Sender<BoxFuture<'static, ()>>,
prog: mpsc::UnboundedSender<TaskOp>,
pub(super) running: Arc<RwLock<Running>>,
}
@ -28,9 +27,10 @@ impl Scheduler {
let scheduler = Self {
file: Arc::new(File::new(prog_tx.clone())),
precache: Arc::new(Precache::new(prog_tx.clone())),
process: Arc::new(Process::new(prog_tx)),
process: Arc::new(Process::new(prog_tx.clone())),
todo: todo_tx,
prog: prog_tx,
running: Default::default(),
};
@ -44,7 +44,7 @@ impl Scheduler {
scheduler
}
fn schedule_micro(&self, rx: Receiver<BoxFuture<'static, ()>>) {
fn schedule_micro(&self, rx: async_channel::Receiver<BoxFuture<'static, ()>>) {
tokio::spawn(async move {
loop {
if let Ok(fut) = rx.recv().await {
@ -54,9 +54,11 @@ impl Scheduler {
});
}
fn schedule_macro(&self, rx: Receiver<BoxFuture<'static, ()>>) {
fn schedule_macro(&self, rx: async_channel::Receiver<BoxFuture<'static, ()>>) {
let file = self.file.clone();
let precache = self.precache.clone();
let prog = self.prog.clone();
let running = self.running.clone();
tokio::spawn(async move {
@ -72,20 +74,18 @@ impl Scheduler {
}
Ok((id, mut op)) = file.recv() => {
if !running.read().exists(id) {
trace!("Skipping task {:?} as it was removed", op);
continue;
}
if let Err(e) = file.work(&mut op).await {
info!("Failed to work on task {:?}: {e}", op);
prog.send(TaskOp::Fail(id, format!("Failed to work on this task: {:?}", e))).ok();
}
}
Ok((id, mut op)) = precache.recv() => {
if !running.read().exists(id) {
trace!("Skipping task {:?} as it was removed", op);
continue;
}
if let Err(e) = precache.work(&mut op).await {
info!("Failed to work on task {:?}: {e}", op);
prog.send(TaskOp::Fail(id, format!("Failed to work on this task: {:?}", e))).ok();
}
}
}
@ -102,8 +102,36 @@ impl Scheduler {
match op {
TaskOp::New(id, size) => {
if let Some(task) = running.write().get_mut(id) {
task.found += 1;
task.todo += size;
task.total += 1;
task.found += size;
}
}
TaskOp::Adv(id, succ, processed) => {
let mut running = running.write();
if let Some(task) = running.get_mut(id) {
task.succ += succ;
task.processed += processed;
}
if succ > 0 {
if let Some(fut) = running.try_remove(id, TaskStage::Pending) {
todo.send_blocking(fut).ok();
}
}
}
TaskOp::Succ(id) => {
if let Some(fut) = running.write().try_remove(id, TaskStage::Dispatched) {
todo.send_blocking(fut).ok();
}
}
TaskOp::Fail(id, reason) => {
if let Some(task) = running.write().get_mut(id) {
task.fail += 1;
task.logs.push_str(&reason);
task.logs.push('\n');
if let Some(logger) = &task.logger {
logger.send(reason).ok();
}
}
}
TaskOp::Log(id, line) => {
@ -116,62 +144,20 @@ impl Scheduler {
}
}
}
TaskOp::Adv(id, processed, size) => {
let mut running = running.write();
if let Some(task) = running.get_mut(id) {
task.processed += processed;
task.done += size;
}
if processed > 0 {
if let Some(fut) = running.try_remove(id, TaskStage::Pending) {
todo.send_blocking(fut).ok();
}
}
}
TaskOp::Done(id) => {
if let Some(fut) = running.write().try_remove(id, TaskStage::Dispatched) {
todo.send_blocking(fut).ok();
}
}
}
}
});
let running = self.running.clone();
let mut last = (100, 0);
tokio::spawn(async move {
let mut last = TasksProgress::default();
loop {
sleep(Duration::from_secs(1)).await;
if running.read().is_empty() {
if last != (100, 0) {
last = (100, 0);
emit!(Progress(last.0, last.1));
}
continue;
}
sleep(Duration::from_millis(500)).await;
let mut tasks = 0u32;
let mut left = 0;
let mut progress = (0, 0);
for task in running.read().values() {
tasks += 1;
left += task.found.saturating_sub(task.processed);
progress = (progress.0 + task.done, progress.1 + task.todo);
}
let mut percent = match progress.1 {
0 => 100u8,
_ => 100.min(progress.0 * 100 / progress.1) as u8,
};
if tasks != 0 {
percent = percent.min(99);
left = left.max(1);
}
if last != (percent, left) {
last = (percent, left);
emit!(Progress(last.0, last.1));
let new = TasksProgress::from(&*running.read());
if last != new {
last = new;
emit!(Progress(new));
}
}
});

View File

@ -1,49 +1,36 @@
use tokio::sync::mpsc;
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct Task {
pub id: usize,
pub name: String,
pub stage: TaskStage,
pub found: u32,
pub processed: u32,
pub total: u32,
pub succ: u32,
pub fail: u32,
pub todo: u64,
pub done: u64,
pub found: u64,
pub processed: u64,
pub logs: String,
pub logger: Option<mpsc::UnboundedSender<String>>,
}
impl Task {
pub fn new(id: usize, name: String) -> Self { Self { id, name, ..Default::default() } }
}
#[derive(Debug)]
pub struct TaskSummary {
pub name: String,
pub found: u32,
pub processed: u32,
pub total: u32,
pub succ: u32,
pub fail: u32,
pub todo: u64,
pub done: u64,
}
impl Task {
pub fn new(id: usize, name: String) -> Self {
Self {
id,
name,
stage: Default::default(),
found: 0,
processed: 0,
todo: 0,
done: 0,
logs: Default::default(),
logger: Default::default(),
}
}
pub found: u64,
pub processed: u64,
}
impl From<&Task> for TaskSummary {
@ -51,25 +38,28 @@ impl From<&Task> for TaskSummary {
TaskSummary {
name: task.name.clone(),
total: task.total,
succ: task.succ,
fail: task.fail,
found: task.found,
processed: task.processed,
todo: task.todo,
done: task.done,
}
}
}
#[derive(Debug)]
pub enum TaskOp {
// task_id, size
// id, size
New(usize, u64),
// task_id, line
Log(usize, String),
// task_id, processed, size
// id, processed, size
Adv(usize, u32, u64),
// task_id
Done(usize),
// id
Succ(usize),
// id
Fail(usize, String),
// id, line
Log(usize, String),
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]

View File

@ -2,11 +2,12 @@ use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, io::{stdout, Wr
use config::{manager::SortBy, open::Opener, OPEN};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use serde::Serialize;
use shared::{Defer, MimeKind, Term, Url};
use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time};
use tracing::trace;
use super::{task::TaskSummary, Scheduler, TASKS_PADDING, TASKS_PERCENT};
use super::{running::Running, task::TaskSummary, Scheduler, TASKS_PADDING, TASKS_PERCENT};
use crate::{emit, files::{File, Files}, input::InputOpt, Event, BLOCKER};
pub struct Tasks {
@ -14,7 +15,7 @@ pub struct Tasks {
pub visible: bool,
pub cursor: usize,
pub progress: (u8, u32),
pub progress: TasksProgress,
}
impl Tasks {
@ -23,7 +24,7 @@ impl Tasks {
scheduler: Arc::new(Scheduler::start()),
visible: false,
cursor: 0,
progress: (100, 0),
progress: Default::default(),
}
}
@ -305,3 +306,32 @@ impl Tasks {
#[inline]
pub fn len(&self) -> usize { self.scheduler.running.read().len() }
}
#[derive(Clone, Copy, Default, Eq, PartialEq, Serialize)]
pub struct TasksProgress {
pub total: u32,
pub succ: u32,
pub fail: u32,
pub found: u64,
pub processed: u64,
}
impl From<&Running> for TasksProgress {
fn from(running: &Running) -> Self {
let mut progress = Self::default();
if running.is_empty() {
return progress;
}
for task in running.values() {
progress.total += task.total;
progress.succ += task.succ;
progress.fail += task.fail;
progress.found += task.found;
progress.processed += task.processed;
}
progress
}
}

View File

@ -10,8 +10,8 @@ use tracing::trace;
use crate::tasks::TaskOp;
pub(crate) struct File {
rx: async_channel::Receiver<FileOp>,
tx: async_channel::Sender<FileOp>,
rx: async_channel::Receiver<FileOp>,
sch: mpsc::UnboundedSender<TaskOp>,
}
@ -92,10 +92,7 @@ impl File {
}
break;
}
Ok(n) => {
self.log(task.id, format!("Paste task advanced {n}: {:?}", task))?;
self.sch.send(TaskOp::Adv(task.id, 0, n))?
}
Ok(n) => self.sch.send(TaskOp::Adv(task.id, 0, n))?,
Err(e) if e.kind() == NotFound => {
trace!("Paste task partially done: {:?}", task);
break;
@ -163,7 +160,7 @@ impl File {
FileOp::Delete(task) => {
if let Err(e) = fs::remove_file(&task.target).await {
if e.kind() != NotFound && fs::symlink_metadata(&task.target).await.is_ok() {
self.log(task.id, format!("Delete task failed: {:?}, {e}", task))?;
self.fail(task.id, format!("Delete task failed: {:?}, {e}", task))?;
Err(e)?
}
}
@ -187,17 +184,11 @@ impl File {
Ok(())
}
#[inline]
fn log(&self, id: usize, line: String) -> Result<()> { Ok(self.sch.send(TaskOp::Log(id, line))?) }
#[inline]
fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) }
pub(crate) async fn paste(&self, mut task: FileOpPaste) -> Result<()> {
if task.cut {
match fs::rename(&task.from, &task.to).await {
Ok(_) => return self.done(task.id),
Err(e) if e.kind() == NotFound => return self.done(task.id),
Ok(_) => return self.succ(task.id),
Err(e) if e.kind() == NotFound => return self.succ(task.id),
_ => {}
}
}
@ -212,7 +203,20 @@ impl File {
} else if meta.is_symlink() {
self.tx.send(FileOp::Link(task.to_link(meta))).await?;
}
return self.done(id);
return self.succ(id);
}
macro_rules! continue_unless_ok {
($result:expr) => {
match $result {
Ok(v) => v,
Err(e) => {
self.sch.send(TaskOp::New(task.id, 0))?;
self.fail(task.id, format!("An error occurred while pasting: {e}"))?;
continue;
}
}
};
}
let root = task.to.clone();
@ -221,27 +225,15 @@ impl File {
while let Some(src) = dirs.pop_front() {
let dest = root.join(src.components().skip(skip).collect::<PathBuf>());
match fs::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => {
self.log(task.id, format!("Create dir failed: {dest:?}, {e}"))?;
continue;
}
_ => {}
}
let mut it = match fs::read_dir(&src).await {
Ok(it) => it,
Err(e) => {
self.log(task.id, format!("Read dir failed: {src:?}, {e}"))?;
continue;
}
};
continue_unless_ok!(match fs::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => Err(e),
_ => Ok(()),
});
let mut it = continue_unless_ok!(fs::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().await {
let src = Url::from(entry.path());
let Ok(meta) = Self::metadata(&src, task.follow).await else {
continue;
};
let meta = continue_unless_ok!(Self::metadata(&src, task.follow).await);
if meta.is_dir() {
dirs.push_back(src);
@ -259,7 +251,7 @@ impl File {
}
}
}
self.done(task.id)
self.succ(task.id)
}
pub(crate) async fn link(&self, mut task: FileOpLink) -> Result<()> {
@ -270,7 +262,7 @@ impl File {
self.sch.send(TaskOp::New(id, task.meta.as_ref().unwrap().len()))?;
self.tx.send(FileOp::Link(task)).await?;
self.done(id)
self.succ(id)
}
pub(crate) async fn delete(&self, mut task: FileOpDelete) -> Result<()> {
@ -280,7 +272,7 @@ impl File {
task.length = meta.len();
self.sch.send(TaskOp::New(id, meta.len()))?;
self.tx.send(FileOp::Delete(task)).await?;
return self.done(id);
return self.succ(id);
}
let mut dirs = VecDeque::from([task.target]);
@ -307,7 +299,7 @@ impl File {
self.tx.send(FileOp::Delete(task.clone())).await?;
}
}
self.done(task.id)
self.succ(task.id)
}
pub(crate) async fn trash(&self, mut task: FileOpTrash) -> Result<()> {
@ -316,7 +308,7 @@ impl File {
self.sch.send(TaskOp::New(id, task.length))?;
self.tx.send(FileOp::Trash(task)).await?;
self.done(id)
self.succ(id)
}
async fn metadata(path: &Path, follow: bool) -> io::Result<Metadata> {
@ -349,6 +341,19 @@ impl File {
}
}
impl File {
#[inline]
fn succ(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Succ(id))?) }
#[inline]
fn fail(&self, id: usize, reason: String) -> Result<()> {
Ok(self.sch.send(TaskOp::Fail(id, reason))?)
}
#[inline]
fn log(&self, id: usize, line: String) -> Result<()> { Ok(self.sch.send(TaskOp::Log(id, line))?) }
}
impl FileOpPaste {
fn to_link(&self, meta: Metadata) -> FileOpLink {
FileOpLink {

View File

@ -10,8 +10,8 @@ use tokio::{fs, sync::mpsc};
use crate::{emit, external, files::FilesOp, tasks::TaskOp};
pub(crate) struct Precache {
rx: async_channel::Receiver<PrecacheOp>,
tx: async_channel::Sender<PrecacheOp>,
rx: async_channel::Receiver<PrecacheOp>,
sch: mpsc::UnboundedSender<TaskOp>,
@ -105,9 +105,6 @@ impl Precache {
Ok(())
}
#[inline]
fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) }
pub(crate) async fn mime(&self, task: PrecacheOpMime) -> Result<()> {
self.sch.send(TaskOp::New(task.id, 0))?;
if let Ok(mimes) = external::file(&task.targets).await {
@ -115,7 +112,7 @@ impl Precache {
}
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
self.done(task.id)
self.succ(task.id)
}
pub(crate) async fn size(&self, task: PrecacheOpSize) -> Result<()> {
@ -133,7 +130,7 @@ impl Precache {
});
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
self.done(task.id)
self.succ(task.id)
}
pub(crate) fn image(&self, id: usize, targets: Vec<Url>) -> Result<()> {
@ -141,7 +138,7 @@ impl Precache {
self.sch.send(TaskOp::New(id, 0))?;
self.tx.send_blocking(PrecacheOp::Image(PrecacheOpImage { id, target }))?;
}
self.done(id)
self.succ(id)
}
pub(crate) fn video(&self, id: usize, targets: Vec<Url>) -> Result<()> {
@ -149,7 +146,7 @@ impl Precache {
self.sch.send(TaskOp::New(id, 0))?;
self.tx.send_blocking(PrecacheOp::Video(PrecacheOpVideo { id, target }))?;
}
self.done(id)
self.succ(id)
}
pub(crate) fn pdf(&self, id: usize, targets: Vec<Url>) -> Result<()> {
@ -157,6 +154,11 @@ impl Precache {
self.sch.send(TaskOp::New(id, 0))?;
self.tx.send_blocking(PrecacheOp::Pdf(PrecacheOpPDF { id, target }))?;
}
self.done(id)
self.succ(id)
}
}
impl Precache {
#[inline]
fn succ(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Succ(id))?) }
}

View File

@ -33,12 +33,6 @@ impl From<&mut ProcessOpOpen> for ShellOpt {
impl Process {
pub(crate) fn new(sch: mpsc::UnboundedSender<TaskOp>) -> Self { Self { sch } }
#[inline]
fn log(&self, id: usize, line: String) -> Result<()> { Ok(self.sch.send(TaskOp::Log(id, line))?) }
#[inline]
fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) }
pub(crate) async fn open(&self, mut task: ProcessOpOpen) -> Result<()> {
let opt = ShellOpt::from(&mut task);
if task.block {
@ -48,11 +42,11 @@ impl Process {
match external::shell(opt) {
Ok(mut child) => {
child.wait().await.ok();
self.done(task.id)?;
self.succ(task.id)?;
}
Err(e) => {
self.sch.send(TaskOp::New(task.id, 0))?;
self.log(task.id, format!("Failed to spawn process: {e}"))?;
self.fail(task.id, format!("Failed to spawn process: {e}"))?;
}
}
return Ok(emit!(Stop(false)).await);
@ -60,10 +54,10 @@ impl Process {
if task.orphan {
match external::shell(opt) {
Ok(_) => self.done(task.id)?,
Ok(_) => self.succ(task.id)?,
Err(e) => {
self.sch.send(TaskOp::New(task.id, 0))?;
self.log(task.id, format!("Failed to spawn process: {e}"))?;
self.fail(task.id, format!("Failed to spawn process: {e}"))?;
}
}
return Ok(());
@ -92,7 +86,7 @@ impl Process {
None => "Process terminated by signal".to_string(),
})?;
if !status.success() {
return Ok(());
return self.fail(task.id, "Process failed".to_string());
}
break;
}
@ -100,6 +94,19 @@ impl Process {
}
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
self.done(task.id)
self.succ(task.id)
}
}
impl Process {
#[inline]
fn succ(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Succ(id))?) }
#[inline]
fn fail(&self, id: usize, reason: String) -> Result<()> {
Ok(self.sch.send(TaskOp::Fail(id, reason))?)
}
#[inline]
fn log(&self, id: usize, line: String) -> Result<()> { Ok(self.sch.send(TaskOp::Log(id, line))?) }
}

View File

@ -1 +1 @@
{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","backstack","natsort","natsort","USERPROFILE"],"language":"en","version":"0.2"}
{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ"],"version":"0.2"}

16
plugin/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "plugin"
version = "0.1.0"
edition = "2021"
[dependencies]
config = { path = "../config" }
core = { path = "../core" }
shared = { path = "../shared" }
# External dependencies
anyhow = "^1"
mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize" ] }
ratatui = "^0"
tracing = "^0"
unicode-width = "^0"

View File

@ -0,0 +1,163 @@
Folder = {
Kind = {
Parent = 0,
Current = 1,
Preview = 2,
},
}
function Folder:by_kind(kind)
if kind == self.Kind.Parent then
return cx.active.parent
elseif kind == self.Kind.Current then
return cx.active.current
elseif kind == self.Kind.Preview then
return cx.active.preview.folder
end
end
function Folder:markers(area, markers)
if #markers == 0 then
return {}
end
local elements = {}
local append = function(last)
local p = ui.Paragraph(
ui.Rect {
x = area.x - 1,
y = area.y + last[1] - 1,
w = 1,
h = 1 + last[2] - last[1],
},
{}
)
if last[3] == 1 then
p = p:style(THEME.marker.copied)
elseif last[3] == 2 then
p = p:style(THEME.marker.cut)
elseif last[3] == 3 then
p = p:style(THEME.marker.selected)
end
elements[#elements + 1] = p
end
local last = { markers[1][1], markers[1][1], markers[1][2] } -- start, end, type
for _, m in ipairs(markers) do
if m[1] - last[2] > 1 or last[3] ~= m[2] then
append(last)
last = { m[1], m[1], m[2] }
else
last[2] = m[1]
end
end
append(last)
return elements
end
function Folder:highlighted_name(file)
-- Complete prefix when searching across directories
local prefix = file:prefix() or ""
if prefix ~= "" then
prefix = prefix .. "/"
end
-- Range highlighting for filenames
local highlights = file:highlights()
local spans = ui.highlight_ranges(prefix .. file.name, highlights)
-- Show symlink target
if MANAGER.show_symlink and file.link_to ~= nil then
spans[#spans + 1] = ui.Span(" -> " .. tostring(file.link_to)):italic()
end
if highlights == nil or not file:is_hovered() then
return spans
end
local found = file:found()
if found ~= nil then
spans[#spans + 1] = ui.Span(string.format(" [%d/%d]", found[1] + 1, found[2])):fg("#ffff32"):italic()
end
return spans
end
function Folder:parent(area)
local folder = self:by_kind(self.Kind.Parent)
if folder == nil then
return {}
end
local items = {}
for _, f in ipairs(folder.window) do
local item = ui.ListItem(" " .. f:icon() .. " " .. f.name .. " ")
if f:is_hovered() then
item = item:style(THEME.files.hovered)
else
item = item:style(f:style())
end
items[#items + 1] = item
end
return { ui.List(area, items) }
end
function Folder:current(area)
local markers = {}
local items = {}
for i, f in ipairs(self:by_kind(self.Kind.Current).window) do
local name = self:highlighted_name(f)
-- Highlight hovered file
local item = ui.ListItem(ui.Line { ui.Span(" " .. f:icon() .. " "), table.unpack(name) })
if f:is_hovered() then
item = item:style(THEME.files.hovered)
else
item = item:style(f:style())
end
items[#items + 1] = item
-- Mark yanked/selected files
local yanked = f:is_yanked()
if yanked ~= 0 then
markers[#markers + 1] = { i, yanked }
elseif f:is_selected() then
markers[#markers + 1] = { i, 3 }
end
end
return { ui.List(area, items), table.unpack(self:markers(area, markers)) }
end
function Folder:preview(area)
local folder = self:by_kind(self.Kind.Preview)
if folder == nil then
return {}
end
local items = {}
for _, f in ipairs(folder.window) do
local item = ui.ListItem(" " .. f:icon() .. " " .. f.name .. " ")
if f:is_hovered() then
item = item:style(THEME.preview.hovered)
else
item = item:style(f:style())
end
items[#items + 1] = item
end
return { ui.List(area, items) }
end
function Folder:render(area, args)
if args.kind == self.Kind.Parent then
return self:parent(area)
elseif args.kind == self.Kind.Current then
return self:current(area)
elseif args.kind == self.Kind.Preview then
return self:preview(area)
end
end

View File

@ -0,0 +1,43 @@
Header = {}
function Header:cwd()
local cwd = cx.active.current.cwd
local span
if not cwd.is_search then
span = ui.Span(utils.readable_path(tostring(cwd)))
else
span = ui.Span(string.format("%s (search: %s)", utils.readable_path(tostring(cwd)), cwd.frag))
end
return span:fg("cyan")
end
function Header:tabs()
local spans = {}
for i = 1, #cx.tabs do
local text = i
if THEME.tabs.max_width > 2 then
text = utils.truncate(text .. " " .. cx.tabs[i]:name(), THEME.tabs.max_width)
end
if i == cx.tabs.idx + 1 then
spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.tabs.active)
else
spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.tabs.inactive)
end
end
return ui.Line(spans)
end
function Header:render(area)
local chunks = ui.Layout()
:direction(ui.Direction.HORIZONTAL)
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
:split(area)
local left = ui.Line { self:cwd() }
local right = ui.Line { self:tabs() }
return {
ui.Paragraph(chunks[1], { left }),
ui.Paragraph(chunks[2], { right }):align(ui.Alignment.RIGHT),
}
end

View File

@ -0,0 +1,150 @@
Status = {}
function Status.style()
if cx.active.mode.is_select then
return THEME.status.mode_select
elseif cx.active.mode.is_unset then
return THEME.status.mode_unset
else
return THEME.status.mode_normal
end
end
function Status:mode()
local mode = tostring(cx.active.mode):upper()
if mode == "UNSET" then
mode = "UN-SET"
end
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()
local h = cx.active.current.hovered
if h == nil then
return ui.Span("")
end
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()
local h = cx.active.current.hovered
if h == nil then
return ui.Span("")
end
return ui.Span(" " .. h.name)
end
function Status:permissions()
local h = cx.active.current.hovered
if h == nil or h.permissions == nil then
return ui.Span("")
end
local spans = {}
for i = 1, #h.permissions do
local c = h.permissions:sub(i, i)
local style = THEME.status.permissions_t
if c == "-" then
style = THEME.status.permissions_s
elseif c == "r" then
style = THEME.status.permissions_r
elseif c == "w" then
style = THEME.status.permissions_w
elseif c == "x" or c == "s" or c == "S" or c == "t" or c == "T" then
style = THEME.status.permissions_x
end
spans[i] = ui.Span(c):style(style)
end
return ui.Line(spans)
end
function Status:percentage()
local percent = 0
local cursor = cx.active.current.cursor
local length = #cx.active.current.files
if cursor ~= 0 and length ~= 0 then
percent = math.floor((cursor + 1) * 100 / length)
end
if percent == 0 then
percent = " Top "
else
percent = string.format(" %3d%% ", percent)
end
local style = self.style()
return ui.Line {
ui.Span(" " .. THEME.status.separator.opening):fg(THEME.status.fancy.bg),
ui.Span(percent):fg(style.bg):bg(THEME.status.fancy.bg),
}
end
function Status:position()
local cursor = cx.active.current.cursor
local length = #cx.active.current.files
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:progress(area, offset)
local progress = cx.tasks.progress
local left = progress.total - progress.succ
if left == 0 then
return {}
end
local gauge = ui.Gauge(ui.Rect {
x = area.x + math.max(0, area.w - offset - 21),
y = area.y,
w = math.min(20, area.w),
h = 1,
})
if progress.fail == 0 then
gauge = gauge:gauge_style(THEME.status.progress_normal)
else
gauge = gauge:gauge_style(THEME.status.progress_error)
end
local percent = 99
if progress.found ~= 0 then
percent = math.min(99, progress.processed * 100 / progress.found)
end
return {
gauge
:percent(percent)
:label(ui.Span(string.format("%3d%%, %d left", percent, left)):style(THEME.status.progress_label)),
}
end
function Status:render(area)
local chunks = ui.Layout()
:direction(ui.Direction.HORIZONTAL)
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
:split(area)
local left = ui.Line { self:mode(), self:size(), self:name() }
local right = ui.Line { self:permissions(), self:percentage(), self:position() }
local progress = self:progress(chunks[2], right:width())
return {
ui.Paragraph(chunks[1], { left }),
ui.Paragraph(chunks[2], { right }):align(ui.Alignment.RIGHT),
table.unpack(progress),
}
end

View File

@ -0,0 +1,22 @@
MIT LICENSE
Copyright (c) 2022 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,312 @@
local inspect = { Options = {} }
inspect.KEY = setmetatable({}, { __tostring = function() return "inspect.KEY" end })
inspect.METATABLE = setmetatable({}, { __tostring = function() return "inspect.METATABLE" end })
local rep = string.rep
local match = string.match
local char = string.char
local gsub = string.gsub
local fmt = string.format
local function rawpairs(t) return next, t, nil end
local function smartQuote(str)
if match(str, '"') and not match(str, "'") then
return "'" .. str .. "'"
end
return '"' .. gsub(str, '"', '\\"') .. '"'
end
local shortControlCharEscapes = {
["\a"] = "\\a",
["\b"] = "\\b",
["\f"] = "\\f",
["\n"] = "\\n",
["\r"] = "\\r",
["\t"] = "\\t",
["\v"] = "\\v",
["\127"] = "\\127",
}
local longControlCharEscapes = { ["\127"] = "\127" }
for i = 0, 31 do
local ch = char(i)
if not shortControlCharEscapes[ch] then
shortControlCharEscapes[ch] = "\\" .. i
longControlCharEscapes[ch] = fmt("\\%03d", i)
end
end
local function escape(str)
return (gsub(gsub(gsub(str, "\\", "\\\\"), "(%c)%f[0-9]", longControlCharEscapes), "%c", shortControlCharEscapes))
end
local luaKeywords = {
["and"] = true,
["break"] = true,
["do"] = true,
["else"] = true,
["elseif"] = true,
["end"] = true,
["false"] = true,
["for"] = true,
["function"] = true,
["goto"] = true,
["if"] = true,
["in"] = true,
["local"] = true,
["nil"] = true,
["not"] = true,
["or"] = true,
["repeat"] = true,
["return"] = true,
["then"] = true,
["true"] = true,
["until"] = true,
["while"] = true,
}
local function isIdentifier(str)
return type(str) == "string" and not not str:match("^[_%a][_%a%d]*$") and not luaKeywords[str]
end
local flr = math.floor
local function isSequenceKey(k, sequenceLength)
return type(k) == "number" and flr(k) == k and 1 <= k and k <= sequenceLength
end
local defaultTypeOrders = {
["number"] = 1,
["boolean"] = 2,
["string"] = 3,
["table"] = 4,
["function"] = 5,
["userdata"] = 6,
["thread"] = 7,
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
if ta == tb and (ta == "string" or ta == "number") then
return a < b
end
local dta = defaultTypeOrders[ta] or 100
local dtb = defaultTypeOrders[tb] or 100
return dta == dtb and ta < tb or dta < dtb
end
local function getKeys(t)
local seqLen = 1
while t[seqLen] ~= nil do
seqLen = seqLen + 1
end
seqLen = seqLen - 1
local keys, keysLen = {}, 0
for k in rawpairs(t) do
if not isSequenceKey(k, seqLen) then
keysLen = keysLen + 1
keys[keysLen] = k
end
end
table.sort(keys, sortKeys)
return keys, keysLen, seqLen
end
local function countCycles(x, cycles)
if type(x) == "table" then
if cycles[x] then
cycles[x] = cycles[x] + 1
else
cycles[x] = 1
for k, v in rawpairs(x) do
countCycles(k, cycles)
countCycles(v, cycles)
end
countCycles(getmetatable(x), cycles)
end
end
end
local function makePath(path, a, b)
local newPath = {}
local len = #path
for i = 1, len do
newPath[i] = path[i]
end
newPath[len + 1] = a
newPath[len + 2] = b
return newPath
end
local function processRecursive(process, item, path, visited)
if item == nil then
return nil
end
if visited[item] then
return visited[item]
end
local processed = process(item, path)
if type(processed) == "table" then
local processedCopy = {}
visited[item] = processedCopy
local processedKey
for k, v in rawpairs(processed) do
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
if processedKey ~= nil then
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
end
end
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
if type(mt) ~= "table" then
mt = nil
end
setmetatable(processedCopy, mt)
processed = processedCopy
end
return processed
end
local function puts(buf, str)
buf.n = buf.n + 1
buf[buf.n] = str
end
local Inspector = {}
local Inspector_mt = { __index = Inspector }
local function tabify(inspector) puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) end
function Inspector:getId(v)
local id = self.ids[v]
local ids = self.ids
if not id then
local tv = type(v)
id = (ids[tv] or 0) + 1
ids[v], ids[tv] = id, id
end
return tostring(id)
end
function Inspector:putValue(v)
local buf = self.buf
local tv = type(v)
if tv == "string" then
puts(buf, smartQuote(escape(v)))
elseif tv == "number" or tv == "boolean" or tv == "nil" or tv == "cdata" or tv == "ctype" then
puts(buf, tostring(v))
elseif tv == "table" and not self.ids[v] then
local t = v
if t == inspect.KEY or t == inspect.METATABLE then
puts(buf, tostring(t))
elseif self.level >= self.depth then
puts(buf, "{...}")
else
if self.cycles[t] > 1 then
puts(buf, fmt("<%d>", self:getId(t)))
end
local keys, keysLen, seqLen = getKeys(t)
puts(buf, "{")
self.level = self.level + 1
for i = 1, seqLen + keysLen do
if i > 1 then
puts(buf, ",")
end
if i <= seqLen then
puts(buf, " ")
self:putValue(t[i])
else
local k = keys[i - seqLen]
tabify(self)
if isIdentifier(k) then
puts(buf, k)
else
puts(buf, "[")
self:putValue(k)
puts(buf, "]")
end
puts(buf, " = ")
self:putValue(t[k])
end
end
local mt = getmetatable(t)
if type(mt) == "table" then
if seqLen + keysLen > 0 then
puts(buf, ",")
end
tabify(self)
puts(buf, "<metatable> = ")
self:putValue(mt)
end
self.level = self.level - 1
if keysLen > 0 or type(mt) == "table" then
tabify(self)
elseif seqLen > 0 then
puts(buf, " ")
end
puts(buf, "}")
end
else
puts(buf, fmt("<%s %d>", tv, self:getId(v)))
end
end
function inspect.inspect(root, options)
options = options or {}
local depth = options.depth or math.huge
local newline = options.newline or "\n"
local indent = options.indent or " "
local process = options.process
if process then
root = processRecursive(process, root, {}, {})
end
local cycles = {}
countCycles(root, cycles)
local inspector = setmetatable({
buf = { n = 0 },
ids = {},
cycles = cycles,
depth = depth,
level = 0,
newline = newline,
indent = indent,
}, Inspector_mt)
inspector:putValue(root)
return table.concat(inspector.buf)
end
setmetatable(inspect, {
__call = function(_, root, options) return inspect.inspect(root, options) end,
})
yazi = yazi or {}
yazi.inspect = inspect
yazi.print = function(...)
local args = { ... }
for i = 1, #args do
print(inspect(args[i]))
end
end

32
plugin/preset/ui.lua Normal file
View File

@ -0,0 +1,32 @@
ui = {
Alignment = {
LEFT = 0,
CENTER = 1,
RIGHT = 2,
},
Direction = {
HORIZONTAL = false,
VERTICAL = true,
},
}
function ui.highlight_ranges(s, ranges)
if ranges == nil or #ranges == 0 then
return { ui.Span(s) }
end
local spans = {}
local last = 0
for _, r in ipairs(ranges) do
if r[1] > last then
spans[#spans + 1] = ui.Span(s:sub(last + 1, r[1]))
end
-- TODO: use a customable style
spans[#spans + 1] = ui.Span(s:sub(r[1] + 1, r[2])):fg("yellow"):italic()
last = r[2]
end
if last < #s then
spans[#spans + 1] = ui.Span(s:sub(last + 1))
end
return spans
end

24
plugin/preset/utils.lua Normal file
View File

@ -0,0 +1,24 @@
utils = utils or {}
function utils.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end
function utils.readable_size(size)
local units = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }
local i = 1
while size > 1024.0 and i < #units do
size = size / 1024.0
i = i + 1
end
return string.format("%.1f %s", size, units[i])
end
function utils.readable_path(path)
local home = os.getenv("HOME")
if home == nil then
return path
elseif string.sub(path, 1, #home) == home then
return "~" .. string.sub(path, #home + 1)
else
return path
end
end

View File

@ -0,0 +1,124 @@
use core::Ctx;
use config::MANAGER;
use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value};
use super::Url;
use crate::LUA;
pub struct Active<'a, 'b> {
scope: &'b mlua::Scope<'a, 'a>,
cx: &'a core::Ctx,
inner: &'a core::manager::Tab,
}
impl<'a, 'b> Active<'a, 'b> {
pub(crate) fn init() -> mlua::Result<()> {
LUA.register_userdata_type::<core::manager::Mode>(|reg| {
reg.add_field_method_get("is_select", |_, me| Ok(me.is_select()));
reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset()));
reg.add_field_method_get("is_visual", |_, me| Ok(me.is_visual()));
reg.add_method("pending", |_, me, (idx, state): (usize, bool)| Ok(me.pending(idx, state)));
reg.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string()));
})?;
LUA.register_userdata_type::<core::manager::Folder>(|reg| {
reg.add_field_method_get("cwd", |_, me| Ok(Url::from(&me.cwd)));
reg.add_field_method_get("offset", |_, me| Ok(me.offset()));
reg.add_field_method_get("cursor", |_, me| Ok(me.cursor()));
reg.add_field_function_get("window", |_, me| me.named_user_value::<Value>("window"));
reg.add_field_function_get("files", |_, me| me.named_user_value::<AnyUserData>("files"));
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<Value>("hovered"));
})?;
LUA.register_userdata_type::<core::manager::Preview>(|reg| {
reg.add_field_function_get("folder", |_, me| me.named_user_value::<Value>("folder"));
})?;
Ok(())
}
pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, cx: &'a Ctx) -> Self {
Self { scope, cx, inner: cx.manager.active() }
}
pub(crate) fn make(&self) -> mlua::Result<AnyUserData<'a>> {
let ud = self.scope.create_any_userdata_ref(self.inner)?;
ud.set_named_user_value("mode", self.scope.create_any_userdata_ref(&self.inner.mode)?)?;
ud.set_named_user_value(
"parent",
self.inner.parent.as_ref().and_then(|p| self.folder(p, None).ok()),
)?;
ud.set_named_user_value("current", self.folder(&self.inner.current, None)?)?;
ud.set_named_user_value("preview", self.preview(self.inner)?)?;
Ok(ud)
}
pub(crate) fn folder(
&self,
inner: &'a core::manager::Folder,
window: Option<(usize, usize)>,
) -> mlua::Result<AnyUserData<'a>> {
let window = window.unwrap_or_else(|| (inner.offset(), MANAGER.layout.folder_height()));
let ud = self.scope.create_any_userdata_ref(inner)?;
ud.set_named_user_value(
"window",
inner
.files
.iter()
.skip(window.0)
.take(window.1)
.enumerate()
.filter_map(|(i, f)| self.file(i, f, inner).ok())
.collect::<Vec<_>>(),
)?;
ud.set_named_user_value("files", self.files(&inner.files)?)?;
// TODO: remove this
ud.set_named_user_value(
"hovered",
inner.hovered.as_ref().and_then(|h| self.file(999, h, inner).ok()),
)?;
Ok(ud)
}
fn files(&self, inner: &'a core::files::Files) -> mlua::Result<AnyUserData<'a>> {
self.scope.create_any_userdata_ref(inner)
}
fn file(
&self,
idx: usize,
inner: &'a core::files::File,
folder: &'a core::manager::Folder,
) -> mlua::Result<AnyUserData<'a>> {
let ud = self.scope.create_any_userdata_ref(inner)?;
ud.set_named_user_value("idx", idx)?;
ud.set_named_user_value("folder", self.scope.create_any_userdata_ref(folder)?)?;
ud.set_named_user_value("manager", self.scope.create_any_userdata_ref(&self.cx.manager)?)?;
Ok(ud)
}
fn preview(&self, tab: &'a core::manager::Tab) -> mlua::Result<AnyUserData<'a>> {
let inner = tab.preview();
let ud = self.scope.create_any_userdata_ref(inner)?;
ud.set_named_user_value(
"folder",
inner
.lock
.as_ref()
.filter(|l| l.is_folder())
.and_then(|l| tab.history(&l.url))
.and_then(|f| self.folder(f, Some((f.offset(), MANAGER.layout.preview_height()))).ok()),
)?;
Ok(ud)
}
}

View File

@ -0,0 +1,8 @@
pub fn init() -> mlua::Result<()> {
super::active::Active::init()?;
super::files::Files::init()?;
super::tabs::Tabs::init()?;
super::tasks::Tasks::init()?;
Ok(())
}

View File

@ -0,0 +1,163 @@
use config::THEME;
use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef};
use super::{Range, Url};
use crate::{layout::Style, LUA};
pub struct File(core::files::File);
impl From<&core::files::File> for File {
fn from(value: &core::files::File) -> Self { Self(value.clone()) }
}
impl UserData for File {
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("url", |_, me| Ok(Url::from(me.0.url())));
fields.add_field_method_get("length", |_, me| Ok(me.0.length()));
fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from)));
fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link()));
fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden()));
}
}
pub struct Files;
impl Files {
pub(crate) fn init() -> mlua::Result<()> {
LUA.register_userdata_type::<core::files::Files>(|reg| {
reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len()));
reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| {
let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| {
let files = me.borrow::<core::files::Files>()?;
let i = i + 1;
Ok(if i > files.len() {
mlua::Variadic::new()
} else {
mlua::Variadic::from_iter([i.into_lua(lua)?, File::from(&files[i - 1]).into_lua(lua)?])
})
})?;
Ok((iter, me, 0))
});
reg.add_function("slice", |_, (me, skip, take): (AnyUserData, usize, usize)| {
let files = me.borrow::<core::files::Files>()?;
Ok(files.iter().skip(skip).take(take).map(File::from).collect::<Vec<_>>())
});
})?;
LUA.register_userdata_type::<core::files::File>(|reg| {
reg.add_field_method_get("url", |_, me| Ok(Url::from(me.url())));
reg.add_field_method_get("length", |_, me| Ok(me.length()));
reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from)));
reg.add_field_method_get("is_link", |_, me| Ok(me.is_link()));
reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden()));
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("permissions", |_, me| {
Ok(shared::permissions(me.meta().permissions()))
});
reg.add_function("prefix", |_, me: AnyUserData| {
let folder = me.named_user_value::<UserDataRef<core::manager::Folder>>("folder")?;
if !folder.cwd.is_search() {
return Ok(None);
}
let file = me.borrow::<core::files::File>()?;
let mut p = file.url().strip_prefix(&folder.cwd).unwrap_or(file.url()).components();
p.next_back();
Ok(Some(p.as_path().to_string_lossy().to_string()))
});
reg.add_method("icon", |_, me, ()| {
Ok(
THEME
.icons
.iter()
.find(|&x| x.name.match_path(me.url(), Some(me.is_dir())))
.map(|x| x.display.to_string()),
)
});
reg.add_function("style", |_, me: AnyUserData| {
let manager = me.named_user_value::<UserDataRef<core::manager::Manager>>("manager")?;
let file = me.borrow::<core::files::File>()?;
let mime = manager.mimetype.get(file.url());
Ok(
THEME
.filetypes
.iter()
.find(|&x| x.matches(file.url(), mime, file.is_dir()))
.map(|x| Style::from(x.style)),
)
});
reg.add_function("is_hovered", |_, me: AnyUserData| {
let folder = me.named_user_value::<UserDataRef<core::manager::Folder>>("folder")?;
let file = me.borrow::<core::files::File>()?;
Ok(matches!(&folder.hovered, Some(f) if f.url() == file.url()))
});
reg.add_function("is_yanked", |_, me: AnyUserData| {
let manager = me.named_user_value::<UserDataRef<core::manager::Manager>>("manager")?;
let file = me.borrow::<core::files::File>()?;
Ok(if !manager.yanked().1.contains(file.url()) {
0u8
} else if manager.yanked().0 {
2u8
} else {
1u8
})
});
reg.add_function("is_selected", |_, me: AnyUserData| {
let manager = me.named_user_value::<UserDataRef<core::manager::Manager>>("manager")?;
let folder = me.named_user_value::<UserDataRef<core::manager::Folder>>("folder")?;
let file = me.borrow::<core::files::File>()?;
let selected = folder.files.is_selected(file.url());
Ok(if !manager.active().mode.is_visual() {
selected
} else {
let idx: usize = me.named_user_value("idx")?;
manager.active().mode.pending(folder.offset() + idx, selected)
})
});
reg.add_function("found", |lua, me: AnyUserData| {
let manager = me.named_user_value::<UserDataRef<core::manager::Manager>>("manager")?;
let Some(finder) = manager.active().finder() else {
return Ok(None);
};
let file = me.borrow::<core::files::File>()?;
if let Some(idx) = finder.matched_idx(file.url()) {
return Some(
lua.create_sequence_from([idx.into_lua(lua)?, finder.matched().len().into_lua(lua)?]),
)
.transpose();
}
Ok(None)
});
reg.add_function("highlights", |_, me: AnyUserData| {
let manager = me.named_user_value::<UserDataRef<core::manager::Manager>>("manager")?;
let Some(finder) = manager.active().finder() else {
return Ok(None);
};
let file = me.borrow::<core::files::File>()?;
let Some(h) = file.name().and_then(|n| finder.highlighted(n)) else {
return Ok(None);
};
Ok(Some(h.into_iter().map(Range::from).collect::<Vec<_>>()))
});
})?;
Ok(())
}
}

View File

@ -0,0 +1,15 @@
#![allow(clippy::module_inception)]
mod active;
mod bindings;
mod files;
mod shared;
mod tabs;
mod tasks;
pub use active::*;
pub use bindings::*;
pub use files::*;
pub use shared::*;
pub use tabs::*;
pub use tasks::*;

View File

@ -0,0 +1,43 @@
use mlua::{IntoLua, MetaMethod, UserData, UserDataRef};
// --- Range
pub struct Range<T>(std::ops::Range<T>);
impl<T> From<std::ops::Range<T>> for Range<T> {
fn from(value: std::ops::Range<T>) -> Self { Self(value) }
}
impl<'lua, T> IntoLua<'lua> for Range<T>
where
T: IntoLua<'lua>,
{
fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value> {
let tbl = lua.create_sequence_from([self.0.start, self.0.end])?;
tbl.into_lua(lua)
}
}
// --- Url
pub struct Url(shared::Url);
impl From<&shared::Url> for Url {
fn from(value: &shared::Url) -> Self { Self(value.clone()) }
}
impl UserData for Url {
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("frag", |_, me| Ok(me.0.frag().map(ToOwned::to_owned)));
fields.add_field_method_get("is_regular", |_, me| Ok(me.0.is_regular()));
fields.add_field_method_get("is_search", |_, me| Ok(me.0.is_search()));
fields.add_field_method_get("is_archive", |_, me| Ok(me.0.is_archive()));
}
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
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()));
}
}

View File

@ -0,0 +1,96 @@
use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value};
use crate::LUA;
pub struct Tabs<'a, 'b> {
scope: &'b mlua::Scope<'a, 'a>,
inner: &'a core::manager::Tabs,
}
impl<'a, 'b> Tabs<'a, 'b> {
pub(crate) fn init() -> mlua::Result<()> {
LUA.register_userdata_type::<core::manager::Tabs>(|reg| {
reg.add_field_method_get("idx", |_, me| Ok(me.idx()));
reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len()));
reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| {
let items = me.named_user_value::<Vec<AnyUserData>>("items")?;
Ok(items.get(index - 1).cloned())
});
})?;
LUA.register_userdata_type::<core::manager::Tab>(|reg| {
reg.add_method("name", |_, me, ()| {
Ok(
me.current
.cwd
.file_name()
.map(|n| n.to_string_lossy())
.or_else(|| Some(me.current.cwd.to_string_lossy()))
.unwrap_or_default()
.into_owned(),
)
});
reg.add_field_function_get("mode", |_, me| me.named_user_value::<AnyUserData>("mode"));
reg.add_field_function_get("parent", |_, me| me.named_user_value::<Value>("parent"));
reg.add_field_function_get("current", |_, me| me.named_user_value::<AnyUserData>("current"));
reg.add_field_function_get("preview", |_, me| me.named_user_value::<AnyUserData>("preview"));
})?;
Ok(())
}
pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a core::manager::Tabs) -> Self {
Self { scope, inner }
}
pub(crate) fn make(&self) -> mlua::Result<AnyUserData<'a>> {
let ud = self.scope.create_any_userdata_ref(self.inner)?;
ud.set_named_user_value(
"items",
self.inner.iter().filter_map(|t| self.tab(t).ok()).collect::<Vec<_>>(),
)?;
Ok(ud)
}
fn tab(&self, inner: &'a core::manager::Tab) -> mlua::Result<AnyUserData<'a>> {
let ud = self.scope.create_any_userdata_ref(inner)?;
ud.set_named_user_value("parent", inner.parent.as_ref().and_then(|p| self.folder(p).ok()))?;
ud.set_named_user_value("current", self.folder(&inner.current)?)?;
ud.set_named_user_value("preview", self.preview(inner)?)?;
Ok(ud)
}
pub(crate) fn folder(&self, inner: &'a core::manager::Folder) -> mlua::Result<AnyUserData<'a>> {
let ud = self.scope.create_any_userdata_ref(inner)?;
ud.set_named_user_value("files", self.files(&inner.files)?)?;
Ok(ud)
}
fn files(&self, inner: &'a core::files::Files) -> mlua::Result<AnyUserData<'a>> {
self.scope.create_any_userdata_ref(inner)
}
fn preview(&self, tab: &'a core::manager::Tab) -> mlua::Result<AnyUserData<'a>> {
let inner = tab.preview();
let ud = self.scope.create_any_userdata_ref(inner)?;
ud.set_named_user_value(
"folder",
inner
.lock
.as_ref()
.filter(|l| l.is_folder())
.and_then(|l| tab.history(&l.url))
.and_then(|f| self.folder(f).ok()),
)?;
Ok(ud)
}
}

View File

@ -0,0 +1,27 @@
use mlua::{AnyUserData, LuaSerdeExt, UserDataFields};
use crate::LUA;
pub struct Tasks<'a, 'b> {
scope: &'b mlua::Scope<'a, 'a>,
inner: &'a core::tasks::Tasks,
}
impl<'a, 'b> Tasks<'a, 'b> {
pub(crate) fn init() -> mlua::Result<()> {
LUA.register_userdata_type::<core::tasks::Tasks>(|reg| {
reg.add_field_method_get("progress", |lua, me| lua.to_value(&me.progress))
})?;
Ok(())
}
pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a core::tasks::Tasks) -> Self {
Self { scope, inner }
}
pub(crate) fn make(&self) -> mlua::Result<AnyUserData<'a>> {
self.scope.create_any_userdata_ref(self.inner)
}
}

74
plugin/src/components.rs Normal file
View File

@ -0,0 +1,74 @@
use mlua::{AnyUserData, Table, TableExt};
use crate::{layout::{Gauge, List, Paragraph, Rect}, GLOBALS, LUA};
#[inline]
fn layout(values: Vec<AnyUserData>, buf: &mut ratatui::prelude::Buffer) -> mlua::Result<()> {
for value in values {
if let Ok(c) = value.take::<Paragraph>() {
c.render(buf)
} else if let Ok(c) = value.take::<List>() {
c.render(buf)
} else if let Ok(c) = value.take::<Gauge>() {
c.render(buf)
}
}
Ok(())
}
// --- Status
pub struct Header;
impl Header {
pub fn render(
self,
area: ratatui::layout::Rect,
buf: &mut ratatui::prelude::Buffer,
) -> mlua::Result<()> {
let comp: Table = GLOBALS.get("Header")?;
let values: Vec<AnyUserData> = comp.call_method::<_, _>("render", Rect(area))?;
layout(values, buf)
}
}
// --- Status
pub struct Status;
impl Status {
pub fn render(
self,
area: ratatui::layout::Rect,
buf: &mut ratatui::prelude::Buffer,
) -> mlua::Result<()> {
let comp: Table = GLOBALS.get("Status")?;
let values: Vec<AnyUserData> = comp.call_method::<_, _>("render", Rect(area))?;
layout(values, buf)
}
}
// --- Folder
pub struct Folder {
pub kind: u8,
}
impl Folder {
fn args(&self) -> mlua::Result<Table> {
let tbl = LUA.create_table()?;
tbl.set("kind", self.kind)?;
Ok(tbl)
}
pub fn render(
self,
area: ratatui::layout::Rect,
buf: &mut ratatui::prelude::Buffer,
) -> mlua::Result<()> {
let comp: Table = GLOBALS.get("Folder")?;
let values: Vec<AnyUserData> =
comp.call_method::<_, _>("render", (Rect(area), self.args()?))?;
layout(values, buf)
}
}

47
plugin/src/config.rs Normal file
View File

@ -0,0 +1,47 @@
use mlua::{LuaSerdeExt, SerializeOptions, Table};
use crate::{layout::Rect, GLOBALS, LUA};
#[derive(Clone, Copy)]
pub(super) struct Config;
impl Config {
pub(super) fn install(self) -> mlua::Result<()> {
let options =
SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false);
self.theme(options)?;
self.manager(options)?;
Ok(())
}
fn theme(self, options: SerializeOptions) -> mlua::Result<()> {
GLOBALS.set("THEME", LUA.to_value_with(&*config::THEME, options)?)
}
fn manager(self, options: SerializeOptions) -> mlua::Result<()> {
let manager = LUA.to_value_with(&*config::MANAGER, options)?;
{
let layout: Table = manager.as_table().unwrap().get("layout")?;
layout.set(
"preview_rect",
LUA.create_function(|_, ()| Ok(Rect(config::MANAGER.layout.preview_rect())))?,
)?;
layout.set(
"preview_height",
LUA.create_function(|_, ()| Ok(config::MANAGER.layout.preview_height()))?,
)?;
layout.set(
"folder_rect",
LUA.create_function(|_, ()| Ok(Rect(config::MANAGER.layout.folder_rect())))?,
)?;
layout.set(
"folder_height",
LUA.create_function(|_, ()| Ok(config::MANAGER.layout.folder_height()))?,
)?;
}
GLOBALS.set("MANAGER", manager)
}
}

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 {}

102
plugin/src/layout/gauge.rs Normal file
View File

@ -0,0 +1,102 @@
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, UserDataMethods, Value};
use ratatui::widgets::Widget;
use super::{Rect, Span, Style};
use crate::{GLOBALS, LUA};
#[derive(Clone, Default)]
pub(crate) struct Gauge {
area: ratatui::layout::Rect,
ratio: f64,
label: Option<ratatui::text::Span<'static>>,
style: Option<ratatui::style::Style>,
gauge_style: Option<ratatui::style::Style>,
}
impl Gauge {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set(
"Gauge",
LUA.create_function(|_, area: Rect| Ok(Gauge { area: area.0, ..Default::default() }))?,
)
}
pub(crate) fn render(self, buf: &mut ratatui::buffer::Buffer) {
let mut gauge = ratatui::widgets::Gauge::default();
gauge = gauge.ratio(self.ratio);
if let Some(label) = self.label {
gauge = gauge.label(label);
}
if let Some(style) = self.style {
gauge = gauge.style(style);
}
if let Some(gauge_style) = self.gauge_style {
gauge = gauge.gauge_style(gauge_style);
}
gauge.render(self.area, buf)
}
}
impl<'lua> FromLua<'lua> for Gauge {
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: "Gauge",
message: Some("expected a Gauge".to_string()),
}),
}
}
}
impl UserData for Gauge {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("percent", |_, (ud, percent): (AnyUserData, u8)| {
if percent > 100 {
return Err(mlua::Error::RuntimeError("percent must be between 0 and 100".to_string()));
}
ud.borrow_mut::<Self>()?.ratio = percent as f64 / 100.0;
Ok(ud)
});
methods.add_function("ratio", |_, (ud, ratio): (AnyUserData, f64)| {
if !(0.0..1.0).contains(&ratio) {
return Err(mlua::Error::RuntimeError("ratio must be between 0 and 1".to_string()));
}
ud.borrow_mut::<Self>()?.ratio = ratio;
Ok(ud)
});
methods.add_function("label", |_, (ud, label): (AnyUserData, Span)| {
ud.borrow_mut::<Self>()?.label = Some(label.0);
Ok(ud)
});
methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.style = match value {
Value::Nil => None,
Value::Table(tbl) => Some(Style::from(tbl).0),
Value::UserData(ud) => Some(ud.borrow::<Style>()?.0),
_ => return Err(mlua::Error::external("expected a Style or Table or nil")),
};
Ok(ud)
});
methods.add_function("gauge_style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.gauge_style = match value {
Value::Nil => None,
Value::Table(tbl) => Some(Style::from(tbl).0),
Value::UserData(ud) => Some(ud.borrow::<Style>()?.0),
_ => return Err(mlua::Error::external("expected a Style or Table or nil")),
};
Ok(ud)
});
}
}

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)
});
}
}

75
plugin/src/layout/line.rs Normal file
View File

@ -0,0 +1,75 @@
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("width", |_, ud: AnyUserData| Ok(ud.borrow_mut::<Self>()?.0.width()));
methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| {
{
let mut me = ud.borrow_mut::<Self>()?;
match value {
Value::Nil => me.0.reset_style(),
Value::Table(tbl) => me.0.patch_style(Style::from(tbl).0),
Value::UserData(ud) => me.0.patch_style(ud.borrow::<Style>()?.0),
_ => return Err(mlua::Error::external("expected a Style or Table or nil")),
}
}
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)
});
}
}

117
plugin/src/layout/list.rs Normal file
View File

@ -0,0 +1,117 @@
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, Value};
use ratatui::widgets::Widget;
use super::{Line, Rect, Span, Style};
use crate::{GLOBALS, LUA};
// --- List
#[derive(Clone)]
pub(crate) struct List {
area: ratatui::layout::Rect,
inner: ratatui::widgets::List<'static>,
}
impl List {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set(
"List",
LUA.create_function(|_, (area, items): (Rect, Vec<ListItem>)| {
let items: Vec<_> = items.into_iter().map(|x| x.into()).collect();
Ok(Self { area: area.0, inner: ratatui::widgets::List::new(items) })
})?,
)
}
pub(crate) fn render(self, buf: &mut ratatui::buffer::Buffer) {
self.inner.render(self.area, buf);
}
}
impl<'lua> FromLua<'lua> for List {
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: "List",
message: Some("expected a List".to_string()),
}),
}
}
}
impl UserData for List {}
// --- ListItem
#[derive(Clone)]
pub(crate) struct ListItem {
content: ratatui::text::Text<'static>,
style: Option<ratatui::style::Style>,
}
impl ListItem {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set(
"ListItem",
LUA.create_function(|_, value: Value| {
match value {
Value::UserData(ud) => {
let content: ratatui::text::Text = if let Ok(line) = ud.take::<Line>() {
line.0.into()
} else if let Ok(span) = ud.take::<Span>() {
span.0.into()
} else {
return Err(mlua::Error::external("expected a String, Line or Span"));
};
return Ok(Self { content, style: None });
}
Value::String(s) => {
return Ok(Self { content: s.to_str()?.to_string().into(), style: None });
}
_ => {}
}
Err(mlua::Error::external("expected a String, Line or Span"))
})?,
)
}
}
impl From<ListItem> for ratatui::widgets::ListItem<'static> {
fn from(value: ListItem) -> Self {
let mut item = Self::new(value.content);
if let Some(style) = value.style {
item = item.style(style)
}
item
}
}
impl<'lua> FromLua<'lua> for ListItem {
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: "ListItem",
message: Some("expected a ListItem".to_string()),
}),
}
}
}
impl UserData for ListItem {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.style = match value {
Value::Nil => None,
Value::Table(tbl) => Some(Style::from(tbl).0),
Value::UserData(ud) => Some(ud.borrow::<Style>()?.0),
_ => return Err(mlua::Error::external("expected a Style or Table or nil")),
};
Ok(ud)
});
}
}

21
plugin/src/layout/mod.rs Normal file
View File

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

View File

@ -0,0 +1,80 @@
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(crate) 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, value): (AnyUserData, Value)| {
{
let mut me = ud.borrow_mut::<Self>()?;
match value {
Value::Nil => me.style = None,
Value::Table(tbl) => me.style = Some(Style::from(tbl).0),
Value::UserData(ud) => me.style = Some(ud.borrow::<Style>()?.0),
_ => return Err(mlua::Error::external("expected a Style or Table or nil")),
}
}
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)
});
}
}

50
plugin/src/layout/rect.rs Normal file
View File

@ -0,0 +1,50 @@
use mlua::{FromLua, Lua, Table, UserData, Value};
use crate::{GLOBALS, LUA};
#[derive(Clone, Copy)]
pub(crate) struct Rect(pub(crate) ratatui::layout::Rect);
impl Rect {
pub(crate) fn install() -> mlua::Result<()> {
let ui: Table = GLOBALS.get("ui")?;
ui.set(
"Rect",
LUA.create_function(|_, args: Table| {
Ok(Self(ratatui::layout::Rect {
x: args.get("x")?,
y: args.get("y")?,
width: args.get("w")?,
height: args.get("h")?,
}))
})?,
)
}
}
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("w", |_, me| Ok(me.0.width));
fields.add_field_method_get("h", |_, me| Ok(me.0.height));
fields.add_field_method_get("left", |_, me| Ok(me.0.left()));
fields.add_field_method_get("right", |_, me| Ok(me.0.right()));
fields.add_field_method_get("top", |_, me| Ok(me.0.top()));
fields.add_field_method_get("bottom", |_, me| Ok(me.0.bottom()));
}
}

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,96 @@
use config::theme::Color;
use mlua::{AnyUserData, FromLua, Lua, Table, UserData, UserDataMethods, Value};
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 From<config::theme::Style> for Style {
fn from(value: config::theme::Style) -> Self { Self(value.into()) }
}
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);
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)
});
}
}

14
plugin/src/lib.rs Normal file
View File

@ -0,0 +1,14 @@
#![allow(clippy::unit_arg)]
mod bindings;
mod components;
mod config;
mod layout;
mod plugin;
mod scope;
mod utils;
pub use components::*;
use config::*;
pub use plugin::*;
pub use scope::*;

57
plugin/src/plugin.rs Normal file
View File

@ -0,0 +1,57 @@
use anyhow::Result;
use config::PLUGINS;
use mlua::{Lua, Table};
use shared::RoCell;
use crate::{bindings, layout, utils};
pub(crate) static LUA: RoCell<Lua> = RoCell::new();
pub(crate) static GLOBALS: RoCell<Table> = RoCell::new();
pub fn init() {
fn stage_1() -> Result<()> {
let lua = Lua::new();
// Base
lua.load(include_str!("../preset/inspect/inspect.lua")).exec()?;
lua.load(include_str!("../preset/ui.lua")).exec()?;
lua.load(include_str!("../preset/utils.lua")).exec()?;
// Components
lua.load(include_str!("../preset/components/folder.lua")).exec()?;
lua.load(include_str!("../preset/components/header.lua")).exec()?;
lua.load(include_str!("../preset/components/status.lua")).exec()?;
// Initialize
LUA.init(lua);
GLOBALS.init(LUA.globals());
utils::init()?;
bindings::init()?;
// Install
crate::Config.install()?;
layout::Constraint::install()?;
layout::Gauge::install()?;
layout::Layout::install()?;
layout::Line::install()?;
layout::List::install()?;
layout::ListItem::install()?;
layout::Paragraph::install()?;
layout::Rect::install()?;
layout::Span::install()?;
layout::Style::install()?;
Ok(())
}
fn stage_2() {
PLUGINS.preload.iter().for_each(|p| {
let b = std::fs::read(p).unwrap_or_else(|_| panic!("failed to read plugin: {p:?}"));
LUA.load(&b).exec().unwrap_or_else(|_| panic!("failed to load plugin: {p:?}"));
});
}
stage_1().expect("failed to initialize Lua");
stage_2();
}

17
plugin/src/scope.rs Normal file
View File

@ -0,0 +1,17 @@
use core::Ctx;
pub use mlua::Scope;
use crate::{bindings, GLOBALS, LUA};
pub fn scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) {
let _ = LUA.scope(|scope| {
let tbl = LUA.create_table()?;
tbl.set("active", bindings::Active::new(scope, cx).make()?)?;
tbl.set("tabs", bindings::Tabs::new(scope, cx.manager.tabs()).make()?)?;
tbl.set("tasks", bindings::Tasks::new(scope, &cx.tasks).make()?)?;
GLOBALS.set("cx", tbl)?;
Ok(f(scope))
});
}

33
plugin/src/utils.rs Normal file
View File

@ -0,0 +1,33 @@
use std::ops::ControlFlow;
use mlua::Table;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::{GLOBALS, LUA};
pub fn init() -> mlua::Result<()> {
let utils: Table = GLOBALS.get("utils")?;
utils.set(
"truncate",
LUA.create_function(|_, (text, max): (String, usize)| {
let mut width = 0;
let flow = text.chars().try_fold(String::with_capacity(max), |mut s, c| {
width += c.width().unwrap_or(0);
if s.width() < max {
s.push(c);
ControlFlow::Continue(s)
} else {
ControlFlow::Break(s)
}
});
Ok(match flow {
ControlFlow::Break(s) => s,
ControlFlow::Continue(s) => s,
})
})?,
)?;
Ok(())
}

View File

@ -9,6 +9,6 @@ crossterm = "^0"
futures = "^0"
libc = "^0"
parking_lot = "^0"
ratatui = { version = "^0" }
ratatui = "^0"
regex = "^1"
tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs" ] }

View File

@ -1,4 +1,4 @@
use std::{borrow::Cow, env, ffi::OsStr, fmt::Display, path::{Component, Path, PathBuf}};
use std::{borrow::Cow, env, path::{Component, Path, PathBuf}};
use tokio::fs;
@ -43,50 +43,6 @@ pub fn expand_url(mut u: Url) -> Url {
u
}
pub struct ShortPath<'a> {
pub prefix: &'a Path,
pub name: &'a OsStr,
}
impl Display for ShortPath<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.prefix == Path::new("") {
return write!(f, "{}", self.name.to_string_lossy());
}
write!(f, "{}/{}", self.prefix.display(), self.name.to_string_lossy())
}
}
pub fn short_path<'a>(p: &'a Path, base: &Path) -> ShortPath<'a> {
let p = p.strip_prefix(base).unwrap_or(p);
let mut parts = p.components();
let name = parts.next_back().and_then(|p| match p {
Component::Normal(p) => Some(p),
_ => None,
});
ShortPath { prefix: parts.as_path(), name: name.unwrap_or_default() }
}
pub fn readable_path(p: &Path) -> String {
if let Some(home) = env::var_os("HOME") {
if let Ok(p) = p.strip_prefix(home) {
return format!("~/{}", p.display());
}
}
p.display().to_string()
}
pub fn readable_size(size: u64) -> String {
let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
let mut size = size as f64;
let mut i = 0;
while size > 1024.0 && i < units.len() - 1 {
size /= 1024.0;
i += 1;
}
format!("{:.1} {}", size, units[i])
}
pub async fn unique_path(mut p: Url) -> Url {
let Some(name) = p.file_name().map(|n| n.to_owned()) else {
return p;
@ -102,7 +58,7 @@ pub async fn unique_path(mut p: Url) -> Url {
p
}
// Parmaters
// Parameters
// * `path`: The absolute path(contains no `/./`) to get relative path.
// * `root`: The absolute path(contains no `/./`) to be compared.
//
@ -149,12 +105,10 @@ pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> {
#[inline]
pub fn optional_bool(s: &str) -> Option<bool> {
if s == "true" {
Some(true)
} else if s == "false" {
Some(false)
} else {
None
match s {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}

View File

@ -1,4 +1,4 @@
use std::{collections::VecDeque, path::{Path, PathBuf}};
use std::{collections::VecDeque, fs::Permissions, path::{Path, PathBuf}};
use anyhow::Result;
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
@ -92,61 +92,68 @@ pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver<Result<u64,
}
// Convert a file mode to a string representation
#[cfg(not(target_os = "windows"))]
#[allow(clippy::collapsible_else_if)]
pub fn file_mode(mode: u32) -> String {
use libc::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFSOCK, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
pub fn permissions(permissions: Permissions) -> Option<String> {
#[cfg(windows)]
return None;
#[cfg(target_os = "macos")]
let m = mode as u16;
#[cfg(target_os = "freebsd")]
let m = mode as u16;
#[cfg(target_os = "netbsd")]
let m = mode;
#[cfg(target_os = "linux")]
let m = mode;
#[cfg(unix)]
Some({
use std::os::unix::prelude::PermissionsExt;
let mut s = String::with_capacity(10);
use libc::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFSOCK, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
// File type
s.push(match m & S_IFMT {
S_IFBLK => 'b',
S_IFCHR => 'c',
S_IFDIR => 'd',
S_IFIFO => 'p',
S_IFLNK => 'l',
S_IFSOCK => 's',
_ => '-',
});
#[cfg(target_os = "macos")]
let m = permissions.mode() as u16;
#[cfg(target_os = "freebsd")]
let m = permissions.mode() as u16;
#[cfg(target_os = "netbsd")]
let m = permissions.mode();
#[cfg(target_os = "linux")]
let m = permissions.mode();
// Owner
s.push(if m & S_IRUSR != 0 { 'r' } else { '-' });
s.push(if m & S_IWUSR != 0 { 'w' } else { '-' });
s.push(if m & S_IXUSR != 0 {
if m & S_ISUID != 0 { 's' } else { 'x' }
} else {
if m & S_ISUID != 0 { 'S' } else { '-' }
});
let mut s = String::with_capacity(10);
// Group
s.push(if m & S_IRGRP != 0 { 'r' } else { '-' });
s.push(if m & S_IWGRP != 0 { 'w' } else { '-' });
s.push(if m & S_IXGRP != 0 {
if m & S_ISGID != 0 { 's' } else { 'x' }
} else {
if m & S_ISGID != 0 { 'S' } else { '-' }
});
// File type
s.push(match m & S_IFMT {
S_IFBLK => 'b',
S_IFCHR => 'c',
S_IFDIR => 'd',
S_IFIFO => 'p',
S_IFLNK => 'l',
S_IFSOCK => 's',
_ => '-',
});
// Other
s.push(if m & S_IROTH != 0 { 'r' } else { '-' });
s.push(if m & S_IWOTH != 0 { 'w' } else { '-' });
s.push(if m & S_IXOTH != 0 {
if m & S_ISVTX != 0 { 't' } else { 'x' }
} else {
if m & S_ISVTX != 0 { 'T' } else { '-' }
});
// Owner
s.push(if m & S_IRUSR != 0 { 'r' } else { '-' });
s.push(if m & S_IWUSR != 0 { 'w' } else { '-' });
s.push(if m & S_IXUSR != 0 {
if m & S_ISUID != 0 { 's' } else { 'x' }
} else {
if m & S_ISUID != 0 { 'S' } else { '-' }
});
s
// Group
s.push(if m & S_IRGRP != 0 { 'r' } else { '-' });
s.push(if m & S_IWGRP != 0 { 'w' } else { '-' });
s.push(if m & S_IXGRP != 0 {
if m & S_ISGID != 0 { 's' } else { 'x' }
} else {
if m & S_ISGID != 0 { 'S' } else { '-' }
});
// Other
s.push(if m & S_IROTH != 0 { 'r' } else { '-' });
s.push(if m & S_IWOTH != 0 { 'w' } else { '-' });
s.push(if m & S_IXOTH != 0 {
if m & S_ISVTX != 0 { 't' } else { 'x' }
} else {
if m & S_ISVTX != 0 { 'T' } else { '-' }
});
s
})
}
// Find the max common root of a list of files