mod inner {
use std::{path::Path, sync::Arc};
use anyhow::{Context, Result};
use ratatui::{
layout::{Alignment, Rect},
style::Modifier,
text::{Line, Span},
widgets::Widget,
Frame,
};
use crate::event::ActionMap;
use crate::modes::{Content, Display, FilterKind, Preview, Search, Selectable, Text, TextKind};
use crate::{
app::{Status, Tab},
config::MENU_STYLES,
};
use crate::{
common::{
PathShortener, UtfWidth, ACTION_LOG_PATH, HELP_FIRST_SENTENCE, HELP_SECOND_SENTENCE,
LAZYGIT, LOG_FIRST_SENTENCE, LOG_SECOND_SENTENCE, NCDU,
},
modes::SAME_WINDOW_TOKEN,
};
#[derive(Clone, Copy, Debug)]
enum Align {
Left,
Right,
}
#[derive(Clone, Debug)]
pub struct ClickableString {
text: String,
action: ActionMap,
width: u16,
left: u16,
right: u16,
}
impl ClickableString {
fn new(text: String, align: Align, action: ActionMap, col: u16) -> Self {
let width = text.utf_width_u16();
let (left, right) = match align {
Align::Left => (col, col + width),
Align::Right => (col.saturating_sub(width + 3), col.saturating_sub(3)),
};
Self {
text,
action,
width,
left,
right,
}
}
pub fn text(&self) -> &str {
self.text.as_str()
}
pub fn width(&self) -> u16 {
self.width
}
}
trait ToLine<'a> {
fn left_to_line(&'a self, effect_reverse: bool) -> Line<'a>;
fn right_to_line(&'a self, effect_reverse: bool) -> Line<'a>;
}
impl<'a> ToLine<'a> for &Vec<ClickableString> {
fn left_to_line(&'a self, effect_reverse: bool) -> Line<'a> {
let left: Vec<_> = std::iter::zip(
self.iter(),
MENU_STYLES
.get()
.expect("Menu colors should be set")
.palette()
.iter()
.cycle(),
)
.map(|(elem, style)| {
let mut style = *style;
if effect_reverse {
style.add_modifier |= Modifier::REVERSED;
}
Span::styled(elem.text(), style)
})
.collect();
Line::from(left).alignment(Alignment::Left)
}
fn right_to_line(&'a self, effect_reverse: bool) -> Line<'a> {
let left: Vec<_> = std::iter::zip(
self.iter(),
MENU_STYLES
.get()
.expect("Menu colors should be set")
.palette()
.iter()
.rev()
.cycle(),
)
.map(|(elem, style)| {
let mut style = *style;
if effect_reverse {
style.add_modifier |= Modifier::REVERSED;
}
Span::styled(elem.text(), style)
})
.collect();
Line::from(left).alignment(Alignment::Right)
}
}
pub trait ClickableLine {
fn left(&self) -> &Vec<ClickableString>;
fn right(&self) -> &Vec<ClickableString>;
fn action(&self, col: u16, is_right: bool) -> &ActionMap {
let offset = self.offset(is_right);
let col = col.saturating_sub(offset);
for clickable in self.left().iter().chain(self.right().iter()) {
if clickable.left <= col && col < clickable.right {
return &clickable.action;
}
}
crate::log_info!("no action found");
&ActionMap::Nothing
}
fn full_width(&self) -> u16;
fn offset(&self, is_right: bool) -> u16 {
if is_right {
self.full_width() / 2 + 2
} else {
1
}
}
fn draw_left(&self, f: &mut Frame, rect: Rect, effect_reverse: bool) {
self.left()
.left_to_line(effect_reverse)
.render(rect, f.buffer_mut());
}
fn draw_right(&self, f: &mut Frame, rect: Rect, effect_reverse: bool) {
self.right()
.right_to_line(effect_reverse)
.render(rect, f.buffer_mut());
}
}
pub struct Header {
left: Vec<ClickableString>,
right: Vec<ClickableString>,
full_width: u16,
}
impl Header {
pub fn new(status: &Status, tab: &Tab) -> Result<Self> {
let full_width = status.term_width();
let canvas_width = status.canvas_width()?;
let left = Self::make_left(tab, canvas_width)?;
let right = Self::make_right(tab, canvas_width)?;
Ok(Self {
left,
right,
full_width,
})
}
fn make_left(tab: &Tab, width: u16) -> Result<Vec<ClickableString>> {
let mut left = 0;
let shorten_path = Self::elem_shorten_path(tab, left)?;
left += shorten_path.width();
let filename = Self::elem_filename(tab, width, left)?;
Ok(vec![shorten_path, filename])
}
fn make_right(tab: &Tab, width: u16) -> Result<Vec<ClickableString>> {
let mut right = width;
let mut right_elems = vec![];
if !tab.search.is_empty() {
let search = Self::elem_search(&tab.search, right);
right -= search.width();
right_elems.push(search)
}
let filter_kind = &tab.settings.filter;
if !matches!(filter_kind, FilterKind::All) {
right_elems.push(Self::elem_filter(filter_kind, right))
}
Ok(right_elems)
}
fn elem_shorten_path(tab: &Tab, left: u16) -> Result<ClickableString> {
Ok(ClickableString::new(
format!(
" {}",
PathShortener::path(&tab.directory.path)
.context("Couldn't parse path")?
.shorten()
),
Align::Left,
ActionMap::Cd,
left,
))
}
fn elem_filename(tab: &Tab, width: u16, left: u16) -> Result<ClickableString> {
let text = match tab.display_mode {
Display::Tree => Self::elem_tree_filename(tab, width)?,
_ => Self::elem_directory_filename(tab),
};
Ok(ClickableString::new(
text,
Align::Left,
ActionMap::Rename,
left,
))
}
fn elem_tree_filename(tab: &Tab, width: u16) -> Result<String> {
Ok(format!(
"{sep}{rel}",
rel = PathShortener::path(tab.tree.selected_path_relative_to_root()?)
.context("Couldn't parse path")?
.with_size(width as usize / 2)
.shorten(),
sep = if tab.tree.root_path() == std::path::Path::new("/") {
""
} else {
"/"
}
))
}
fn elem_directory_filename(tab: &Tab) -> String {
if tab.directory.is_dotdot_selected() {
"".to_owned()
} else if let Some(fileinfo) = tab.directory.selected() {
fileinfo.filename_without_dot_dotdot()
} else {
"".to_owned()
}
}
fn elem_search(search: &Search, right: u16) -> ClickableString {
ClickableString::new(search.to_string(), Align::Right, ActionMap::Search, right)
}
fn elem_filter(filter: &FilterKind, right: u16) -> ClickableString {
ClickableString::new(format!(" {filter}"), Align::Right, ActionMap::Filter, right)
}
}
static EMPTY_VEC: Vec<ClickableString> = vec![];
impl ClickableLine for Header {
fn left(&self) -> &Vec<ClickableString> {
&self.left
}
fn right(&self) -> &Vec<ClickableString> {
&self.right
}
fn full_width(&self) -> u16 {
self.full_width
}
}
pub struct Footer {
left: Vec<ClickableString>,
full_width: u16,
}
impl ClickableLine for Footer {
fn left(&self) -> &Vec<ClickableString> {
&self.left
}
fn right(&self) -> &Vec<ClickableString> {
&EMPTY_VEC
}
fn full_width(&self) -> u16 {
self.full_width
}
}
impl Footer {
fn footer_actions() -> [ActionMap; 6] {
[
ActionMap::Nothing, ActionMap::Custom(SAME_WINDOW_TOKEN.to_owned() + " " + NCDU),
ActionMap::Sort,
ActionMap::Custom(SAME_WINDOW_TOKEN.to_owned() + " " + LAZYGIT),
ActionMap::DisplayFlagged,
ActionMap::Sort,
]
}
pub fn new(status: &Status, tab: &Tab) -> Result<Self> {
let full_width = status.term_width();
let canvas_width = status.canvas_width()?;
let left = Self::make_elems(status, tab, canvas_width)?;
Ok(Self { left, full_width })
}
fn make_elems(status: &Status, tab: &Tab, width: u16) -> 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(),
Align::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)?,
Self::string_used_space(tab),
Self::string_disk_space(&disk_space),
Self::string_git_string(tab)?,
Self::string_first_row_flags(status),
Self::string_sort_kind(tab),
])
}
fn make_padded_strings(raw_strings: &[String], total_width: u16) -> Vec<String> {
let total_width = total_width as usize;
let used_width = raw_strings.iter().map(|s| s.utf_width()).sum();
let available_width = total_width.saturating_sub(used_width);
let margin_width = available_width / (2 * raw_strings.len());
let margin = " ".repeat(margin_width);
let mut padded_strings: Vec<String> = raw_strings
.iter()
.map(|content| format!("{margin}{content}{margin}"))
.collect();
let rest = total_width
.saturating_sub(padded_strings.iter().map(|s| s.utf_width()).sum::<usize>());
padded_strings[raw_strings.len().saturating_sub(1)].push_str(&" ".repeat(rest));
padded_strings
}
fn string_first_row_position(tab: &Tab) -> Result<String> {
let len: u16;
let index: u16;
if tab.display_mode.is_tree() {
index = tab.tree.selected_node().context("no node")?.index() as u16 + 1;
len = tab.tree.len() as u16;
} else {
index = tab.directory.index as u16 + 1;
len = tab.directory.len() as u16;
}
Ok(format!(" {index} / {len} "))
}
fn string_used_space(tab: &Tab) -> String {
if tab.visual {
"VISUAL".to_owned()
} else {
format!(" {} ", tab.directory.used_space())
}
}
fn string_disk_space(disk_space: &str) -> String {
format!(" Avail: {disk_space} ")
}
fn string_git_string(tab: &Tab) -> Result<String> {
Ok(format!(" {} ", tab.directory.git_string()?))
}
fn string_sort_kind(tab: &Tab) -> String {
format!(" {} ", &tab.settings.sort_kind)
}
fn string_first_row_flags(status: &Status) -> String {
let nb_flagged = status.menu.flagged.len();
let flag_string = if nb_flagged > 1 { "flags" } else { "flag" };
format!(" {nb_flagged} {flag_string} ",)
}
}
pub struct PreviewHeader {
left: Vec<ClickableString>,
right: Vec<ClickableString>,
full_width: u16,
}
impl ClickableLine for PreviewHeader {
fn left(&self) -> &Vec<ClickableString> {
&self.left
}
fn right(&self) -> &Vec<ClickableString> {
&self.right
}
fn full_width(&self) -> u16 {
self.full_width
}
}
impl PreviewHeader {
pub fn into_default_preview(status: &Status, tab: &Tab, width: u16) -> Self {
Self {
left: Self::default_preview(status, tab, width),
right: vec![],
full_width: width,
}
}
pub fn new(status: &Status, tab: &Tab, width: u16) -> Self {
Self {
left: Self::pair_to_clickable(&Self::strings_left(status, tab), width),
right: Self::pair_to_clickable(&Self::strings_right(tab), width),
full_width: width,
}
}
fn pair_to_clickable(pairs: &[(String, Align)], width: u16) -> Vec<ClickableString> {
let mut left = 0;
let mut right = width;
let mut elems = vec![];
for (text, align) in pairs.iter() {
let pos = if let Align::Left = align { left } else { right };
let elem = ClickableString::new(
text.to_owned(),
align.to_owned(),
ActionMap::Nothing,
pos,
);
match align {
Align::Left => {
left += elem.width();
}
Align::Right => {
right -= elem.width();
}
}
elems.push(elem)
}
elems
}
fn strings_left(status: &Status, tab: &Tab) -> Vec<(String, Align)> {
match &tab.preview {
Preview::Text(text_content) => match text_content.kind {
TextKind::CommandStdout => Self::make_colored_text(text_content),
TextKind::Help => Self::make_help(),
TextKind::Log => Self::make_log(),
_ => Self::make_default_preview(status, tab),
},
_ => Self::make_default_preview(status, tab),
}
}
fn strings_right(tab: &Tab) -> Vec<(String, Align)> {
let index = match &tab.preview {
Preview::Empty => 0,
Preview::Image(image) => image.index + 1,
_ => tab.window.bottom,
};
vec![(
format!(" {index} / {len} ", len = tab.preview.len()),
Align::Right,
)]
}
fn make_help() -> Vec<(String, Align)> {
vec![
(HELP_FIRST_SENTENCE.to_owned(), Align::Left),
(
format!(" Version: {v} ", v = std::env!("CARGO_PKG_VERSION")),
Align::Left,
),
(HELP_SECOND_SENTENCE.to_owned(), Align::Right),
]
}
fn make_log() -> Vec<(String, Align)> {
vec![
(LOG_FIRST_SENTENCE.to_owned(), Align::Left),
(ACTION_LOG_PATH.to_owned(), Align::Left),
(LOG_SECOND_SENTENCE.to_owned(), Align::Right),
]
}
fn make_colored_text(colored_text: &Text) -> Vec<(String, Align)> {
vec![
(" Command output: ".to_owned(), Align::Left),
(
format!(" {command} ", command = colored_text.title),
Align::Right,
),
]
}
fn pick_previewed_fileinfo(status: &Status) -> Arc<Path> {
if status.session.dual() && status.session.preview() {
status.tabs[1].preview.filepath()
} else {
status.current_tab().preview.filepath()
}
}
fn make_default_preview(status: &Status, tab: &Tab) -> Vec<(String, Align)> {
vec![
(
format!(" Preview as {kind} ", kind = tab.preview.kind_display()),
Align::Left,
),
(
format!(
" {filepath} ",
filepath = Self::pick_previewed_fileinfo(status).display()
),
Align::Left,
),
]
}
pub fn default_preview(status: &Status, tab: &Tab, width: u16) -> Vec<ClickableString> {
Self::pair_to_clickable(&Self::make_default_preview(status, tab), width)
}
}
}
pub use inner::{ClickableLine, ClickableString, Footer, Header, PreviewHeader};