diff --git a/Cargo.toml b/Cargo.toml index 7f2c7137..e3d7df32 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 2d73f53d..a4e16775 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 525f5a6f..733e79bb 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 { diff --git a/app/src/executor.rs b/app/src/executor.rs index 8799e84f..3e000400 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -3,8 +3,6 @@ use core::{emit, files::FilesSorter, input::InputMode, manager::FinderCase}; 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 index 4371b95f..292a3c6e 100644 --- a/app/src/header/layout.rs +++ b/app/src/header/layout.rs @@ -1,8 +1,9 @@ +use core::Ctx; + 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, diff --git a/app/src/header/tabs.rs b/app/src/header/tabs.rs index eeadcd4e..8ad53780 100644 --- a/app/src/header/tabs.rs +++ b/app/src/header/tabs.rs @@ -1,11 +1,10 @@ +use core::Ctx; 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, } diff --git a/app/src/help/bindings.rs b/app/src/help/bindings.rs index f3fb2733..2bb6c64f 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 140ff44a..fc4f9025 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, diff --git a/app/src/input/input.rs b/app/src/input/input.rs index f58fb0d0..b4935dd6 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 825752a5..69b3b9f9 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -1,13 +1,13 @@ #![allow(clippy::module_inception)] mod app; -mod context; mod executor; mod header; mod help; mod input; mod logs; mod manager; +mod parser; mod root; mod select; mod signals; @@ -16,9 +16,9 @@ mod tasks; mod which; use app::*; -use context::*; use executor::*; use logs::*; +use parser::*; use root::*; use signals::*; @@ -30,6 +30,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 6f018c2e..afc3ff33 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -1,11 +1,9 @@ -use core::files::File; +use core::{files::File, Ctx}; use config::{MANAGER, THEME}; use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}}; use shared::short_path; -use crate::Ctx; - pub(super) struct Folder<'a> { cx: &'a Ctx, folder: &'a core::manager::Folder, diff --git a/app/src/manager/layout.rs b/app/src/manager/layout.rs index 8aeb6f13..453e0ce4 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, diff --git a/app/src/manager/preview.rs b/app/src/manager/preview.rs index c75df174..96d9ccb6 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, diff --git a/app/src/parser.rs b/app/src/parser.rs new file mode 100644 index 00000000..6cc962a1 --- /dev/null +++ b/app/src/parser.rs @@ -0,0 +1,70 @@ +use config::theme::Color; +use ratatui::{style::{Modifier, Style}, text::{Line, Span}, widgets::Paragraph}; + +pub struct Parser; + +impl Parser { + pub fn span(s: &str) -> Span<'static> { + let Some((args, content)) = s.split_once(';') else { + return Span::raw(s.to_string()); + }; + + let args = args.split(',').collect::>(); + if args.len() != 4 { + return Span::raw(s.to_string()); + } + + let mut style = Style::new(); + if let Ok(fg) = Color::try_from(args[0]) { + style = style.fg(fg.into()); + } + if let Ok(bg) = Color::try_from(args[1]) { + style = style.bg(bg.into()); + } + if let Ok(uc) = Color::try_from(args[2]) { + style = style.underline_color(uc.into()); + } + if let Ok(modifier) = args[3].parse::() { + style = style.add_modifier(Modifier::from_bits_truncate(modifier)); + } + Span::styled(content.to_string(), style) + } + + pub fn line(s: &str) -> Line<'static> { + let mut last = '\0'; + let mut spans: Vec = vec![String::new()]; + + for c in s.chars() { + if c == '\n' && last == '\\' { + let last = spans.last_mut().unwrap(); + last.pop(); + last.push(c); + } else if c == '\n' { + spans.push(String::new()); + } else { + spans.last_mut().unwrap().push(c); + } + last = c; + } + Line::from(spans.into_iter().map(|s| Self::span(&s)).collect::>()) + } + + pub fn paragraph(s: &str) -> Paragraph { + let mut last = '\0'; + let mut lines: Vec = vec![String::new()]; + + for c in s.chars() { + if c == '\0' && last == '\\' { + let last = lines.last_mut().unwrap(); + last.pop(); + last.push(c); + } else if c == '\0' { + lines.push(String::new()); + } else { + lines.last_mut().unwrap().push(c); + } + last = c; + } + Paragraph::new(lines.into_iter().map(|s| Self::line(&s)).collect::>()) + } +} diff --git a/app/src/root.rs b/app/src/root.rs index cb612eb1..8ce31b97 100644 --- a/app/src/root.rs +++ b/app/src/root.rs @@ -1,6 +1,8 @@ +use core::Ctx; + use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; -use super::{header, input, manager, select, status, tasks, which, Ctx}; +use super::{header, input, manager, select, status, tasks, which}; use crate::help; pub(super) struct Root<'a> { diff --git a/app/src/select/select.rs b/app/src/select/select.rs index 7846385a..3912e47d 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 index 233f7d1f..7f76a38c 100644 --- a/app/src/status/layout.rs +++ b/app/src/status/layout.rs @@ -1,7 +1,9 @@ -use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::Widget}; +use core::Ctx; -use super::{Left, Right}; -use crate::Ctx; +use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, text::Line, widgets::{Paragraph, Widget}}; +use tracing::info; + +use crate::Parser; pub(crate) struct Layout<'a> { cx: &'a Ctx, @@ -18,7 +20,32 @@ impl<'a> Widget for Layout<'a> { .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); + // Left::new(self.cx).render(chunks[0], buf); + // Right::new(self.cx).render(chunks[1], buf); + + let mut spans = vec![]; + if let Ok(mode) = plugin::Status::mode(self.cx) { + spans.extend(Parser::line(&mode).spans); + } + + let x = plugin::Status::size(self.cx); + if x.is_err() { + info!("Error: {:?}", x); + return; + } + if let Ok(size) = x { + spans.extend(Parser::line(&size).spans); + } + + let x = plugin::Status::name(self.cx); + if x.is_err() { + info!("Error: {:?}", x); + return; + } + if let Ok(name) = x { + spans.extend(Parser::line(&name).spans); + } + + Paragraph::new(Line::from(spans)).render(chunks[0], buf); } } diff --git a/app/src/status/left.rs b/app/src/status/left.rs index 10f84a93..de8287ec 100644 --- a/app/src/status/left.rs +++ b/app/src/status/left.rs @@ -1,8 +1,6 @@ -use config::THEME; -use ratatui::{buffer::Buffer, layout::Rect, style::Modifier, text::{Line, Span}, widgets::{Paragraph, Widget}}; -use shared::readable_size; +use core::Ctx; -use crate::Ctx; +use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; pub(super) struct Left<'a> { cx: &'a Ctx, @@ -14,40 +12,35 @@ impl<'a> Left<'a> { 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); + // let folder = self.cx.manager.current(); + // let mode = self.cx.manager.active().mode(); + // + // // 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/progress.rs b/app/src/status/progress.rs index 0985e0a3..3bc1fa8f 100644 --- a/app/src/status/progress.rs +++ b/app/src/status/progress.rs @@ -1,8 +1,8 @@ +use core::Ctx; + 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, } @@ -19,11 +19,11 @@ impl<'a> Widget for Progress<'a> { } Gauge::default() - .gauge_style(THEME.progress.gauge.get()) + .gauge_style(THEME.status.progress_gauge.get()) .percent(progress.0 as u16) .label(Span::styled( format!("{:>3}%, {} left", progress.0, progress.1), - THEME.progress.label.get(), + THEME.status.progress_label.get(), )) .render(area, buf); } diff --git a/app/src/status/right.rs b/app/src/status/right.rs index 45e00da0..4e326856 100644 --- a/app/src/status/right.rs +++ b/app/src/status/right.rs @@ -1,94 +1,96 @@ +use core::Ctx; + 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<'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); + // 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 7e0b2ddc..14b1bd64 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, diff --git a/app/src/which/layout.rs b/app/src/which/layout.rs index a7a1935c..ddbff14b 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, diff --git a/config/preset/theme.toml b/config/preset/theme.toml index 443df4af..71dcddb8 100644 --- a/config/preset/theme.toml +++ b/config/preset/theme.toml @@ -4,20 +4,18 @@ 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 } + +# Progress +progress_label = { fg = "#FFFFFF", bold = true } +progress_gauge = { fg = "#FFA577", bg = "#484D66" } [selection] hovered = { fg = "#1E2031", bg = "#80AEFA" } diff --git a/config/src/bindings.rs b/config/src/bindings.rs new file mode 100644 index 00000000..e69de29b diff --git a/config/src/lib.rs b/config/src/lib.rs index 5e6086c5..8a081e30 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -2,6 +2,7 @@ use shared::RoCell; +mod bindings; mod boot; pub mod keymap; mod log; diff --git a/config/src/theme/color.rs b/config/src/theme/color.rs index 8bf85221..c4bc193d 100644 --- a/config/src/theme/color.rs +++ b/config/src/theme/color.rs @@ -1,47 +1,53 @@ -use std::ops::Deref; - use anyhow::{bail, Result}; use ratatui::style; -use serde::Deserialize; +use serde::{Deserialize, Serialize, Serializer}; #[derive(Deserialize)] #[serde(try_from = "String")] -pub struct Color(pub(super) style::Color); +pub struct Color([u8; 3]); -impl Default for Color { - fn default() -> Self { Self(style::Color::Reset) } +impl TryFrom<&str> for Color { + type Error = anyhow::Error; + + fn try_from(s: &str) -> Result { + if s.len() != 7 { + bail!("Invalid color: {s}"); + } + Ok(Self([ + u8::from_str_radix(&s[1..3], 16)?, + u8::from_str_radix(&s[3..5], 16)?, + u8::from_str_radix(&s[5..7], 16)?, + ])) + } } impl TryFrom for Color { type Error = 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 try_from(s: String) -> Result { Self::try_from(s.as_str()) } } -impl Deref for Color { - type Target = style::Color; +impl From<&Color> for style::Color { + fn from(&Color(rgb): &Color) -> Self { style::Color::Rgb(rgb[0], rgb[1], rgb[2]) } +} - fn deref(&self) -> &Self::Target { &self.0 } +impl From for style::Color { + fn from(Color(rgb): Color) -> Self { style::Color::Rgb(rgb[0], rgb[1], rgb[2]) } } impl Color { - pub fn fg(&self) -> style::Style { style::Style::new().fg(self.0) } + #[inline] + pub fn fg(&self) -> style::Style { style::Style::new().fg(self.into()) } - pub fn bg(&self) -> style::Style { style::Style::new().bg(self.0) } + #[inline] + pub fn bg(&self) -> style::Style { style::Style::new().bg(self.into()) } } -#[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: Serializer, + { + serializer.serialize_str(&format!("#{:02X}{:02X}{:02X}", self.0[0], self.0[1], self.0[2])) + } } diff --git a/config/src/theme/filetype.rs b/config/src/theme/filetype.rs index b439821a..a54b2640 100644 --- a/config/src/theme/filetype.rs +++ b/config/src/theme/filetype.rs @@ -38,8 +38,10 @@ impl Filetype { mime: Option, fg: Option, bg: Option, - bold: Option, - underline: Option, + #[serde(default)] + bold: bool, + #[serde(default)] + underline: bool, } Ok( diff --git a/config/src/theme/list.rs b/config/src/theme/list.rs new file mode 100644 index 00000000..7db9415d --- /dev/null +++ b/config/src/theme/list.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +use super::Style; + +#[derive(Deserialize, Serialize)] +pub struct Selection { + pub hovered: Style, +} + +#[derive(Deserialize, Serialize)] +pub struct Marker { + pub selecting: Style, + pub selected: Style, +} diff --git a/config/src/theme/mod.rs b/config/src/theme/mod.rs index ed9aa3df..ee9b13fb 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 00000000..7f4e1c46 --- /dev/null +++ b/config/src/theme/status.rs @@ -0,0 +1,25 @@ +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_gauge: 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 865b7de3..c5a88a18 100644 --- a/config/src/theme/style.rs +++ b/config/src/theme/style.rs @@ -1,14 +1,16 @@ use ratatui::style::{self, Modifier}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use super::Color; -#[derive(Deserialize)] +#[derive(Deserialize, Serialize)] pub struct Style { pub fg: Option, pub bg: Option, - pub bold: Option, - pub underline: Option, + #[serde(default)] + pub bold: bool, + #[serde(default)] + pub underline: bool, } impl Style { @@ -16,24 +18,16 @@ impl Style { let mut style = style::Style::new(); if let Some(fg) = &self.fg { - style = style.fg(fg.0); + style = style.fg(fg.into()); } if let Some(bg) = &self.bg { - style = style.bg(bg.0); + style = style.bg(bg.into()); } - if let Some(bold) = self.bold { - if bold { - style = style.add_modifier(Modifier::BOLD); - } else { - style = style.remove_modifier(Modifier::BOLD); - } + if self.bold { + style = style.add_modifier(Modifier::BOLD); } - if let Some(underline) = self.underline { - if underline { - style = style.add_modifier(Modifier::UNDERLINED); - } else { - style = style.remove_modifier(Modifier::UNDERLINED); - } + if self.underline { + style = style.add_modifier(Modifier::UNDERLINED); } style } diff --git a/config/src/theme/theme.rs b/config/src/theme/theme.rs index 4ae4cd09..af73fab7 100644 --- a/config/src/theme/theme.rs +++ b/config/src/theme/theme.rs @@ -1,13 +1,13 @@ 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::{Filetype, Icon, Marker, Selection, Status, Style}; use crate::{validation::check_validation, MERGED_THEME}; -#[derive(Deserialize, Validate)] +#[derive(Deserialize, Serialize, Validate)] pub struct Tab { pub active: Style, pub inactive: Style, @@ -15,60 +15,22 @@ pub struct Tab { 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 status: Status, - pub progress: Progress, pub selection: Selection, 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, - #[serde(deserialize_with = "Icon::deserialize")] + #[serde(deserialize_with = "Icon::deserialize", skip_serializing)] pub icons: Vec, } diff --git a/app/src/context.rs b/core/src/context.rs similarity index 86% rename from app/src/context.rs rename to core/src/context.rs index aab59192..0e031918 100644 --- a/app/src/context.rs +++ b/core/src/context.rs @@ -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) } } diff --git a/core/src/files/file.rs b/core/src/files/file.rs index b1b1d756..b73f02e4 100644 --- a/core/src/files/file.rs +++ b/core/src/files/file.rs @@ -75,7 +75,10 @@ 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 } } diff --git a/core/src/lib.rs b/core/src/lib.rs index e637118f..5cbd0fc9 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -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::*; diff --git a/core/src/manager/mode.rs b/core/src/manager/mode.rs index 5dacbd6b..1a80ab2b 100644 --- a/core/src/manager/mode.rs +++ b/core/src/manager/mode.rs @@ -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)> { 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() } } diff --git a/plugin/Cargo.toml b/plugin/Cargo.toml new file mode 100644 index 00000000..021ee291 --- /dev/null +++ b/plugin/Cargo.toml @@ -0,0 +1,14 @@ +[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 = [ "lua54", "serialize" ] } +tracing = "^0" diff --git a/plugin/preset/inspect/LICENSE b/plugin/preset/inspect/LICENSE new file mode 100644 index 00000000..6149bd78 --- /dev/null +++ b/plugin/preset/inspect/LICENSE @@ -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. diff --git a/plugin/preset/inspect/inspect.lua b/plugin/preset/inspect/inspect.lua new file mode 100644 index 00000000..2c80726a --- /dev/null +++ b/plugin/preset/inspect/inspect.lua @@ -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, " = ") + 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 diff --git a/plugin/preset/line.lua b/plugin/preset/line.lua new file mode 100644 index 00000000..8f04ffd0 --- /dev/null +++ b/plugin/preset/line.lua @@ -0,0 +1,28 @@ +local Line = {} + +function Line:new(...) + local o = { + spans = { ... }, + } + setmetatable(o, self) + self.__index = self + return o +end + +function Line:from(spans) return self:new(table.unpack(spans)) end + +function Line:to_string() + local s = "" + for _, span in ipairs(self.spans) do + s = s .. span:to_string():gsub("\n", "\\\n") .. "\n" + end + return s.sub(s, 1, -2) +end + +setmetatable(Line, { + __call = function(self, ...) return self:new(...) end, + __tostring = function(self) return self:to_string() end, +}) + +yazi = yazi or {} +yazi.Line = Line diff --git a/plugin/preset/paragraph.lua b/plugin/preset/paragraph.lua new file mode 100644 index 00000000..2bdd92a9 --- /dev/null +++ b/plugin/preset/paragraph.lua @@ -0,0 +1,28 @@ +local Paragraph = {} + +function Paragraph:new(...) + local o = { + lines = { ... }, + } + setmetatable(o, self) + self.__index = self + return o +end + +function Paragraph:from(lines) return self:new(table.unpack(lines)) end + +function Paragraph:to_string() + local s = "" + for _, line in ipairs(self.lines) do + s = s .. line:to_string():gsub("\0", "\\\0") .. "\0" + end + return s.sub(s, 1, -2) +end + +setmetatable(Paragraph, { + __call = function(self, ...) return self:new(...) end, + __tostring = function(self) return self:to_string() end, +}) + +yazi = yazi or {} +yazi.Paragraph = Paragraph diff --git a/plugin/preset/span.lua b/plugin/preset/span.lua new file mode 100644 index 00000000..0a253771 --- /dev/null +++ b/plugin/preset/span.lua @@ -0,0 +1,124 @@ +local Span = {} + +function Span:new(content) + local o = { + content = content, + foreground = "", + background = "", + modifier = 0, + } + setmetatable(o, self) + self.__index = self + return o +end + +function Span:fg(color) + self.foreground = color + return self +end + +function Span:bg(color) + self.background = color + return self +end + +function Span:bold() + self.modifier = self.modifier | 1 + return self +end + +function Span:dim() + self.modifier = self.modifier | 2 + return self +end + +function Span:italic() + self.modifier = self.modifier | 4 + return self +end + +function Span:underline() + self.modifier = self.modifier | 8 + return self +end + +function Span:blink() + self.modifier = self.modifier | 16 + return self +end + +function Span:blink_rapid() + self.modifier = self.modifier | 32 + return self +end + +function Span:reverse() + self.modifier = self.modifier | 64 + return self +end + +function Span:hidden() + self.modifier = self.modifier | 128 + return self +end + +function Span:crossed() + self.modifier = self.modifier | 256 + return self +end + +function Span:reset() + self.foreground = "" + self.background = "" + self.modifier = 0 + return self +end + +function Span:style(style) + if style.fg then + self:fg(style.fg) + end + if style.bg then + self:bg(style.bg) + end + if style.bold then + self:bold() + end + if style.dim then + self:dim() + end + if style.italic then + self:italic() + end + if style.underline then + self:underline() + end + if style.blink then + self:blink() + end + if style.blink_rapid then + self:blink_rapid() + end + if style.reverse then + self:reverse() + end + if style.hidden then + self:hidden() + end + if style.crossed then + self:crossed() + end + return self +end + +function Span:to_string() + return string.format("%s,%s,,%s;%s", self.foreground, self.background, self.modifier, self.content) +end + +setmetatable(Span, { + __call = function(self, content) return self:new(content) end, + __tostring = function(self) return self:to_string() end, +}) + +yazi = yazi or {} +yazi.Span = Span diff --git a/plugin/preset/status.lua b/plugin/preset/status.lua new file mode 100644 index 00000000..7dfa3f11 --- /dev/null +++ b/plugin/preset/status.lua @@ -0,0 +1,42 @@ +function mode() + local mode = cx.manager.mode:upper() + if mode == "UNSET" then + mode = "UN-SET" + end + + return yazi + .Line( + yazi.Span(THEME.status.separator.opening):fg(THEME.status.mode_normal.bg), + yazi.Span(" " .. mode .. " "):style(THEME.status.mode_normal) + ) + :to_string() +end + +function size() + local hovered = cx.manager.hovered + if hovered == nil then + return "" + end + + return yazi + .Line( + yazi.Span(" " .. hovered.length .. " "):fg(THEME.status.mode_normal.bg):bg(THEME.status.fancy.bg), + yazi.Span(THEME.status.separator.closing):fg(THEME.status.fancy.bg) + ) + :to_string() +end + +function name() + local hovered = cx.manager.hovered + if hovered == nil then + return "" + end + + return yazi.Span(" " .. yazi.basename(hovered.url)):to_string() +end + +function permissions() end + +function percentage() end + +function position() end diff --git a/plugin/preset/utils.lua b/plugin/preset/utils.lua new file mode 100644 index 00000000..e69da097 --- /dev/null +++ b/plugin/preset/utils.lua @@ -0,0 +1,3 @@ +yazi = yazi or {} + +function yazi.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end diff --git a/plugin/src/bindings.rs b/plugin/src/bindings.rs new file mode 100644 index 00000000..fb76b4d8 --- /dev/null +++ b/plugin/src/bindings.rs @@ -0,0 +1,65 @@ +use mlua::{LuaSerdeExt, UserData, Value}; +use shared::Url; + +// Manager + +pub struct Manager<'a>(&'a core::manager::Manager); + +impl<'a> Manager<'a> { + pub fn new(manager: &'a core::manager::Manager) -> Self { Self(manager) } +} + +impl<'a> UserData for Manager<'a> { + fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("mode", |_, this| Ok(this.0.active().mode().to_string())); + + fields.add_field_method_get("hovered", |_, this| { + Ok(this.0.current().hovered.as_ref().map(File::from)) + }) + } +} + +// Tasks + +pub struct Tasks<'a>(&'a core::tasks::Tasks); + +impl<'a> Tasks<'a> { + pub fn new(tasks: &'a core::tasks::Tasks) -> Self { Self(tasks) } +} + +impl<'a> UserData for Tasks<'a> { + fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("progress", |lua, this| lua.to_value(&this.0.progress)) + } +} + +// File + +pub struct File { + pub(super) url: Url, + pub(super) length: u64, + pub(super) link_to: Option, + pub(super) is_link: bool, +} + +impl From<&core::files::File> for File { + fn from(value: &core::files::File) -> Self { + Self { + url: value.url_owned(), + length: value.length(), + link_to: value.link_to().cloned(), + is_link: value.is_link(), + } + } +} + +impl UserData for File { + fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("url", |_, this| Ok(this.url.to_string_lossy().to_string())); + fields.add_field_method_get("length", |_, this| Ok(this.length)); + fields.add_field_method_get("link_to", |_, this| { + Ok(this.link_to.as_ref().map(|l| l.to_string_lossy().to_string())) + }); + fields.add_field_method_get("is_link", |_, this| Ok(this.is_link)); + } +} diff --git a/plugin/src/lib.rs b/plugin/src/lib.rs new file mode 100644 index 00000000..b0120294 --- /dev/null +++ b/plugin/src/lib.rs @@ -0,0 +1,9 @@ +#![allow(clippy::unit_arg)] + +mod bindings; +mod plugin; +mod status; + +pub use bindings::*; +pub use plugin::*; +pub use status::*; diff --git a/plugin/src/plugin.rs b/plugin/src/plugin.rs new file mode 100644 index 00000000..e5fa888a --- /dev/null +++ b/plugin/src/plugin.rs @@ -0,0 +1,24 @@ +use anyhow::Result; +use config::THEME; +use mlua::{Lua, LuaSerdeExt}; +use shared::RoCell; + +pub(crate) static LUA: RoCell = RoCell::new(); + +pub fn init() { + fn inner() -> Result<()> { + let lua = Lua::new(); + lua.load(include_str!("../preset/utils.lua")).exec()?; + lua.load(include_str!("../preset/inspect/inspect.lua")).exec()?; + lua.load(include_str!("../preset/span.lua")).exec()?; + lua.load(include_str!("../preset/line.lua")).exec()?; + lua.load(include_str!("../preset/paragraph.lua")).exec()?; + + lua.load(include_str!("../preset/status.lua")).exec()?; + + lua.globals().set("THEME", lua.to_value(&*THEME)?)?; + Ok(LUA.init(lua)) + } + + inner().expect("failed to initialize Lua"); +} diff --git a/plugin/src/status.rs b/plugin/src/status.rs new file mode 100644 index 00000000..e4ecb7ec --- /dev/null +++ b/plugin/src/status.rs @@ -0,0 +1,44 @@ +use core::Ctx; + +use mlua::{Function, Result}; + +use crate::{bindings, LUA}; + +pub struct Status; + +impl Status { + fn scoped Result>(cx: &Ctx, f: F) -> Result { + LUA.scope(|scope| { + let manager = scope.create_nonstatic_userdata(bindings::Manager::new(&cx.manager))?; + let tasks = scope.create_nonstatic_userdata(bindings::Tasks::new(&cx.tasks))?; + + let cx = LUA.create_table()?; + cx.set("manager", manager)?; + cx.set("tasks", tasks)?; + LUA.globals().set("cx", cx)?; + + f() + }) + } + + pub fn mode(cx: &Ctx) -> Result { + Self::scoped(cx, || { + let mode: Function = LUA.globals().get("mode")?; + mode.call::<_, String>(()) + }) + } + + pub fn size(cx: &Ctx) -> Result { + Self::scoped(cx, || { + let size: Function = LUA.globals().get("size")?; + size.call::<_, String>(()) + }) + } + + pub fn name(cx: &Ctx) -> Result { + Self::scoped(cx, || { + let size: Function = LUA.globals().get("name")?; + size.call::<_, String>(()) + }) + } +}