diff --git a/Cargo.lock b/Cargo.lock index 68b627e46..51fc9a5ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 7f2c7137b..e3d7df328 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "app", "config", "core", + "plugin", "shared", ] diff --git a/app/Cargo.toml b/app/Cargo.toml index 2d73f53dc..a4e167755 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" adaptor = { path = "../adaptor" } config = { path = "../config" } core = { path = "../core" } +plugin = { path = "../plugin" } shared = { path = "../shared" } # External dependencies diff --git a/app/src/app.rs b/app/src/app.rs index 525f5a6fe..74870fd1d 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -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); } diff --git a/app/src/executor.rs b/app/src/executor.rs index 8799e84f4..401b36e74 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -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 { diff --git a/app/src/header/layout.rs b/app/src/header/layout.rs deleted file mode 100644 index 4371b95fe..000000000 --- a/app/src/header/layout.rs +++ /dev/null @@ -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); - } -} diff --git a/app/src/header/mod.rs b/app/src/header/mod.rs deleted file mode 100644 index bf8f01166..000000000 --- a/app/src/header/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod layout; -mod tabs; - -pub(super) use layout::*; -use tabs::*; diff --git a/app/src/header/tabs.rs b/app/src/header/tabs.rs deleted file mode 100644 index eeadcd4e8..000000000 --- a/app/src/header/tabs.rs +++ /dev/null @@ -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::>(), - ); - - Paragraph::new(line).alignment(Alignment::Right).render(area, buf); - } -} diff --git a/app/src/help/bindings.rs b/app/src/help/bindings.rs index f3fb27338..2bb6c64f8 100644 --- a/app/src/help/bindings.rs +++ b/app/src/help/bindings.rs @@ -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, diff --git a/app/src/help/layout.rs b/app/src/help/layout.rs index 140ff44a9..d12acdb82 100644 --- a/app/src/help/layout.rs +++ b/app/src/help/layout.rs @@ -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); diff --git a/app/src/input/input.rs b/app/src/input/input.rs index f58fb0d02..b4935dd61 100644 --- a/app/src/input/input.rs +++ b/app/src/input/input.rs @@ -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, } diff --git a/app/src/main.rs b/app/src/main.rs index 825752a50..d9b0e68c0 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -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 diff --git a/app/src/manager/folder.rs b/app/src/manager/folder.rs index 6f018c2e9..58b984644 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -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, -} - -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 { - 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))]) - } +pub(super) enum Folder { + Parent = 0, + Current = 1, + Preview = 2, } -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); + } } } diff --git a/app/src/manager/layout.rs b/app/src/manager/layout.rs index 8aeb6f134..80c3bf5c7 100644 --- a/app/src/manager/layout.rs +++ b/app/src/manager/layout.rs @@ -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)); diff --git a/app/src/manager/preview.rs b/app/src/manager/preview.rs index c75df1748..db3abc073 100644 --- a/app/src/manager/preview.rs +++ b/app/src/manager/preview.rs @@ -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()); diff --git a/app/src/root.rs b/app/src/root.rs index cb612eb13..7abd11abf 100644 --- a/app/src/root.rs +++ b/app/src/root.rs @@ -1,6 +1,9 @@ +use core::Ctx; + use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; +use tracing::error; -use super::{header, input, manager, select, status, tasks, which, Ctx}; +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); diff --git a/app/src/select/select.rs b/app/src/select/select.rs index 7846385a5..3912e47d8 100644 --- a/app/src/select/select.rs +++ b/app/src/select/select.rs @@ -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, diff --git a/app/src/status/layout.rs b/app/src/status/layout.rs deleted file mode 100644 index 233f7d1fe..000000000 --- a/app/src/status/layout.rs +++ /dev/null @@ -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); - } -} diff --git a/app/src/status/left.rs b/app/src/status/left.rs deleted file mode 100644 index 10f84a939..000000000 --- a/app/src/status/left.rs +++ /dev/null @@ -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); - } -} diff --git a/app/src/status/mod.rs b/app/src/status/mod.rs deleted file mode 100644 index 35d6b256b..000000000 --- a/app/src/status/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod layout; -mod left; -mod progress; -mod right; - -pub(super) use layout::*; -use left::*; -use progress::*; -use right::*; diff --git a/app/src/status/progress.rs b/app/src/status/progress.rs deleted file mode 100644 index 0985e0a3c..000000000 --- a/app/src/status/progress.rs +++ /dev/null @@ -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); - } -} diff --git a/app/src/status/right.rs b/app/src/status/right.rs deleted file mode 100644 index 45e00da00..000000000 --- a/app/src/status/right.rs +++ /dev/null @@ -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 { - // 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 { - // 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); - } -} diff --git a/app/src/tasks/layout.rs b/app/src/tasks/layout.rs index 7e0b2ddcf..099057fc1 100644 --- a/app/src/tasks/layout.rs +++ b/app/src/tasks/layout.rs @@ -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] } } diff --git a/app/src/which/layout.rs b/app/src/which/layout.rs index a7a1935c5..686bf8be2 100644 --- a/app/src/which/layout.rs +++ b/app/src/which/layout.rs @@ -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); diff --git a/config/preset/theme.toml b/config/preset/theme.toml index 443df4af2..0e0ed90fb 100644 --- a/config/preset/theme.toml +++ b/config/preset/theme.toml @@ -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 } diff --git a/config/preset/yazi.toml b/config/preset/yazi.toml index f5deb078c..b650e1bcb 100644 --- a/config/preset/yazi.toml +++ b/config/preset/yazi.toml @@ -70,5 +70,8 @@ micro_workers = 5 macro_workers = 10 bizarre_retry = 5 +[plugins] +preload = [] + [log] enabled = false diff --git a/config/src/lib.rs b/config/src/lib.rs index 5e6086c55..4d6e45a65 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -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 = RoCell::new(); pub static LOG: RoCell = RoCell::new(); pub static MANAGER: RoCell = RoCell::new(); pub static OPEN: RoCell = RoCell::new(); +pub static PLUGINS: RoCell = RoCell::new(); pub static PREVIEW: RoCell = RoCell::new(); pub static TASKS: RoCell = RoCell::new(); pub static THEME: RoCell = 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); diff --git a/config/src/manager/layout.rs b/config/src/manager/layout.rs index d7df50093..96730b021 100644 --- a/config/src/manager/layout.rs +++ b/config/src/manager/layout.rs @@ -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")] pub struct ManagerLayout { pub parent: u32, diff --git a/config/src/manager/manager.rs b/config/src/manager/manager.rs index f1acf3807..69386b40d 100644 --- a/config/src/manager/manager.rs +++ b/config/src/manager/manager.rs @@ -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, diff --git a/config/src/manager/sorting.rs b/config/src/manager/sorting.rs index 6d0b565fc..35c937ebb 100644 --- a/config/src/manager/sorting.rs +++ b/config/src/manager/sorting.rs @@ -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] diff --git a/config/src/plugins/mod.rs b/config/src/plugins/mod.rs new file mode 100644 index 000000000..1e08e1acc --- /dev/null +++ b/config/src/plugins/mod.rs @@ -0,0 +1,3 @@ +mod plugins; + +pub use plugins::*; diff --git a/config/src/plugins/plugins.rs b/config/src/plugins/plugins.rs new file mode 100644 index 000000000..75da19882 --- /dev/null +++ b/config/src/plugins/plugins.rs @@ -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, +} + +impl Default for Plugins { + fn default() -> Self { + #[derive(Deserialize)] + struct Outer { + plugins: Plugins, + } + + let mut plugins = toml::from_str::(&MERGED_YAZI).unwrap().plugins; + + plugins.preload.iter_mut().for_each(|p| { + *p = expand_path(&p); + }); + + plugins + } +} diff --git a/config/src/theme/color.rs b/config/src/theme/color.rs index 8bf85221d..db154cc0b 100644 --- a/config/src/theme/color.rs +++ b/config/src/theme/color.rs @@ -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 TryFrom for Color { - type Error = anyhow::Error; +impl FromStr for Color { + type Err = anyhow::Error; - fn try_from(s: String) -> Result { - 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 from_str(s: &str) -> Result { + ratatui::style::Color::from_str(s).map(Self).map_err(|_| anyhow::anyhow!("invalid color")) } } -impl Deref for Color { - type Target = style::Color; +impl TryFrom for Color { + type Error = anyhow::Error; - fn deref(&self) -> &Self::Target { &self.0 } + fn try_from(s: String) -> Result { Self::from_str(s.as_str()) } } -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) } +impl From for ratatui::style::Color { + fn from(value: Color) -> Self { value.0 } } -#[derive(Deserialize)] -pub struct ColorGroup { - pub normal: Color, - pub select: Color, - pub unset: Color, +impl Serialize for Color { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.to_string().serialize(serializer) + } } diff --git a/config/src/theme/filetype.rs b/config/src/theme/filetype.rs index b439821a8..2d5474c16 100644 --- a/config/src/theme/filetype.rs +++ b/config/src/theme/filetype.rs @@ -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, @@ -30,16 +30,31 @@ impl Filetype { { #[derive(Deserialize)] struct FiletypeOuter { - rules: Vec, + rules: Vec, } #[derive(Deserialize)] - struct FiletypeOuterStyle { - name: Option, - mime: Option, - fg: Option, - bg: Option, - bold: Option, - underline: Option, + struct FiletypeRule { + name: Option, + mime: Option, + + fg: Option, + bg: Option, + #[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::>(), ) diff --git a/config/src/theme/list.rs b/config/src/theme/list.rs new file mode 100644 index 000000000..5ce1556bd --- /dev/null +++ b/config/src/theme/list.rs @@ -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, +} diff --git a/config/src/theme/mod.rs b/config/src/theme/mod.rs index ed9aa3dfa..ee9b13fb6 100644 --- a/config/src/theme/mod.rs +++ b/config/src/theme/mod.rs @@ -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::*; diff --git a/config/src/theme/status.rs b/config/src/theme/status.rs new file mode 100644 index 000000000..672f0e0bf --- /dev/null +++ b/config/src/theme/status.rs @@ -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, +} diff --git a/config/src/theme/style.rs b/config/src/theme/style.rs index 865b7de3c..2b8daad2b 100644 --- a/config/src/theme/style.rs +++ b/config/src/theme/style.rs @@ -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, - pub bg: Option, - pub bold: Option, - pub underline: Option, + pub fg: Option, + pub bg: Option, + pub modifier: Modifier, +} + +impl Serialize for Style { + fn serialize(&self, serializer: S) -> Result + 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