mirror of
https://github.com/sxyazi/yazi.git
synced 2024-11-28 02:46:33 +03:00
feat: new theme system (#161)
This commit is contained in:
parent
15c34fed5c
commit
1a2798eb15
118
Cargo.lock
generated
118
Cargo.lock
generated
@ -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"
|
||||
|
@ -5,6 +5,7 @@ members = [
|
||||
"app",
|
||||
"config",
|
||||
"core",
|
||||
"plugin",
|
||||
"shared",
|
||||
]
|
||||
|
||||
|
@ -7,6 +7,7 @@ edition = "2021"
|
||||
adaptor = { path = "../adaptor" }
|
||||
config = { path = "../config" }
|
||||
core = { path = "../core" }
|
||||
plugin = { path = "../plugin" }
|
||||
shared = { path = "../shared" }
|
||||
|
||||
# External dependencies
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
mod layout;
|
||||
mod tabs;
|
||||
|
||||
pub(super) use layout::*;
|
||||
use tabs::*;
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
mod layout;
|
||||
mod left;
|
||||
mod progress;
|
||||
mod right;
|
||||
|
||||
pub(super) use layout::*;
|
||||
use left::*;
|
||||
use progress::*;
|
||||
use right::*;
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 }
|
||||
|
@ -70,5 +70,8 @@ micro_workers = 5
|
||||
macro_workers = 10
|
||||
bizarre_retry = 5
|
||||
|
||||
[plugins]
|
||||
preload = []
|
||||
|
||||
[log]
|
||||
enabled = false
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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]
|
||||
|
3
config/src/plugins/mod.rs
Normal file
3
config/src/plugins/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod plugins;
|
||||
|
||||
pub use plugins::*;
|
29
config/src/plugins/plugins.rs
Normal file
29
config/src/plugins/plugins.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
@ -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
15
config/src/theme/list.rs
Normal 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,
|
||||
}
|
@ -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::*;
|
||||
|
33
config/src/theme/status.rs
Normal file
33
config/src/theme/status.rs
Normal 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,
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) => {
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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)]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))?) }
|
||||
}
|
||||
|
@ -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))?) }
|
||||
}
|
||||
|
@ -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
16
plugin/Cargo.toml
Normal 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"
|
163
plugin/preset/components/folder.lua
Normal file
163
plugin/preset/components/folder.lua
Normal 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
|
43
plugin/preset/components/header.lua
Normal file
43
plugin/preset/components/header.lua
Normal file
@ -0,0 +1,43 @@
|
||||
Header = {}
|
||||
|
||||
function Header:cwd()
|
||||
local cwd = cx.active.current.cwd
|
||||
|
||||
local span
|
||||
if not cwd.is_search then
|
||||
span = ui.Span(utils.readable_path(tostring(cwd)))
|
||||
else
|
||||
span = ui.Span(string.format("%s (search: %s)", utils.readable_path(tostring(cwd)), cwd.frag))
|
||||
end
|
||||
return span:fg("cyan")
|
||||
end
|
||||
|
||||
function Header:tabs()
|
||||
local spans = {}
|
||||
for i = 1, #cx.tabs do
|
||||
local text = i
|
||||
if THEME.tabs.max_width > 2 then
|
||||
text = utils.truncate(text .. " " .. cx.tabs[i]:name(), THEME.tabs.max_width)
|
||||
end
|
||||
if i == cx.tabs.idx + 1 then
|
||||
spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.tabs.active)
|
||||
else
|
||||
spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.tabs.inactive)
|
||||
end
|
||||
end
|
||||
return ui.Line(spans)
|
||||
end
|
||||
|
||||
function Header:render(area)
|
||||
local chunks = ui.Layout()
|
||||
:direction(ui.Direction.HORIZONTAL)
|
||||
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
|
||||
:split(area)
|
||||
|
||||
local left = ui.Line { self:cwd() }
|
||||
local right = ui.Line { self:tabs() }
|
||||
return {
|
||||
ui.Paragraph(chunks[1], { left }),
|
||||
ui.Paragraph(chunks[2], { right }):align(ui.Alignment.RIGHT),
|
||||
}
|
||||
end
|
150
plugin/preset/components/status.lua
Normal file
150
plugin/preset/components/status.lua
Normal 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
|
22
plugin/preset/inspect/LICENSE
Normal file
22
plugin/preset/inspect/LICENSE
Normal 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.
|
312
plugin/preset/inspect/inspect.lua
Normal file
312
plugin/preset/inspect/inspect.lua
Normal 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
32
plugin/preset/ui.lua
Normal 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
24
plugin/preset/utils.lua
Normal 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
|
124
plugin/src/bindings/active.rs
Normal file
124
plugin/src/bindings/active.rs
Normal 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)
|
||||
}
|
||||
}
|
8
plugin/src/bindings/bindings.rs
Normal file
8
plugin/src/bindings/bindings.rs
Normal 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(())
|
||||
}
|
163
plugin/src/bindings/files.rs
Normal file
163
plugin/src/bindings/files.rs
Normal 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(())
|
||||
}
|
||||
}
|
15
plugin/src/bindings/mod.rs
Normal file
15
plugin/src/bindings/mod.rs
Normal 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::*;
|
43
plugin/src/bindings/shared.rs
Normal file
43
plugin/src/bindings/shared.rs
Normal 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()));
|
||||
}
|
||||
}
|
96
plugin/src/bindings/tabs.rs
Normal file
96
plugin/src/bindings/tabs.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value};
|
||||
|
||||
use crate::LUA;
|
||||
|
||||
pub struct Tabs<'a, 'b> {
|
||||
scope: &'b mlua::Scope<'a, 'a>,
|
||||
|
||||
inner: &'a core::manager::Tabs,
|
||||
}
|
||||
|
||||
impl<'a, 'b> Tabs<'a, 'b> {
|
||||
pub(crate) fn init() -> mlua::Result<()> {
|
||||
LUA.register_userdata_type::<core::manager::Tabs>(|reg| {
|
||||
reg.add_field_method_get("idx", |_, me| Ok(me.idx()));
|
||||
reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len()));
|
||||
reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| {
|
||||
let items = me.named_user_value::<Vec<AnyUserData>>("items")?;
|
||||
Ok(items.get(index - 1).cloned())
|
||||
});
|
||||
})?;
|
||||
|
||||
LUA.register_userdata_type::<core::manager::Tab>(|reg| {
|
||||
reg.add_method("name", |_, me, ()| {
|
||||
Ok(
|
||||
me.current
|
||||
.cwd
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy())
|
||||
.or_else(|| Some(me.current.cwd.to_string_lossy()))
|
||||
.unwrap_or_default()
|
||||
.into_owned(),
|
||||
)
|
||||
});
|
||||
|
||||
reg.add_field_function_get("mode", |_, me| me.named_user_value::<AnyUserData>("mode"));
|
||||
reg.add_field_function_get("parent", |_, me| me.named_user_value::<Value>("parent"));
|
||||
reg.add_field_function_get("current", |_, me| me.named_user_value::<AnyUserData>("current"));
|
||||
reg.add_field_function_get("preview", |_, me| me.named_user_value::<AnyUserData>("preview"));
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a core::manager::Tabs) -> Self {
|
||||
Self { scope, inner }
|
||||
}
|
||||
|
||||
pub(crate) fn make(&self) -> mlua::Result<AnyUserData<'a>> {
|
||||
let ud = self.scope.create_any_userdata_ref(self.inner)?;
|
||||
|
||||
ud.set_named_user_value(
|
||||
"items",
|
||||
self.inner.iter().filter_map(|t| self.tab(t).ok()).collect::<Vec<_>>(),
|
||||
)?;
|
||||
|
||||
Ok(ud)
|
||||
}
|
||||
|
||||
fn tab(&self, inner: &'a core::manager::Tab) -> mlua::Result<AnyUserData<'a>> {
|
||||
let ud = self.scope.create_any_userdata_ref(inner)?;
|
||||
|
||||
ud.set_named_user_value("parent", inner.parent.as_ref().and_then(|p| self.folder(p).ok()))?;
|
||||
ud.set_named_user_value("current", self.folder(&inner.current)?)?;
|
||||
ud.set_named_user_value("preview", self.preview(inner)?)?;
|
||||
|
||||
Ok(ud)
|
||||
}
|
||||
|
||||
pub(crate) fn folder(&self, inner: &'a core::manager::Folder) -> mlua::Result<AnyUserData<'a>> {
|
||||
let ud = self.scope.create_any_userdata_ref(inner)?;
|
||||
ud.set_named_user_value("files", self.files(&inner.files)?)?;
|
||||
|
||||
Ok(ud)
|
||||
}
|
||||
|
||||
fn files(&self, inner: &'a core::files::Files) -> mlua::Result<AnyUserData<'a>> {
|
||||
self.scope.create_any_userdata_ref(inner)
|
||||
}
|
||||
|
||||
fn preview(&self, tab: &'a core::manager::Tab) -> mlua::Result<AnyUserData<'a>> {
|
||||
let inner = tab.preview();
|
||||
|
||||
let ud = self.scope.create_any_userdata_ref(inner)?;
|
||||
ud.set_named_user_value(
|
||||
"folder",
|
||||
inner
|
||||
.lock
|
||||
.as_ref()
|
||||
.filter(|l| l.is_folder())
|
||||
.and_then(|l| tab.history(&l.url))
|
||||
.and_then(|f| self.folder(f).ok()),
|
||||
)?;
|
||||
|
||||
Ok(ud)
|
||||
}
|
||||
}
|
27
plugin/src/bindings/tasks.rs
Normal file
27
plugin/src/bindings/tasks.rs
Normal 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
74
plugin/src/components.rs
Normal 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
47
plugin/src/config.rs
Normal 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)
|
||||
}
|
||||
}
|
54
plugin/src/layout/constraint.rs
Normal file
54
plugin/src/layout/constraint.rs
Normal 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
102
plugin/src/layout/gauge.rs
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
89
plugin/src/layout/layout.rs
Normal file
89
plugin/src/layout/layout.rs
Normal 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
75
plugin/src/layout/line.rs
Normal 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
117
plugin/src/layout/list.rs
Normal 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
21
plugin/src/layout/mod.rs
Normal 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::*;
|
80
plugin/src/layout/paragraph.rs
Normal file
80
plugin/src/layout/paragraph.rs
Normal 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
50
plugin/src/layout/rect.rs
Normal 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
90
plugin/src/layout/span.rs
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
96
plugin/src/layout/style.rs
Normal file
96
plugin/src/layout/style.rs
Normal 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
14
plugin/src/lib.rs
Normal 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
57
plugin/src/plugin.rs
Normal 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
17
plugin/src/scope.rs
Normal 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
33
plugin/src/utils.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use mlua::Table;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
use crate::{GLOBALS, LUA};
|
||||
|
||||
pub fn init() -> mlua::Result<()> {
|
||||
let utils: Table = GLOBALS.get("utils")?;
|
||||
|
||||
utils.set(
|
||||
"truncate",
|
||||
LUA.create_function(|_, (text, max): (String, usize)| {
|
||||
let mut width = 0;
|
||||
let flow = text.chars().try_fold(String::with_capacity(max), |mut s, c| {
|
||||
width += c.width().unwrap_or(0);
|
||||
if s.width() < max {
|
||||
s.push(c);
|
||||
ControlFlow::Continue(s)
|
||||
} else {
|
||||
ControlFlow::Break(s)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(match flow {
|
||||
ControlFlow::Break(s) => s,
|
||||
ControlFlow::Continue(s) => s,
|
||||
})
|
||||
})?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -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" ] }
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
103
shared/src/fs.rs
103
shared/src/fs.rs
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user