From 216a86f593615fc6a3287584436a0288571a33ea Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Sun, 14 Jan 2024 12:44:56 +0530 Subject: [PATCH] Implement preview pane Display preview in the selection pane when selection is empty. Adds: - xplr.util.preview - xplr.fn.builtin.fmt_general_preview_renderer --- docs/en/src/configuration.md | 14 +++++++++ docs/en/src/general-config.md | 12 ++++++++ docs/en/src/xplr.util.md | 32 ++++++++++++++++---- src/config.rs | 10 +++++++ src/init.lua | 20 +++++++++++++ src/lua/util.rs | 56 +++++++++++++++++++++++++++++++---- src/ui.rs | 55 +++++++++++++++++++++++++++++++++- 7 files changed, 188 insertions(+), 11 deletions(-) diff --git a/docs/en/src/configuration.md b/docs/en/src/configuration.md index ef84ab0..a5a8b00 100644 --- a/docs/en/src/configuration.md +++ b/docs/en/src/configuration.md @@ -47,6 +47,20 @@ that can be overwritten. Tries to auto complete the path in the input buffer +#### xplr.fn.builtin.fmt_general_selection_item + +Formats each node in the selection + +#### xplr.fn.builtin.fmt_general_preview_renderer + +Renders the focused node in preview pane + +See: [xplr.util.preview](https://xplr.dev/en/xplr.util#xplrutilpreview) + +The focused node is passed as the node value, and layout_hight is passed +dynamically. +When there is no item under focus, the node value will be nil. + #### xplr.fn.builtin.fmt_general_table_row_cols_0 Renders the first column in the table diff --git a/docs/en/src/general-config.md b/docs/en/src/general-config.md index 75fa861..4e8faf7 100644 --- a/docs/en/src/general-config.md +++ b/docs/en/src/general-config.md @@ -193,6 +193,18 @@ Style for each item in the selection list. Type: [Style](https://xplr.dev/en/style) +#### xplr.config.general.preview.renderer.format + +Preview renderer for the path under focus. + +Type: nullable string + +#### xplr.config.general.preview.renderer.style + +Style for preview panel. + +Type: [Style](https://xplr.dev/en/style) + #### xplr.config.general.search.algorithm The default search algorithm diff --git a/docs/en/src/xplr.util.md b/docs/en/src/xplr.util.md index 5c1684f..fae3e7e 100644 --- a/docs/en/src/xplr.util.md +++ b/docs/en/src/xplr.util.md @@ -110,12 +110,12 @@ xplr.util.path_split(".././foo") ### xplr.util.node -Get [Node][5] information of a given path. +Get [Node][2] information of a given path. Doesn't check if the path exists. Returns nil if the path is "/". Errors out if absolute path can't be obtained. -Type: function( path:string ) -> [Node][5]|nil +Type: function( path:string ) -> [Node][2]|nil Example: @@ -129,9 +129,9 @@ xplr.util.node("/") ### xplr.util.node_type -Get the configured [Node Type][6] of a given [Node][5]. +Get the configured [Node Type][6] of a given [Node][2]. -Type: function( [Node][5], [xplr.config.node_types][7]|nil ) -> [Node Type][6] +Type: function( [Node][2], [xplr.config.node_types][7]|nil ) -> [Node Type][6] If the second argument is missing, global config `xplr.config.node_types` will be used. @@ -501,11 +501,33 @@ xplr.util.permissions_octal(app.focused_node.permission) -- { 0, 7, 5, 4 } ``` +### xplr.util.preview + +Renders a preview of the given node as string. + +You probably want to use it inside the function mentioned in +[xplr.config.general.preview.renderer.format][9], or inside a +[custom dynamic layout][10]. + +Type: function( { node:[Node][2]|nil, layout_size:[Size][5] } ) -> string + +Example: + +```lua +xplr.util.preview({ + node = xplr.util.node("/foo"), + layout_size = { x = 0, y = 0, height = 10, width = 10 }, +}) +-- "..." +``` + [1]: https://xplr.dev/en/lua-function-calls#explorer-config [2]: https://xplr.dev/en/lua-function-calls#node [3]: https://xplr.dev/en/style [4]: https://xplr.dev/en/layout -[5]: https://xplr.dev/en/lua-function-calls#node +[5]: https://xplr.dev/en/layout#size [6]: https://xplr.dev/en/node-type [7]: https://xplr.dev/en/node_types [8]: https://xplr.dev/en/column-renderer#permission +[9]: https://xplr.dev/en/general-config#xplrconfiggeneralpreviewrendererformat +[10]: https://xplr.dev/en/layout#dynamic diff --git a/src/config.rs b/src/config.rs index db08f86..17f48ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -189,6 +189,13 @@ pub struct SelectionConfig { pub item: UiElement, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PreviewConfig { + #[serde(default)] + pub renderer: UiElement, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SearchConfig { @@ -318,6 +325,9 @@ pub struct GeneralConfig { #[serde(default)] pub selection: SelectionConfig, + #[serde(default)] + pub preview: PreviewConfig, + #[serde(default)] pub search: SearchConfig, diff --git a/src/init.lua b/src/init.lua index 89d29aa..adff7f5 100644 --- a/src/init.lua +++ b/src/init.lua @@ -256,6 +256,16 @@ xplr.config.general.selection.item.format = "builtin.fmt_general_selection_item" -- Type: [Style](https://xplr.dev/en/style) xplr.config.general.selection.item.style = {} +-- Preview renderer for the path under focus. +-- +-- Type: nullable string +xplr.config.general.preview.renderer.format = "builtin.fmt_general_preview_renderer" + +-- Style for preview panel. +-- +-- Type: [Style](https://xplr.dev/en/style) +xplr.config.general.preview.renderer.style = {} + -- The default search algorithm -- -- Type: [Search Algorithm](https://xplr.dev/en/searching#algorithm) @@ -2948,6 +2958,7 @@ xplr.fn.builtin.try_complete_path = function(m) end end +-- Formats each node in the selection xplr.fn.builtin.fmt_general_selection_item = function(n) local nl = xplr.util.paint("\\n", { add_modifiers = { "Italic", "Dim" } }) local sh_config = { with_prefix_dots = true, without_suffix_dots = true } @@ -2961,6 +2972,15 @@ xplr.fn.builtin.fmt_general_selection_item = function(n) return xplr.util.paint(shortened:gsub("\n", nl), style) end +-- Renders the focused node in preview pane +-- +-- See: [xplr.util.preview](https://xplr.dev/en/xplr.util#xplrutilpreview) +-- +-- The focused node is passed as the node value, and layout_hight is passed +-- dynamically. +-- When there is no item under focus, the node value will be nil. +xplr.fn.builtin.fmt_general_preview_renderer = xplr.util.preview + -- Renders the first column in the table xplr.fn.builtin.fmt_general_table_row_cols_0 = function(m) local r = "" diff --git a/src/lua/util.rs b/src/lua/util.rs index 21d8ce7..cf3197e 100644 --- a/src/lua/util.rs +++ b/src/lua/util.rs @@ -10,6 +10,7 @@ use crate::permissions::Octal; use crate::permissions::Permissions; use crate::ui; use crate::ui::Layout; +use crate::ui::PreviewRendererArgs; use crate::ui::Style; use crate::ui::WrapOptions; use anyhow::Result; @@ -195,12 +196,12 @@ pub fn path_split<'a>(util: Table<'a>, lua: &Lua) -> Result> { Ok(util) } -/// Get [Node][5] information of a given path. +/// Get [Node][2] information of a given path. /// Doesn't check if the path exists. /// Returns nil if the path is "/". /// Errors out if absolute path can't be obtained. /// -/// Type: function( path:string ) -> [Node][5]|nil +/// Type: function( path:string ) -> [Node][2]|nil /// /// Example: /// @@ -230,9 +231,9 @@ pub fn node<'a>(util: Table<'a>, lua: &Lua) -> Result> { Ok(util) } -/// Get the configured [Node Type][6] of a given [Node][5]. +/// Get the configured [Node Type][6] of a given [Node][2]. /// -/// Type: function( [Node][5], [xplr.config.node_types][7]|nil ) -> [Node Type][6] +/// Type: function( [Node][2], [xplr.config.node_types][7]|nil ) -> [Node Type][6] /// /// If the second argument is missing, global config `xplr.config.node_types` /// will be used. @@ -824,15 +825,59 @@ pub fn permissions_octal<'a>(util: Table<'a>, lua: &Lua) -> Result> { Ok(util) } +/// Renders a preview of the given node as string. +/// +/// You probably want to use it inside the function mentioned in +/// [xplr.config.general.preview.renderer.format][9], or inside a +/// [custom dynamic layout][10]. +/// +/// Type: function( { node:[Node][2]|nil, layout_size:[Size][5] } ) -> string +/// +/// Example: +/// +/// ```lua +/// xplr.util.preview({ +/// node = xplr.util.node("/foo"), +/// layout_size = { x = 0, y = 0, height = 10, width = 10 }, +/// }) +/// -- "..." +/// ``` +pub fn preview<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(|lua, args: Table| { + let args: PreviewRendererArgs = lua.from_value(Value::Table(args))?; + let Some(node) = args.node else { + return Ok("".into()); + }; + + let size = args.layout_size; + + let preview = if node.is_dir { + "Directory".to_string() + } else if node.is_file { + "File".to_string() + } else if node.is_symlink { + "Symlink".to_string() + } else { + "Unknown".to_string() + }; + + Ok(preview) + })?; + util.set("preview", func)?; + Ok(util) +} + /// /// [1]: https://xplr.dev/en/lua-function-calls#explorer-config /// [2]: https://xplr.dev/en/lua-function-calls#node /// [3]: https://xplr.dev/en/style /// [4]: https://xplr.dev/en/layout -/// [5]: https://xplr.dev/en/lua-function-calls#node +/// [5]: https://xplr.dev/en/layout#size /// [6]: https://xplr.dev/en/node-type /// [7]: https://xplr.dev/en/node_types /// [8]: https://xplr.dev/en/column-renderer#permission +/// [9]: https://xplr.dev/en/general-config#xplrconfiggeneralpreviewrendererformat +/// [10]: https://xplr.dev/en/layout#dynamic pub(crate) fn create_table(lua: &Lua) -> Result { let mut util = lua.create_table()?; @@ -867,6 +912,7 @@ pub(crate) fn create_table(lua: &Lua) -> Result
{ util = layout_replace(util, lua)?; util = permissions_rwx(util, lua)?; util = permissions_octal(util, lua)?; + util = preview(util, lua)?; Ok(util) } diff --git a/src/ui.rs b/src/ui.rs index ba7ecb3..5b7928a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -853,6 +853,53 @@ fn draw_table( f.render_widget(table, layout_size); } +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct PreviewRendererArgs { + pub node: Option, + pub layout_size: TuiRect, +} + +fn draw_preview( + f: &mut Frame, + _screen_size: TuiRect, + layout_size: TuiRect, + app: &app::App, + lua: &Lua, +) { + let panel_config = &app.config.general.panel_ui; + let config = panel_config + .default + .to_owned() + .extend(&panel_config.selection); + + let renderer = app + .config + .general + .preview + .renderer + .format + .clone() + .unwrap_or_default(); + + let preview = if let Some(node) = app.focused_node() { + let args = PreviewRendererArgs { + node: Some(node.to_owned()), + layout_size, + }; + lua::serialize::(lua, &args) + .and_then(|args| lua::call(lua, &renderer, args)) + .unwrap_or_else(|e| format!("{e:?}")) + } else { + String::new() + }; + + let preview = + Paragraph::new(string_to_text(preview)).block(block(config, " Preview ".into())); + + f.render_widget(preview, layout_size); +} + fn draw_selection( f: &mut Frame, _screen_size: TuiRect, @@ -1360,7 +1407,13 @@ pub fn draw_layout( draw_sort_n_filter(f, screen_size, layout_size, app, lua) } Layout::HelpMenu => draw_help_menu(f, screen_size, layout_size, app, lua), - Layout::Selection => draw_selection(f, screen_size, layout_size, app, lua), + Layout::Selection => { + if app.selection.is_empty() { + draw_preview(f, screen_size, layout_size, app, lua); + } else { + draw_selection(f, screen_size, layout_size, app, lua); + } + } Layout::InputAndLogs => { if app.input.buffer.is_some() { draw_input_buffer(f, screen_size, layout_size, app, lua);