feat: better theme system

This commit is contained in:
sxyazi 2023-09-16 21:46:48 +08:00
parent 15c34fed5c
commit 9ff11b2dbc
No known key found for this signature in database
48 changed files with 1095 additions and 286 deletions

View file

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

View file

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

View file

@ -1,4 +1,4 @@
use core::{emit, files::FilesOp, input::InputMode, Event}; use core::{emit, files::FilesOp, input::InputMode, Ctx, Event};
use std::ffi::OsString; use std::ffi::OsString;
use anyhow::{Ok, Result}; use anyhow::{Ok, Result};
@ -7,7 +7,7 @@ use crossterm::event::KeyEvent;
use shared::{expand_url, Term}; use shared::{expand_url, Term};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use crate::{Ctx, Executor, Logs, Root, Signals}; use crate::{Executor, Logs, Root, Signals};
pub(super) struct App { pub(super) struct App {
cx: Ctx, cx: Ctx,
@ -21,7 +21,7 @@ impl App {
let term = Term::start()?; let term = Term::start()?;
let signals = Signals::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 { while let Some(event) = app.signals.recv().await {
match event { match event {

View file

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

View file

@ -1,8 +1,9 @@
use core::Ctx;
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Paragraph, Widget}}; use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Paragraph, Widget}};
use shared::readable_path; use shared::readable_path;
use super::Tabs; use super::Tabs;
use crate::Ctx;
pub(crate) struct Layout<'a> { pub(crate) struct Layout<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

@ -1,11 +1,10 @@
use core::Ctx;
use std::ops::ControlFlow; use std::ops::ControlFlow;
use config::THEME; use config::THEME;
use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, text::{Line, Span}, widgets::{Paragraph, Widget}}; use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, text::{Line, Span}, widgets::{Paragraph, Widget}};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::Ctx;
pub(super) struct Tabs<'a> { pub(super) struct Tabs<'a> {
cx: &'a Ctx, cx: &'a Ctx,
} }

View file

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

View file

@ -1,7 +1,8 @@
use core::Ctx;
use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, style::{Color, Style}, widgets::{Clear, Paragraph, Widget}}; use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, style::{Color, Style}, widgets::{Clear, Paragraph, Widget}};
use super::Bindings; use super::Bindings;
use crate::Ctx;
pub(crate) struct Layout<'a> { pub(crate) struct Layout<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

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

View file

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

View file

@ -1,11 +1,9 @@
use core::files::File; use core::{files::File, Ctx};
use config::{MANAGER, THEME}; use config::{MANAGER, THEME};
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}};
use shared::short_path; use shared::short_path;
use crate::Ctx;
pub(super) struct Folder<'a> { pub(super) struct Folder<'a> {
cx: &'a Ctx, cx: &'a Ctx,
folder: &'a core::manager::Folder, folder: &'a core::manager::Folder,

View file

@ -1,8 +1,9 @@
use core::Ctx;
use config::MANAGER; use config::MANAGER;
use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::{Block, Borders, Padding, Widget}}; use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::{Block, Borders, Padding, Widget}};
use super::{Folder, Preview}; use super::{Folder, Preview};
use crate::Ctx;
pub(crate) struct Layout<'a> { pub(crate) struct Layout<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

