From ce52bcdf943ccc1d41afc8f4f3508954e695a215 Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Wed, 1 May 2024 13:55:51 +0530 Subject: [PATCH] Revert vimlike scrolling Use stateful ui widget. --- src/app.rs | 74 +- src/compat.rs | 27 +- src/config.rs | 14 +- src/directory_buffer.rs | 159 +++-- src/dirs.rs | 2 +- src/explorer.rs | 51 +- src/input.rs | 2 +- src/runner.rs | 26 +- src/ui.rs | 1473 +++++++++++++++++++-------------------- 9 files changed, 907 insertions(+), 921 deletions(-) diff --git a/src/app.rs b/src/app.rs index 19e3268..dd521d2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -335,12 +335,12 @@ impl App { &config .general .initial_mode - .to_owned() + .clone() .unwrap_or_else(|| "default".into()), ) { Some(m) => m.clone().sanitized( config.general.read_only, - config.general.global_key_bindings.to_owned(), + config.general.global_key_bindings.clone(), ), None => { bail!("'default' mode is missing") @@ -351,7 +351,7 @@ impl App { &config .general .initial_layout - .to_owned() + .clone() .unwrap_or_else(|| "default".into()), ) { Some(l) => l.clone(), @@ -752,10 +752,8 @@ impl App { self.explorer_config.clone(), self.pwd.clone().into(), focus.as_ref().map(PathBuf::from), - self.directory_buffer - .as_ref() - .map(|d| d.scroll_state.get_focus()) - .unwrap_or(0), + self.directory_buffer.as_ref().map(|d| d.focus).unwrap_or(0), + self.config.general.vimlike_scrolling, ) { Ok(dir) => self.set_directory(dir), Err(e) => { @@ -794,7 +792,7 @@ impl App { } } - dir.scroll_state.set_focus(0); + dir.focus = 0; if save_history { if let Some(n) = self.focused_node() { @@ -812,7 +810,7 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.scroll_state.set_focus(dir.total.saturating_sub(1)); + dir.focus = dir.total.saturating_sub(1); if let Some(n) = dir.focused_node() { self.history = history.push(n.absolute_path.clone()); @@ -823,15 +821,15 @@ impl App { fn focus_previous(mut self) -> Result { let bounded = self.config.general.enforce_bounded_index_navigation; - if let Some(dir) = self.directory_buffer_mut() { - if dir.scroll_state.get_focus() == 0 { - if !bounded { - dir.scroll_state.set_focus(dir.total.saturating_sub(1)); + dir.focus = if dir.focus == 0 { + if bounded { + dir.focus + } else { + dir.total.saturating_sub(1) } } else { - dir.scroll_state - .set_focus(dir.scroll_state.get_focus().saturating_sub(1)); + dir.focus.saturating_sub(1) }; }; Ok(self) @@ -884,8 +882,7 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.scroll_state - .set_focus(dir.scroll_state.get_focus().saturating_sub(index)); + dir.focus = dir.focus.saturating_sub(index); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); } @@ -908,16 +905,18 @@ impl App { fn focus_next(mut self) -> Result { let bounded = self.config.general.enforce_bounded_index_navigation; - if let Some(dir) = self.directory_buffer_mut() { - if (dir.scroll_state.get_focus() + 1) == dir.total { - if !bounded { - dir.scroll_state.set_focus(0); + dir.focus = if (dir.focus + 1) == dir.total { + if bounded { + dir.focus + } else { + 0 } } else { - dir.scroll_state.set_focus(dir.scroll_state.get_focus() + 1); + dir.focus + 1 } }; + Ok(self) } @@ -968,12 +967,10 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.scroll_state.set_focus( - dir.scroll_state - .get_focus() - .saturating_add(index) - .min(dir.total.saturating_sub(1)), - ); + dir.focus = dir + .focus + .saturating_add(index) + .min(dir.total.saturating_sub(1)); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); @@ -998,7 +995,7 @@ impl App { fn follow_symlink(self) -> Result { if let Some(pth) = self .focused_node() - .and_then(|n| n.symlink.to_owned().map(|s| s.absolute_path)) + .and_then(|n| n.symlink.clone().map(|s| s.absolute_path)) { self.focus_path(&pth, true) } else { @@ -1241,8 +1238,7 @@ impl App { fn focus_by_index(mut self, index: usize) -> Result { let history = self.history.clone(); if let Some(dir) = self.directory_buffer_mut() { - dir.scroll_state - .set_focus(index.min(dir.total.saturating_sub(1))); + dir.focus = index.min(dir.total.saturating_sub(1)); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); } @@ -1279,7 +1275,7 @@ impl App { history = history.push(n.absolute_path.clone()); } } - dir_buf.scroll_state.set_focus(focus); + dir_buf.focus = focus; if save_history { if let Some(n) = dir_buf.focused_node() { self.history = history.push(n.absolute_path.clone()); @@ -1386,7 +1382,7 @@ impl App { self = self.push_mode(); self.mode = mode.sanitized( self.config.general.read_only, - self.config.general.global_key_bindings.to_owned(), + self.config.general.global_key_bindings.clone(), ); // Hooks @@ -1411,7 +1407,7 @@ impl App { self = self.push_mode(); self.mode = mode.sanitized( self.config.general.read_only, - self.config.general.global_key_bindings.to_owned(), + self.config.general.global_key_bindings.clone(), ); // Hooks @@ -1438,7 +1434,7 @@ impl App { fn switch_layout_builtin(mut self, layout: &str) -> Result { if let Some(l) = self.config.layouts.builtin.get(layout) { - self.layout = l.to_owned(); + self.layout = l.clone(); // Hooks if !self.hooks.on_layout_switch.is_empty() { @@ -1454,7 +1450,7 @@ impl App { fn switch_layout_custom(mut self, layout: &str) -> Result { if let Some(l) = self.config.layouts.get_custom(layout) { - self.layout = l.to_owned(); + self.layout = l.clone(); // Hooks if !self.hooks.on_layout_switch.is_empty() { @@ -1579,7 +1575,7 @@ impl App { pub fn select(mut self) -> Result { let count = self.selection.len(); - if let Some(n) = self.focused_node().map(|n| n.to_owned()) { + if let Some(n) = self.focused_node().cloned() { self.selection.insert(n); } @@ -1634,7 +1630,7 @@ impl App { pub fn un_select(mut self) -> Result { let count = self.selection.len(); - if let Some(n) = self.focused_node().map(|n| n.to_owned()) { + if let Some(n) = self.focused_node().cloned() { self.selection .retain(|s| s.absolute_path != n.absolute_path); } @@ -1808,7 +1804,7 @@ impl App { .config .general .initial_sorting - .to_owned() + .clone() .unwrap_or_default(); Ok(self) } diff --git a/src/compat.rs b/src/compat.rs index bafd50c..640503b 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -7,7 +7,7 @@ use crate::ui::block; use crate::ui::string_to_text; use crate::ui::Constraint; use crate::ui::ContentRendererArg; -use mlua::Lua; +use crate::ui::UI; use serde::{Deserialize, Serialize}; use tui::layout::Constraint as TuiConstraint; use tui::layout::Rect as TuiRect; @@ -60,12 +60,11 @@ pub struct CustomContent { /// A cursed function from crate::ui. pub fn draw_custom_content( + ui: &mut UI, f: &mut Frame, - screen_size: TuiRect, layout_size: TuiRect, app: &app::App, content: CustomContent, - lua: &Lua, ) { let config = app.config.general.panel_ui.default.clone(); let title = content.title; @@ -85,12 +84,12 @@ pub fn draw_custom_content( let ctx = ContentRendererArg { app: app.to_lua_ctx_light(), layout_size: layout_size.into(), - screen_size: screen_size.into(), + screen_size: ui.screen_size.into(), }; - let render = lua::serialize(lua, &ctx) + let render = lua::serialize(ui.lua, &ctx) .map(|arg| { - lua::call(lua, &render, arg).unwrap_or_else(|e| format!("{e:?}")) + lua::call(ui.lua, &render, arg).unwrap_or_else(|e| format!("{e:?}")) }) .unwrap_or_else(|e| e.to_string()); @@ -121,12 +120,12 @@ pub fn draw_custom_content( let ctx = ContentRendererArg { app: app.to_lua_ctx_light(), layout_size: layout_size.into(), - screen_size: screen_size.into(), + screen_size: ui.screen_size.into(), }; - let items = lua::serialize(lua, &ctx) + let items = lua::serialize(ui.lua, &ctx) .map(|arg| { - lua::call(lua, &render, arg) + lua::call(ui.lua, &render, arg) .unwrap_or_else(|e| vec![format!("{e:?}")]) }) .unwrap_or_else(|e| vec![e.to_string()]) @@ -161,7 +160,7 @@ pub fn draw_custom_content( let widths = widths .into_iter() - .map(|w| w.to_tui(screen_size, layout_size)) + .map(|w| w.to_tui(ui.screen_size, layout_size)) .collect::>(); let content = Table::new(rows, widths) @@ -182,12 +181,12 @@ pub fn draw_custom_content( let ctx = ContentRendererArg { app: app.to_lua_ctx_light(), layout_size: layout_size.into(), - screen_size: screen_size.into(), + screen_size: ui.screen_size.into(), }; - let rows = lua::serialize(lua, &ctx) + let rows = lua::serialize(ui.lua, &ctx) .map(|arg| { - lua::call(lua, &render, arg) + lua::call(ui.lua, &render, arg) .unwrap_or_else(|e| vec![vec![format!("{e:?}")]]) }) .unwrap_or_else(|e| vec![vec![e.to_string()]]) @@ -204,7 +203,7 @@ pub fn draw_custom_content( let widths = widths .into_iter() - .map(|w| w.to_tui(screen_size, layout_size)) + .map(|w| w.to_tui(ui.screen_size, layout_size)) .collect::>(); let mut content = Table::new(rows, &widths).block(block( diff --git a/src/config.rs b/src/config.rs index 05f9998..64663c5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -55,7 +55,7 @@ pub struct NodeTypeConfig { impl NodeTypeConfig { pub fn extend(mut self, other: &Self) -> Self { self.style = self.style.extend(&other.style); - self.meta.extend(other.meta.to_owned()); + self.meta.extend(other.meta.clone()); self } } @@ -85,11 +85,11 @@ pub struct NodeTypesConfig { impl NodeTypesConfig { pub fn get(&self, node: &Node) -> NodeTypeConfig { let mut node_type = if node.is_symlink { - self.symlink.to_owned() + self.symlink.clone() } else if node.is_dir { - self.directory.to_owned() + self.directory.clone() } else { - self.file.to_owned() + self.file.clone() }; let mut me = node.mime_essence.splitn(2, '/'); @@ -141,7 +141,7 @@ pub struct UiElement { impl UiElement { pub fn extend(mut self, other: &Self) -> Self { - self.format = other.format.to_owned().or(self.format); + self.format = other.format.clone().or(self.format); self.style = self.style.extend(&other.style); self } @@ -641,8 +641,8 @@ impl PanelUiConfig { pub fn extend(mut self, other: &Self) -> Self { self.title = self.title.extend(&other.title); self.style = self.style.extend(&other.style); - self.borders = other.borders.to_owned().or(self.borders); - self.border_type = other.border_type.to_owned().or(self.border_type); + self.borders = other.borders.clone().or(self.borders); + self.border_type = other.border_type.or(self.border_type); self.border_style = self.border_style.extend(&other.border_style); self } diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index aa60eeb..a625b40 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -1,32 +1,52 @@ use crate::node::Node; use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; use time::OffsetDateTime; -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] pub struct ScrollState { - current_focus: usize, + pub current_focus: usize, pub last_focus: Option, pub skipped_rows: usize, /* The number of visible next lines when scrolling towards either ends of the view port */ pub initial_preview_cushion: usize, + + pub vimlike_scrolling: bool, } impl ScrollState { - pub fn set_focus(&mut self, current_focus: usize) { + pub fn new(current_focus: usize, total: usize, vimlike_scrolling: bool) -> Self { + let initial_preview_cushion = 5; + Self { + current_focus, + last_focus: None, + skipped_rows: 0, + initial_preview_cushion, + vimlike_scrolling, + } + .update_skipped_rows(initial_preview_cushion + 1, total) + } + + pub fn set_focus(mut self, current_focus: usize) -> Self { self.last_focus = Some(self.current_focus); self.current_focus = current_focus; + self } - pub fn get_focus(&self) -> usize { - self.current_focus + pub fn update_skipped_rows(self, height: usize, total: usize) -> Self { + if self.vimlike_scrolling { + self.update_skipped_rows_vimlike(height, total) + } else { + self.update_skipped_rows_paginated(height) + } } - pub fn calc_skipped_rows( - &self, - height: usize, - total: usize, - vimlike_scrolling: bool, - ) -> usize { + pub fn update_skipped_rows_paginated(mut self, height: usize) -> Self { + self.skipped_rows = height * (self.current_focus / height.max(1)); + self + } + + pub fn update_skipped_rows_vimlike(mut self, height: usize, total: usize) -> Self { let preview_cushion = if height >= self.initial_preview_cushion * 3 { self.initial_preview_cushion } else if height >= 9 { @@ -47,45 +67,52 @@ impl ScrollState { .saturating_sub(preview_cushion + 1) .min(total.saturating_sub(preview_cushion + 1)); - if !vimlike_scrolling { - height * (self.current_focus / height.max(1)) - } else if last_focus.is_none() { - // Just entered the directory - 0 - } else if current_focus == 0 { + self.skipped_rows = if current_focus == 0 { // When focus goes to first node 0 } else if current_focus == total.saturating_sub(1) { // When focus goes to last node total.saturating_sub(height) - } else if (start_cushion_row..=end_cushion_row).contains(¤t_focus) { + } else if current_focus > start_cushion_row && current_focus <= end_cushion_row { // If within cushioned area; do nothing first_visible_row - } else if current_focus > last_focus.unwrap() { - // When scrolling down the cushioned area - if current_focus > total.saturating_sub(preview_cushion + 1) { - // When focusing the last nodes; always view the full last page - total.saturating_sub(height) - } else { - // When scrolling down the cushioned area without reaching the last nodes - current_focus.saturating_sub(height.saturating_sub(preview_cushion + 1)) - } - } else if current_focus < last_focus.unwrap() { - // When scrolling up the cushioned area - if current_focus < preview_cushion { - // When focusing the first nodes; always view the full first page - 0 - } else if current_focus > end_cushion_row { - // When scrolling up from the last rows; do nothing - first_visible_row - } else { - // When scrolling up the cushioned area without reaching the first nodes - current_focus.saturating_sub(preview_cushion) + } else if let Some(last_focus) = last_focus { + match current_focus.cmp(&last_focus) { + Ordering::Greater => { + // When scrolling down the cushioned area + if current_focus > total.saturating_sub(preview_cushion + 1) { + // When focusing the last nodes; always view the full last page + total.saturating_sub(height) + } else { + // When scrolling down the cushioned area without reaching the last nodes + current_focus + .saturating_sub(height.saturating_sub(preview_cushion + 1)) + } + } + + Ordering::Less => { + // When scrolling up the cushioned area + if current_focus < preview_cushion { + // When focusing the first nodes; always view the full first page + 0 + } else if current_focus > end_cushion_row { + // When scrolling up from the last rows; do nothing + first_visible_row + } else { + // When scrolling up the cushioned area without reaching the first nodes + current_focus.saturating_sub(preview_cushion) + } + } + Ordering::Equal => { + // Do nothing + first_visible_row + } } } else { - // If nothing matches; do nothing + // Just entered dir first_visible_row - } + }; + self } } @@ -94,31 +121,26 @@ pub struct DirectoryBuffer { pub parent: String, pub nodes: Vec, pub total: usize, - pub scroll_state: ScrollState, + pub focus: usize, #[serde(skip, default = "now")] pub explored_at: OffsetDateTime, } impl DirectoryBuffer { - pub fn new(parent: String, nodes: Vec, current_focus: usize) -> Self { + pub fn new(parent: String, nodes: Vec, focus: usize) -> Self { let total = nodes.len(); Self { parent, nodes, total, - scroll_state: ScrollState { - current_focus, - last_focus: None, - skipped_rows: 0, - initial_preview_cushion: 5, - }, + focus, explored_at: now(), } } pub fn focused_node(&self) -> Option<&Node> { - self.nodes.get(self.scroll_state.current_focus) + self.nodes.get(self.focus) } } @@ -133,36 +155,39 @@ mod tests { use super::*; #[test] - fn test_calc_skipped_rows_non_vimlike_scrolling() { + fn test_update_skipped_rows_paginated() { let state = ScrollState { current_focus: 10, last_focus: Some(8), skipped_rows: 0, initial_preview_cushion: 5, + vimlike_scrolling: false, }; let height = 5; - let total = 20; - let vimlike_scrolling = false; + let total = 100; - let result = state.calc_skipped_rows(height, total, vimlike_scrolling); - assert_eq!(result, height * (state.current_focus / height.max(1))); + let state = state.update_skipped_rows(height, total); + assert_eq!( + state.skipped_rows, + height * (state.current_focus / height.max(1)) + ); } #[test] - fn test_calc_skipped_rows_entered_directory() { + fn test_update_skipped_rows_entered_directory() { let state = ScrollState { - current_focus: 10, + current_focus: 100, last_focus: None, skipped_rows: 0, initial_preview_cushion: 5, + vimlike_scrolling: true, }; let height = 5; - let total = 20; - let vimlike_scrolling = true; + let total = 200; - let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + let result = state.update_skipped_rows(height, total).skipped_rows; assert_eq!(result, 0); } @@ -173,13 +198,13 @@ mod tests { last_focus: Some(8), skipped_rows: 5, initial_preview_cushion: 5, + vimlike_scrolling: true, }; let height = 5; let total = 20; - let vimlike_scrolling = true; - let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + let result = state.update_skipped_rows(height, total).skipped_rows; assert_eq!(result, 0); } @@ -190,13 +215,13 @@ mod tests { last_focus: Some(18), skipped_rows: 15, initial_preview_cushion: 5, + vimlike_scrolling: true, }; let height = 5; let total = 20; - let vimlike_scrolling = true; - let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + let result = state.update_skipped_rows(height, total).skipped_rows; assert_eq!(result, 15); } @@ -207,13 +232,13 @@ mod tests { last_focus: Some(10), skipped_rows: 10, initial_preview_cushion: 5, + vimlike_scrolling: true, }; let height = 5; let total = 20; - let vimlike_scrolling = true; - let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + let result = state.update_skipped_rows(height, total).skipped_rows; assert_eq!(result, 10); } @@ -224,13 +249,13 @@ mod tests { last_focus: Some(10), skipped_rows: 10, initial_preview_cushion: 5, + vimlike_scrolling: true, }; let height = 5; let total = 20; - let vimlike_scrolling = true; - let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + let result = state.update_skipped_rows(height, total).skipped_rows; assert_eq!(result, 7); } diff --git a/src/dirs.rs b/src/dirs.rs index 4bbad82..60a7eca 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -22,5 +22,5 @@ pub fn runtime_dir() -> PathBuf { else { return env::temp_dir(); }; - dir.to_owned() + dir.clone() } diff --git a/src/explorer.rs b/src/explorer.rs index 1f66d56..2ab45dc 100644 --- a/src/explorer.rs +++ b/src/explorer.rs @@ -45,6 +45,7 @@ pub(crate) fn explore_sync( parent: PathBuf, focused_path: Option, fallback_focus: usize, + vimlike_scrolling: bool, ) -> Result { let nodes = explore(&parent, &config)?; let focus_index = if config.searcher.is_some() { @@ -73,26 +74,33 @@ pub(crate) fn explore_async( parent: PathBuf, focused_path: Option, fallback_focus: usize, + vimlike_scrolling: bool, tx_msg_in: Sender, ) { thread::spawn(move || { - explore_sync(config, parent.clone(), focused_path, fallback_focus) - .and_then(|buf| { - tx_msg_in - .send(Task::new( - MsgIn::Internal(InternalMsg::SetDirectory(buf)), - None, - )) - .map_err(Error::new) - }) - .unwrap_or_else(|e| { - tx_msg_in - .send(Task::new( - MsgIn::External(ExternalMsg::LogError(e.to_string())), - None, - )) - .unwrap_or_default(); // Let's not panic if xplr closes. - }) + explore_sync( + config, + parent.clone(), + focused_path, + fallback_focus, + vimlike_scrolling, + ) + .and_then(|buf| { + tx_msg_in + .send(Task::new( + MsgIn::Internal(InternalMsg::SetDirectory(buf)), + None, + )) + .map_err(Error::new) + }) + .unwrap_or_else(|e| { + tx_msg_in + .send(Task::new( + MsgIn::External(ExternalMsg::LogError(e.to_string())), + None, + )) + .unwrap_or_default(); // Let's not panic if xplr closes. + }) }); } @@ -101,6 +109,7 @@ pub(crate) fn explore_recursive_async( parent: PathBuf, focused_path: Option, fallback_focus: usize, + vimlike_scrolling: bool, tx_msg_in: Sender, ) { explore_async( @@ -108,6 +117,7 @@ pub(crate) fn explore_recursive_async( parent.clone(), focused_path, fallback_focus, + vimlike_scrolling, tx_msg_in.clone(), ); if let Some(grand_parent) = parent.parent() { @@ -116,6 +126,7 @@ pub(crate) fn explore_recursive_async( grand_parent.into(), parent.file_name().map(|p| p.into()), 0, + vimlike_scrolling, tx_msg_in, ); } @@ -130,7 +141,7 @@ mod tests { let config = ExplorerConfig::default(); let path = PathBuf::from("."); - let r = explore_sync(config, path, None, 0); + let r = explore_sync(config, path, None, 0, false); assert!(r.is_ok()); } @@ -140,7 +151,7 @@ mod tests { let config = ExplorerConfig::default(); let path = PathBuf::from("/there/is/no/path"); - let r = explore_sync(config, path, None, 0); + let r = explore_sync(config, path, None, 0, false); assert!(r.is_err()); } @@ -169,7 +180,7 @@ mod tests { let path = PathBuf::from("."); let (tx_msg_in, rx_msg_in) = mpsc::channel(); - explore_async(config, path, None, 0, tx_msg_in.clone()); + explore_async(config, path, None, 0, false, tx_msg_in.clone()); let task = rx_msg_in.recv().unwrap(); let dbuf = extract_dirbuf_from_msg(task.msg); diff --git a/src/input.rs b/src/input.rs index dd1fba5..b9f9115 100644 --- a/src/input.rs +++ b/src/input.rs @@ -647,7 +647,7 @@ impl Key { Self::ShiftZ => Some('Z'), Self::Space => Some(' '), - Self::Special(c) => Some(c.to_owned()), + Self::Special(c) => Some(*c), _ => None, } diff --git a/src/runner.rs b/src/runner.rs index 90c5b53..a3c900e 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -8,7 +8,8 @@ use crate::explorer; use crate::lua; use crate::pipe; use crate::pwd_watcher; -use crate::ui; +use crate::ui::NO_COLOR; +use crate::ui::UI; use crate::yaml; use anyhow::{bail, Error, Result}; use crossterm::event; @@ -89,7 +90,7 @@ fn call( let focus_index = app .directory_buffer .as_ref() - .map(|d| d.scroll_state.get_focus()) + .map(|d| d.focus) .unwrap_or_default() .to_string(); @@ -279,16 +280,14 @@ impl Runner { app.explorer_config.clone(), app.pwd.clone().into(), self.focused_path, - app.directory_buffer - .as_ref() - .map(|d| d.scroll_state.get_focus()) - .unwrap_or(0), + app.directory_buffer.as_ref().map(|d| d.focus).unwrap_or(0), + app.config.general.vimlike_scrolling, tx_msg_in.clone(), ); tx_pwd_watcher.send(app.pwd.clone())?; let mut result = Ok(None); - let session_path = app.session_path.to_owned(); + let session_path = app.session_path.clone(); term::enable_raw_mode()?; @@ -344,6 +343,9 @@ impl Runner { None, ))?; + // UI + let mut ui = UI::new(&lua); + 'outer: for task in rx_msg_in { match app.handle_task(task) { Ok(a) => { @@ -433,8 +435,9 @@ impl Runner { .map(|n| n.relative_path.clone().into()), app.directory_buffer .as_ref() - .map(|d| d.scroll_state.get_focus()) + .map(|d| d.focus) .unwrap_or(0), + app.config.general.vimlike_scrolling, tx_msg_in.clone(), ); tx_pwd_watcher.send(app.pwd.clone())?; @@ -448,8 +451,9 @@ impl Runner { .map(|n| n.relative_path.clone().into()), app.directory_buffer .as_ref() - .map(|d| d.scroll_state.get_focus()) + .map(|d| d.focus) .unwrap_or(0), + app.config.general.vimlike_scrolling, tx_msg_in.clone(), ); tx_pwd_watcher.send(app.pwd.clone())?; @@ -479,7 +483,7 @@ impl Runner { tx_pwd_watcher.send(app.pwd.clone())?; // OSC 7: Change CWD - if !(*ui::NO_COLOR) { + if !(*NO_COLOR) { write!( terminal.backend_mut(), "\x1b]7;file://{}{}\x1b\\", @@ -496,7 +500,7 @@ impl Runner { } // UI - terminal.draw(|f| ui::draw(f, &mut app, &lua))?; + terminal.draw(|f| ui.draw(f, &app))?; } EnableMouse => { diff --git a/src/ui.rs b/src/ui.rs index 2f62610..d1813c8 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -60,6 +60,32 @@ pub fn string_to_text<'a>(string: String) -> Text<'a> { } } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Rect { + x: u16, + y: u16, + height: u16, + width: u16, +} + +impl From for Rect { + fn from(tui: TuiRect) -> Self { + Self { + x: tui.x, + y: tui.y, + height: tui.height, + width: tui.width, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct ContentRendererArg { + pub app: app::LuaContextLight, + pub screen_size: Rect, + pub layout_size: Rect, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct LayoutOptions { @@ -81,7 +107,7 @@ impl LayoutOptions { self.margin = other.margin.or(self.margin); self.horizontal_margin = other.horizontal_margin.or(self.horizontal_margin); self.vertical_margin = other.vertical_margin.or(self.vertical_margin); - self.constraints = other.constraints.to_owned().or(self.constraints); + self.constraints = other.constraints.clone().or(self.constraints); self } } @@ -154,7 +180,7 @@ impl Layout { }, ) => Self::Horizontal { config: sconfig.extend(oconfig), - splits: osplits.to_owned(), + splits: osplits.clone(), }, ( @@ -168,9 +194,9 @@ impl Layout { }, ) => Self::Vertical { config: sconfig.extend(oconfig), - splits: osplits.to_owned(), + splits: osplits.clone(), }, - (_, other) => other.to_owned(), + (_, other) => other.clone(), } } @@ -192,7 +218,7 @@ impl Layout { }, other => { if other == *target { - replacement.to_owned() + replacement.clone() } else { other } @@ -364,14 +390,10 @@ impl Style { pub fn extend(mut self, other: &Self) -> Self { self.fg = other.fg.or(self.fg); self.bg = other.bg.or(self.bg); - self.add_modifiers = extend_optional_modifiers( - self.add_modifiers, - other.add_modifiers.to_owned(), - ); - self.sub_modifiers = extend_optional_modifiers( - self.sub_modifiers, - other.sub_modifiers.to_owned(), - ); + self.add_modifiers = + extend_optional_modifiers(self.add_modifiers, other.add_modifiers.clone()); + self.sub_modifiers = + extend_optional_modifiers(self.sub_modifiers, other.sub_modifiers.clone()); self } } @@ -594,12 +616,12 @@ pub struct ResolvedNodeUiMetadata { impl From for ResolvedNodeUiMetadata { fn from(node: ResolvedNode) -> Self { Self { - absolute_path: node.absolute_path.to_owned(), - extension: node.extension.to_owned(), + absolute_path: node.absolute_path.clone(), + extension: node.extension.clone(), is_dir: node.is_dir, is_file: node.is_file, is_readonly: node.is_readonly, - mime_essence: node.mime_essence.to_owned(), + mime_essence: node.mime_essence.clone(), size: node.size, human_size: node.human_size, created: node.created, @@ -663,21 +685,21 @@ impl NodeUiMetadata { style: Style, ) -> Self { Self { - parent: node.parent.to_owned(), - relative_path: node.relative_path.to_owned(), - absolute_path: node.absolute_path.to_owned(), - extension: node.extension.to_owned(), + parent: node.parent.clone(), + relative_path: node.relative_path.clone(), + absolute_path: node.absolute_path.clone(), + extension: node.extension.clone(), is_symlink: node.is_symlink, is_broken: node.is_broken, is_dir: node.is_dir, is_file: node.is_file, is_readonly: node.is_readonly, - mime_essence: node.mime_essence.to_owned(), + mime_essence: node.mime_essence.clone(), size: node.size, - human_size: node.human_size.to_owned(), - permissions: node.permissions.to_owned(), - canonical: node.canonical.to_owned().map(ResolvedNode::into), - symlink: node.symlink.to_owned().map(ResolvedNode::into), + human_size: node.human_size.clone(), + permissions: node.permissions, + canonical: node.canonical.clone().map(ResolvedNode::into), + symlink: node.symlink.clone().map(ResolvedNode::into), created: node.created, last_modified: node.last_modified, uid: node.uid, @@ -703,7 +725,7 @@ pub fn block<'a>(config: PanelUiConfig, default_title: String) -> Block<'a> { .borders(TuiBorders::from_bits_truncate( config .borders - .to_owned() + .clone() .unwrap_or_default() .iter() .map(|b| b.bits()) @@ -718,797 +740,726 @@ pub fn block<'a>(config: PanelUiConfig, default_title: String) -> Block<'a> { .border_style(config.border_style) } -fn draw_table( - f: &mut Frame, - screen_size: TuiRect, - layout_size: TuiRect, - app: &mut app::App, - lua: &Lua, -) { - let panel_config = &app.config.general.panel_ui; - let config = panel_config.default.to_owned().extend(&panel_config.table); - let app_config = app.config.to_owned(); - let header_height = app_config.general.table.header.height.unwrap_or(1); - let height: usize = - (layout_size.height.max(header_height + 2) - (header_height + 2)).into(); - let row_style = app_config.general.table.row.style.to_owned(); - - let rows = app - .directory_buffer - .as_mut() - .map(|dir| { - dir.scroll_state.skipped_rows = dir.scroll_state.calc_skipped_rows( - height, - dir.total, - app.config.general.vimlike_scrolling, - ); - dir.nodes - .iter() - .enumerate() - .skip(dir.scroll_state.skipped_rows) - .take(height) - .map(|(index, node)| { - let is_focused = dir.scroll_state.get_focus() == index; - - let is_selected = app - .selection - .iter() - .any(|s| s.absolute_path == node.absolute_path); - - let is_first = index == 0; - let is_last = index == dir.total.max(1) - 1; - - let tree = app_config - .general - .table - .tree - .to_owned() - .map(|t| { - if is_last { - t.2.format - } else if is_first { - t.0.format - } else { - t.1.format - } - }) - .unwrap_or_default(); +pub struct UI<'lua> { + pub lua: &'lua Lua, + pub screen_size: TuiRect, +} - let node_type = app_config.node_types.get(node); +impl<'lua> UI<'lua> { + pub fn new(lua: &'lua Lua) -> Self { + let screen_size = Default::default(); + Self { lua, screen_size } + } +} - let (relative_index, is_before_focus, is_after_focus) = - match dir.scroll_state.get_focus().cmp(&index) { - Ordering::Greater => { - (dir.scroll_state.get_focus() - index, true, false) - } - Ordering::Less => { - (index - dir.scroll_state.get_focus(), false, true) - } - Ordering::Equal => (0, false, false), +impl UI<'_> { + fn draw_table(&mut self, f: &mut Frame, layout_size: TuiRect, app: &app::App) { + let panel_config = &app.config.general.panel_ui; + let config = panel_config.default.clone().extend(&panel_config.table); + let app_config = app.config.clone(); + let header_height = app_config.general.table.header.height.unwrap_or(1); + let height: usize = + (layout_size.height.max(header_height + 2) - (header_height + 2)).into(); + let row_style = app_config.general.table.row.style.clone(); + + let rows = app + .directory_buffer + .as_ref() + .map(|dir| { + dir.nodes + .iter() + .enumerate() + .skip(height * (dir.focus / height.max(1))) + .take(height) + .map(|(index, node)| { + let is_focused = dir.focus == index; + + let is_selected = app + .selection + .iter() + .any(|s| s.absolute_path == node.absolute_path); + + let is_first = index == 0; + let is_last = index == dir.total.max(1) - 1; + + let tree = app_config + .general + .table + .tree + .clone() + .map(|t| { + if is_last { + t.2.format + } else if is_first { + t.0.format + } else { + t.1.format + } + }) + .unwrap_or_default(); + + let node_type = app_config.node_types.get(node); + + let (relative_index, is_before_focus, is_after_focus) = + match dir.focus.cmp(&index) { + Ordering::Greater => (dir.focus - index, true, false), + Ordering::Less => (index - dir.focus, false, true), + Ordering::Equal => (0, false, false), + }; + + let (mut prefix, mut suffix, mut style) = { + let ui = app_config.general.default_ui.clone(); + (ui.prefix, ui.suffix, ui.style.extend(&node_type.style)) }; - let (mut prefix, mut suffix, mut style) = { - let ui = app_config.general.default_ui.to_owned(); - (ui.prefix, ui.suffix, ui.style.extend(&node_type.style)) - }; - - if is_focused && is_selected { - let ui = app_config.general.focus_selection_ui.to_owned(); - prefix = ui.prefix.to_owned().or(prefix); - suffix = ui.suffix.to_owned().or(suffix); - style = style.extend(&ui.style); - } else if is_selected { - let ui = app_config.general.selection_ui.to_owned(); - prefix = ui.prefix.to_owned().or(prefix); - suffix = ui.suffix.to_owned().or(suffix); - style = style.extend(&ui.style); - } else if is_focused { - let ui = app_config.general.focus_ui.to_owned(); - prefix = ui.prefix.to_owned().or(prefix); - suffix = ui.suffix.to_owned().or(suffix); - style = style.extend(&ui.style); - }; + if is_focused && is_selected { + let ui = app_config.general.focus_selection_ui.clone(); + prefix = ui.prefix.clone().or(prefix); + suffix = ui.suffix.clone().or(suffix); + style = style.extend(&ui.style); + } else if is_selected { + let ui = app_config.general.selection_ui.clone(); + prefix = ui.prefix.clone().or(prefix); + suffix = ui.suffix.clone().or(suffix); + style = style.extend(&ui.style); + } else if is_focused { + let ui = app_config.general.focus_ui.clone(); + prefix = ui.prefix.clone().or(prefix); + suffix = ui.suffix.clone().or(suffix); + style = style.extend(&ui.style); + }; - let meta = NodeUiMetadata::new( - node, - index, - relative_index, - is_before_focus, - is_after_focus, - tree.unwrap_or_default(), - prefix.unwrap_or_default(), - suffix.unwrap_or_default(), - is_selected, - is_focused, - dir.total, - node_type.meta, - style, - ); - - let cols = lua::serialize::(lua, &meta) - .map(|v| { - app_config - .general - .table - .row - .cols - .to_owned() - .unwrap_or_default() - .iter() - .filter_map(|c| { - c.format.as_ref().map(|f| { - let out = lua::call(lua, f, v.clone()) - .unwrap_or_else(|e| format!("{e:?}")); - (string_to_text(out), c.style.to_owned()) + let meta = NodeUiMetadata::new( + node, + index, + relative_index, + is_before_focus, + is_after_focus, + tree.unwrap_or_default(), + prefix.unwrap_or_default(), + suffix.unwrap_or_default(), + is_selected, + is_focused, + dir.total, + node_type.meta, + style, + ); + + let cols = lua::serialize::(self.lua, &meta) + .map(|v| { + app_config + .general + .table + .row + .cols + .clone() + .unwrap_or_default() + .iter() + .filter_map(|c| { + c.format.as_ref().map(|f| { + let out = lua::call(self.lua, f, v.clone()) + .unwrap_or_else(|e| format!("{e:?}")); + (string_to_text(out), c.style.clone()) + }) }) - }) - .collect::>() - }) - .unwrap_or_default() - .into_iter() - .map(|(text, style)| Cell::from(text).style(style)) - .collect::>(); + .collect::>() + }) + .unwrap_or_default() + .into_iter() + .map(|(text, style)| Cell::from(text).style(style)) + .collect::>(); + + Row::new(cols).style(row_style.clone()) + }) + .collect::>() + }) + .unwrap_or_default(); + + let table_constraints: Vec = app_config + .general + .table + .col_widths + .clone() + .unwrap_or_default() + .into_iter() + .map(|c| c.to_tui(self.screen_size, layout_size)) + .collect(); + + let pwd = if let Some(vroot) = app.vroot.as_ref() { + app.pwd.strip_prefix(vroot).unwrap_or(&app.pwd) + } else { + &app.pwd + } + .trim_matches('/'); - Row::new(cols).style(row_style.to_owned()) - }) - .collect::>() - }) - .unwrap_or_default(); - - let table_constraints: Vec = app_config - .general - .table - .col_widths - .to_owned() - .unwrap_or_default() - .into_iter() - .map(|c| c.to_tui(screen_size, layout_size)) - .collect(); - - let pwd = if let Some(vroot) = app.vroot.as_ref() { - app.pwd.strip_prefix(vroot).unwrap_or(&app.pwd) - } else { - &app.pwd + let pwd = path::escape(pwd); + + let vroot_indicator = if app.vroot.is_some() { "vroot:" } else { "" }; + + let node_count = app.directory_buffer.as_ref().map(|d| d.total).unwrap_or(0); + let node_count = if node_count == 0 { + String::new() + } else { + format!("({node_count}) ") + }; + + let table = Table::new(rows, table_constraints) + .style(app_config.general.table.style.clone()) + .highlight_style(app_config.general.focus_ui.style.clone()) + .column_spacing(app_config.general.table.col_spacing.unwrap_or_default()) + .block(block( + config, + format!(" {vroot_indicator}/{pwd} {node_count}"), + )); + + let table = table.clone().header( + Row::new( + app_config + .general + .table + .header + .cols + .clone() + .unwrap_or_default() + .iter() + .map(|c| { + Cell::from(c.format.clone().unwrap_or_default()) + .style(c.style.clone()) + }) + .collect::>(), + ) + .height(header_height) + .style(app_config.general.table.header.style.clone()), + ); + + f.render_widget(table, layout_size); } - .trim_matches('/'); - let pwd = path::escape(pwd); + fn draw_selection(&mut self, f: &mut Frame, layout_size: TuiRect, app: &app::App) { + let panel_config = &app.config.general.panel_ui; + let config = panel_config.default.clone().extend(&panel_config.selection); - let vroot_indicator = if app.vroot.is_some() { "vroot:" } else { "" }; + let selection_count = app.selection.len(); - let node_count = app.directory_buffer.as_ref().map(|d| d.total).unwrap_or(0); - let node_count = if node_count == 0 { - String::new() - } else { - format!("({node_count}) ") - }; - - let table = Table::new(rows, table_constraints) - .style(app_config.general.table.style.to_owned()) - .highlight_style(app_config.general.focus_ui.style.to_owned()) - .column_spacing(app_config.general.table.col_spacing.unwrap_or_default()) - .block(block( - config, - format!(" {vroot_indicator}/{pwd} {node_count}"), - )); + let selection: Vec = app + .selection + .iter() + .rev() + .take((layout_size.height.max(2) - 2).into()) + .rev() + .map(|n| { + let out = app + .config + .general + .selection + .item + .format + .as_ref() + .map(|f| { + lua::serialize::(self.lua, n) + .and_then(|n| lua::call(self.lua, f, n)) + .unwrap_or_else(|e| format!("{e:?}")) + }) + .unwrap_or_else(|| n.absolute_path.clone()); + string_to_text(out) + }) + .map(|i| { + ListItem::new(i).style(app.config.general.selection.item.style.clone()) + }) + .collect(); - let table = table.to_owned().header( - Row::new( - app_config - .general - .table - .header - .cols - .to_owned() - .unwrap_or_default() - .iter() - .map(|c| { - Cell::from(c.format.to_owned().unwrap_or_default()) - .style(c.style.to_owned()) - }) - .collect::>(), - ) - .height(header_height) - .style(app_config.general.table.header.style.to_owned()), - ); + // Selected items + let selection_count = if selection_count == 0 { + String::new() + } else { + format!("({selection_count}) ") + }; - f.render_widget(table, layout_size); -} + let selection_list = List::new(selection) + .block(block(config, format!(" Selection {selection_count}"))); + + f.render_widget(selection_list, layout_size); + } + + fn draw_help_menu(&mut self, f: &mut Frame, layout_size: TuiRect, app: &app::App) { + let panel_config = &app.config.general.panel_ui; + + let config = panel_config.default.clone().extend(&panel_config.help_menu); + + let help_menu_rows = app + .mode + .help_menu() + .into_iter() + .map(|l| match l { + HelpMenuLine::Paragraph(p) => Row::new([Cell::from(p)].to_vec()), + HelpMenuLine::KeyMap(k, remaps, h) => Row::new({ + if app.config.general.hide_remaps_in_help_menu { + [Cell::from(k), Cell::from(h)].to_vec() + } else { + [Cell::from(k), Cell::from(remaps.join("|")), Cell::from(h)] + .to_vec() + } + }), + }) + .collect::>(); + + let widths = if app.config.general.hide_remaps_in_help_menu { + vec![TuiConstraint::Percentage(20), TuiConstraint::Percentage(80)] + } else { + vec![ + TuiConstraint::Percentage(20), + TuiConstraint::Percentage(20), + TuiConstraint::Percentage(60), + ] + }; + let help_menu = Table::new(help_menu_rows, widths).block(block( + config, + format!(" Help [{}{}] ", &app.mode.name, read_only_indicator(app)), + )); + f.render_widget(help_menu, layout_size); + } -fn draw_selection( - 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 selection_count = app.selection.len(); - - let selection: Vec = app - .selection - .iter() - .rev() - .take((layout_size.height.max(2) - 2).into()) - .rev() - .map(|n| { - let out = app - .config - .general - .selection - .item - .format + fn draw_input_buffer( + &mut self, + f: &mut Frame, + layout_size: TuiRect, + app: &app::App, + ) { + if let Some(input) = app.input.buffer.as_ref() { + let panel_config = &app.config.general.panel_ui; + let config = panel_config + .default + .clone() + .extend(&panel_config.input_and_logs); + + let cursor_offset_left = config + .borders .as_ref() - .map(|f| { - lua::serialize::(lua, n) - .and_then(|n| lua::call(lua, f, n)) - .unwrap_or_else(|e| format!("{e:?}")) - }) - .unwrap_or_else(|| n.absolute_path.clone()); - string_to_text(out) - }) - .map(|i| { - ListItem::new(i).style(app.config.general.selection.item.style.to_owned()) - }) - .collect(); - - // Selected items - let selection_count = if selection_count == 0 { - String::new() - } else { - format!("({selection_count}) ") - }; + .map(|b| b.contains(&Border::Left)) + .unwrap_or(false) as u16 + + app.input.prompt.chars().count() as u16; - let selection_list = List::new(selection) - .block(block(config, format!(" Selection {selection_count}"))); + let cursor_offset_right = config + .borders + .as_ref() + .map(|b| b.contains(&Border::Right)) + .unwrap_or(false) as u16 + + 1; + + let offset_width = cursor_offset_left + cursor_offset_right; + let width = layout_size.width.max(offset_width) - offset_width; + let scroll = input.visual_scroll(width.into()) as u16; + + let input_buf = Paragraph::new(Line::from(vec![ + Span::styled( + app.input.prompt.clone(), + app.config.general.prompt.style.clone(), + ), + Span::raw(input.value()), + ])) + .scroll((0, scroll)) + .block(block( + config, + format!( + " Input [{}{}]{} ", + app.mode.name, + read_only_indicator(app), + selection_indicator(app), + ), + )); + + f.render_widget(input_buf, layout_size); + f.set_cursor( + // Put cursor past the end of the input text + layout_size.x + + (input.visual_cursor() as u16).min(width) + + cursor_offset_left, + // Move one line down, from the border to the input line + layout_size.y + 1, + ); + }; + } - f.render_widget(selection_list, layout_size); -} + fn draw_sort_n_filter( + &mut self, + f: &mut Frame, + layout_size: TuiRect, + app: &app::App, + ) { + let panel_config = &app.config.general.panel_ui; + let config = panel_config + .default + .clone() + .extend(&panel_config.sort_and_filter); + let ui = app.config.general.sort_and_filter_ui.clone(); + let filter_by: &IndexSet = &app.explorer_config.filters; + let sort_by: &IndexSet = &app.explorer_config.sorters; + let search = app.explorer_config.searcher.as_ref(); + + let defaultui = &ui.default_identifier; + let forwardui = defaultui + .clone() + .extend(&ui.sort_direction_identifiers.forward); + let reverseui = defaultui + .clone() + .extend(&ui.sort_direction_identifiers.reverse); + + let orderedui = defaultui + .clone() + .extend(&ui.search_direction_identifiers.ordered); + let unorderedui = defaultui + .clone() + .extend(&ui.search_direction_identifiers.unordered); + + let is_ordered_search = search.as_ref().map(|s| !s.unordered).unwrap_or(false); + + let mut spans = filter_by + .iter() + .map(|f| { + ui.filter_identifiers + .get(&f.filter) + .map(|u| { + let ui = defaultui.clone().extend(u); + ( + Span::styled( + ui.format.clone().unwrap_or_default(), + ui.style.clone(), + ), + Span::styled(f.input.clone(), ui.style), + ) + }) + .unwrap_or((Span::raw("f"), Span::raw(""))) + }) + .chain(search.iter().map(|s| { + ui.search_identifiers + .get(&s.algorithm) + .map(|u| { + let direction = if s.unordered { + &unorderedui + } else { + &orderedui + }; + let ui = defaultui.clone().extend(u); + let f = ui + .format + .as_ref() + .map(|f| format!("{f}{p}", p = &s.pattern)) + .unwrap_or_else(|| s.pattern.clone()); + ( + Span::styled(f, ui.style), + Span::styled( + direction.format.clone().unwrap_or_default(), + direction.style.clone(), + ), + ) + }) + .unwrap_or((Span::raw("/"), Span::raw(&s.pattern))) + })) + .chain( + sort_by + .iter() + .map(|s| { + let direction = if s.reverse { &reverseui } else { &forwardui }; + ui.sorter_identifiers + .get(&s.sorter) + .map(|u| { + let ui = defaultui.clone().extend(u); + ( + Span::styled( + ui.format.clone().unwrap_or_default(), + ui.style, + ), + Span::styled( + direction.format.clone().unwrap_or_default(), + direction.style.clone(), + ), + ) + }) + .unwrap_or((Span::raw("s"), Span::raw(""))) + }) + .take(if !is_ordered_search { sort_by.len() } else { 0 }), + ) + .zip(std::iter::repeat(Span::styled( + ui.separator.format.clone().unwrap_or_default(), + ui.separator.style.clone(), + ))) + .flat_map(|((a, b), c)| vec![a, b, c]) + .collect::>(); + + spans.pop(); + + let item_count = filter_by.len() + sort_by.len(); + let item_count = if item_count == 0 { + String::new() + } else { + format!("({item_count}) ") + }; -fn draw_help_menu( - f: &mut Frame, - _screen_size: TuiRect, - layout_size: TuiRect, - app: &app::App, - _: &Lua, -) { - let panel_config = &app.config.general.panel_ui; - - let config = panel_config - .default - .to_owned() - .extend(&panel_config.help_menu); - - let help_menu_rows = app - .mode - .help_menu() - .into_iter() - .map(|l| match l { - HelpMenuLine::Paragraph(p) => Row::new([Cell::from(p)].to_vec()), - HelpMenuLine::KeyMap(k, remaps, h) => Row::new({ - if app.config.general.hide_remaps_in_help_menu { - [Cell::from(k), Cell::from(h)].to_vec() - } else { - [Cell::from(k), Cell::from(remaps.join("|")), Cell::from(h)].to_vec() - } - }), - }) - .collect::>(); + let p = Paragraph::new(Line::from(spans)) + .block(block(config, format!(" Sort & filter {item_count}"))); - let widths = if app.config.general.hide_remaps_in_help_menu { - vec![TuiConstraint::Percentage(20), TuiConstraint::Percentage(80)] - } else { - vec![ - TuiConstraint::Percentage(20), - TuiConstraint::Percentage(20), - TuiConstraint::Percentage(60), - ] - }; - let help_menu = Table::new(help_menu_rows, widths).block(block( - config, - format!(" Help [{}{}] ", &app.mode.name, read_only_indicator(app)), - )); - f.render_widget(help_menu, layout_size); -} + f.render_widget(p, layout_size); + } -fn draw_input_buffer( - f: &mut Frame, - _screen_size: TuiRect, - layout_size: TuiRect, - app: &app::App, - _: &Lua, -) { - if let Some(input) = app.input.buffer.as_ref() { + fn draw_logs(&mut self, f: &mut Frame, layout_size: TuiRect, app: &app::App) { let panel_config = &app.config.general.panel_ui; let config = panel_config .default - .to_owned() + .clone() .extend(&panel_config.input_and_logs); + let logs_config = app.config.general.logs.clone(); + let logs = if app.logs_hidden { + vec![] + } else { + app.logs + .iter() + .rev() + .take(layout_size.height as usize) + .map(|log| { + let fd = format_description!("[hour]:[minute]:[second]"); + let time = + log.created_at.format(fd).unwrap_or_else(|_| "when?".into()); + let cfg = match log.level { + app::LogLevel::Info => &logs_config.info, + app::LogLevel::Warning => &logs_config.warning, + app::LogLevel::Success => &logs_config.success, + app::LogLevel::Error => &logs_config.error, + }; - let cursor_offset_left = config - .borders - .as_ref() - .map(|b| b.contains(&Border::Left)) - .unwrap_or(false) as u16 - + app.input.prompt.chars().count() as u16; + let prefix = + format!("{time}|{0}", cfg.format.clone().unwrap_or_default()); - let cursor_offset_right = config - .borders - .as_ref() - .map(|b| b.contains(&Border::Right)) - .unwrap_or(false) as u16 - + 1; - - let offset_width = cursor_offset_left + cursor_offset_right; - let width = layout_size.width.max(offset_width) - offset_width; - let scroll = input.visual_scroll(width.into()) as u16; - - let input_buf = Paragraph::new(Line::from(vec![ - Span::styled( - app.input.prompt.to_owned(), - app.config.general.prompt.style.to_owned(), - ), - Span::raw(input.value()), - ])) - .scroll((0, scroll)) - .block(block( + let padding = " ".repeat(prefix.chars().count()); + + let txt = log + .message + .lines() + .enumerate() + .map(|(i, line)| { + if i == 0 { + format!("{prefix}) {line}") + } else { + format!("{padding} {line}") + } + }) + .take(layout_size.height as usize) + .collect::>() + .join("\n"); + + ListItem::new(txt).style(cfg.style.clone()) + }) + .collect::>() + }; + + let logs_count = app.logs.len(); + let logs_count = if logs_count == 0 { + String::new() + } else { + format!(" ({logs_count})") + }; + + let logs_list = List::new(logs).block(block( config, format!( - " Input [{}{}]{} ", + " Logs{} [{}{}]{} ", + logs_count, app.mode.name, read_only_indicator(app), - selection_indicator(app), + selection_indicator(app) ), )); - f.render_widget(input_buf, layout_size); - f.set_cursor( - // Put cursor past the end of the input text - layout_size.x - + (input.visual_cursor() as u16).min(width) - + cursor_offset_left, - // Move one line down, from the border to the input line - layout_size.y + 1, - ); - }; -} - -fn draw_sort_n_filter( - f: &mut Frame, - _screen_size: TuiRect, - layout_size: TuiRect, - app: &app::App, - _: &Lua, -) { - let panel_config = &app.config.general.panel_ui; - let config = panel_config - .default - .to_owned() - .extend(&panel_config.sort_and_filter); - let ui = app.config.general.sort_and_filter_ui.to_owned(); - let filter_by: &IndexSet = &app.explorer_config.filters; - let sort_by: &IndexSet = &app.explorer_config.sorters; - let search = app.explorer_config.searcher.as_ref(); - - let defaultui = &ui.default_identifier; - let forwardui = defaultui - .to_owned() - .extend(&ui.sort_direction_identifiers.forward); - let reverseui = defaultui - .to_owned() - .extend(&ui.sort_direction_identifiers.reverse); - - let orderedui = defaultui - .to_owned() - .extend(&ui.search_direction_identifiers.ordered); - let unorderedui = defaultui - .to_owned() - .extend(&ui.search_direction_identifiers.unordered); - - let is_ordered_search = search.as_ref().map(|s| !s.unordered).unwrap_or(false); - - let mut spans = filter_by - .iter() - .map(|f| { - ui.filter_identifiers - .get(&f.filter) - .map(|u| { - let ui = defaultui.to_owned().extend(u); - ( - Span::styled( - ui.format.to_owned().unwrap_or_default(), - ui.style.to_owned(), - ), - Span::styled(f.input.to_owned(), ui.style), - ) - }) - .unwrap_or((Span::raw("f"), Span::raw(""))) - }) - .chain(search.iter().map(|s| { - ui.search_identifiers - .get(&s.algorithm) - .map(|u| { - let direction = if s.unordered { - &unorderedui - } else { - &orderedui - }; - let ui = defaultui.to_owned().extend(u); - let f = ui - .format - .as_ref() - .map(|f| format!("{f}{p}", p = &s.pattern)) - .unwrap_or_else(|| s.pattern.clone()); - ( - Span::styled(f, ui.style), - Span::styled( - direction.format.to_owned().unwrap_or_default(), - direction.style.to_owned(), - ), - ) - }) - .unwrap_or((Span::raw("/"), Span::raw(&s.pattern))) - })) - .chain( - sort_by - .iter() - .map(|s| { - let direction = if s.reverse { &reverseui } else { &forwardui }; - ui.sorter_identifiers - .get(&s.sorter) - .map(|u| { - let ui = defaultui.to_owned().extend(u); - ( - Span::styled( - ui.format.to_owned().unwrap_or_default(), - ui.style, - ), - Span::styled( - direction.format.to_owned().unwrap_or_default(), - direction.style.to_owned(), - ), - ) - }) - .unwrap_or((Span::raw("s"), Span::raw(""))) - }) - .take(if !is_ordered_search { sort_by.len() } else { 0 }), - ) - .zip(std::iter::repeat(Span::styled( - ui.separator.format.to_owned().unwrap_or_default(), - ui.separator.style.to_owned(), - ))) - .flat_map(|((a, b), c)| vec![a, b, c]) - .collect::>(); - - spans.pop(); - - let item_count = filter_by.len() + sort_by.len(); - let item_count = if item_count == 0 { - String::new() - } else { - format!("({item_count}) ") - }; - - let p = Paragraph::new(Line::from(spans)) - .block(block(config, format!(" Sort & filter {item_count}"))); - - f.render_widget(p, layout_size); -} + f.render_widget(logs_list, layout_size); + } -fn draw_logs( - f: &mut Frame, - _screen_size: TuiRect, - layout_size: TuiRect, - app: &app::App, - _: &Lua, -) { - let panel_config = &app.config.general.panel_ui; - let config = panel_config - .default - .to_owned() - .extend(&panel_config.input_and_logs); - let logs_config = app.config.general.logs.to_owned(); - let logs = if app.logs_hidden { - vec![] - } else { - app.logs - .iter() - .rev() - .take(layout_size.height as usize) - .map(|log| { - let fd = format_description!("[hour]:[minute]:[second]"); - let time = log.created_at.format(fd).unwrap_or_else(|_| "when?".into()); - let cfg = match log.level { - app::LogLevel::Info => &logs_config.info, - app::LogLevel::Warning => &logs_config.warning, - app::LogLevel::Success => &logs_config.success, - app::LogLevel::Error => &logs_config.error, - }; + fn draw_nothing(&mut self, f: &mut Frame, layout_size: TuiRect, app: &app::App) { + let panel_config = &app.config.general.panel_ui; + let config = panel_config.default.clone(); + let nothing = Paragraph::new("").block(block(config, "".into())); + f.render_widget(nothing, layout_size); + } - let prefix = - format!("{time}|{0}", cfg.format.to_owned().unwrap_or_default()); + fn draw_dynamic( + &mut self, + f: &mut Frame, + layout_size: TuiRect, + app: &app::App, + func: &str, + ) { + let ctx = ContentRendererArg { + app: app.to_lua_ctx_light(), + layout_size: layout_size.into(), + screen_size: self.screen_size.into(), + }; - let padding = " ".repeat(prefix.chars().count()); + let panel: CustomPanel = lua::serialize(self.lua, &ctx) + .and_then(|arg| lua::call(self.lua, func, arg)) + .unwrap_or_else(|e| CustomPanel::CustomParagraph { + ui: app.config.general.panel_ui.default.clone(), + body: format!("{e:?}"), + }); - let txt = log - .message - .lines() - .enumerate() - .map(|(i, line)| { - if i == 0 { - format!("{prefix}) {line}") - } else { - format!("{padding} {line}") - } - }) - .take(layout_size.height as usize) - .collect::>() - .join("\n"); + self.draw_static(f, layout_size, app, panel); + } - ListItem::new(txt).style(cfg.style.to_owned()) - }) - .collect::>() - }; + fn draw_static( + &mut self, + f: &mut Frame, + layout_size: TuiRect, + app: &app::App, + panel: CustomPanel, + ) { + let defaultui = app.config.general.panel_ui.default.clone(); + match panel { + CustomPanel::CustomParagraph { ui, body } => { + let config = defaultui.extend(&ui); + let body = string_to_text(body); + let content = Paragraph::new(body).block(block(config, "".into())); + f.render_widget(content, layout_size); + } - let logs_count = app.logs.len(); - let logs_count = if logs_count == 0 { - String::new() - } else { - format!(" ({logs_count})") - }; - - let logs_list = List::new(logs).block(block( - config, - format!( - " Logs{} [{}{}]{} ", - logs_count, - app.mode.name, - read_only_indicator(app), - selection_indicator(app) - ), - )); - - f.render_widget(logs_list, layout_size); -} + CustomPanel::CustomList { ui, body } => { + let config = defaultui.extend(&ui); -pub fn draw_nothing( - 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(); - let nothing = Paragraph::new("").block(block(config, "".into())); - f.render_widget(nothing, layout_size); -} + let items = body + .into_iter() + .map(string_to_text) + .map(ListItem::new) + .collect::>(); -pub fn draw_dynamic( - f: &mut Frame, - screen_size: TuiRect, - layout_size: TuiRect, - app: &mut app::App, - func: &str, - lua: &Lua, -) { - let ctx = ContentRendererArg { - app: app.to_lua_ctx_light(), - layout_size: layout_size.into(), - screen_size: screen_size.into(), - }; - - let panel: CustomPanel = lua::serialize(lua, &ctx) - .and_then(|arg| lua::call(lua, func, arg)) - .unwrap_or_else(|e| CustomPanel::CustomParagraph { - ui: app.config.general.panel_ui.default.clone(), - body: format!("{e:?}"), - }); + let content = List::new(items).block(block(config, "".into())); + f.render_widget(content, layout_size); + } - draw_static(f, screen_size, layout_size, app, panel, lua); -} + CustomPanel::CustomTable { + ui, + widths, + col_spacing, + body, + } => { + let config = defaultui.extend(&ui); + let rows = body + .into_iter() + .map(|cols| { + Row::new( + cols.into_iter() + .map(string_to_text) + .map(Cell::from) + .collect::>(), + ) + }) + .collect::>(); -pub fn draw_static( - f: &mut Frame, - screen_size: TuiRect, - layout_size: TuiRect, - app: &mut app::App, - panel: CustomPanel, - _lua: &Lua, -) { - let defaultui = app.config.general.panel_ui.default.clone(); - match panel { - CustomPanel::CustomParagraph { ui, body } => { - let config = defaultui.extend(&ui); - let body = string_to_text(body); - let content = Paragraph::new(body).block(block(config, "".into())); - f.render_widget(content, layout_size); - } + let widths = widths + .into_iter() + .map(|w| w.to_tui(self.screen_size, layout_size)) + .collect::>(); - CustomPanel::CustomList { ui, body } => { - let config = defaultui.extend(&ui); + let content = Table::new(rows, widths) + .column_spacing(col_spacing.unwrap_or(1)) + .block(block(config, "".into())); - let items = body - .into_iter() - .map(string_to_text) - .map(ListItem::new) - .collect::>(); + f.render_widget(content, layout_size); + } - let content = List::new(items).block(block(config, "".into())); - f.render_widget(content, layout_size); + CustomPanel::CustomLayout(layout) => { + self.draw_layout(layout, f, layout_size, app); + } } + } - CustomPanel::CustomTable { - ui, - widths, - col_spacing, - body, - } => { - let config = defaultui.extend(&ui); - let rows = body - .into_iter() - .map(|cols| { - Row::new( - cols.into_iter() - .map(string_to_text) - .map(Cell::from) - .collect::>(), + fn draw_layout( + &mut self, + layout: Layout, + f: &mut Frame, + layout_size: TuiRect, + app: &app::App, + ) { + match layout { + Layout::Nothing => self.draw_nothing(f, layout_size, app), + Layout::Table => self.draw_table(f, layout_size, app), + Layout::SortAndFilter => self.draw_sort_n_filter(f, layout_size, app), + Layout::HelpMenu => self.draw_help_menu(f, layout_size, app), + Layout::Selection => self.draw_selection(f, layout_size, app), + Layout::InputAndLogs => { + if app.input.buffer.is_some() { + self.draw_input_buffer(f, layout_size, app); + } else { + self.draw_logs(f, layout_size, app); + }; + } + Layout::Static(panel) => self.draw_static(f, layout_size, app, *panel), + Layout::Dynamic(ref func) => self.draw_dynamic(f, layout_size, app, func), + Layout::CustomContent(content) => { + draw_custom_content(self, f, layout_size, app, *content) + } + Layout::Horizontal { config, splits } => { + let chunks = TuiLayout::default() + .direction(Direction::Horizontal) + .constraints( + config + .constraints + .clone() + .unwrap_or_default() + .iter() + .map(|c| c.to_tui(self.screen_size, layout_size)) + .collect::>(), ) - }) - .collect::>(); - - let widths = widths - .into_iter() - .map(|w| w.to_tui(screen_size, layout_size)) - .collect::>(); - - let content = Table::new(rows, widths) - .column_spacing(col_spacing.unwrap_or(1)) - .block(block(config, "".into())); - - f.render_widget(content, layout_size); - } + .horizontal_margin( + config + .horizontal_margin + .or(config.margin) + .unwrap_or_default(), + ) + .vertical_margin( + config.vertical_margin.or(config.margin).unwrap_or_default(), + ) + .split(layout_size); - CustomPanel::CustomLayout(layout) => { - draw_layout(layout, f, screen_size, layout_size, app, _lua); - } - } -} + splits + .into_iter() + .zip(chunks.iter()) + .for_each(|(split, chunk)| self.draw_layout(split, f, *chunk, app)); + } -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct Rect { - x: u16, - y: u16, - height: u16, - width: u16, -} + Layout::Vertical { config, splits } => { + let chunks = TuiLayout::default() + .direction(Direction::Vertical) + .constraints( + config + .constraints + .clone() + .unwrap_or_default() + .iter() + .map(|c| c.to_tui(self.screen_size, layout_size)) + .collect::>(), + ) + .horizontal_margin( + config + .horizontal_margin + .or(config.margin) + .unwrap_or_default(), + ) + .vertical_margin( + config.vertical_margin.or(config.margin).unwrap_or_default(), + ) + .split(layout_size); -impl From for Rect { - fn from(tui: TuiRect) -> Self { - Self { - x: tui.x, - y: tui.y, - height: tui.height, - width: tui.width, + splits + .into_iter() + .zip(chunks.iter()) + .for_each(|(split, chunk)| self.draw_layout(split, f, *chunk, app)); + } } } -} - -#[derive(Debug, Clone, Serialize)] -pub struct ContentRendererArg { - pub app: app::LuaContextLight, - pub screen_size: Rect, - pub layout_size: Rect, -} -pub fn draw_layout( - layout: Layout, - f: &mut Frame, - screen_size: TuiRect, - layout_size: TuiRect, - app: &mut app::App, - lua: &Lua, -) { - match layout { - Layout::Nothing => draw_nothing(f, screen_size, layout_size, app, lua), - Layout::Table => draw_table(f, screen_size, layout_size, app, lua), - Layout::SortAndFilter => { - 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::InputAndLogs => { - if app.input.buffer.is_some() { - draw_input_buffer(f, screen_size, layout_size, app, lua); - } else { - draw_logs(f, screen_size, layout_size, app, lua); - }; - } - Layout::Static(panel) => { - draw_static(f, screen_size, layout_size, app, *panel, lua) - } - Layout::Dynamic(ref func) => { - draw_dynamic(f, screen_size, layout_size, app, func, lua) - } - Layout::CustomContent(content) => { - draw_custom_content(f, screen_size, layout_size, app, *content, lua) - } - Layout::Horizontal { config, splits } => { - let chunks = TuiLayout::default() - .direction(Direction::Horizontal) - .constraints( - config - .constraints - .to_owned() - .unwrap_or_default() - .iter() - .map(|c| c.to_tui(screen_size, layout_size)) - .collect::>(), - ) - .horizontal_margin( - config - .horizontal_margin - .or(config.margin) - .unwrap_or_default(), - ) - .vertical_margin( - config.vertical_margin.or(config.margin).unwrap_or_default(), - ) - .split(layout_size); - - splits - .into_iter() - .zip(chunks.iter()) - .for_each(|(split, chunk)| { - draw_layout(split, f, screen_size, *chunk, app, lua) - }); - } - - Layout::Vertical { config, splits } => { - let chunks = TuiLayout::default() - .direction(Direction::Vertical) - .constraints( - config - .constraints - .to_owned() - .unwrap_or_default() - .iter() - .map(|c| c.to_tui(screen_size, layout_size)) - .collect::>(), - ) - .horizontal_margin( - config - .horizontal_margin - .or(config.margin) - .unwrap_or_default(), - ) - .vertical_margin( - config.vertical_margin.or(config.margin).unwrap_or_default(), - ) - .split(layout_size); - - splits - .into_iter() - .zip(chunks.iter()) - .for_each(|(split, chunk)| { - draw_layout(split, f, screen_size, *chunk, app, lua) - }); - } + pub fn draw(&mut self, f: &mut Frame, app: &app::App) { + self.screen_size = f.size(); + let layout = app.mode.layout.as_ref().unwrap_or(&app.layout).clone(); + self.draw_layout(layout, f, self.screen_size, app); } } -pub fn draw(f: &mut Frame, app: &mut app::App, lua: &Lua) { - let screen_size = f.size(); - let layout = app.mode.layout.as_ref().unwrap_or(&app.layout).to_owned(); - - draw_layout(layout, f, screen_size, screen_size, app, lua); -} - #[cfg(test)] mod tests { use super::*; @@ -1543,7 +1494,7 @@ mod tests { }; assert_eq!( - a.to_owned().extend(&b), + a.clone().extend(&b), Style { fg: Some(Color::Red), bg: Some(Color::Blue), @@ -1563,7 +1514,7 @@ mod tests { ); assert_eq!( - a.to_owned().extend(&c), + a.clone().extend(&c), Style { fg: Some(Color::Cyan), bg: Some(Color::Magenta),