diff options
Diffstat (limited to 'src/app/header_footer.rs')
-rw-r--r-- | src/app/header_footer.rs | 650 |
1 files changed, 400 insertions, 250 deletions
diff --git a/src/app/header_footer.rs b/src/app/header_footer.rs index a17a346..3969712 100644 --- a/src/app/header_footer.rs +++ b/src/app/header_footer.rs @@ -1,233 +1,273 @@ mod inner { use anyhow::{Context, Result}; - use unicode_segmentation::UnicodeSegmentation; use crate::app::{Status, Tab}; + use crate::common::{ + UtfWidth, HELP_FIRST_SENTENCE, HELP_SECOND_SENTENCE, LOG_FIRST_SENTENCE, + LOG_SECOND_SENTENCE, + }; use crate::event::ActionMap; - use crate::modes::Selectable; - use crate::modes::{shorten_path, Display}; - use crate::modes::{Content, FilterKind}; - - /// Action for every element of the first line. - /// It should match the order of the `FirstLine::make_string` static method. - const HEADER_ACTIONS: [ActionMap; 4] = [ - ActionMap::Cd, - ActionMap::Rename, - ActionMap::Search, - ActionMap::Filter, - ]; - - const FOOTER_ACTIONS: [ActionMap; 7] = [ - ActionMap::Nothing, // position - ActionMap::Ncdu, - ActionMap::Sort, - ActionMap::LazyGit, - ActionMap::Jump, - ActionMap::Sort, - ActionMap::Nothing, // for out of bounds - ]; - - pub trait ClickableLine: ClickableLineInner { - fn strings(&self) -> &Vec<String>; + use crate::modes::{ + shorten_path, ColoredText, Content, Display, FileInfo, FilterKind, Preview, Search, + Selectable, TextKind, + }; + + #[derive(Clone, Copy)] + pub enum HorizontalAlign { + Left, + Right, + } + + /// A footer or header element that can be clicked + /// + /// Holds a text and an action. + /// It knows where it's situated on the line + #[derive(Clone)] + pub struct ClickableString { + text: String, + action: ActionMap, + width: usize, + left: usize, + right: usize, + } + + impl ClickableString { + /// Creates a new `ClickableString`. + /// It calculates its position with `col` and `align`. + /// If left aligned, the text size will be added to `col` and the text will span from col to col + width. + /// otherwise, the text will spawn from col - width to col. + fn new(text: String, align: HorizontalAlign, action: ActionMap, col: usize) -> Self { + let width = text.utf_width(); + let (left, right) = match align { + HorizontalAlign::Left => (col, col + width), + HorizontalAlign::Right => (col - width - 3, col - 3), + }; + Self { + text, + action, + width, + left, + right, + } + } + + /// Text content of the element. + pub fn text(&self) -> &str { + self.text.as_str() + } + + pub fn col(&self) -> usize { + self.left + } + + pub fn width(&self) -> usize { + self.width + } + } + + /// A line of element that can be clicked on. + pub trait ClickableLine { + /// Reference to the elements + fn elems(&self) -> &Vec<ClickableString>; /// Action for each associated file. fn action(&self, col: usize, is_right: bool) -> &ActionMap { - let mut sum = 0; let offset = self.offset(is_right); - for (index, size) in self.sizes().iter().enumerate() { - sum += size; - if col <= sum + offset { - return self.action_index(index); + let col = col - offset; + for clickable in self.elems().iter() { + if clickable.left <= col && col < clickable.right { + return &clickable.action; } } + + crate::log_info!("no action found"); &ActionMap::Nothing } - } - - pub trait ClickableLineInner { - fn width(&self) -> usize; - fn sizes(&self) -> &Vec<usize>; - fn action_index(&self, index: usize) -> &ActionMap; - + /// Full width of the terminal + fn full_width(&self) -> usize; + /// canvas width of the window + fn canvas_width(&self) -> usize; + /// used offset. + /// 1 if the text is on left tab, + /// width / 2 + 2 otherwise. fn offset(&self, is_right: bool) -> usize { if is_right { - self.width() / 2 + 2 + self.full_width() / 2 + 2 } else { 1 } } - - /// Returns the lengths of every displayed string. - /// It uses `unicode_segmentation::UnicodeSegmentation::graphemes` - /// to measure used space. - /// It's not the number of bytes used since those strings may contain - /// any UTF-8 grapheme. - fn make_sizes(strings: &[String]) -> Vec<usize> { - strings - .iter() - .map(|s| s.graphemes(true).collect::<Vec<&str>>().iter().len()) - .collect() - } } - /// A bunch of strings displaying the status of the current directory. - /// It provides an `action` method to make the first line clickable. + /// Header for tree & directory display mode. pub struct Header { - strings: Vec<String>, - sizes: Vec<usize>, - width: usize, - actions: Vec<ActionMap>, - } - - impl ClickableLine for Header { - /// Vector of displayed strings. - fn strings(&self) -> &Vec<String> { - self.strings.as_ref() - } + elems: Vec<ClickableString>, + canvas_width: usize, + full_width: usize, } - impl ClickableLineInner for Header { - fn sizes(&self) -> &Vec<usize> { - self.sizes.as_ref() - } - - fn action_index(&self, index: usize) -> &ActionMap { - self.actions(index) - } - - fn width(&self) -> usize { - self.width - } - } - - // should it be held somewhere ? impl Header { - /// Create the strings associated with the selected tab directory + /// Creates a new header pub fn new(status: &Status, tab: &Tab) -> Result<Self> { - let (width, _) = status.internal_settings.term.term_size()?; - let (strings, actions) = Self::make_strings_actions(tab, width)?; - let sizes = Self::make_sizes(&strings); + let full_width = status.internal_settings.term_size()?.0; + let canvas_width = status.canvas_width()?; + let elems = Self::make_elems(tab, canvas_width)?; Ok(Self { - strings, - sizes, - width, - actions, + elems, + canvas_width, + full_width, }) } - fn actions(&self, index: usize) -> &ActionMap { - &self.actions[index] - } - - // TODO! refactor using a `struct thing { string, start, end, action }` - /// Returns a bunch of displayable strings. - /// Watchout: - /// 1. the length of the vector MUST BE the length of `ACTIONS` minus one. - /// 2. the order must be respected. - fn make_strings_actions(tab: &Tab, width: usize) -> Result<(Vec<String>, Vec<ActionMap>)> { - let mut strings = vec![ - Self::string_shorten_path(tab)?, - Self::string_first_row_selected_file(tab, width)?, - ]; - let mut actions: Vec<ActionMap> = HEADER_ACTIONS[0..2].into(); - if let Some(searched) = &tab.searched { - strings.push(Self::string_searched(searched)); - actions.push(HEADER_ACTIONS[2].clone()); - } - if !matches!(tab.settings.filter, FilterKind::All) { - strings.push(Self::string_filter(tab)); - actions.push(HEADER_ACTIONS[3].clone()); + fn make_elems(tab: &Tab, width: usize) -> Result<Vec<ClickableString>> { + let mut left = 0; + let mut right = width; + let shorten_path = Self::elem_shorten_path(tab, left)?; + left += shorten_path.width(); + + let filename = Self::elem_filename(tab, width, left)?; + + let mut elems = vec![shorten_path, filename]; + + if !tab.search.is_empty() { + let search = Self::elem_search(&tab.search, right); + right -= search.width(); + elems.push(search); } - Ok((strings, actions)) - } - fn string_filter(tab: &Tab) -> String { - format!(" {filter} ", filter = tab.settings.filter) - } + let filter_kind = &tab.settings.filter; + if !matches!(filter_kind, FilterKind::All) { + let filter = Self::elem_filter(filter_kind, right); + elems.push(filter); + } - fn string_searched(searched: &str) -> String { - format!(" Searched: {searched} ") + Ok(elems) } - fn string_shorten_path(tab: &Tab) -> Result<String> { - Ok(format!(" {}", shorten_path(&tab.directory.path, None)?)) + fn elem_shorten_path(tab: &Tab, left: usize) -> Result<ClickableString> { + Ok(ClickableString::new( + format!(" {}", shorten_path(&tab.directory.path, None)?), + HorizontalAlign::Left, + ActionMap::Cd, + left, + )) } - fn string_first_row_selected_file(tab: &Tab, width: usize) -> Result<String> { - match tab.display_mode { - Display::Tree => Ok(format!( + fn elem_filename(tab: &Tab, width: usize, left: usize) -> Result<ClickableString> { + let text = match tab.display_mode { + Display::Tree => format!( "/{rel}", rel = shorten_path(tab.tree.selected_path_relative_to_root()?, Some(width / 2))? - )), + ), _ => { if let Some(fileinfo) = tab.directory.selected() { - Ok(fileinfo.filename_without_dot_dotdot()) + fileinfo.filename_without_dot_dotdot() } else { - Ok("".to_owned()) + "".to_owned() } } - } + }; + Ok(ClickableString::new( + text, + HorizontalAlign::Left, + ActionMap::Rename, + left, + )) + } + + fn elem_search(search: &Search, right: usize) -> ClickableString { + ClickableString::new( + search.to_string(), + HorizontalAlign::Right, + ActionMap::Search, + right, + ) + } + + fn elem_filter(filter: &FilterKind, right: usize) -> ClickableString { + ClickableString::new( + format!(" {filter}"), + HorizontalAlign::Right, + ActionMap::Filter, + right, + ) } } - /// A clickable footer. - /// Every displayed element knows were it starts and ends. - /// It allows the user to click on them. - /// Those element are linked by their index to an action. - pub struct Footer { - strings: Vec<String>, - sizes: Vec<usize>, - width: usize, + impl ClickableLine for Header { + fn elems(&self) -> &Vec<ClickableString> { + &self.elems + } + fn canvas_width(&self) -> usize { + self.canvas_width + } + fn full_width(&self) -> usize { + self.full_width + } } - impl ClickableLine for Footer { - /// Vector of displayed strings. - fn strings(&self) -> &Vec<String> { - self.strings.as_ref() - } + /// Default footer for display directory & tree. + pub struct Footer { + elems: Vec<ClickableString>, + canvas_width: usize, + full_width: usize, } - impl ClickableLineInner for Footer { - fn sizes(&self) -> &Vec<usize> { - self.sizes.as_ref() + impl ClickableLine for Footer { + fn elems(&self) -> &Vec<ClickableString> { + &self.elems } - - fn action_index(&self, index: usize) -> &ActionMap { - &FOOTER_ACTIONS[index] + fn canvas_width(&self) -> usize { + self.canvas_width } - fn width(&self) -> usize { - self.width + fn full_width(&self) -> usize { + self.full_width } } impl Footer { - /// Create the strings associated with the selected tab directory + const FOOTER_ACTIONS: [ActionMap; 6] = [ + ActionMap::Nothing, // position + ActionMap::Ncdu, + ActionMap::Sort, + ActionMap::LazyGit, + ActionMap::DisplayFlagged, + ActionMap::Sort, + ]; + + /// Creates a new footer pub fn new(status: &Status, tab: &Tab) -> Result<Self> { - let (width, _) = status.internal_settings.term.term_size()?; - let used_width = if status.display_settings.use_dual_tab(width) { - width / 2 - } else { - width - }; - let disk_space = status.disk_spaces_of_selected(); - let raw_strings = Self::make_raw_strings(status, tab, disk_space)?; - let strings = Self::make_padded_strings(&raw_strings, used_width); - let sizes = Self::make_sizes(&strings); - + let full_width = status.internal_settings.term_size()?.0; + let canvas_width = status.canvas_width()?; + let elems = Self::make_elems(status, tab, canvas_width)?; Ok(Self { - strings, - sizes, - width, + elems, + canvas_width, + full_width, }) } - // TODO! refactor using a `struct thing { string, start, end, action }` - /// Returns a bunch of displayable strings. - /// Watchout: - /// 1. the length of the vector MUST BE the length of `ACTIONS` minus one. - /// 2. the order must be respected. + fn make_elems(status: &Status, tab: &Tab, width: usize) -> Result<Vec<ClickableString>> { + let disk_space = status.disk_spaces_of_selected(); + let raw_strings = Self::make_raw_strings(status, tab, disk_space)?; + let padded_strings = Self::make_padded_strings(&raw_strings, width); + let mut left = 0; + let mut elems = vec![]; + for (index, string) in padded_strings.iter().enumerate() { + let elem = ClickableString::new( + string.to_owned(), + HorizontalAlign::Left, + Self::FOOTER_ACTIONS[index].to_owned(), + left, + ); + left += elem.width(); + elems.push(elem) + } + Ok(elems) + } + fn make_raw_strings(status: &Status, tab: &Tab, disk_space: String) -> Result<Vec<String>> { Ok(vec![ Self::string_first_row_position(tab)?, @@ -241,10 +281,7 @@ mod inner { /// Pad every string of `raw_strings` with enough space to fill a line. fn make_padded_strings(raw_strings: &[String], total_width: usize) -> Vec<String> { - let used_width: usize = raw_strings - .iter() - .map(|s| s.graphemes(true).collect::<Vec<&str>>().iter().len()) - .sum(); + let used_width: usize = raw_strings.iter().map(|s| s.utf_width()).sum(); let available_width = total_width.checked_sub(used_width).unwrap_or_default(); let margin_width = available_width / (2 * raw_strings.len()); let margin = " ".repeat(margin_width); @@ -290,30 +327,52 @@ mod inner { } } + /// Header for the display of flagged files pub struct FlaggedHeader { - strings: Vec<String>, - sizes: Vec<usize>, - width: usize, + elems: Vec<ClickableString>, + canvas_width: usize, + full_width: usize, + } + + impl ClickableLine for FlaggedHeader { + fn elems(&self) -> &Vec<ClickableString> { + &self.elems + } + fn canvas_width(&self) -> usize { + self.canvas_width + } + fn full_width(&self) -> usize { + self.full_width + } } impl FlaggedHeader { - const ACTIONS: [ActionMap; 2] = [ActionMap::ResetMode, ActionMap::OpenFile]; + const ACTIONS: [ActionMap; 3] = + [ActionMap::ResetMode, ActionMap::OpenFile, ActionMap::Search]; + /// Creates a new header. pub fn new(status: &Status) -> Result<Self> { - let strings = Self::make_strings(status); - let sizes = Self::make_sizes(&strings); - let (width, _) = status.internal_settings.term.term_size()?; + let full_width = status.internal_settings.term_size()?.0; + let canvas_width = status.canvas_width()?; + let elems = Self::make_elems(status, full_width); Ok(Self { - strings, - sizes, - width, + elems, + canvas_width, + full_width, }) } - fn make_strings(status: &Status) -> Vec<String> { - vec![ + fn make_elems(status: &Status, width: usize) -> Vec<ClickableString> { + let title = ClickableString::new( "Fuzzy files".to_owned(), + HorizontalAlign::Left, + Self::ACTIONS[0].to_owned(), + 0, + ); + let left = title.width(); + + let flagged = ClickableString::new( status .menu .flagged @@ -321,89 +380,68 @@ mod inner { .unwrap_or(&std::path::PathBuf::new()) .to_string_lossy() .to_string(), - ] - } - - fn make_sizes(strings: &[String]) -> Vec<usize> { - strings - .iter() - .map(|s| s.graphemes(true).collect::<Vec<&str>>().iter().len()) - .collect() - } - - fn actions(&self, index: usize) -> &ActionMap { - &Self::ACTIONS[index] + HorizontalAlign::Left, + Self::ACTIONS[1].to_owned(), + left, + ); + let searched = Header::elem_search(&status.current_tab().search, width); + vec![title, flagged, searched] } } - impl ClickableLine for FlaggedHeader { - /// Vector of displayed strings. - fn strings(&self) -> &Vec<String> { - self.strings.as_ref() - } - } - - impl ClickableLineInner for FlaggedHeader { - fn sizes(&self) -> &Vec<usize> { - self.sizes.as_ref() - } - - fn action_index(&self, index: usize) -> &ActionMap { - self.actions(index) - } - - fn width(&self) -> usize { - self.width - } - } + /// Footer for the flagged files display pub struct FlaggedFooter { - strings: Vec<String>, - sizes: Vec<usize>, - width: usize, + elems: Vec<ClickableString>, + canvas_width: usize, + full_width: usize, } impl ClickableLine for FlaggedFooter { - /// Vector of displayed strings. - fn strings(&self) -> &Vec<String> { - self.strings.as_ref() + fn elems(&self) -> &Vec<ClickableString> { + &self.elems } - } - - impl ClickableLineInner for FlaggedFooter { - fn sizes(&self) -> &Vec<usize> { - self.sizes.as_ref() + fn canvas_width(&self) -> usize { + self.canvas_width } - - fn action_index(&self, index: usize) -> &ActionMap { - &Self::ACTIONS[index] - } - - fn width(&self) -> usize { - self.width + fn full_width(&self) -> usize { + self.full_width } } impl FlaggedFooter { - const ACTIONS: [ActionMap; 2] = [ActionMap::Nothing, ActionMap::Jump]; + const ACTIONS: [ActionMap; 2] = [ActionMap::Nothing, ActionMap::DisplayFlagged]; + /// Creates a new footer pub fn new(status: &Status) -> Result<Self> { - let (width, _) = status.internal_settings.term.term_size()?; - let used_width = if status.display_settings.use_dual_tab(width) { - width / 2 - } else { - width - }; + let full_width = status.internal_settings.term.term_size()?.0; + let canvas_width = status.canvas_width()?; let raw_strings = Self::make_strings(status); - let sizes = Self::make_sizes(&raw_strings); - let strings = Footer::make_padded_strings(&raw_strings, used_width); + let strings = Footer::make_padded_strings(&raw_strings, full_width); + let elems = Self::make_elems(strings); Ok(Self { - strings, - sizes, - width, + elems, + canvas_width, + full_width, }) } + fn make_elems(padded_strings: Vec<String>) -> Vec<ClickableString> { + let mut elems = vec![]; + let mut left = 0; + for (index, string) in padded_strings.iter().enumerate() { + let elem = ClickableString::new( + string.to_owned(), + HorizontalAlign::Left, + Self::ACTIONS[index].to_owned(), + left, + ); + left += elem.width(); + elems.push(elem) + } + elems + } + fn make_strings(status: &Status) -> Vec<String> { let index = if status.menu.flagged.is_empty() { 0 @@ -415,14 +453,126 @@ mod inner { format!(" {nb} flags", nb = status.menu.flagged.len()), ] } + } - fn make_sizes(strings: &[String]) -> Vec<usize> { - strings - .iter() - .map(|s| s.graphemes(true).collect::<Vec<&str>>().iter().len()) - .collect() + pub struct PreviewHeader; + + impl PreviewHeader { + pub fn elems(status: &Status, tab: &Tab, width: usize) -> Vec<ClickableString> { + let pairs = Self::strings(status, tab); + Self::pair_to_clickable(&pairs, width) + } + + fn pair_to_clickable( + pairs: &[(String, HorizontalAlign)], + width: usize, + ) -> Vec<ClickableString> { + let mut left = 0; + let mut right = width; + let mut elems = vec![]; + for (text, align) in pairs.iter() { + let pos = if let HorizontalAlign::Left = align { + left + } else { + right + }; + let elem = ClickableString::new( + text.to_owned(), + align.to_owned(), + ActionMap::Nothing, + pos, + ); + match align { + HorizontalAlign::Left => { + left += elem.width(); + } + HorizontalAlign::Right => { + right -= elem.width(); + } + } + elems.push(elem) + } + elems + } + + fn strings(status: &Status, tab: &Tab) -> Vec<(String, HorizontalAlign)> { + match &tab.preview { + Preview::Text(text_content) => match text_content.kind { + TextKind::HELP => Self::make_help(), + TextKind::LOG => Self::make_log(), + _ => Self::make_default_preview(status, tab), + }, + Preview::ColoredText(colored_text) => Self::make_colored_text(colored_text), + _ => Self::make_default_preview(status, tab), + } + } + + fn make_help() -> Vec<(String, HorizontalAlign)> { + vec![ + (HELP_FIRST_SENTENCE.to_owned(), HorizontalAlign::Left), + ( + format!(" Version: {v} ", v = std::env!("CARGO_PKG_VERSION")), + HorizontalAlign::Left, + ), + (HELP_SECOND_SENTENCE.to_owned(), HorizontalAlign::Right), + ] + } + + fn make_log() -> Vec<(String, HorizontalAlign)> { + vec![ + (LOG_FIRST_SENTENCE.to_owned(), HorizontalAlign::Left), + (LOG_SECOND_SENTENCE.to_owned(), HorizontalAlign::Right), + ] + } + + fn make_colored_text(colored_text: &ColoredText) -> Vec<(String, HorizontalAlign)> { + vec![ + (" Command: ".to_owned(), HorizontalAlign::Left), + ( + format!(" {command} ", command = colored_text.title()), + HorizontalAlign::Right, + ), + ] + } + + fn _pick_previewed_fileinfo(status: &Status) -> Result<FileInfo> { + if status.display_settings.dual() && status.display_settings.preview() { + status.tabs[0].current_file() + } else { + status.current_tab().current_file() + } + } + + fn make_default_preview(status: &Status, tab: &Tab) -> Vec<(String, HorizontalAlign)> { + if let Ok(fileinfo) = Self::_pick_previewed_fileinfo(status) { + let mut strings = vec![(" Preview ".to_owned(), HorizontalAlign::Left)]; + if !tab.preview.is_empty() { + let index = match &tab.preview { + Preview::Ueberzug(image) => image.index + 1, + _ => tab.window.bottom, + }; + strings.push(( + format!(" {index} / {len} ", len = tab.preview.len()), + HorizontalAlign::Right, + )); + }; + strings.push(( + format!(" {} ", fileinfo.path.display()), + HorizontalAlign::Left, + )); + strings + } else { + vec![("".to_owned(), HorizontalAlign::Left)] + } + } + + /// Make a default preview header + pub fn default_preview(status: &Status, tab: &Tab, width: usize) -> Vec<ClickableString> { + Self::pair_to_clickable(&Self::make_default_preview(status, tab), width) } } } -pub use inner::{ClickableLine, FlaggedFooter, FlaggedHeader, Footer, Header}; +pub use inner::{ + ClickableLine, ClickableString, FlaggedFooter, FlaggedHeader, Footer, Header, PreviewHeader, +}; |