@ -1,10 +1,9 @@
use core::manager::PreviewData; use core::{manager::PreviewData, Ctx};
use ansi_to_tui::IntoText; use ansi_to_tui::IntoText;
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}};
use super::Folder; use super::Folder;
use crate::Ctx;
pub(super) struct Preview<'a> { pub(super) struct Preview<'a> {
cx: &'a Ctx, cx: &'a Ctx,

70
app/src/parser.rs Normal file
View file

@ -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::<Vec<_>>();
if args.len() != 4 {
return Span::raw(s.to_string());
}
let mut style = Style::new();
if let Ok(fg) = Color::try_from(args[0]) {
style = style.fg(fg.into());
}
if let Ok(bg) = Color::try_from(args[1]) {
style = style.bg(bg.into());
}
if let Ok(uc) = Color::try_from(args[2]) {
style = style.underline_color(uc.into());
}
if let Ok(modifier) = args[3].parse::<u16>() {
style = style.add_modifier(Modifier::from_bits_truncate(modifier));
}
Span::styled(content.to_string(), style)
}
pub fn line(s: &str) -> Line<'static> {
let mut last = '\0';
let mut spans: Vec<String> = vec![String::new()];
for c in s.chars() {
if c == '\n' && last == '\\' {
let last = spans.last_mut().unwrap();
last.pop();
last.push(c);
} else if c == '\n' {
spans.push(String::new());
} else {
spans.last_mut().unwrap().push(c);
}
last = c;
}
Line::from(spans.into_iter().map(|s| Self::span(&s)).collect::<Vec<_>>())
}
pub fn paragraph(s: &str) -> Paragraph {
let mut last = '\0';
let mut lines: Vec<String> = 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::<Vec<_>>())
}
}

View file

@ -1,6 +1,8 @@
use core::Ctx;
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; 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; use crate::help;
pub(super) struct Root<'a> { pub(super) struct Root<'a> {

View file

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

View file

@ -1,7 +1,9 @@
use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::Widget}; use core::Ctx;
use super::{Left, Right}; use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, text::Line, widgets::{Paragraph, Widget}};
use crate::Ctx; use tracing::info;
use crate::Parser;
pub(crate) struct Layout<'a> { pub(crate) struct Layout<'a> {
cx: &'a Ctx, cx: &'a Ctx,
@ -18,7 +20,32 @@ impl<'a> Widget for Layout<'a> {
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(area); .split(area);
Left::new(self.cx).render(chunks[0], buf); // Left::new(self.cx).render(chunks[0], buf);
Right::new(self.cx).render(chunks[1], 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);
} }
} }

View file

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

View file

@ -1,8 +1,8 @@
use core::Ctx;
use config::THEME; use config::THEME;
use ratatui::{buffer::Buffer, layout::Rect, text::Span, widgets::{Gauge, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, text::Span, widgets::{Gauge, Widget}};
use crate::Ctx;
pub(super) struct Progress<'a> { pub(super) struct Progress<'a> {
cx: &'a Ctx, cx: &'a Ctx,
} }
@ -19,11 +19,11 @@ impl<'a> Widget for Progress<'a> {
} }
Gauge::default() Gauge::default()
.gauge_style(THEME.progress.gauge.get()) .gauge_style(THEME.status.progress_gauge.get())
.percent(progress.0 as u16) .percent(progress.0 as u16)
.label(Span::styled( .label(Span::styled(
format!("{:>3}%, {} left", progress.0, progress.1), format!("{:>3}%, {} left", progress.0, progress.1),
THEME.progress.label.get(), THEME.status.progress_label.get(),
)) ))
.render(area, buf); .render(area, buf);
} }

View file

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

View file

@ -1,9 +1,8 @@
use core::tasks::TASKS_PERCENT; use core::{tasks::TASKS_PERCENT, Ctx};
use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Direction, Rect}, style::{Color, Modifier, Style}, widgets::{Block, BorderType, Borders, List, ListItem, Padding, Widget}}; use 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 super::Clear;
use crate::Ctx;
pub(crate) struct Layout<'a> { pub(crate) struct Layout<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

@ -1,7 +1,8 @@
use core::Ctx;
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Block, Clear, Widget}}; use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Block, Clear, Widget}};
use super::Side; use super::Side;
use crate::Ctx;
pub(crate) struct Which<'a> { pub(crate) struct Which<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

