fm/app/
header_footer.rs

1mod inner {
2    use std::{path::Path, sync::Arc};
3
4    use anyhow::{Context, Result};
5    use ratatui::{
6        layout::{Alignment, Rect},
7        style::Modifier,
8        text::{Line, Span},
9        widgets::Widget,
10        Frame,
11    };
12
13    use crate::event::ActionMap;
14    use crate::modes::{Content, Display, FilterKind, Preview, Search, Selectable, Text, TextKind};
15    use crate::{
16        app::{Status, Tab},
17        config::MENU_STYLES,
18    };
19    use crate::{
20        common::{
21            PathShortener, UtfWidth, ACTION_LOG_PATH, HELP_FIRST_SENTENCE, HELP_SECOND_SENTENCE,
22            LAZYGIT, LOG_FIRST_SENTENCE, LOG_SECOND_SENTENCE, NCDU,
23        },
24        modes::SAME_WINDOW_TOKEN,
25    };
26
27    /// Should the content be aligned left or right ? No centering.
28    #[derive(Clone, Copy, Debug)]
29    enum Align {
30        Left,
31        Right,
32    }
33
34    /// A footer or header element that can be clicked
35    ///
36    /// Holds a text and an action.
37    /// It knows where it's situated on the line
38    #[derive(Clone, Debug)]
39    pub struct ClickableString {
40        text: String,
41        action: ActionMap,
42        width: u16,
43        left: u16,
44        right: u16,
45    }
46
47    impl ClickableString {
48        /// Creates a new `ClickableString`.
49        /// It calculates its position with `col` and `align`.
50        /// If left aligned, the text size will be added to `col` and the text will span from col to col + width.
51        /// otherwise, the text will spawn from col - width to col.
52        fn new(text: String, align: Align, action: ActionMap, col: u16) -> Self {
53            let width = text.utf_width_u16();
54            let (left, right) = match align {
55                Align::Left => (col, col + width),
56                Align::Right => (col.saturating_sub(width + 3), col.saturating_sub(3)),
57            };
58            Self {
59                text,
60                action,
61                width,
62                left,
63                right,
64            }
65        }
66
67        /// Text content of the element.
68        pub fn text(&self) -> &str {
69            self.text.as_str()
70        }
71
72        pub fn width(&self) -> u16 {
73            self.width
74        }
75    }
76
77    trait ToLine<'a> {
78        fn left_to_line(&'a self, effect_reverse: bool) -> Line<'a>;
79        fn right_to_line(&'a self, effect_reverse: bool) -> Line<'a>;
80    }
81
82    impl<'a> ToLine<'a> for &Vec<ClickableString> {
83        fn left_to_line(&'a self, effect_reverse: bool) -> Line<'a> {
84            let left: Vec<_> = std::iter::zip(
85                self.iter(),
86                MENU_STYLES
87                    .get()
88                    .expect("Menu colors should be set")
89                    .palette()
90                    .iter()
91                    .cycle(),
92            )
93            .map(|(elem, style)| {
94                let mut style = *style;
95                if effect_reverse {
96                    style.add_modifier |= Modifier::REVERSED;
97                }
98                Span::styled(elem.text(), style)
99            })
100            .collect();
101            Line::from(left).alignment(Alignment::Left)
102        }
103
104        fn right_to_line(&'a self, effect_reverse: bool) -> Line<'a> {
105            let left: Vec<_> = std::iter::zip(
106                self.iter(),
107                MENU_STYLES
108                    .get()
109                    .expect("Menu colors should be set")
110                    .palette()
111                    .iter()
112                    .rev()
113                    .cycle(),
114            )
115            .map(|(elem, style)| {
116                let mut style = *style;
117                if effect_reverse {
118                    style.add_modifier |= Modifier::REVERSED;
119                }
120                Span::styled(elem.text(), style)
121            })
122            .collect();
123            Line::from(left).alignment(Alignment::Right)
124        }
125    }
126
127    /// A line of element that can be clicked on.
128    pub trait ClickableLine {
129        /// Reference to the elements
130        fn left(&self) -> &Vec<ClickableString>;
131        fn right(&self) -> &Vec<ClickableString>;
132        /// Action for each associated file.
133        fn action(&self, col: u16, is_right: bool) -> &ActionMap {
134            let offset = self.offset(is_right);
135            let col = col.saturating_sub(offset);
136            for clickable in self.left().iter().chain(self.right().iter()) {
137                if clickable.left <= col && col < clickable.right {
138                    return &clickable.action;
139                }
140            }
141
142            crate::log_info!("no action found");
143            &ActionMap::Nothing
144        }
145        /// Full width of the terminal
146        fn full_width(&self) -> u16;
147        /// used offset.
148        /// 1 if the text is on left tab,
149        /// width / 2 + 2 otherwise.
150        fn offset(&self, is_right: bool) -> u16 {
151            if is_right {
152                self.full_width() / 2 + 2
153            } else {
154                1
155            }
156        }
157
158        /// Draw the left aligned elements of the line.
159        fn draw_left(&self, f: &mut Frame, rect: Rect, effect_reverse: bool) {
160            self.left()
161                .left_to_line(effect_reverse)
162                .render(rect, f.buffer_mut());
163        }
164
165        /// Draw the right aligned elements of the line.
166        fn draw_right(&self, f: &mut Frame, rect: Rect, effect_reverse: bool) {
167            self.right()
168                .right_to_line(effect_reverse)
169                .render(rect, f.buffer_mut());
170        }
171    }
172
173    /// Header for tree & directory display mode.
174    pub struct Header {
175        left: Vec<ClickableString>,
176        right: Vec<ClickableString>,
177        full_width: u16,
178    }
179
180    impl Header {
181        /// Creates a new header
182        pub fn new(status: &Status, tab: &Tab) -> Result<Self> {
183            let full_width = status.term_width();
184            let canvas_width = status.canvas_width()?;
185            let left = Self::make_left(tab, canvas_width)?;
186            let right = Self::make_right(tab, canvas_width)?;
187
188            Ok(Self {
189                left,
190                right,
191                full_width,
192            })
193        }
194
195        fn make_left(tab: &Tab, width: u16) -> Result<Vec<ClickableString>> {
196            let mut left = 0;
197            let shorten_path = Self::elem_shorten_path(tab, left)?;
198            left += shorten_path.width();
199
200            let filename = Self::elem_filename(tab, width, left)?;
201
202            Ok(vec![shorten_path, filename])
203        }
204
205        fn make_right(tab: &Tab, width: u16) -> Result<Vec<ClickableString>> {
206            let mut right = width;
207            let mut right_elems = vec![];
208
209            if !tab.search.is_empty() {
210                let search = Self::elem_search(&tab.search, right);
211                right -= search.width();
212                right_elems.push(search)
213            }
214
215            let filter_kind = &tab.settings.filter;
216            if !matches!(filter_kind, FilterKind::All) {
217                right_elems.push(Self::elem_filter(filter_kind, right))
218            }
219
220            Ok(right_elems)
221        }
222
223        fn elem_shorten_path(tab: &Tab, left: u16) -> Result<ClickableString> {
224            Ok(ClickableString::new(
225                format!(
226                    " {}",
227                    PathShortener::path(&tab.directory.path)
228                        .context("Couldn't parse path")?
229                        .shorten()
230                ),
231                Align::Left,
232                ActionMap::Cd,
233                left,
234            ))
235        }
236
237        fn elem_filename(tab: &Tab, width: u16, left: u16) -> Result<ClickableString> {
238            let text = match tab.display_mode {
239                Display::Tree => Self::elem_tree_filename(tab, width)?,
240                _ => Self::elem_directory_filename(tab),
241            };
242            Ok(ClickableString::new(
243                text,
244                Align::Left,
245                ActionMap::Rename,
246                left,
247            ))
248        }
249
250        fn elem_tree_filename(tab: &Tab, width: u16) -> Result<String> {
251            Ok(format!(
252                "{sep}{rel}",
253                rel = PathShortener::path(tab.tree.selected_path_relative_to_root()?)
254                    .context("Couldn't parse path")?
255                    .with_size(width as usize / 2)
256                    .shorten(),
257                sep = if tab.tree.root_path() == std::path::Path::new("/") {
258                    ""
259                } else {
260                    "/"
261                }
262            ))
263        }
264
265        fn elem_directory_filename(tab: &Tab) -> String {
266            if tab.directory.is_dotdot_selected() {
267                "".to_owned()
268            } else if let Some(fileinfo) = tab.directory.selected() {
269                fileinfo.filename_without_dot_dotdot()
270            } else {
271                "".to_owned()
272            }
273        }
274
275        fn elem_search(search: &Search, right: u16) -> ClickableString {
276            ClickableString::new(search.to_string(), Align::Right, ActionMap::Search, right)
277        }
278
279        fn elem_filter(filter: &FilterKind, right: u16) -> ClickableString {
280            ClickableString::new(format!(" {filter}"), Align::Right, ActionMap::Filter, right)
281        }
282    }
283
284    static EMPTY_VEC: Vec<ClickableString> = vec![];
285
286    impl ClickableLine for Header {
287        fn left(&self) -> &Vec<ClickableString> {
288            &self.left
289        }
290        fn right(&self) -> &Vec<ClickableString> {
291            &self.right
292        }
293        fn full_width(&self) -> u16 {
294            self.full_width
295        }
296    }
297
298    /// Default footer for display directory & tree.
299    pub struct Footer {
300        left: Vec<ClickableString>,
301        full_width: u16,
302    }
303
304    impl ClickableLine for Footer {
305        fn left(&self) -> &Vec<ClickableString> {
306            &self.left
307        }
308        fn right(&self) -> &Vec<ClickableString> {
309            &EMPTY_VEC
310        }
311
312        fn full_width(&self) -> u16 {
313            self.full_width
314        }
315    }
316
317    impl Footer {
318        fn footer_actions() -> [ActionMap; 6] {
319            [
320                ActionMap::Nothing, // position
321                ActionMap::Custom(SAME_WINDOW_TOKEN.to_owned() + " " + NCDU),
322                ActionMap::Sort,
323                ActionMap::Custom(SAME_WINDOW_TOKEN.to_owned() + " " + LAZYGIT),
324                ActionMap::DisplayFlagged,
325                ActionMap::Sort,
326            ]
327        }
328
329        /// Creates a new footer
330        pub fn new(status: &Status, tab: &Tab) -> Result<Self> {
331            let full_width = status.term_width();
332            let canvas_width = status.canvas_width()?;
333            let left = Self::make_elems(status, tab, canvas_width)?;
334            Ok(Self { left, full_width })
335        }
336
337        fn make_elems(status: &Status, tab: &Tab, width: u16) -> Result<Vec<ClickableString>> {
338            let disk_space = status.disk_spaces_of_selected();
339            let raw_strings = Self::make_raw_strings(status, tab, disk_space)?;
340            let padded_strings = Self::make_padded_strings(&raw_strings, width);
341            let mut left = 0;
342            let mut elems = vec![];
343            for (index, string) in padded_strings.iter().enumerate() {
344                let elem = ClickableString::new(
345                    string.to_owned(),
346                    Align::Left,
347                    Self::footer_actions()[index].to_owned(),
348                    left,
349                );
350                left += elem.width();
351                elems.push(elem)
352            }
353            Ok(elems)
354        }
355
356        fn make_raw_strings(status: &Status, tab: &Tab, disk_space: String) -> Result<Vec<String>> {
357            Ok(vec![
358                Self::string_first_row_position(tab)?,
359                Self::string_used_space(tab),
360                Self::string_disk_space(&disk_space),
361                Self::string_git_string(tab)?,
362                Self::string_first_row_flags(status),
363                Self::string_sort_kind(tab),
364            ])
365        }
366
367        /// Pad every string of `raw_strings` with enough space to fill a line.
368        fn make_padded_strings(raw_strings: &[String], total_width: u16) -> Vec<String> {
369            let total_width = total_width as usize;
370            let used_width = raw_strings.iter().map(|s| s.utf_width()).sum();
371            let available_width = total_width.saturating_sub(used_width);
372            let margin_width = available_width / (2 * raw_strings.len());
373            let margin = " ".repeat(margin_width);
374            let mut padded_strings: Vec<String> = raw_strings
375                .iter()
376                .map(|content| format!("{margin}{content}{margin}"))
377                .collect();
378            let rest = total_width
379                .saturating_sub(padded_strings.iter().map(|s| s.utf_width()).sum::<usize>());
380            padded_strings[raw_strings.len().saturating_sub(1)].push_str(&" ".repeat(rest));
381            padded_strings
382        }
383
384        fn string_first_row_position(tab: &Tab) -> Result<String> {
385            let len: u16;
386            let index: u16;
387            if tab.display_mode.is_tree() {
388                index = tab.tree.selected_node().context("no node")?.index() as u16 + 1;
389                len = tab.tree.len() as u16;
390            } else {
391                index = tab.directory.index as u16 + 1;
392                len = tab.directory.len() as u16;
393            }
394            Ok(format!(" {index} / {len} "))
395        }
396
397        fn string_used_space(tab: &Tab) -> String {
398            if tab.visual {
399                "VISUAL".to_owned()
400            } else {
401                format!(" {} ", tab.directory.used_space())
402            }
403        }
404
405        fn string_disk_space(disk_space: &str) -> String {
406            format!(" Avail: {disk_space} ")
407        }
408
409        fn string_git_string(tab: &Tab) -> Result<String> {
410            Ok(format!(" {} ", tab.directory.git_string()?))
411        }
412
413        fn string_sort_kind(tab: &Tab) -> String {
414            format!(" {} ", &tab.settings.sort_kind)
415        }
416
417        fn string_first_row_flags(status: &Status) -> String {
418            let nb_flagged = status.menu.flagged.len();
419            let flag_string = if nb_flagged > 1 { "flags" } else { "flag" };
420            format!(" {nb_flagged} {flag_string} ",)
421        }
422    }
423
424    /// Empty struct used to attach actions the first line of previews.
425    /// What content ? What kind of preview ?
426    pub struct PreviewHeader {
427        left: Vec<ClickableString>,
428        right: Vec<ClickableString>,
429        full_width: u16,
430    }
431
432    impl ClickableLine for PreviewHeader {
433        fn left(&self) -> &Vec<ClickableString> {
434            &self.left
435        }
436        fn right(&self) -> &Vec<ClickableString> {
437            &self.right
438        }
439        fn full_width(&self) -> u16 {
440            self.full_width
441        }
442    }
443
444    impl PreviewHeader {
445        pub fn into_default_preview(status: &Status, tab: &Tab, width: u16) -> Self {
446            Self {
447                left: Self::default_preview(status, tab, width),
448                right: vec![],
449                full_width: width,
450            }
451        }
452
453        pub fn new(status: &Status, tab: &Tab, width: u16) -> Self {
454            Self {
455                left: Self::pair_to_clickable(&Self::strings_left(status, tab), width),
456                right: Self::pair_to_clickable(&Self::strings_right(tab), width),
457                full_width: width,
458            }
459        }
460
461        fn pair_to_clickable(pairs: &[(String, Align)], width: u16) -> Vec<ClickableString> {
462            let mut left = 0;
463            let mut right = width;
464            let mut elems = vec![];
465            for (text, align) in pairs.iter() {
466                let pos = if let Align::Left = align { left } else { right };
467                let elem = ClickableString::new(
468                    text.to_owned(),
469                    align.to_owned(),
470                    ActionMap::Nothing,
471                    pos,
472                );
473                match align {
474                    Align::Left => {
475                        left += elem.width();
476                    }
477                    Align::Right => {
478                        right -= elem.width();
479                    }
480                }
481                elems.push(elem)
482            }
483            elems
484        }
485
486        fn strings_left(status: &Status, tab: &Tab) -> Vec<(String, Align)> {
487            match &tab.preview {
488                Preview::Text(text_content) => match text_content.kind {
489                    TextKind::CommandStdout => Self::make_colored_text(text_content),
490                    TextKind::Help => Self::make_help(),
491                    TextKind::Log => Self::make_log(),
492                    _ => Self::make_default_preview(status, tab),
493                },
494                _ => Self::make_default_preview(status, tab),
495            }
496        }
497
498        fn strings_right(tab: &Tab) -> Vec<(String, Align)> {
499            let index = match &tab.preview {
500                Preview::Empty => 0,
501                Preview::Image(image) => image.index + 1,
502                _ => tab.window.bottom,
503            };
504            vec![(
505                format!(" {index} / {len} ", len = tab.preview.len()),
506                Align::Right,
507            )]
508        }
509
510        fn make_help() -> Vec<(String, Align)> {
511            vec![
512                (HELP_FIRST_SENTENCE.to_owned(), Align::Left),
513                (
514                    format!(" Version: {v} ", v = std::env!("CARGO_PKG_VERSION")),
515                    Align::Left,
516                ),
517                (HELP_SECOND_SENTENCE.to_owned(), Align::Right),
518            ]
519        }
520
521        fn make_log() -> Vec<(String, Align)> {
522            vec![
523                (LOG_FIRST_SENTENCE.to_owned(), Align::Left),
524                (ACTION_LOG_PATH.to_owned(), Align::Left),
525                (LOG_SECOND_SENTENCE.to_owned(), Align::Right),
526            ]
527        }
528
529        fn make_colored_text(colored_text: &Text) -> Vec<(String, Align)> {
530            vec![
531                (" Command output: ".to_owned(), Align::Left),
532                (
533                    format!(" {command} ", command = colored_text.title),
534                    Align::Right,
535                ),
536            ]
537        }
538
539        fn pick_previewed_fileinfo(status: &Status) -> Arc<Path> {
540            if status.session.dual() && status.session.preview() {
541                status.tabs[1].preview.filepath()
542            } else {
543                status.current_tab().preview.filepath()
544            }
545        }
546
547        fn make_default_preview(status: &Status, tab: &Tab) -> Vec<(String, Align)> {
548            vec![
549                (
550                    format!(" Preview as {kind} ", kind = tab.preview.kind_display()),
551                    Align::Left,
552                ),
553                (
554                    format!(
555                        " {filepath} ",
556                        filepath = Self::pick_previewed_fileinfo(status).display()
557                    ),
558                    Align::Left,
559                ),
560            ]
561        }
562
563        /// Make a default preview header
564        pub fn default_preview(status: &Status, tab: &Tab, width: u16) -> Vec<ClickableString> {
565            Self::pair_to_clickable(&Self::make_default_preview(status, tab), width)
566        }
567    }
568}
569
570pub use inner::{ClickableLine, ClickableString, Footer, Header, PreviewHeader};