summaryrefslogtreecommitdiffstats
path: root/src/app/header_footer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/header_footer.rs')
-rw-r--r--src/app/header_footer.rs650
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,
+};