@ -4,20 +4,18 @@ inactive = { fg = "#C8D3F8", bg = "#484D66" }
max_width = 1 max_width = 1
[status] [status]
primary = { normal = "#80AEFA", select = "#CD9EFC", unset = "#FFA577" } plain = { fg = "#FFFFFF" }
secondary = { normal = "#1E2031", select = "#23273B", unset = "#23273B" } fancy = { bg = "#45475D" }
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" }
separator = { opening = "", closing = "" } separator = { opening = "", closing = "" }
[progress] # Mode
gauge = { fg = "#FFA577", bg = "#484D66" } mode_normal = { fg = "#181827", bg = "#7DB5FF", bold = true }
label = { fg = "#FFFFFF", 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] [selection]
hovered = { fg = "#1E2031", bg = "#80AEFA" } hovered = { fg = "#1E2031", bg = "#80AEFA" }

0
config/src/bindings.rs Normal file
View file

View file

@ -2,6 +2,7 @@
use shared::RoCell; use shared::RoCell;
mod bindings;
mod boot; mod boot;
pub mod keymap; pub mod keymap;
mod log; mod log;

View file

@ -1,47 +1,53 @@
use std::ops::Deref;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use ratatui::style; use ratatui::style;
use serde::Deserialize; use serde::{Deserialize, Serialize, Serializer};
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(try_from = "String")] #[serde(try_from = "String")]
pub struct Color(pub(super) style::Color); pub struct Color([u8; 3]);
impl Default for Color { impl TryFrom<&str> for Color {
fn default() -> Self { Self(style::Color::Reset) } type Error = anyhow::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
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<String> for Color { impl TryFrom<String> for Color {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(s: String) -> Result<Self, Self::Error> { fn try_from(s: String) -> Result<Self, Self::Error> { Self::try_from(s.as_str()) }
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)?,
)))
}
} }
impl Deref for Color { impl From<&Color> for style::Color {
type Target = 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<Color> for style::Color {
fn from(Color(rgb): Color) -> Self { style::Color::Rgb(rgb[0], rgb[1], rgb[2]) }
} }
impl Color { 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)] impl Serialize for Color {
pub struct ColorGroup { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
pub normal: Color, where
pub select: Color, S: Serializer,
pub unset: Color, {
serializer.serialize_str(&format!("#{:02X}{:02X}{:02X}", self.0[0], self.0[1], self.0[2]))
}
} }

View file

@ -38,8 +38,10 @@ impl Filetype {
mime: Option<Pattern>, mime: Option<Pattern>,
fg: Option<Color>, fg: Option<Color>,
bg: Option<Color>, bg: Option<Color>,
bold: Option<bool>, #[serde(default)]
underline: Option<bool>, bold: bool,
#[serde(default)]
underline: bool,
} }
Ok( Ok(

14
config/src/theme/list.rs Normal file
View file

@ -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,
}

View file

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

View file

@ -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,
}

View file

