From 3a9739da32c3f3815d62d826acb3623fed2f6e75 Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Wed, 3 Jan 2024 17:29:34 +0530 Subject: [PATCH 1/4] Minor fix --- src/app.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1ae8dfc..02584d7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -414,13 +414,19 @@ impl App { prompt: config.general.prompt.format.clone().unwrap_or_default(), }; + let hist = if &pwd == "/" { + pwd.clone() + } else { + format!("{0}/", &pwd) + }; + let mut app = Self { bin, version: VERSION.to_string(), config, vroot, initial_vroot, - pwd: pwd.clone(), + pwd, initial_pwd, directory_buffer: Default::default(), last_focus: Default::default(), @@ -435,7 +441,7 @@ impl App { explorer_config, logs: Default::default(), logs_hidden: Default::default(), - history: History::default().push(format!("{pwd}/")), + history: History::default().push(hist), last_modes: Default::default(), hostname, hooks, From 2ce7a91b714edc567628e45606c0922054ac33cd Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Sat, 13 Jan 2024 20:29:47 +0530 Subject: [PATCH 2/4] Fix ScrollUpHalf --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 02584d7..49f3b33 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1301,7 +1301,7 @@ impl App { } pub fn scroll_up_half(mut self) -> Result { - self.msg_out.push_back(MsgOut::ScrollUp); + self.msg_out.push_back(MsgOut::ScrollUpHalf); Ok(self) } From 19df482b4d0f4eac8a8fede94811a34bd8594d19 Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Sun, 14 Jan 2024 12:44:56 +0530 Subject: [PATCH 3/4] Implement preview pane Display preview in the selection pane when selection is empty. Adds: - xplr.util.preview - xplr.fn.builtin.fmt_general_preview_renderer - xplr.config.general.panel_ui.preview --- docs/en/src/configuration.md | 14 +++++ docs/en/src/general-config.md | 47 +++++++++++++++ docs/en/src/xplr.util.md | 32 +++++++++-- src/config.rs | 13 +++++ src/init.lua | 55 +++++++++++++++++- src/lua/util.rs | 105 ++++++++++++++++++++++++++++++++-- src/ui.rs | 55 +++++++++++++++++- 7 files changed, 308 insertions(+), 13 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..da387df 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 @@ -542,6 +554,41 @@ Style of the selection panel borders. Type: [Style](https://xplr.dev/en/style) +#### xplr.config.general.panel_ui.preview.title.format + +The content for the preview panel title. + +Type: nullable string + +#### xplr.config.general.panel_ui.preview.title.style + +Style of the preview panel title. + +Type: [Style](https://xplr.dev/en/style) + +#### xplr.config.general.panel_ui.preview.borders + +#### xplr.config.general.panel_ui.preview.style + +Style of the preview panel. + +Type: [Style](https://xplr.dev/en/style) +Defines where to show borders for the preview panel. + +Type: nullable list of [Border](https://xplr.dev/en/borders#border) + +#### xplr.config.general.panel_ui.preview.border_type + +Type of the borders for preview panel. + +Type: nullable [Border Type](https://xplr.dev/en/borders#border-type) + +#### xplr.config.general.panel_ui.preview.border_style + +Style of the preview panel borders. + +Type: [Style](https://xplr.dev/en/style) + #### xplr.config.general.panel_ui.sort_and_filter.title.format The content for the sort & filter panel title. diff --git a/docs/en/src/xplr.util.md b/docs/en/src/xplr.util.md index 5c1684f..7c9ec8a 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 }, +}) +-- "Preview of /foo" +``` + [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..1c0557f 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 { @@ -275,6 +282,9 @@ pub struct PanelUi { #[serde(default)] pub selection: PanelUiConfig, + #[serde(default)] + pub preview: PanelUiConfig, + #[serde(default)] pub input_and_logs: PanelUiConfig, @@ -318,6 +328,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..197111e 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) @@ -634,12 +644,43 @@ xplr.config.general.panel_ui.selection.borders = nil -- Type of the borders for selection panel. -- -- Type: nullable [Border Type](https://xplr.dev/en/borders#border-type) -xplr.config.general.panel_ui.selection.border_type = nil +xplr.config.general.panel_ui.selection.border_type = "Double" -- Style of the selection panel borders. -- -- Type: [Style](https://xplr.dev/en/style) -xplr.config.general.panel_ui.selection.border_style = {} +xplr.config.general.panel_ui.selection.border_style = { + fg = "Red", +} + +-- The content for the preview panel title. +-- +-- Type: nullable string +xplr.config.general.panel_ui.preview.title.format = nil + +-- Style of the preview panel title. +-- +-- Type: [Style](https://xplr.dev/en/style) +xplr.config.general.panel_ui.preview.title.style = {} + +-- Style of the preview panel. +-- +-- Type: [Style](https://xplr.dev/en/style) +xplr.config.general.panel_ui.preview.style = {} +-- Defines where to show borders for the preview panel. +-- +-- Type: nullable list of [Border](https://xplr.dev/en/borders#border) +xplr.config.general.panel_ui.preview.borders = nil + +-- Type of the borders for preview panel. +-- +-- Type: nullable [Border Type](https://xplr.dev/en/borders#border-type) +xplr.config.general.panel_ui.preview.border_type = nil + +-- Style of the preview panel borders. +-- +-- Type: [Style](https://xplr.dev/en/style) +xplr.config.general.panel_ui.preview.border_style = {} -- The content for the sort & filter panel title. -- @@ -2948,6 +2989,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 +3003,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..30e6861 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; @@ -25,8 +26,11 @@ use serde::{Deserialize, Serialize}; use serde_json as json; use serde_yaml as yaml; use std::borrow::Cow; +use std::io::BufRead; +use std::iter; use std::path::PathBuf; use std::process::Command; +use std::{fs, io}; /// Get the xplr version details. /// @@ -195,12 +199,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 +234,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 +828,105 @@ 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 }, +/// }) +/// -- "Preview of /foo" +/// ``` +pub fn preview<'a>(util: Table<'a>, lua: &Lua) -> Result> { + fn format_node(node: &Node) -> String { + format!( + "{} +{} +{} +{}:{}", + node.mime_essence, + node.permissions.to_string(), + node.human_size, + node.uid, + node.gid + ) + } + + 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_file { + let file = fs::File::open(&node.absolute_path)?; + let reader = io::BufReader::new(file); + let mut lines = vec![]; + for line in reader.lines() { + let Ok(mut line) = line else { + return Ok(format_node(&node)); + }; + line.truncate(size.width.into()); + lines.push(line); + if lines.len() >= size.height.into() { + break; + } + } + lines.join("\n") + } else if node.is_dir { + match fs::read_dir(node.absolute_path) { + Ok(nodes) => iter::once(node.relative_path) + .chain( + nodes + .map(|d| { + d.ok() + .map(|d| d.file_name().to_string_lossy().to_string()) + .map(|n| format!(" {n}")) + .unwrap_or_else(|| "???".into()) + }) + .take(size.height.into()), + ) + .collect::>() + .join("\n"), + Err(err) => err.to_string(), + } + } else if node.is_symlink && node.is_broken { + "-> ×".into() + } else if node.is_symlink { + node.symlink + .map(|s| format!(" -> {}", s.absolute_path)) + .unwrap_or_default() + } else { + format_node(&node) + }; + + 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 +961,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..6ad28d8 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.preview); + + 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); From bb7053baa9b89632550ba9f5d405b361376e637a Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Wed, 17 Jan 2024 12:48:45 +0530 Subject: [PATCH 4/4] Better preview --- src/init.lua | 10 +++------ src/lua/util.rs | 57 +++++++++++++++++++++++++++++++++++-------------- src/ui.rs | 8 +++---- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/init.lua b/src/init.lua index 197111e..651c8f0 100644 --- a/src/init.lua +++ b/src/init.lua @@ -264,7 +264,7 @@ xplr.config.general.preview.renderer.format = "builtin.fmt_general_preview_rende -- Style for preview panel. -- -- Type: [Style](https://xplr.dev/en/style) -xplr.config.general.preview.renderer.style = {} +xplr.config.general.preview.renderer.style = { add_modifiers = { "Dim" } } -- The default search algorithm -- @@ -348,9 +348,7 @@ xplr.config.general.sort_and_filter_ui.separator.format = " › " -- The style of the separator for the Sort & filter panel. -- -- Type: [Style](https://xplr.dev/en/style) -xplr.config.general.sort_and_filter_ui.separator.style = { - add_modifiers = { "Dim" }, -} +xplr.config.general.sort_and_filter_ui.separator.style = { add_modifiers = { "Dim" } } -- The content of the default identifier in Sort & filter panel. -- @@ -649,9 +647,7 @@ xplr.config.general.panel_ui.selection.border_type = "Double" -- Style of the selection panel borders. -- -- Type: [Style](https://xplr.dev/en/style) -xplr.config.general.panel_ui.selection.border_style = { - fg = "Red", -} +xplr.config.general.panel_ui.selection.border_style = { fg = "Red" } -- The content for the preview panel title. -- diff --git a/src/lua/util.rs b/src/lua/util.rs index 30e6861..7e0d1c6 100644 --- a/src/lua/util.rs +++ b/src/lua/util.rs @@ -848,15 +848,12 @@ pub fn permissions_octal<'a>(util: Table<'a>, lua: &Lua) -> Result> { pub fn preview<'a>(util: Table<'a>, lua: &Lua) -> Result> { fn format_node(node: &Node) -> String { format!( - "{} -{} -{} -{}:{}", + "• T: {}\n• P: {}\n• O: {}:{}\n• S: {}", node.mime_essence, node.permissions.to_string(), - node.human_size, node.uid, - node.gid + node.gid, + node.human_size, ) } @@ -868,8 +865,19 @@ pub fn preview<'a>(util: Table<'a>, lua: &Lua) -> Result> { let size = args.layout_size; - let preview = if node.is_file { - let file = fs::File::open(&node.absolute_path)?; + let preview = if node + .canonical + .as_ref() + .map(|c| c.is_file) + .unwrap_or(node.is_file) + { + let path = node + .canonical + .as_ref() + .map(|c| &c.absolute_path) + .unwrap_or(&node.absolute_path); + + let file = fs::File::open(path)?; let reader = io::BufReader::new(file); let mut lines = vec![]; for line in reader.lines() { @@ -883,16 +891,33 @@ pub fn preview<'a>(util: Table<'a>, lua: &Lua) -> Result> { } } lines.join("\n") - } else if node.is_dir { - match fs::read_dir(node.absolute_path) { - Ok(nodes) => iter::once(node.relative_path) + } else if node + .canonical + .as_ref() + .map(|c| c.is_dir) + .unwrap_or(node.is_dir) + { + let path = node + .symlink + .as_ref() + .map(|c| &c.absolute_path) + .unwrap_or(&node.relative_path); + + match fs::read_dir(path) { + Ok(nodes) => iter::once(format!("▼ {}/", path)) .chain( nodes + .filter_map(|d| d.ok()) .map(|d| { - d.ok() - .map(|d| d.file_name().to_string_lossy().to_string()) - .map(|n| format!(" {n}")) - .unwrap_or_else(|| "???".into()) + if d.file_type() + .ok() + .map(|t| t.is_dir()) + .unwrap_or(false) + { + format!(" ▷ {}/", d.file_name().to_string_lossy()) + } else { + format!(" {}", d.file_name().to_string_lossy()) + } }) .take(size.height.into()), ) @@ -904,7 +929,7 @@ pub fn preview<'a>(util: Table<'a>, lua: &Lua) -> Result> { "-> ×".into() } else if node.is_symlink { node.symlink - .map(|s| format!(" -> {}", s.absolute_path)) + .map(|s| format!("-> {}", s.absolute_path)) .unwrap_or_default() } else { format_node(&node) diff --git a/src/ui.rs b/src/ui.rs index 6ad28d8..dbd0294 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -894,10 +894,10 @@ fn draw_preview( String::new() }; - let preview = - Paragraph::new(string_to_text(preview)).block(block(config, " Preview ".into())); - - f.render_widget(preview, layout_size); + let mut text = string_to_text(preview); + text.patch_style(app.config.general.preview.renderer.style.to_owned().into()); + let block = Paragraph::new(text).block(block(config, " Preview ".into())); + f.render_widget(block, layout_size); } fn draw_selection(