diff --git a/Cargo.lock b/Cargo.lock index d7541033..c2f2b3a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1364,6 +1364,7 @@ dependencies = [ "ratatui", "shared", "tracing", + "unicode-width", ] [[package]] diff --git a/app/src/header/layout.rs b/app/src/header/layout.rs index 4755b9bd..122102d8 100644 --- a/app/src/header/layout.rs +++ b/app/src/header/layout.rs @@ -1,34 +1,13 @@ -use core::Ctx; +use ratatui::{prelude::{Buffer, Rect}, widgets::Widget}; +use tracing::info; -use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Paragraph, Widget}}; -use shared::readable_path; +pub(crate) struct Layout; -use super::Tabs; - -pub(crate) struct Layout<'a> { - cx: &'a Ctx, -} - -impl<'a> Layout<'a> { - pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Layout<'a> { +impl Widget for Layout { fn render(self, area: Rect, buf: &mut Buffer) { - let chunks = layout::Layout::new() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area); - - let cwd = &self.cx.manager.current().cwd; - let location = if cwd.is_search() { - format!("{} (search: {})", readable_path(cwd), cwd.frag().unwrap()) - } else { - readable_path(cwd) - }; - - Paragraph::new(location).style(Style::new().fg(Color::Cyan)).render(chunks[0], buf); - - Tabs::new(self.cx).render(chunks[1], buf); + let x = plugin::Header.render(area, buf); + if x.is_err() { + info!("{:?}", x); + } } } diff --git a/app/src/header/mod.rs b/app/src/header/mod.rs index bf8f0116..03520470 100644 --- a/app/src/header/mod.rs +++ b/app/src/header/mod.rs @@ -1,5 +1,3 @@ mod layout; -mod tabs; pub(super) use layout::*; -use tabs::*; diff --git a/app/src/header/tabs.rs b/app/src/header/tabs.rs deleted file mode 100644 index d072b187..00000000 --- a/app/src/header/tabs.rs +++ /dev/null @@ -1,62 +0,0 @@ -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}; - -pub(super) struct Tabs<'a> { - cx: &'a Ctx, -} - -impl<'a> Tabs<'a> { - pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } } - - fn truncate(&self, name: &str) -> String { - let mut width = 0; - let flow = - name.chars().try_fold(String::with_capacity(THEME.tab.max_width as usize), |mut s, c| { - width += c.width().unwrap_or(0); - if s.width() < THEME.tab.max_width as usize { - s.push(c); - ControlFlow::Continue(s) - } else { - ControlFlow::Break(s) - } - }); - - match flow { - ControlFlow::Break(s) => s, - ControlFlow::Continue(s) => s, - } - } -} - -impl<'a> Widget for Tabs<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - let tabs = self.cx.manager.tabs(); - - let line = Line::from( - tabs - .iter() - .enumerate() - .map(|(i, tab)| { - let mut text = format!("{}", i + 1); - if THEME.tab.max_width >= 3 { - text.push(' '); - text.push_str(tab.name()); - text = self.truncate(&text); - } - - if i == tabs.idx() { - Span::styled(format!(" {text} "), THEME.tab.active.into()) - } else { - Span::styled(format!(" {text} "), THEME.tab.inactive.into()) - } - }) - .collect::>(), - ); - - Paragraph::new(line).alignment(Alignment::Right).render(area, buf); - } -} diff --git a/app/src/root.rs b/app/src/root.rs index fa7d348f..68985030 100644 --- a/app/src/root.rs +++ b/app/src/root.rs @@ -20,7 +20,7 @@ impl<'a> Widget for Root<'a> { .constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)]) .split(area); - header::Layout::new(self.cx).render(chunks[0], buf); + header::Layout.render(chunks[0], buf); manager::Layout::new(self.cx).render(chunks[1], buf); status::Layout.render(chunks[2], buf); diff --git a/config/preset/theme.toml b/config/preset/theme.toml index 999243fd..0213dcb5 100644 --- a/config/preset/theme.toml +++ b/config/preset/theme.toml @@ -1,4 +1,4 @@ -[tab] +[tabs] active = { fg = "#1E2031", bg = "#80AEFA" } inactive = { fg = "#C8D3F8", bg = "#484D66" } max_width = 1 diff --git a/config/src/theme/theme.rs b/config/src/theme/theme.rs index 5b324e72..97dd66c0 100644 --- a/config/src/theme/theme.rs +++ b/config/src/theme/theme.rs @@ -8,7 +8,7 @@ use super::{Files, Filetype, Icon, Marker, Status, Style}; use crate::{validation::check_validation, MERGED_THEME}; #[derive(Deserialize, Serialize, Validate)] -pub struct Tab { +pub struct Tabs { pub active: Style, pub inactive: Style, #[validate(range(min = 1, message = "Must be greater than 0"))] @@ -23,7 +23,7 @@ pub struct Preview { #[derive(Deserialize, Serialize)] pub struct Theme { - pub tab: Tab, + pub tabs: Tabs, pub status: Status, pub files: Files, pub marker: Marker, @@ -38,7 +38,7 @@ impl Default for Theme { fn default() -> Self { let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap(); - check_validation(theme.tab.validate()); + check_validation(theme.tabs.validate()); theme.preview.syntect_theme = expand_path(&theme.preview.syntect_theme); diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index 8839501f..32aa91d7 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -85,7 +85,7 @@ impl Manager { pub fn yank(&mut self, cut: bool) -> bool { self.yanked.0 = cut; self.yanked.1 = self.selected().into_iter().map(|f| f.url_owned()).collect(); - false + true } pub fn quit(&self, tasks: &Tasks, no_cwd_file: bool) -> bool { diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs index 67cfddd7..3069053d 100644 --- a/core/src/manager/tab.rs +++ b/core/src/manager/tab.rs @@ -458,18 +458,6 @@ impl Tab { pub fn in_selecting(&self) -> bool { self.mode.is_visual() || self.current.files.has_selected() } // --- Current - // TODO: remove this - #[inline] - pub fn name(&self) -> &str { - self - .current - .cwd - .file_name() - .and_then(|n| n.to_str()) - .or_else(|| self.current.cwd.to_str()) - .unwrap_or_default() - } - pub fn selected(&self) -> Vec<&File> { let pending = self.mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default(); let selected = self.current.files.selected(&pending, self.mode.is_unset()); diff --git a/plugin/Cargo.toml b/plugin/Cargo.toml index 1cfca9ff..1cb5b58b 100644 --- a/plugin/Cargo.toml +++ b/plugin/Cargo.toml @@ -9,7 +9,8 @@ core = { path = "../core" } shared = { path = "../shared" } # External dependencies -anyhow = "^1" -mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize" ] } -tracing = "^0" -ratatui = "^0" +anyhow = "^1" +mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize" ] } +ratatui = "^0" +tracing = "^0" +unicode-width = "^0" diff --git a/plugin/preset/components/folder.lua b/plugin/preset/components/folder.lua index 0aca2185..6a5a72ff 100644 --- a/plugin/preset/components/folder.lua +++ b/plugin/preset/components/folder.lua @@ -23,13 +23,24 @@ function Folder:markers(area, markers) local elements = {} local append = function(last) - local rect = ui.Rect { - x = area.x - 1, - y = area.y + last[1] - 1, - w = 1, - h = 1 + last[2] - last[1], - } - elements[#elements + 1] = ui.Paragraph(rect, {}):style(THEME.marker.selected) + local p = ui.Paragraph( + ui.Rect { + x = area.x - 1, + y = area.y + last[1] - 1, + w = 1, + h = 1 + last[2] - last[1], + }, + {} + ) + + if last[3] == 1 then + p = p:style(THEME.marker.copied) + elseif last[3] == 2 then + p = p:style(THEME.marker.cut) + elseif last[3] == 3 then + p = p:style(THEME.marker.selected) + end + elements[#elements + 1] = p end local last = { markers[1][1], markers[1][1], markers[1][2] } -- start, end, type @@ -89,9 +100,12 @@ function Folder:current(area) end items[#items + 1] = item - -- Mark selected/yanked files - if f:selected() then - markers[#markers + 1] = { i, 1 } + -- Mark yanked/selected files + local yanked = f:yanked() + if yanked ~= 0 then + markers[#markers + 1] = { i, yanked } + elseif f:selected() then + markers[#markers + 1] = { i, 3 } end end diff --git a/plugin/preset/components/header.lua b/plugin/preset/components/header.lua new file mode 100644 index 00000000..57dd5965 --- /dev/null +++ b/plugin/preset/components/header.lua @@ -0,0 +1,43 @@ +Header = {} + +function Header:cwd() + local cwd = cx.active.current.cwd + + local span + if not cwd.is_search then + span = ui.Span(utils.readable_path(tostring(cwd))) + else + span = ui.Span(string.format("%s (search: %s)", utils.readable_path(tostring(cwd)), cwd.frag)) + end + return span:fg("cyan") +end + +function Header:tabs() + local spans = {} + for i = 1, #cx.tabs do + local text = i + if THEME.tabs.max_width > 2 then + text = utils.truncate(text .. " " .. cx.tabs[i]:name(), THEME.tabs.max_width) + end + if i == cx.tabs.idx + 1 then + spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.tabs.active) + else + spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.tabs.inactive) + end + end + return ui.Line(spans) +end + +function Header:render(area) + local chunks = ui.Layout() + :direction(ui.Direction.HORIZONTAL) + :constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) }) + :split(area) + + local left = ui.Line { self:cwd() } + local right = ui.Line { self:tabs() } + return { + ui.Paragraph(chunks[1], { left }), + ui.Paragraph(chunks[2], { right }):align(ui.Alignment.RIGHT), + } +end diff --git a/plugin/preset/components/status.lua b/plugin/preset/components/status.lua index 3bb5faac..40e66048 100644 --- a/plugin/preset/components/status.lua +++ b/plugin/preset/components/status.lua @@ -113,14 +113,19 @@ function Status:progress(area, offset) h = 1, }) - local percent = 0 - if progress.processed ~= 0 then - percent = math.floor(progress.processed * 100 / progress.found) + if progress.fail == 0 then + gauge = gauge:gauge_style(THEME.status.progress_normal) + else + gauge = gauge:gauge_style(THEME.status.progress_error) + end + + local percent = 99 + if progress.found ~= 0 then + percent = math.min(99, progress.processed * 100 / progress.found) end return { gauge - :gauge_style(THEME.status.progress_normal) :percent(percent) :label(ui.Span(string.format("%3d%%, %d left", percent, left)):style(THEME.status.progress_label)), } diff --git a/plugin/preset/utils.lua b/plugin/preset/utils.lua index f7aa3d3f..05c3d4be 100644 --- a/plugin/preset/utils.lua +++ b/plugin/preset/utils.lua @@ -11,3 +11,14 @@ function utils.readable_size(size) end return string.format("%.1f %s", size, units[i]) end + +function utils.readable_path(path) + local home = os.getenv("HOME") + if home == nil then + return path + elseif string.sub(path, 1, #home) == home then + return "~" .. string.sub(path, #home + 1) + else + return path + end +end diff --git a/plugin/src/bindings/tab.rs b/plugin/src/bindings/active.rs similarity index 58% rename from plugin/src/bindings/tab.rs rename to plugin/src/bindings/active.rs index 278c3648..ae0604c7 100644 --- a/plugin/src/bindings/tab.rs +++ b/plugin/src/bindings/active.rs @@ -1,43 +1,20 @@ use core::Ctx; use config::{MANAGER, THEME}; -use mlua::{AnyUserData, Function, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; +use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value}; use super::{Range, Url}; use crate::{layout::Style, LUA}; -struct File(core::files::File); - -impl From<&core::files::File> for File { - fn from(value: &core::files::File) -> Self { Self(value.clone()) } -} - -impl UserData for File { - fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("url", |_, me| Ok(Url::from(me.0.url()))); - fields.add_field_method_get("length", |_, me| Ok(me.0.length())); - fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from))); - fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link())); - fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden())); - } -} - -pub struct Tab<'a, 'b> { +pub struct Active<'a, 'b> { scope: &'b mlua::Scope<'a, 'a>, cx: &'a core::Ctx, inner: &'a core::manager::Tab, } -impl<'a, 'b> Tab<'a, 'b> { +impl<'a, 'b> Active<'a, 'b> { pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { - reg.add_field_function_get("mode", |_, me| me.named_user_value::("mode")); - reg.add_field_function_get("parent", |_, me| me.named_user_value::("parent")); - reg.add_field_function_get("current", |_, me| me.named_user_value::("current")); - reg.add_field_function_get("preview", |_, me| me.named_user_value::("preview")); - })?; - LUA.register_userdata_type::(|reg| { reg.add_field_method_get("is_select", |_, me| Ok(me.is_select())); reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); @@ -57,58 +34,6 @@ impl<'a, 'b> Tab<'a, 'b> { reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); })?; - LUA.register_userdata_type::(|reg| { - reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); - - reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| { - let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| { - let files = me.borrow::()?; - let i = i + 1; - Ok(if i > files.len() { - mlua::Variadic::new() - } else { - mlua::Variadic::from_iter([i.into_lua(lua)?, File::from(&files[i - 1]).into_lua(lua)?]) - }) - })?; - Ok((iter, me, 0)) - }); - - reg.add_function("slice", |_, (me, skip, take): (AnyUserData, usize, usize)| { - let files = me.borrow::()?; - Ok(files.iter().skip(skip).take(take).map(File::from).collect::>()) - }); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("name", |_, me| { - Ok(me.url().file_name().map(|n| n.to_string_lossy().to_string())) - }); - reg.add_function("icon", |_, me: AnyUserData| { - me.named_user_value::("icon")?.call::<_, String>(()) - }); - reg.add_function("style", |_, me: AnyUserData| { - me.named_user_value::("style")?.call::<_, Style>(()) - }); - reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); - reg.add_function("selected", |_, me: AnyUserData| { - me.named_user_value::("selected")?.call::<_, bool>(me) - }); - reg.add_function("highlights", |_, me: AnyUserData| { - me.named_user_value::("highlights")?.call::<_, Value>(()) - }); - - reg.add_field_method_get("url", |_, me| Ok(Url::from(me.url()))); - reg.add_field_method_get("length", |_, me| Ok(me.length())); - reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from))); - reg.add_field_method_get("is_link", |_, me| Ok(me.is_link())); - reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); - - // Meta - reg.add_field_method_get("permissions", |_, me| { - Ok(shared::permissions(me.meta().permissions())) - }); - })?; - LUA.register_userdata_type::(|reg| { reg.add_field_function_get("folder", |_, me| me.named_user_value::("folder")); })?; @@ -116,13 +41,8 @@ impl<'a, 'b> Tab<'a, 'b> { Ok(()) } - pub(crate) fn new( - scope: &'b mlua::Scope<'a, 'a>, - - cx: &'a Ctx, - inner: &'a core::manager::Tab, - ) -> Self { - Self { scope, cx, inner } + pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, cx: &'a Ctx) -> Self { + Self { scope, cx, inner: cx.manager.active() } } pub(crate) fn make(&self) -> mlua::Result> { @@ -212,6 +132,20 @@ impl<'a, 'b> Tab<'a, 'b> { matches!(&folder.hovered, Some(f) if f.url() == inner.url()), )?; + ud.set_named_user_value( + "yanked", + self.scope.create_function(|_, ()| { + let (cut, urls) = self.cx.manager.yanked(); + Ok(if !urls.contains(inner.url()) { + 0u8 + } else if *cut { + 2u8 + } else { + 1u8 + }) + })?, + )?; + ud.set_named_user_value( "selected", self.scope.create_function(|_, me: AnyUserData| { diff --git a/plugin/src/bindings/bindings.rs b/plugin/src/bindings/bindings.rs index 71d40d38..76c3d7af 100644 --- a/plugin/src/bindings/bindings.rs +++ b/plugin/src/bindings/bindings.rs @@ -1,5 +1,7 @@ pub fn init() -> mlua::Result<()> { - super::tab::Tab::init()?; + super::active::Active::init()?; + super::files::Files::init()?; + super::tabs::Tabs::init()?; super::tasks::Tasks::init()?; Ok(()) diff --git a/plugin/src/bindings/files.rs b/plugin/src/bindings/files.rs new file mode 100644 index 00000000..d633c060 --- /dev/null +++ b/plugin/src/bindings/files.rs @@ -0,0 +1,83 @@ +use mlua::{AnyUserData, Function, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; + +use super::Url; +use crate::{layout::Style, LUA}; + +pub struct File(core::files::File); + +impl From<&core::files::File> for File { + fn from(value: &core::files::File) -> Self { Self(value.clone()) } +} + +impl UserData for File { + fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("url", |_, me| Ok(Url::from(me.0.url()))); + fields.add_field_method_get("length", |_, me| Ok(me.0.length())); + fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from))); + fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link())); + fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden())); + } +} + +pub struct Files; + +impl Files { + pub(crate) fn init() -> mlua::Result<()> { + LUA.register_userdata_type::(|reg| { + reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); + + reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| { + let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| { + let files = me.borrow::()?; + let i = i + 1; + Ok(if i > files.len() { + mlua::Variadic::new() + } else { + mlua::Variadic::from_iter([i.into_lua(lua)?, File::from(&files[i - 1]).into_lua(lua)?]) + }) + })?; + Ok((iter, me, 0)) + }); + + reg.add_function("slice", |_, (me, skip, take): (AnyUserData, usize, usize)| { + let files = me.borrow::()?; + Ok(files.iter().skip(skip).take(take).map(File::from).collect::>()) + }); + })?; + + LUA.register_userdata_type::(|reg| { + reg.add_field_method_get("name", |_, me| { + Ok(me.url().file_name().map(|n| n.to_string_lossy().to_string())) + }); + reg.add_function("icon", |_, me: AnyUserData| { + me.named_user_value::("icon")?.call::<_, String>(()) + }); + reg.add_function("style", |_, me: AnyUserData| { + me.named_user_value::("style")?.call::<_, Style>(()) + }); + reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); + reg.add_function("yanked", |_, me: AnyUserData| { + me.named_user_value::("yanked")?.call::<_, u8>(me) + }); + reg.add_function("selected", |_, me: AnyUserData| { + me.named_user_value::("selected")?.call::<_, bool>(me) + }); + reg.add_function("highlights", |_, me: AnyUserData| { + me.named_user_value::("highlights")?.call::<_, Value>(()) + }); + + reg.add_field_method_get("url", |_, me| Ok(Url::from(me.url()))); + reg.add_field_method_get("length", |_, me| Ok(me.length())); + reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from))); + reg.add_field_method_get("is_link", |_, me| Ok(me.is_link())); + reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); + + // Meta + reg.add_field_method_get("permissions", |_, me| { + Ok(shared::permissions(me.meta().permissions())) + }); + })?; + + Ok(()) + } +} diff --git a/plugin/src/bindings/mod.rs b/plugin/src/bindings/mod.rs index 2befe99a..60bbb912 100644 --- a/plugin/src/bindings/mod.rs +++ b/plugin/src/bindings/mod.rs @@ -1,11 +1,15 @@ #![allow(clippy::module_inception)] +mod active; mod bindings; +mod files; mod shared; -mod tab; +mod tabs; mod tasks; +pub use active::*; pub use bindings::*; +pub use files::*; pub use shared::*; -pub use tab::*; +pub use tabs::*; pub use tasks::*; diff --git a/plugin/src/bindings/shared.rs b/plugin/src/bindings/shared.rs index 5a02eff3..3806486b 100644 --- a/plugin/src/bindings/shared.rs +++ b/plugin/src/bindings/shared.rs @@ -25,6 +25,13 @@ impl From<&shared::Url> for Url { } impl UserData for Url { + fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("frag", |_, me| Ok(me.0.frag().map(ToOwned::to_owned))); + fields.add_field_method_get("is_regular", |_, me| Ok(me.0.is_regular())); + fields.add_field_method_get("is_search", |_, me| Ok(me.0.is_search())); + fields.add_field_method_get("is_archive", |_, me| Ok(me.0.is_archive())); + } + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_meta_function( MetaMethod::Eq, diff --git a/plugin/src/bindings/tabs.rs b/plugin/src/bindings/tabs.rs new file mode 100644 index 00000000..7b6aaec9 --- /dev/null +++ b/plugin/src/bindings/tabs.rs @@ -0,0 +1,96 @@ +use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value}; + +use crate::LUA; + +pub struct Tabs<'a, 'b> { + scope: &'b mlua::Scope<'a, 'a>, + + inner: &'a core::manager::Tabs, +} + +impl<'a, 'b> Tabs<'a, 'b> { + pub(crate) fn init() -> mlua::Result<()> { + LUA.register_userdata_type::(|reg| { + reg.add_field_method_get("idx", |_, me| Ok(me.idx())); + reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); + reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| { + let items = me.named_user_value::>("items")?; + Ok(items.get(index - 1).cloned()) + }); + })?; + + LUA.register_userdata_type::(|reg| { + reg.add_method("name", |_, me, ()| { + Ok( + me.current + .cwd + .file_name() + .map(|n| n.to_string_lossy()) + .or_else(|| Some(me.current.cwd.to_string_lossy())) + .unwrap_or_default() + .into_owned(), + ) + }); + + reg.add_field_function_get("mode", |_, me| me.named_user_value::("mode")); + reg.add_field_function_get("parent", |_, me| me.named_user_value::("parent")); + reg.add_field_function_get("current", |_, me| me.named_user_value::("current")); + reg.add_field_function_get("preview", |_, me| me.named_user_value::("preview")); + })?; + + Ok(()) + } + + pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a core::manager::Tabs) -> Self { + Self { scope, inner } + } + + pub(crate) fn make(&self) -> mlua::Result> { + let ud = self.scope.create_any_userdata_ref(self.inner)?; + + ud.set_named_user_value( + "items", + self.inner.iter().filter_map(|t| self.tab(t).ok()).collect::>(), + )?; + + Ok(ud) + } + + fn tab(&self, inner: &'a core::manager::Tab) -> mlua::Result> { + let ud = self.scope.create_any_userdata_ref(inner)?; + + ud.set_named_user_value("parent", inner.parent.as_ref().and_then(|p| self.folder(p).ok()))?; + ud.set_named_user_value("current", self.folder(&inner.current)?)?; + ud.set_named_user_value("preview", self.preview(inner)?)?; + + Ok(ud) + } + + pub(crate) fn folder(&self, inner: &'a core::manager::Folder) -> mlua::Result> { + let ud = self.scope.create_any_userdata_ref(inner)?; + ud.set_named_user_value("files", self.files(&inner.files)?)?; + + Ok(ud) + } + + fn files(&self, inner: &'a core::files::Files) -> mlua::Result> { + self.scope.create_any_userdata_ref(inner) + } + + fn preview(&self, tab: &'a core::manager::Tab) -> mlua::Result> { + let inner = tab.preview(); + + let ud = self.scope.create_any_userdata_ref(inner)?; + ud.set_named_user_value( + "folder", + inner + .lock + .as_ref() + .filter(|l| l.is_folder()) + .and_then(|l| tab.history(&l.url)) + .and_then(|f| self.folder(f).ok()), + )?; + + Ok(ud) + } +} diff --git a/plugin/src/bindings/tasks.rs b/plugin/src/bindings/tasks.rs index dd7ef7b9..3e60aa77 100644 --- a/plugin/src/bindings/tasks.rs +++ b/plugin/src/bindings/tasks.rs @@ -2,9 +2,13 @@ use mlua::{AnyUserData, LuaSerdeExt, UserDataFields}; use crate::LUA; -pub struct Tasks; +pub struct Tasks<'a, 'b> { + scope: &'b mlua::Scope<'a, 'a>, -impl Tasks { + inner: &'a core::tasks::Tasks, +} + +impl<'a, 'b> Tasks<'a, 'b> { pub(crate) fn init() -> mlua::Result<()> { LUA.register_userdata_type::(|reg| { reg.add_field_method_get("progress", |lua, me| lua.to_value(&me.progress)) @@ -13,10 +17,11 @@ impl Tasks { Ok(()) } - pub(crate) fn make<'a>( - scope: &mlua::Scope<'a, 'a>, - inner: &'a core::tasks::Tasks, - ) -> mlua::Result> { - scope.create_any_userdata_ref(inner) + pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a core::tasks::Tasks) -> Self { + Self { scope, inner } + } + + pub(crate) fn make(&self) -> mlua::Result> { + self.scope.create_any_userdata_ref(self.inner) } } diff --git a/plugin/src/components.rs b/plugin/src/components.rs index cbe73459..e7e61068 100644 --- a/plugin/src/components.rs +++ b/plugin/src/components.rs @@ -16,6 +16,22 @@ fn layout(values: Vec, buf: &mut ratatui::prelude::Buffer) -> mlua: Ok(()) } +// --- Status +pub struct Header; + +impl Header { + pub fn render( + self, + area: ratatui::layout::Rect, + buf: &mut ratatui::prelude::Buffer, + ) -> mlua::Result<()> { + let comp: Table = GLOBALS.get("Header")?; + let values: Vec = comp.call_method::<_, _>("render", Rect(area))?; + + layout(values, buf) + } +} + // --- Status pub struct Status; diff --git a/plugin/src/lib.rs b/plugin/src/lib.rs index f4926f6b..92d08af7 100644 --- a/plugin/src/lib.rs +++ b/plugin/src/lib.rs @@ -6,6 +6,7 @@ mod config; mod layout; mod plugin; mod scope; +mod utils; pub use components::*; use config::*; diff --git a/plugin/src/plugin.rs b/plugin/src/plugin.rs index dffcdd79..0f97e825 100644 --- a/plugin/src/plugin.rs +++ b/plugin/src/plugin.rs @@ -2,7 +2,7 @@ use anyhow::Result; use mlua::{Lua, Table}; use shared::RoCell; -use crate::{bindings, layout}; +use crate::{bindings, layout, utils}; pub(crate) static LUA: RoCell = RoCell::new(); pub(crate) static GLOBALS: RoCell = RoCell::new(); @@ -17,12 +17,14 @@ pub fn init() { lua.load(include_str!("../preset/inspect/inspect.lua")).exec()?; // Components - lua.load(include_str!("../preset/components/status.lua")).exec()?; lua.load(include_str!("../preset/components/folder.lua")).exec()?; + lua.load(include_str!("../preset/components/header.lua")).exec()?; + lua.load(include_str!("../preset/components/status.lua")).exec()?; // Initialize LUA.init(lua); GLOBALS.init(LUA.globals()); + utils::init()?; bindings::init()?; // Install diff --git a/plugin/src/scope.rs b/plugin/src/scope.rs index 00becba9..34e9f37f 100644 --- a/plugin/src/scope.rs +++ b/plugin/src/scope.rs @@ -7,8 +7,9 @@ use crate::{bindings, GLOBALS, LUA}; pub fn scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) { let _ = LUA.scope(|scope| { let tbl = LUA.create_table()?; - tbl.set("active", bindings::Tab::new(scope, cx, cx.manager.active()).make()?)?; - tbl.set("tasks", bindings::Tasks::make(scope, &cx.tasks)?)?; + tbl.set("active", bindings::Active::new(scope, cx).make()?)?; + tbl.set("tabs", bindings::Tabs::new(scope, cx.manager.tabs()).make()?)?; + tbl.set("tasks", bindings::Tasks::new(scope, &cx.tasks).make()?)?; GLOBALS.set("cx", tbl)?; Ok(f(scope)) diff --git a/plugin/src/utils.rs b/plugin/src/utils.rs new file mode 100644 index 00000000..37e7506b --- /dev/null +++ b/plugin/src/utils.rs @@ -0,0 +1,33 @@ +use std::ops::ControlFlow; + +use mlua::Table; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; + +use crate::{GLOBALS, LUA}; + +pub fn init() -> mlua::Result<()> { + let utils: Table = GLOBALS.get("utils")?; + + utils.set( + "truncate", + LUA.create_function(|_, (text, max): (String, usize)| { + let mut width = 0; + let flow = text.chars().try_fold(String::with_capacity(max), |mut s, c| { + width += c.width().unwrap_or(0); + if s.width() < max { + s.push(c); + ControlFlow::Continue(s) + } else { + ControlFlow::Break(s) + } + }); + + Ok(match flow { + ControlFlow::Break(s) => s, + ControlFlow::Continue(s) => s, + }) + })?, + )?; + + Ok(()) +}