fm/app/
header_footer.rs

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