@ -1,14 +1,16 @@
use ratatui::style::{self, Modifier}; use ratatui::style::{self, Modifier};
use serde::Deserialize; use serde::{Deserialize, Serialize};
use super::Color; use super::Color;
#[derive(Deserialize)] #[derive(Deserialize, Serialize)]
pub struct Style { pub struct Style {
pub fg: Option<Color>, pub fg: Option<Color>,
pub bg: Option<Color>, pub bg: Option<Color>,
pub bold: Option<bool>, #[serde(default)]
pub underline: Option<bool>, pub bold: bool,
#[serde(default)]
pub underline: bool,
} }
impl Style { impl Style {
@ -16,24 +18,16 @@ impl Style {
let mut style = style::Style::new(); let mut style = style::Style::new();
if let Some(fg) = &self.fg { if let Some(fg) = &self.fg {
style = style.fg(fg.0); style = style.fg(fg.into());
} }
if let Some(bg) = &self.bg { if let Some(bg) = &self.bg {
style = style.bg(bg.0); style = style.bg(bg.into());
} }
if let Some(bold) = self.bold { if self.bold {
if bold { style = style.add_modifier(Modifier::BOLD);
style = style.add_modifier(Modifier::BOLD);
} else {
style = style.remove_modifier(Modifier::BOLD);
}
} }
if let Some(underline) = self.underline { if self.underline {
if underline { style = style.add_modifier(Modifier::UNDERLINED);
style = style.add_modifier(Modifier::UNDERLINED);
} else {
style = style.remove_modifier(Modifier::UNDERLINED);
}
} }
style style
} }

View file

@ -1,13 +1,13 @@
use std::path::PathBuf; use std::path::PathBuf;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use shared::expand_path; use shared::expand_path;
use validator::Validate; use validator::Validate;
use super::{ColorGroup, Filetype, Icon, Style}; use super::{Filetype, Icon, Marker, Selection, Status, Style};
use crate::{validation::check_validation, MERGED_THEME}; use crate::{validation::check_validation, MERGED_THEME};
#[derive(Deserialize, Validate)] #[derive(Deserialize, Serialize, Validate)]
pub struct Tab { pub struct Tab {
pub active: Style, pub active: Style,
pub inactive: Style, pub inactive: Style,
@ -15,60 +15,22 @@ pub struct Tab {
pub max_width: u8, pub max_width: u8,
} }
#[derive(Deserialize)] #[derive(Deserialize, Serialize)]
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)]
pub struct Preview { pub struct Preview {
pub hovered: Style, pub hovered: Style,
pub syntect_theme: PathBuf, pub syntect_theme: PathBuf,
} }
#[derive(Deserialize)] #[derive(Deserialize, Serialize)]
pub struct Theme { pub struct Theme {
pub tab: Tab, pub tab: Tab,
pub status: Status, pub status: Status,
pub progress: Progress,
pub selection: Selection, pub selection: Selection,
pub marker: Marker, pub marker: Marker,
pub preview: Preview, pub preview: Preview,
#[serde(rename = "filetype", deserialize_with = "Filetype::deserialize")] #[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)]
pub filetypes: Vec<Filetype>, pub filetypes: Vec<Filetype>,
#[serde(deserialize_with = "Icon::deserialize")] #[serde(deserialize_with = "Icon::deserialize", skip_serializing)]
pub icons: Vec<Icon>, pub icons: Vec<Icon>,
} }

View file

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

View file

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

View file

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

View file

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

14
plugin/Cargo.toml Normal file
View file

@ -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"

View file

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

View file

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

28
plugin/preset/line.lua Normal file
View file

@ -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

View file

@ -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

124
plugin/preset/span.lua Normal file
View file

@ -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

42
plugin/preset/status.lua Normal file
View file

@ -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

3
plugin/preset/utils.lua Normal file
View file

@ -0,0 +1,3 @@
yazi = yazi or {}
function yazi.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end

65
plugin/src/bindings.rs Normal file
View file

@ -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<Url>,
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));
}
}

9
plugin/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
#![allow(clippy::unit_arg)]
mod bindings;
mod plugin;
mod status;
pub use bindings::*;
pub use plugin::*;
pub use status::*;

24
plugin/src/plugin.rs Normal file
View file

@ -0,0 +1,24 @@
use anyhow::Result;
use config::THEME;
use mlua::{Lua, LuaSerdeExt};
use shared::RoCell;
pub(crate) static LUA: RoCell<Lua> = 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");
}

44
plugin/src/status.rs Normal file
View file

@ -0,0 +1,44 @@
use core::Ctx;
use mlua::{Function, Result};
use crate::{bindings, LUA};
pub struct Status;
impl Status {
fn scoped<T, F: FnOnce() -> Result<T>>(cx: &Ctx, f: F) -> Result<T> {
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<String> {
Self::scoped(cx, || {
let mode: Function = LUA.globals().get("mode")?;
mode.call::<_, String>(())
})
}
pub fn size(cx: &Ctx) -> Result<String> {
Self::scoped(cx, || {
let size: Function = LUA.globals().get("size")?;
size.call::<_, String>(())
})
}
pub fn name(cx: &Ctx) -> Result<String> {
Self::scoped(cx, || {
let size: Function = LUA.globals().get("name")?;
size.call::<_, String>(())
})
}
}