fm/io/
display.rs

1use std::{
2    io::{self, Stdout, Write},
3    rc::Rc,
4};
5
6use anyhow::{Context, Result};
7use crossterm::{
8    execute,
9    terminal::{disable_raw_mode, LeaveAlternateScreen},
10};
11use nucleo::Config;
12use parking_lot::MutexGuard;
13use ratatui::{
14    backend::CrosstermBackend,
15    layout::{Constraint, Direction, Layout, Offset, Position, Rect, Size},
16    prelude::*,
17    style::{Color, Modifier, Style},
18    text::{Line, Span},
19    widgets::{Block, Borders, Paragraph},
20    Frame, Terminal,
21};
22
23use crate::io::{read_last_log_line, DrawMenu, ImageAdapter};
24use crate::log_info;
25use crate::modes::{
26    highlighted_text, parse_input_permission, AnsiString, BinLine, BinaryContent, Content,
27    ContentWindow, CursorOffset, Display as DisplayMode, DisplayedImage, FileInfo, FuzzyFinder,
28    HLContent, Input, InputSimple, LineDisplay, Menu as MenuMode, MoreInfos, Navigate,
29    NeedConfirmation, Preview, Remote, SecondLine, Selectable, TLine, TakeSkip, TakeSkipEnum, Text,
30    TextKind, Trash, Tree,
31};
32use crate::{
33    app::{ClickableLine, Footer, Header, PreviewHeader, Status, Tab},
34    modes::Users,
35};
36use crate::{
37    colored_skip_take,
38    config::{with_icon, with_icon_metadata, ColorG, Gradient, MATCHER, MENU_STYLES},
39};
40use crate::{common::path_to_string, modes::Icon};
41
42use super::ImageDisplayer;
43
44pub trait Offseted {
45    fn offseted(&self, x: u16, y: u16) -> Self;
46}
47
48impl Offseted for Rect {
49    /// Returns a new rect moved by x horizontally and y vertically and constrained to the current rect.
50    /// It won't draw outside of the original rect since it it also intersected by the original rect.
51    fn offseted(&self, x: u16, y: u16) -> Self {
52        self.offset(Offset {
53            x: x as i32,
54            y: y as i32,
55        })
56        .intersection(*self)
57    }
58}
59
60/// Common trait all "window" should implement.
61/// It's mostly used as an entry point for the rendering and should call another method.
62trait Draw {
63    /// Entry point for window rendering.
64    fn draw(&self, f: &mut Frame, rect: &Rect);
65}
66
67macro_rules! colored_iter {
68    ($t:ident) => {
69        std::iter::zip(
70            $t.iter(),
71            Gradient::new(
72                ColorG::from_ratatui(
73                    MENU_STYLES
74                        .get()
75                        .expect("Menu colors should be set")
76                        .first
77                        .fg
78                        .unwrap_or(Color::Rgb(0, 0, 0)),
79                )
80                .unwrap_or_default(),
81                ColorG::from_ratatui(
82                    MENU_STYLES
83                        .get()
84                        .expect("Menu colors should be set")
85                        .palette_3
86                        .fg
87                        .unwrap_or(Color::Rgb(0, 0, 0)),
88                )
89                .unwrap_or_default(),
90                $t.len(),
91            )
92            .gradient()
93            .map(|color| Style::from(color)),
94        )
95    };
96}
97
98/// At least 120 chars width to display 2 tabs.
99pub const MIN_WIDTH_FOR_DUAL_PANE: u16 = 120;
100
101enum TabPosition {
102    Left,
103    Right,
104}
105
106/// Bunch of attributes describing the state of a main window
107/// relatively to other windows
108struct FilesAttributes {
109    /// is this the left or right window ?
110    tab_position: TabPosition,
111    /// is this tab selected ?
112    is_selected: bool,
113    /// is there a menuary window ?
114    has_window_below: bool,
115}
116
117impl FilesAttributes {
118    fn new(tab_position: TabPosition, is_selected: bool, has_window_below: bool) -> Self {
119        Self {
120            tab_position,
121            is_selected,
122            has_window_below,
123        }
124    }
125
126    fn is_right(&self) -> bool {
127        matches!(self.tab_position, TabPosition::Right)
128    }
129}
130
131struct FilesBuilder;
132
133impl FilesBuilder {
134    fn dual(status: &Status) -> (Files<'_>, Files<'_>) {
135        let first_selected = status.focus.is_left();
136        let menu_selected = !first_selected;
137        let attributes_left = FilesAttributes::new(
138            TabPosition::Left,
139            first_selected,
140            status.tabs[0].need_menu_window(),
141        );
142        let files_left = Files::new(status, 0, attributes_left);
143        let attributes_right = FilesAttributes::new(
144            TabPosition::Right,
145            menu_selected,
146            status.tabs[1].need_menu_window(),
147        );
148        let files_right = Files::new(status, 1, attributes_right);
149        (files_left, files_right)
150    }
151
152    fn single(status: &Status) -> Files<'_> {
153        let attributes_left =
154            FilesAttributes::new(TabPosition::Left, true, status.tabs[0].need_menu_window());
155        Files::new(status, 0, attributes_left)
156    }
157}
158
159struct Files<'a> {
160    status: &'a Status,
161    tab: &'a Tab,
162    attributes: FilesAttributes,
163}
164
165impl<'a> Files<'a> {
166    fn draw(&self, f: &mut Frame, rect: &Rect, image_adapter: &mut ImageAdapter) {
167        let use_log_line = self.use_log_line();
168        let rects = Rects::files(rect, use_log_line);
169
170        if self.should_preview_in_right_tab() {
171            self.preview_in_right_tab(f, &rects[0], &rects[2], image_adapter);
172            return;
173        }
174
175        self.header(f, &rects[0]);
176        self.copy_progress_bar(f, &rects[1]);
177        self.second_line(f, &rects[1]);
178        self.content(f, &rects[1], &rects[2], image_adapter);
179        if use_log_line {
180            self.log_line(f, &rects[3]);
181        }
182        self.footer(f, rects.last().expect("Shouldn't be empty"));
183    }
184}
185
186impl<'a> Files<'a> {
187    fn new(status: &'a Status, index: usize, attributes: FilesAttributes) -> Self {
188        Self {
189            status,
190            tab: &status.tabs[index],
191            attributes,
192        }
193    }
194
195    fn use_log_line(&self) -> bool {
196        matches!(
197            self.tab.display_mode,
198            DisplayMode::Directory | DisplayMode::Tree
199        ) && !self.attributes.has_window_below
200            && !self.attributes.is_right()
201    }
202
203    fn should_preview_in_right_tab(&self) -> bool {
204        self.status.session.dual() && self.is_right() && self.status.session.preview()
205    }
206
207    fn preview_in_right_tab(
208        &self,
209        f: &mut Frame,
210        header_rect: &Rect,
211        content_rect: &Rect,
212        image_adapter: &mut ImageAdapter,
213    ) {
214        let tab = &self.status.tabs[1];
215        PreviewHeader::into_default_preview(self.status, tab, content_rect.width).draw_left(
216            f,
217            *header_rect,
218            self.status.index == 1,
219        );
220        PreviewDisplay::new_with_args(self.status, tab).draw(f, content_rect, image_adapter);
221    }
222
223    fn is_right(&self) -> bool {
224        self.attributes.is_right()
225    }
226
227    fn header(&self, f: &mut Frame, rect: &Rect) {
228        FilesHeader::new(self.status, self.tab, self.attributes.is_selected).draw(f, rect);
229    }
230
231    /// Display a copy progress bar on the left tab.
232    /// Nothing is drawn if there's no copy atm.
233    /// If the copy file queue has length > 1, we also display its size.
234    fn copy_progress_bar(&self, f: &mut Frame, rect: &Rect) {
235        if self.is_right() {
236            return;
237        }
238        CopyProgressBar::new(self.status).draw(f, rect);
239    }
240
241    fn second_line(&self, f: &mut Frame, rect: &Rect) {
242        if matches!(
243            self.tab.display_mode,
244            DisplayMode::Directory | DisplayMode::Tree
245        ) {
246            FilesSecondLine::new(self.status, self.tab).draw(f, rect);
247        }
248    }
249
250    fn content(
251        &self,
252        f: &mut Frame,
253        second_line_rect: &Rect,
254        content_rect: &Rect,
255        image_adapter: &mut ImageAdapter,
256    ) {
257        match &self.tab.display_mode {
258            DisplayMode::Directory => DirectoryDisplay::new(self).draw(f, content_rect),
259            DisplayMode::Tree => TreeDisplay::new(self).draw(f, content_rect),
260            DisplayMode::Preview => PreviewDisplay::new(self).draw(f, content_rect, image_adapter),
261            DisplayMode::Fuzzy => FuzzyDisplay::new(self).fuzzy(f, second_line_rect, content_rect),
262        }
263    }
264
265    fn log_line(&self, f: &mut Frame, rect: &Rect) {
266        LogLine.draw(f, rect);
267    }
268
269    fn footer(&self, f: &mut Frame, rect: &Rect) {
270        FilesFooter::new(self.status, self.tab, self.attributes.is_selected).draw(f, rect);
271    }
272}
273
274struct CopyProgressBar<'a> {
275    status: &'a Status,
276}
277
278impl<'a> Draw for CopyProgressBar<'a> {
279    fn draw(&self, f: &mut Frame, rect: &Rect) {
280        let Some(content) = self.status.internal_settings.format_copy_progress() else {
281            return;
282        };
283        let p_rect = rect.offseted(1, 0);
284        Span::styled(
285            &content,
286            MENU_STYLES
287                .get()
288                .expect("Menu colors should be set")
289                .palette_2,
290        )
291        .render(p_rect, f.buffer_mut());
292    }
293}
294
295impl<'a> CopyProgressBar<'a> {
296    fn new(status: &'a Status) -> Self {
297        Self { status }
298    }
299}
300
301struct FuzzyDisplay<'a> {
302    status: &'a Status,
303}
304
305impl<'a> FuzzyDisplay<'a> {
306    fn new(files: &'a Files) -> Self {
307        Self {
308            status: files.status,
309        }
310    }
311
312    fn fuzzy(&self, f: &mut Frame, second_line_rect: &Rect, content_rect: &Rect) {
313        let Some(fuzzy) = &self.status.fuzzy else {
314            return;
315        };
316        let rects = Rects::fuzzy(content_rect);
317
318        self.draw_prompt(fuzzy, f, second_line_rect);
319        self.draw_match_counts(fuzzy, f, &rects[0]);
320        self.draw_matches(fuzzy, f, rects[1]);
321    }
322
323    /// Draw the matched items
324    fn draw_match_counts(&self, fuzzy: &FuzzyFinder<String>, f: &mut Frame, rect: &Rect) {
325        let match_info = self.line_match_info(fuzzy);
326        let match_count_paragraph = Self::paragraph_match_count(match_info);
327        f.render_widget(match_count_paragraph, *rect);
328    }
329
330    fn draw_prompt(&self, fuzzy: &FuzzyFinder<String>, f: &mut Frame, rect: &Rect) {
331        // Render the prompt string at the bottom
332        let input = fuzzy.input.string();
333        let prompt_paragraph = Paragraph::new(vec![Line::from(vec![
334            Span::styled(
335                "> ",
336                MENU_STYLES
337                    .get()
338                    .expect("MENU_STYLES should be set")
339                    .palette_3,
340            ),
341            Span::styled(
342                input,
343                MENU_STYLES
344                    .get()
345                    .expect("MENU_STYLES should be set")
346                    .palette_2,
347            ),
348        ])])
349        .block(Block::default().borders(Borders::NONE));
350
351        f.render_widget(prompt_paragraph, *rect);
352        self.set_cursor_position(f, rect, &fuzzy.input);
353    }
354
355    fn set_cursor_position(&self, f: &mut Frame, rect: &Rect, input: &Input) {
356        // Move the cursor to the prompt
357        f.set_cursor_position(Position {
358            x: rect.x + input.index() as u16 + 2,
359            y: rect.y,
360        });
361    }
362
363    fn line_match_info(&self, fuzzy: &FuzzyFinder<String>) -> Line<'_> {
364        Line::from(vec![
365            Span::styled("  ", Style::default().fg(Color::Yellow)),
366            Span::styled(
367                format!("{}", fuzzy.matched_item_count),
368                Style::default()
369                    .fg(Color::Yellow)
370                    .add_modifier(Modifier::ITALIC),
371            ),
372            Span::styled(" / ", Style::default().fg(Color::Yellow)),
373            Span::styled(
374                format!("{}", fuzzy.item_count),
375                Style::default().fg(Color::Yellow),
376            ),
377            Span::raw(" "),
378        ])
379    }
380
381    fn paragraph_match_count(match_info: Line) -> Paragraph {
382        Paragraph::new(match_info)
383            .style(Style::default())
384            .right_aligned()
385            .block(Block::default().borders(Borders::NONE))
386    }
387
388    fn draw_matches(&self, fuzzy: &FuzzyFinder<String>, f: &mut Frame, rect: Rect) {
389        let snapshot = fuzzy.matcher.snapshot();
390        let (top, bottom) = fuzzy.top_bottom();
391        let mut indices = vec![];
392        let mut matcher = MATCHER.lock();
393        matcher.config = Config::DEFAULT;
394        let is_file = fuzzy.kind.is_file();
395        if is_file {
396            matcher.config.set_match_paths();
397        }
398        snapshot
399            .matched_items(top..bottom)
400            .enumerate()
401            .for_each(|(index, t)| {
402                snapshot.pattern().column_pattern(0).indices(
403                    t.matcher_columns[0].slice(..),
404                    &mut matcher,
405                    &mut indices,
406                );
407                let text = t.matcher_columns[0].to_string();
408                let highlights_usize = Self::highlights_indices(&mut indices);
409                let is_flagged = is_file
410                    && self
411                        .status
412                        .menu
413                        .flagged
414                        .contains(std::path::Path::new(&text));
415
416                let line = highlighted_text(
417                    &text,
418                    &highlights_usize,
419                    index as u32 + top == fuzzy.index,
420                    is_file,
421                    is_flagged,
422                );
423                let line_rect = Self::line_rect(rect, index);
424                line.render(line_rect, f.buffer_mut());
425            });
426    }
427
428    fn highlights_indices(indices: &mut Vec<u32>) -> Vec<usize> {
429        indices.sort_unstable();
430        indices.dedup();
431        let highlights = indices.drain(..);
432        highlights.map(|index| index as usize).collect()
433    }
434
435    fn line_rect(rect: Rect, index: usize) -> Rect {
436        let mut line_rect = rect;
437        line_rect.y += index as u16;
438        line_rect
439    }
440}
441
442struct DirectoryDisplay<'a> {
443    status: &'a Status,
444    tab: &'a Tab,
445}
446
447impl<'a> Draw for DirectoryDisplay<'a> {
448    fn draw(&self, f: &mut Frame, rect: &Rect) {
449        self.files(f, rect)
450    }
451}
452
453impl<'a> DirectoryDisplay<'a> {
454    fn new(files: &'a Files) -> Self {
455        Self {
456            status: files.status,
457            tab: files.tab,
458        }
459    }
460
461    /// Displays the current directory content, one line per item like in
462    /// `ls -l`.
463    ///
464    /// Only the files around the selected one are displayed.
465    /// We reverse the attributes of the selected one, underline the flagged files.
466    /// When we display a simpler version, the menu line is used to display the
467    /// metadata of the selected file.
468    fn files(&self, f: &mut Frame, rect: &Rect) {
469        let group_owner_sizes = self.group_owner_size();
470        let p_rect = rect.offseted(2, 0);
471        let formater = Self::pick_formater(self.status.session.metadata(), p_rect.width);
472        let with_icon = with_icon();
473        let lines: Vec<_> = self
474            .tab
475            .dir_enum_skip_take()
476            .map(|(index, file)| {
477                self.files_line(group_owner_sizes, index, file, &formater, with_icon)
478            })
479            .collect();
480        Paragraph::new(lines).render(p_rect, f.buffer_mut());
481    }
482
483    fn pick_formater(with_metadata: bool, width: u16) -> Formater {
484        let kind = FormatKind::from_flags(with_metadata, width);
485
486        match kind {
487            FormatKind::Metadata => FileFormater::metadata,
488            FormatKind::MetadataNoGroup => FileFormater::metadata_no_group,
489            FormatKind::MetadataNoPermissions => FileFormater::metadata_no_permissions,
490            FormatKind::MetadataNoOwner => FileFormater::metadata_no_owner,
491            FormatKind::Simple => FileFormater::simple,
492        }
493    }
494
495    fn group_owner_size(&self) -> (usize, usize) {
496        if self.status.session.metadata() {
497            (
498                self.tab.directory.group_column_width(),
499                self.tab.directory.owner_column_width(),
500            )
501        } else {
502            (0, 0)
503        }
504    }
505
506    fn files_line<'b>(
507        &self,
508        group_owner_sizes: (usize, usize),
509        index: usize,
510        file: &FileInfo,
511        formater: &fn(&FileInfo, (usize, usize)) -> String,
512        with_icon: bool,
513    ) -> Line<'b> {
514        let mut style = file.style();
515        self.reverse_selected(index, &mut style);
516        self.color_searched(file, &mut style);
517        let mut content = formater(file, group_owner_sizes);
518
519        content.push(' ');
520        if with_icon {
521            content.push_str(file.icon());
522        }
523        content.push_str(&file.filename);
524
525        Line::from(vec![
526            self.span_flagged_symbol(file, &mut style),
527            Span::styled(content, style),
528        ])
529    }
530
531    fn reverse_selected(&self, index: usize, style: &mut Style) {
532        if index == self.tab.directory.index {
533            style.add_modifier |= Modifier::REVERSED;
534        }
535    }
536
537    fn color_searched(&self, file: &FileInfo, style: &mut Style) {
538        if self.tab.search.is_match(&file.filename) {
539            style.fg = MENU_STYLES
540                .get()
541                .expect("Menu style should be set")
542                .palette_4
543                .fg;
544        }
545    }
546
547    fn span_flagged_symbol<'b>(&self, file: &FileInfo, style: &mut Style) -> Span<'b> {
548        if self.status.menu.flagged.contains(&file.path) {
549            style.add_modifier |= Modifier::BOLD;
550            Span::styled(
551                "â–ˆ",
552                MENU_STYLES.get().expect("Menu colors should be set").second,
553            )
554        } else {
555            Span::raw("")
556        }
557    }
558}
559
560type Formater = fn(&FileInfo, (usize, usize)) -> String;
561
562struct FileFormater;
563
564impl FileFormater {
565    fn metadata(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
566        file.format_base(owner_sizes.1, owner_sizes.0)
567    }
568
569    fn metadata_no_group(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
570        file.format_no_group(owner_sizes.1)
571    }
572
573    fn metadata_no_permissions(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
574        file.format_no_permissions(owner_sizes.1)
575    }
576
577    fn metadata_no_owner(file: &FileInfo, _owner_sizes: (usize, usize)) -> String {
578        file.format_no_owner()
579    }
580
581    fn simple(_file: &FileInfo, _owner_sizes: (usize, usize)) -> String {
582        " ".to_owned()
583    }
584}
585
586#[derive(Debug)]
587enum FormatKind {
588    Metadata,
589    MetadataNoGroup,
590    MetadataNoPermissions,
591    MetadataNoOwner,
592    Simple,
593}
594
595impl FormatKind {
596    #[rustfmt::skip]
597    fn from_flags(
598        with_metadata: bool,
599        width: u16,
600    ) -> Self {
601        let wide_enough_for_group = width > 70;
602        let wide_enough_for_metadata = width > 50;
603        let wide_enough_for_permissions = width > 40;
604        let wide_enough_for_owner = width > 30;
605
606        match (
607            with_metadata,
608            wide_enough_for_group,
609            wide_enough_for_metadata,
610            wide_enough_for_permissions,
611            wide_enough_for_owner,
612        ) {
613            (true, true,  _,    _,       _)     => Self::Metadata,
614            (true, false, true, _,       _)     => Self::MetadataNoGroup,
615            (true, _,     _,    true,    _)     => Self::MetadataNoPermissions,
616            (true, _,     _,    _,    true)     => Self::MetadataNoOwner,
617            _ => Self::Simple,
618        }
619    }
620}
621
622struct TreeDisplay<'a> {
623    status: &'a Status,
624    tab: &'a Tab,
625}
626
627impl<'a> Draw for TreeDisplay<'a> {
628    fn draw(&self, f: &mut Frame, rect: &Rect) {
629        self.tree(f, rect)
630    }
631}
632
633impl<'a> TreeDisplay<'a> {
634    fn new(files: &'a Files) -> Self {
635        Self {
636            status: files.status,
637            tab: files.tab,
638        }
639    }
640
641    fn tree(&self, f: &mut Frame, rect: &Rect) {
642        Self::tree_content(
643            self.status,
644            &self.tab.tree,
645            &self.tab.users,
646            &self.tab.window,
647            self.status.session.metadata(),
648            f,
649            rect,
650        )
651    }
652
653    fn tree_content(
654        status: &Status,
655        tree: &Tree,
656        users: &Users,
657        window: &ContentWindow,
658        with_metadata: bool,
659        f: &mut Frame,
660        rect: &Rect,
661    ) {
662        let p_rect = rect.offseted(1, 0);
663        let width = p_rect.width.saturating_sub(6);
664        let formater = DirectoryDisplay::pick_formater(with_metadata, width);
665        let with_icon = Self::use_icon(with_metadata);
666        let lines: Vec<_> = tree
667            .lines_enum_skip_take(window)
668            .filter_map(|(index, line_builder)| {
669                Self::tree_line(
670                    status,
671                    index == 0,
672                    line_builder,
673                    &formater,
674                    users,
675                    with_icon,
676                )
677                .ok()
678            })
679            .collect();
680        Paragraph::new(lines).render(p_rect, f.buffer_mut());
681    }
682
683    fn use_icon(with_metadata: bool) -> bool {
684        (!with_metadata && with_icon()) || with_icon_metadata()
685    }
686
687    fn tree_line<'b>(
688        status: &Status,
689        with_offset: bool,
690        line_builder: &'b TLine,
691        formater: &Formater,
692        users: &Users,
693        with_icon: bool,
694    ) -> Result<Line<'b>> {
695        let path = line_builder.path();
696        let fileinfo = FileInfo::new(&line_builder.path, users)?;
697        let mut style = fileinfo.style();
698        Self::reverse_flagged(line_builder, &mut style);
699        Self::color_searched(status, &fileinfo, &mut style);
700        Ok(Line::from(vec![
701            Self::span_flagged_symbol(status, path, &mut style),
702            Self::metadata(&fileinfo, formater, style),
703            Self::prefix(line_builder),
704            Self::whitespaces(status, path, with_offset),
705            Self::filename(line_builder, with_icon, style),
706        ]))
707    }
708
709    fn reverse_flagged(line_builder: &TLine, style: &mut Style) {
710        if line_builder.is_selected {
711            style.add_modifier |= Modifier::REVERSED;
712        }
713    }
714
715    fn color_searched(status: &Status, file: &FileInfo, style: &mut Style) {
716        if status.current_tab().search.is_match(&file.filename) {
717            style.fg = MENU_STYLES
718                .get()
719                .expect("Menu style should be set")
720                .palette_4
721                .fg;
722        }
723    }
724
725    fn span_flagged_symbol<'b>(
726        status: &Status,
727        path: &std::path::Path,
728        style: &mut Style,
729    ) -> Span<'b> {
730        if status.menu.flagged.contains(path) {
731            style.add_modifier |= Modifier::BOLD;
732            Span::styled(
733                "â–ˆ",
734                MENU_STYLES.get().expect("Menu colors should be set").second,
735            )
736        } else {
737            Span::raw(" ")
738        }
739    }
740
741    fn metadata<'b>(fileinfo: &FileInfo, formater: &Formater, style: Style) -> Span<'b> {
742        Span::styled(formater(fileinfo, (6, 6)), style)
743    }
744
745    fn prefix(line_builder: &TLine) -> Span<'_> {
746        Span::raw(line_builder.prefix())
747    }
748
749    fn whitespaces<'b>(status: &Status, path: &std::path::Path, with_offset: bool) -> Span<'b> {
750        Span::raw(" ".repeat(status.menu.flagged.contains(path) as usize + with_offset as usize))
751    }
752
753    fn filename<'b>(line_builder: &TLine, with_icon: bool, style: Style) -> Span<'b> {
754        Span::styled(line_builder.filename(with_icon), style)
755    }
756}
757
758struct PreviewDisplay<'a> {
759    status: &'a Status,
760    tab: &'a Tab,
761}
762
763/// Display a scrollable preview of a file.
764/// Multiple modes are supported :
765/// if the filename extension is recognized, the preview is highlighted,
766/// if the file content is recognized as binary, an hex dump is previewed with 16 bytes lines,
767/// else the content is supposed to be text and shown as such.
768/// It may fail to recognize some usual extensions, notably `.toml`.
769/// It may fail to recognize small files (< 1024 bytes).
770impl<'a> PreviewDisplay<'a> {
771    fn draw(&self, f: &mut Frame, rect: &Rect, image_adapter: &mut ImageAdapter) {
772        self.preview(f, rect, image_adapter)
773    }
774}
775
776impl<'a> PreviewDisplay<'a> {
777    fn new(files: &'a Files) -> Self {
778        Self {
779            status: files.status,
780            tab: files.tab,
781        }
782    }
783
784    fn new_with_args(status: &'a Status, tab: &'a Tab) -> Self {
785        Self { status, tab }
786    }
787
788    fn preview(&self, f: &mut Frame, rect: &Rect, image_adapter: &mut ImageAdapter) {
789        let tab = self.tab;
790        let window = &tab.window;
791        let length = tab.preview.len();
792        match &tab.preview {
793            Preview::Syntaxed(syntaxed) => {
794                let number_col_width = Self::number_width(length);
795                self.syntaxed(f, syntaxed, length, rect, number_col_width, window)
796            }
797            Preview::Binary(bin) => self.binary(f, bin, length, rect, window),
798            Preview::Image(image) => self.image(image, rect, image_adapter),
799            Preview::Tree(tree_preview) => self.tree_preview(f, tree_preview, window, rect),
800            Preview::Text(ansi_text)
801                if matches!(ansi_text.kind, TextKind::CommandStdout | TextKind::Plugin) =>
802            {
803                self.ansi_text(f, ansi_text, length, rect, window)
804            }
805            Preview::Text(text) => self.normal_text(f, text, length, rect, window),
806
807            Preview::Empty => (),
808        };
809    }
810
811    fn line_number_span<'b>(
812        line_number_to_print: &usize,
813        number_col_width: usize,
814        style: Style,
815    ) -> Span<'b> {
816        Span::styled(
817            format!("{line_number_to_print:>number_col_width$}  "),
818            style,
819        )
820    }
821
822    /// Number of digits in decimal representation
823    fn number_width(mut number: usize) -> usize {
824        let mut width = 0;
825        while number != 0 {
826            width += 1;
827            number /= 10;
828        }
829        width
830    }
831
832    /// Draw every line of the text
833    fn normal_text(
834        &self,
835        f: &mut Frame,
836        text: &Text,
837        length: usize,
838        rect: &Rect,
839        window: &ContentWindow,
840    ) {
841        let p_rect = rect.offseted(2, 0);
842        let lines: Vec<_> = text
843            .take_skip(window.top, window.bottom, length)
844            .map(Line::raw)
845            .collect();
846        Paragraph::new(lines).render(p_rect, f.buffer_mut());
847    }
848
849    fn syntaxed(
850        &self,
851        f: &mut Frame,
852        syntaxed: &HLContent,
853        length: usize,
854        rect: &Rect,
855        number_col_width: usize,
856        window: &ContentWindow,
857    ) {
858        let p_rect = rect.offseted(3, 0);
859        let number_col_style = MENU_STYLES.get().expect("").first;
860        let lines: Vec<_> = syntaxed
861            .take_skip_enum(window.top, window.bottom, length)
862            .map(|(index, vec_line)| {
863                let mut line = vec![Self::line_number_span(
864                    &index,
865                    number_col_width,
866                    number_col_style,
867                )];
868                line.append(
869                    &mut vec_line
870                        .iter()
871                        .map(|token| Span::styled(&token.content, token.style))
872                        .collect::<Vec<_>>(),
873                );
874                Line::from(line)
875            })
876            .collect();
877        Paragraph::new(lines).render(p_rect, f.buffer_mut());
878    }
879
880    fn binary(
881        &self,
882        f: &mut Frame,
883        bin: &BinaryContent,
884        length: usize,
885        rect: &Rect,
886        window: &ContentWindow,
887    ) {
888        let p_rect = rect.offseted(3, 0);
889        let line_number_width_hex = bin.number_width_hex();
890        let (style_number, style_ascii) = {
891            let ms = MENU_STYLES.get().expect("Menu colors should be set");
892            (ms.first, ms.second)
893        };
894        let lines: Vec<_> = (*bin)
895            .take_skip_enum(window.top, window.bottom, length)
896            .map(|(index, bin_line)| {
897                Line::from(vec![
898                    Span::styled(
899                        BinLine::format_line_nr_hex(index + 1 + window.top, line_number_width_hex),
900                        style_number,
901                    ),
902                    Span::raw(bin_line.format_hex()),
903                    Span::raw(" "),
904                    Span::styled(bin_line.format_as_ascii(), style_ascii),
905                ])
906            })
907            .collect();
908        Paragraph::new(lines).render(p_rect, f.buffer_mut());
909    }
910
911    /// Draw the image with correct adapter in the current window.
912    /// The position is absolute, which is problematic when the app is embeded into a floating terminal.
913    fn image(&self, image: &DisplayedImage, rect: &Rect, image_adapter: &mut ImageAdapter) {
914        if let Err(e) = image_adapter.draw(image, *rect) {
915            log_info!("Couldn't display {path}: {e:?}", path = image.identifier);
916        }
917    }
918
919    fn tree_preview(&self, f: &mut Frame, tree: &Tree, window: &ContentWindow, rect: &Rect) {
920        TreeDisplay::tree_content(self.status, tree, &self.tab.users, window, false, f, rect)
921    }
922
923    fn ansi_text(
924        &self,
925        f: &mut Frame,
926        ansi_text: &Text,
927        length: usize,
928        rect: &Rect,
929        window: &ContentWindow,
930    ) {
931        let p_rect = rect.offseted(3, 0);
932        let lines: Vec<_> = ansi_text
933            .take_skip(window.top, window.bottom, length)
934            .map(|line| {
935                Line::from(
936                    AnsiString::parse(line)
937                        .iter()
938                        .map(|(chr, style)| Span::styled(chr.to_string(), style))
939                        .collect::<Vec<_>>(),
940                )
941            })
942            .collect();
943        Paragraph::new(lines).render(p_rect, f.buffer_mut());
944    }
945}
946
947struct FilesHeader<'a> {
948    status: &'a Status,
949    tab: &'a Tab,
950    is_selected: bool,
951}
952
953impl<'a> Draw for FilesHeader<'a> {
954    /// Display the top line on terminal.
955    /// Its content depends on the mode.
956    /// In normal mode we display the path and number of files.
957    /// something else.
958    /// The colors are reversed when the tab is selected. It gives a visual indication of where he is.
959    fn draw(&self, f: &mut Frame, rect: &Rect) {
960        let width = rect.width;
961        let header: Box<dyn ClickableLine> = match self.tab.display_mode {
962            DisplayMode::Preview => Box::new(PreviewHeader::new(self.status, self.tab, width)),
963            _ => Box::new(Header::new(self.status, self.tab).expect("Couldn't build header")),
964        };
965        header.draw_left(f, *rect, self.is_selected);
966        header.draw_right(f, *rect, self.is_selected);
967    }
968}
969
970impl<'a> FilesHeader<'a> {
971    fn new(status: &'a Status, tab: &'a Tab, is_selected: bool) -> Self {
972        Self {
973            status,
974            tab,
975            is_selected,
976        }
977    }
978}
979
980#[derive(Default)]
981struct FilesSecondLine {
982    content: Option<String>,
983    style: Option<Style>,
984}
985
986impl Draw for FilesSecondLine {
987    fn draw(&self, f: &mut Frame, rect: &Rect) {
988        let p_rect = rect.offseted(1, 0);
989        if let (Some(content), Some(style)) = (&self.content, &self.style) {
990            Span::styled(content, *style).render(p_rect, f.buffer_mut());
991        };
992    }
993}
994
995impl FilesSecondLine {
996    fn new(status: &Status, tab: &Tab) -> Self {
997        if tab.display_mode.is_preview() || status.session.metadata() {
998            return Self::default();
999        };
1000        if let Ok(file) = tab.current_file() {
1001            Self::second_line_detailed(&file)
1002        } else {
1003            Self::default()
1004        }
1005    }
1006
1007    fn second_line_detailed(file: &FileInfo) -> Self {
1008        let owner_size = file.owner.len();
1009        let group_size = file.group.len();
1010        let mut style = file.style();
1011        style.add_modifier ^= Modifier::REVERSED;
1012
1013        Self {
1014            content: Some(file.format_metadata(owner_size, group_size)),
1015            style: Some(style),
1016        }
1017    }
1018}
1019
1020struct LogLine;
1021
1022impl Draw for LogLine {
1023    fn draw(&self, f: &mut Frame, rect: &Rect) {
1024        let p_rect = rect.offseted(4, 0);
1025        let log = &read_last_log_line();
1026        Span::styled(
1027            log,
1028            MENU_STYLES.get().expect("Menu colors should be set").second,
1029        )
1030        .render(p_rect, f.buffer_mut());
1031    }
1032}
1033
1034struct FilesFooter<'a> {
1035    status: &'a Status,
1036    tab: &'a Tab,
1037    is_selected: bool,
1038}
1039
1040impl<'a> Draw for FilesFooter<'a> {
1041    /// Display the top line on terminal.
1042    /// Its content depends on the mode.
1043    /// In normal mode we display the path and number of files.
1044    /// When a confirmation is needed we ask the user to input `'y'` or
1045    /// something else.
1046    /// Returns the result of the number of printed chars.
1047    /// The colors are reversed when the tab is selected. It gives a visual indication of where he is.
1048    fn draw(&self, f: &mut Frame, rect: &Rect) {
1049        match self.tab.display_mode {
1050            DisplayMode::Preview => (),
1051            _ => {
1052                let Ok(footer) = Footer::new(self.status, self.tab) else {
1053                    return;
1054                };
1055                // let p_rect = rect.offseted(0, rect.height.saturating_sub(1));
1056                footer.draw_left(f, *rect, self.is_selected);
1057            }
1058        }
1059    }
1060}
1061
1062impl<'a> FilesFooter<'a> {
1063    fn new(status: &'a Status, tab: &'a Tab, is_selected: bool) -> Self {
1064        Self {
1065            status,
1066            tab,
1067            is_selected,
1068        }
1069    }
1070}
1071
1072struct Menu<'a> {
1073    status: &'a Status,
1074    tab: &'a Tab,
1075}
1076
1077impl<'a> Draw for Menu<'a> {
1078    fn draw(&self, f: &mut Frame, rect: &Rect) {
1079        if !self.tab.need_menu_window() {
1080            return;
1081        }
1082        let mode = self.tab.menu_mode;
1083        self.cursor(f, rect);
1084        MenuFirstLine::new(self.status, rect).draw(f, rect);
1085        self.menu_line(f, rect);
1086        self.content_per_mode(f, rect, mode);
1087        self.binds_per_mode(f, rect, mode);
1088    }
1089}
1090
1091impl<'a> Menu<'a> {
1092    fn new(status: &'a Status, index: usize) -> Self {
1093        Self {
1094            status,
1095            tab: &status.tabs[index],
1096        }
1097    }
1098
1099    /// Render a generic content of elements which are references to str.
1100    /// It creates a new rect, offseted by `x, y` and intersected with rect.
1101    /// Each element of content is wraped by a styled span (with his own style) and then wrapped by a line.
1102    /// The iteration only take enough element to be displayed in the rect.
1103    /// Then we create a paragraph with default parameters and render it.
1104    fn render_content<T>(content: &[T], f: &mut Frame, rect: &Rect, x: u16, y: u16)
1105    where
1106        T: AsRef<str>,
1107    {
1108        let p_rect = rect.offseted(x, y);
1109        let lines: Vec<_> = colored_iter!(content)
1110            .map(|(text, style)| Line::from(vec![Span::styled(text.as_ref(), style)]))
1111            .take(p_rect.height as usize + 2)
1112            .collect();
1113        Paragraph::new(lines).render(p_rect, f.buffer_mut());
1114    }
1115
1116    /// Hide the cursor if the current mode doesn't require one.
1117    /// Otherwise, display a cursor in the top row, at a correct column.
1118    ///
1119    /// # Errors
1120    ///
1121    /// may fail if we can't display on the terminal.
1122    fn cursor(&self, f: &mut Frame, rect: &Rect) {
1123        if self.tab.menu_mode.show_cursor() {
1124            let offset = self.tab.menu_mode.cursor_offset();
1125            let avail = rect.width.saturating_sub(offset + 1) as usize;
1126            let cursor_index = self.status.menu.input.display_index(avail) as u16;
1127            let x = rect.x + offset + cursor_index;
1128            f.set_cursor_position(Position::new(x, rect.y));
1129        }
1130    }
1131
1132    fn menu_line(&self, f: &mut Frame, rect: &Rect) {
1133        let menu_style = MENU_STYLES.get().expect("Menu colors should be set");
1134        let menu = menu_style.second;
1135        match self.tab.menu_mode {
1136            MenuMode::InputSimple(InputSimple::Chmod) => {
1137                let first = menu_style.first;
1138                self.menu_line_chmod(f, rect, first, menu);
1139            }
1140            MenuMode::InputSimple(InputSimple::Remote) => {
1141                let palette_3 = menu_style.palette_3;
1142                self.menu_line_remote(f, rect, palette_3);
1143            }
1144            edit => {
1145                let rect = rect.offseted(2, 1);
1146                Span::styled(edit.second_line(), menu).render(rect, f.buffer_mut());
1147            }
1148        };
1149    }
1150
1151    fn menu_line_chmod(&self, f: &mut Frame, rect: &Rect, first: Style, menu: Style) {
1152        let input = self.status.menu.input.string();
1153        let (text, is_valid) = parse_input_permission(&input);
1154        let style = if is_valid { first } else { menu };
1155        let p_rect = rect.offseted(11, 1);
1156        Line::styled(text.as_ref(), style).render(p_rect, f.buffer_mut());
1157    }
1158
1159    fn menu_line_remote(&self, f: &mut Frame, rect: &Rect, first: Style) {
1160        let input = self.status.menu.input.string();
1161        let current_path = path_to_string(&self.tab.current_directory_path());
1162
1163        if let Some(remote) = Remote::from_input(input, &current_path) {
1164            let command = format!("{command:?}", command = remote.command());
1165            let p_rect = rect.offseted(4, 8);
1166            Line::styled(command, first).render(p_rect, f.buffer_mut());
1167        };
1168    }
1169
1170    fn content_per_mode(&self, f: &mut Frame, rect: &Rect, mode: MenuMode) {
1171        match mode {
1172            MenuMode::Navigate(mode) => self.navigate(mode, f, rect),
1173            MenuMode::NeedConfirmation(mode) => self.confirm(mode, f, rect),
1174            MenuMode::InputCompleted(_) => self.completion(f, rect),
1175            MenuMode::InputSimple(mode) => Self::input_simple(mode.lines(), f, rect),
1176            _ => (),
1177        }
1178    }
1179
1180    fn binds_per_mode(&self, f: &mut Frame, rect: &Rect, mode: MenuMode) {
1181        if mode == MenuMode::Navigate(Navigate::Trash) {
1182            return;
1183        }
1184        let p_rect = rect.offseted(2, rect.height.saturating_sub(2));
1185        Span::styled(
1186            mode.binds_per_mode(),
1187            MENU_STYLES.get().expect("Menu colors should be set").second,
1188        )
1189        .render(p_rect, f.buffer_mut());
1190    }
1191
1192    fn input_simple(lines: &[&str], f: &mut Frame, rect: &Rect) {
1193        let mut p_rect = rect.offseted(4, ContentWindow::WINDOW_MARGIN_TOP_U16);
1194        p_rect.height = p_rect.height.saturating_sub(2);
1195        Self::render_content(lines, f, &p_rect, 0, 0);
1196    }
1197
1198    fn navigate(&self, navigate: Navigate, f: &mut Frame, rect: &Rect) {
1199        if navigate.simple_draw_menu() {
1200            return self.status.menu.draw_navigate(f, rect, navigate);
1201        }
1202        match navigate {
1203            Navigate::Cloud => self.cloud(f, rect),
1204            Navigate::Context => self.context(f, rect),
1205            Navigate::TempMarks(_) => self.temp_marks(f, rect),
1206            Navigate::Flagged => self.flagged(f, rect),
1207            Navigate::History => self.history(f, rect),
1208            Navigate::Picker => self.picker(f, rect),
1209            Navigate::Trash => self.trash(f, rect),
1210            _ => unreachable!("menu.simple_draw_menu should cover this mode"),
1211        }
1212    }
1213
1214    fn history(&self, f: &mut Frame, rect: &Rect) {
1215        let selectable = &self.tab.history;
1216        let mut window = ContentWindow::new(selectable.len(), rect.height as usize);
1217        window.scroll_to(selectable.index);
1218        selectable.draw_menu(f, rect, &window)
1219    }
1220
1221    fn trash(&self, f: &mut Frame, rect: &Rect) {
1222        let trash = &self.status.menu.trash;
1223        if trash.content().is_empty() {
1224            self.trash_is_empty(f, rect)
1225        } else {
1226            self.trash_content(f, rect, trash)
1227        };
1228    }
1229
1230    fn trash_content(&self, f: &mut Frame, rect: &Rect, trash: &Trash) {
1231        trash.draw_menu(f, rect, &self.status.menu.window);
1232
1233        let p_rect = rect.offseted(2, rect.height.saturating_sub(2));
1234        Span::styled(
1235            &trash.help,
1236            MENU_STYLES.get().expect("Menu colors should be set").second,
1237        )
1238        .render(p_rect, f.buffer_mut());
1239    }
1240
1241    fn trash_is_empty(&self, f: &mut Frame, rect: &Rect) {
1242        Self::content_line(
1243            f,
1244            rect,
1245            0,
1246            "Trash is empty",
1247            MENU_STYLES.get().expect("Menu colors should be set").second,
1248        );
1249    }
1250
1251    fn cloud(&self, f: &mut Frame, rect: &Rect) {
1252        let cloud = &self.status.menu.cloud;
1253        let mut desc = cloud.desc();
1254        if let Some((index, metadata)) = &cloud.metadata_repr {
1255            if index == &cloud.index {
1256                desc = format!("{desc} - {metadata}");
1257            }
1258        }
1259        let p_rect = rect.offseted(2, 2);
1260        Span::styled(
1261            desc,
1262            MENU_STYLES
1263                .get()
1264                .expect("Menu colors should be set")
1265                .palette_4,
1266        )
1267        .render(p_rect, f.buffer_mut());
1268        cloud.draw_menu(f, rect, &self.status.menu.window)
1269    }
1270
1271    fn picker(&self, f: &mut Frame, rect: &Rect) {
1272        let selectable = &self.status.menu.picker;
1273        selectable.draw_menu(f, rect, &self.status.menu.window);
1274        if let Some(desc) = &selectable.desc {
1275            let p_rect = rect.offseted(10, 0);
1276            Span::styled(
1277                desc,
1278                MENU_STYLES.get().expect("Menu colors should be set").first,
1279            )
1280            .render(p_rect, f.buffer_mut());
1281        }
1282    }
1283
1284    fn temp_marks(&self, f: &mut Frame, rect: &Rect) {
1285        let selectable = &self.status.menu.temp_marks;
1286        selectable.draw_menu(f, rect, &self.status.menu.window);
1287    }
1288
1289    fn context(&self, f: &mut Frame, rect: &Rect) {
1290        self.context_selectable(f, rect);
1291        self.context_more_infos(f, rect)
1292    }
1293
1294    fn context_selectable(&self, f: &mut Frame, rect: &Rect) {
1295        self.status
1296            .menu
1297            .context
1298            .draw_menu(f, rect, &self.status.menu.window);
1299    }
1300
1301    fn context_more_infos(&self, f: &mut Frame, rect: &Rect) {
1302        let Ok(file_info) = &self.tab.current_file() else {
1303            return;
1304        };
1305        let space_used = self.status.menu.context.content.len() as u16;
1306        let lines = MoreInfos::new(file_info, &self.status.internal_settings.opener).to_lines();
1307        let more_infos: Vec<&String> = lines.iter().filter(|line| !line.is_empty()).collect();
1308        Self::render_content(&more_infos, f, rect, 4, 3 + space_used);
1309    }
1310
1311    fn flagged(&self, f: &mut Frame, rect: &Rect) {
1312        self.flagged_files(f, rect);
1313        self.flagged_selected(f, rect);
1314    }
1315
1316    fn flagged_files(&self, f: &mut Frame, rect: &Rect) {
1317        self.status
1318            .menu
1319            .flagged
1320            .draw_menu(f, rect, &self.status.menu.window);
1321    }
1322
1323    fn flagged_selected(&self, f: &mut Frame, rect: &Rect) {
1324        if let Some(selected) = self.status.menu.flagged.selected() {
1325            let Ok(fileinfo) = FileInfo::new(selected, &self.tab.users) else {
1326                return;
1327            };
1328            let p_rect = rect.offseted(2, 2);
1329            Span::styled(fileinfo.format_metadata(6, 6), fileinfo.style())
1330                .render(p_rect, f.buffer_mut());
1331        };
1332    }
1333
1334    /// Display the possible completion items. The currently selected one is
1335    /// reversed.
1336    fn completion(&self, f: &mut Frame, rect: &Rect) {
1337        self.status
1338            .menu
1339            .completion
1340            .draw_menu(f, rect, &self.status.menu.window)
1341    }
1342
1343    /// Display a list of edited (deleted, copied, moved, trashed) files for confirmation
1344    fn confirm(&self, confirmed_mode: NeedConfirmation, f: &mut Frame, rect: &Rect) {
1345        let dest = path_to_string(
1346            &self
1347                .tab
1348                .directory_of_selected()
1349                .unwrap_or_else(|_| std::path::Path::new("")),
1350        );
1351
1352        Self::content_line(
1353            f,
1354            rect,
1355            0,
1356            &confirmed_mode.confirmation_string(&dest),
1357            MENU_STYLES.get().expect("Menu colors should be set").second,
1358        );
1359        match confirmed_mode {
1360            NeedConfirmation::EmptyTrash => self.confirm_empty_trash(f, rect),
1361            NeedConfirmation::BulkAction => self.confirm_bulk(f, rect),
1362            NeedConfirmation::DeleteCloud => self.confirm_delete_cloud(f, rect),
1363            _ => self.confirm_default(f, rect),
1364        };
1365    }
1366
1367    fn confirm_default(&self, f: &mut Frame, rect: &Rect) {
1368        self.status
1369            .menu
1370            .flagged
1371            .draw_menu(f, rect, &self.status.menu.window);
1372    }
1373
1374    fn confirm_bulk(&self, f: &mut Frame, rect: &Rect) {
1375        let content = self.status.menu.bulk.format_confirmation();
1376
1377        let mut p_rect = rect.offseted(4, 3);
1378        p_rect.height = p_rect.height.saturating_sub(2);
1379        // let content = self.content();
1380        let window = &self.status.menu.window;
1381        use std::cmp::min;
1382        let lines: Vec<_> = colored_skip_take!(content, window)
1383            .map(|(index, item, style)| {
1384                Line::styled(item, self.status.menu.bulk.style(index, &style))
1385            })
1386            .collect();
1387        Paragraph::new(lines).render(p_rect, f.buffer_mut());
1388    }
1389
1390    fn confirm_delete_cloud(&self, f: &mut Frame, rect: &Rect) {
1391        let line = if let Some(selected) = &self.status.menu.cloud.selected() {
1392            &format!(
1393                "{desc}{sel}",
1394                desc = self.status.menu.cloud.desc(),
1395                sel = selected.path()
1396            )
1397        } else {
1398            "No selected file"
1399        };
1400        Self::content_line(
1401            f,
1402            rect,
1403            3,
1404            line,
1405            MENU_STYLES
1406                .get()
1407                .context("MENU_STYLES should be set")
1408                .expect("Couldn't read MENU_STYLES")
1409                .palette_4,
1410        );
1411    }
1412
1413    fn confirm_empty_trash(&self, f: &mut Frame, rect: &Rect) {
1414        if self.status.menu.trash.is_empty() {
1415            self.trash_is_empty(f, rect)
1416        } else {
1417            self.confirm_non_empty_trash(f, rect)
1418        }
1419    }
1420
1421    fn confirm_non_empty_trash(&self, f: &mut Frame, rect: &Rect) {
1422        self.status
1423            .menu
1424            .trash
1425            .draw_menu(f, rect, &self.status.menu.window);
1426    }
1427
1428    fn content_line(f: &mut Frame, rect: &Rect, row: u16, text: &str, style: Style) {
1429        let p_rect = rect.offseted(4, row + ContentWindow::WINDOW_MARGIN_TOP_U16);
1430        Span::styled(text, style).render(p_rect, f.buffer_mut());
1431    }
1432}
1433
1434/// First line of every menu. This is where inputs are shown.
1435/// For most of the menus, it's their name, some spaces and the input typed by the user.
1436pub struct MenuFirstLine {
1437    content: Vec<String>,
1438}
1439
1440impl Draw for MenuFirstLine {
1441    fn draw(&self, f: &mut Frame, rect: &Rect) {
1442        let spans: Vec<_> = std::iter::zip(
1443            self.content.iter(),
1444            MENU_STYLES
1445                .get()
1446                .expect("Menu colors should be set")
1447                .palette()
1448                .iter()
1449                .cycle(),
1450        )
1451        .map(|(text, style)| Span::styled(text, *style))
1452        .collect();
1453        let p_rect = rect.offseted(Self::LEFT_MARGIN, 0);
1454        Line::from(spans).render(p_rect, f.buffer_mut());
1455    }
1456}
1457
1458impl MenuFirstLine {
1459    /// Number of spaces between rect border and first char of the line.
1460    pub const LEFT_MARGIN: u16 = 2;
1461
1462    fn new(status: &Status, rect: &Rect) -> Self {
1463        Self {
1464            content: status.current_tab().menu_mode.line_display(status, rect),
1465        }
1466    }
1467}
1468
1469/// Methods used to create the various rects
1470struct Rects;
1471
1472impl Rects {
1473    const FILES_WITH_LOGLINE: &[Constraint] = &[
1474        Constraint::Length(1),
1475        Constraint::Length(1),
1476        Constraint::Fill(1),
1477        Constraint::Length(1),
1478        Constraint::Length(1),
1479    ];
1480
1481    const FILES_WITHOUT_LOGLINE: &[Constraint] = &[
1482        Constraint::Length(1),
1483        Constraint::Length(1),
1484        Constraint::Fill(1),
1485        Constraint::Length(1),
1486    ];
1487
1488    /// Main rect of the application
1489    fn full_rect(width: u16, height: u16) -> Rect {
1490        Rect::new(0, 0, width, height)
1491    }
1492
1493    /// Main rect but inside its border
1494    fn inside_border_rect(width: u16, height: u16) -> Rect {
1495        Rect::new(1, 1, width.saturating_sub(2), height.saturating_sub(2))
1496    }
1497
1498    /// Horizontal split the inside rect in two
1499    fn left_right_inside_rects(rect: Rect) -> Rc<[Rect]> {
1500        Layout::new(
1501            Direction::Horizontal,
1502            [Constraint::Min(rect.width / 2), Constraint::Fill(1)],
1503        )
1504        .split(rect)
1505    }
1506
1507    /// Bordered rects of the four windows
1508    fn dual_bordered_rect(
1509        parent_wins: Rc<[Rect]>,
1510        have_menu_left: bool,
1511        have_menu_right: bool,
1512    ) -> Vec<Rect> {
1513        let mut bordered_wins =
1514            Self::vertical_split_border(parent_wins[0], have_menu_left).to_vec();
1515        bordered_wins
1516            .append(&mut Self::vertical_split_border(parent_wins[1], have_menu_right).to_vec());
1517        bordered_wins
1518    }
1519
1520    /// Inside rects of the four windows.
1521    fn dual_inside_rect(rect: Rect, have_menu_left: bool, have_menu_right: bool) -> Vec<Rect> {
1522        let left_right = Self::left_right_rects(rect);
1523        let mut areas = Self::vertical_split_inner(left_right[0], have_menu_left).to_vec();
1524        areas.append(&mut Self::vertical_split_inner(left_right[2], have_menu_right).to_vec());
1525        areas
1526    }
1527
1528    /// Main inside rect for left and right.
1529    /// It also returns a padding rect which should be ignored by the caller.
1530    fn left_right_rects(rect: Rect) -> Rc<[Rect]> {
1531        Layout::new(
1532            Direction::Horizontal,
1533            [
1534                Constraint::Min(rect.width / 2 - 1),
1535                Constraint::Max(2),
1536                Constraint::Min(rect.width / 2 - 2),
1537            ],
1538        )
1539        .split(rect)
1540    }
1541
1542    /// Vertical split used to split the inside windows of a pane, left or right.
1543    /// It also recturns a padding rect which should be ignored by the caller.
1544    fn vertical_split_inner(parent_win: Rect, have_menu: bool) -> Rc<[Rect]> {
1545        if have_menu {
1546            Layout::new(
1547                Direction::Vertical,
1548                [
1549                    Constraint::Min(parent_win.height / 2 - 1),
1550                    Constraint::Max(2),
1551                    Constraint::Fill(1),
1552                ],
1553            )
1554            .split(parent_win)
1555        } else {
1556            Rc::new([parent_win, Rect::default(), Rect::default()])
1557        }
1558    }
1559
1560    /// Vertical split used to create the bordered rects of a pane, left or right.
1561    fn vertical_split_border(parent_win: Rect, have_menu: bool) -> Rc<[Rect]> {
1562        let percent = if have_menu { 50 } else { 100 };
1563        Layout::new(
1564            Direction::Vertical,
1565            [Constraint::Percentage(percent), Constraint::Fill(1)],
1566        )
1567        .split(parent_win)
1568    }
1569
1570    /// Split the rect vertically like this:
1571    /// 1   :       header
1572    /// 1   :       copy progress bar or second line
1573    /// fill:       content
1574    /// 1   :       log line
1575    /// 1   :       footer
1576    fn files(rect: &Rect, use_log_line: bool) -> Rc<[Rect]> {
1577        Layout::new(
1578            Direction::Vertical,
1579            if use_log_line {
1580                Self::FILES_WITH_LOGLINE
1581            } else {
1582                Self::FILES_WITHOUT_LOGLINE
1583            },
1584        )
1585        .split(*rect)
1586    }
1587
1588    fn fuzzy(area: &Rect) -> Rc<[Rect]> {
1589        Layout::default()
1590            .direction(Direction::Vertical)
1591            .constraints([Constraint::Length(1), Constraint::Min(0)])
1592            .split(*area)
1593    }
1594}
1595
1596/// Is responsible for displaying content in the terminal.
1597/// It uses an already created terminal.
1598pub struct Display {
1599    /// The Crossterm terminal attached to the display.
1600    /// It will print every symbol shown on screen.
1601    term: Terminal<CrosstermBackend<Stdout>>,
1602    /// The adapter instance used to draw the images
1603    image_adapter: ImageAdapter,
1604}
1605
1606impl Display {
1607    /// Returns a new `Display` instance from a terminal object.
1608    pub fn new(term: Terminal<CrosstermBackend<Stdout>>) -> Self {
1609        log_info!("starting display...");
1610        let image_adapter = ImageAdapter::detect();
1611        Self {
1612            term,
1613            image_adapter,
1614        }
1615    }
1616
1617    /// Display every possible content in the terminal.
1618    ///
1619    /// The top line
1620    ///
1621    /// The files if we're displaying them
1622    ///
1623    /// The cursor if a content is editable
1624    ///
1625    /// The help if `Mode::Help`
1626    ///
1627    /// The jump_list if `Mode::Jump`
1628    ///
1629    /// The completion list if any.
1630    ///
1631    /// The preview in preview mode.
1632    /// Displays one pane or two panes, depending of the width and current
1633    /// status of the application.
1634    pub fn display_all(&mut self, status: &MutexGuard<Status>) {
1635        io::stdout().flush().expect("Couldn't flush the stdout");
1636        if status.should_tabs_images_be_cleared() {
1637            self.clear_images();
1638        }
1639        if status.should_be_cleared() {
1640            self.term.clear().expect("Couldn't clear the terminal");
1641        }
1642        let Ok(Size { width, height }) = self.term.size() else {
1643            return;
1644        };
1645        let full_rect = Rects::full_rect(width, height);
1646        let inside_border_rect = Rects::inside_border_rect(width, height);
1647        let borders = Self::borders(status);
1648        if Self::use_dual_pane(status, width) {
1649            self.draw_dual(full_rect, inside_border_rect, borders, status);
1650        } else {
1651            self.draw_single(full_rect, inside_border_rect, borders, status);
1652        };
1653    }
1654
1655    /// Left File, Left Menu, Right File, Right Menu
1656    fn borders(status: &Status) -> [Style; 4] {
1657        let menu_styles = MENU_STYLES.get().expect("MENU_STYLES should be set");
1658        let mut borders = [menu_styles.inert_border; 4];
1659        let selected_border = menu_styles.selected_border;
1660        borders[status.focus.index()] = selected_border;
1661        borders
1662    }
1663
1664    /// True iff we need to display both panes
1665    fn use_dual_pane(status: &Status, width: u16) -> bool {
1666        status.session.dual() && width > MIN_WIDTH_FOR_DUAL_PANE
1667    }
1668
1669    fn draw_dual(
1670        &mut self,
1671        full_rect: Rect,
1672        inside_border_rect: Rect,
1673        borders: [Style; 4],
1674        status: &Status,
1675    ) {
1676        let (file_left, file_right) = FilesBuilder::dual(status);
1677        let menu_left = Menu::new(status, 0);
1678        let menu_right = Menu::new(status, 1);
1679        let parent_wins = Rects::left_right_inside_rects(full_rect);
1680        let have_menu_left = status.tabs[0].need_menu_window();
1681        let have_menu_right = status.tabs[1].need_menu_window();
1682        let bordered_wins = Rects::dual_bordered_rect(parent_wins, have_menu_left, have_menu_right);
1683        let inside_wins =
1684            Rects::dual_inside_rect(inside_border_rect, have_menu_left, have_menu_right);
1685        self.render_dual(
1686            borders,
1687            bordered_wins,
1688            inside_wins,
1689            (file_left, file_right),
1690            (menu_left, menu_right),
1691        );
1692    }
1693
1694    fn render_dual(
1695        &mut self,
1696        borders: [Style; 4],
1697        bordered_wins: Vec<Rect>,
1698        inside_wins: Vec<Rect>,
1699        files: (Files, Files),
1700        menus: (Menu, Menu),
1701    ) {
1702        let _ = self.term.draw(|f| {
1703            // 0 File Left | 3 File Right
1704            // 1 padding   | 4 padding
1705            // 2 Menu Left | 5 Menu Right
1706            Self::draw_dual_borders(borders, f, &bordered_wins);
1707            files.0.draw(f, &inside_wins[0], &mut self.image_adapter);
1708            menus.0.draw(f, &inside_wins[2]);
1709            files.1.draw(f, &inside_wins[3], &mut self.image_adapter);
1710            menus.1.draw(f, &inside_wins[5]);
1711        });
1712    }
1713
1714    fn draw_single(
1715        &mut self,
1716        rect: Rect,
1717        inside_border_rect: Rect,
1718        borders: [Style; 4],
1719        status: &Status,
1720    ) {
1721        let file_left = FilesBuilder::single(status);
1722        let menu_left = Menu::new(status, 0);
1723        let need_menu = status.tabs[0].need_menu_window();
1724        let bordered_wins = Rects::vertical_split_border(rect, need_menu);
1725        let inside_wins = Rects::vertical_split_inner(inside_border_rect, need_menu);
1726        self.render_single(borders, bordered_wins, inside_wins, file_left, menu_left)
1727    }
1728
1729    fn render_single(
1730        &mut self,
1731        borders: [Style; 4],
1732        bordered_wins: Rc<[Rect]>,
1733        inside_wins: Rc<[Rect]>,
1734        file_left: Files,
1735        menu_left: Menu,
1736    ) {
1737        let _ = self.term.draw(|f| {
1738            Self::draw_single_borders(borders, f, &bordered_wins);
1739            file_left.draw(f, &inside_wins[0], &mut self.image_adapter);
1740            menu_left.draw(f, &inside_wins[2]);
1741        });
1742    }
1743
1744    fn draw_n_borders(n: usize, borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1745        for i in 0..n {
1746            let bordered_block = Block::default()
1747                .borders(Borders::ALL)
1748                .border_style(borders[i]);
1749            f.render_widget(bordered_block, wins[i]);
1750        }
1751    }
1752
1753    fn draw_dual_borders(borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1754        Self::draw_n_borders(4, borders, f, wins)
1755    }
1756
1757    fn draw_single_borders(borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1758        Self::draw_n_borders(2, borders, f, wins)
1759    }
1760
1761    /// Clear all images.
1762    pub fn clear_images(&mut self) {
1763        log_info!("display.clear_images()");
1764        self.image_adapter
1765            .clear_all()
1766            .expect("Couldn't clear all the images");
1767        self.term.clear().expect("Couldn't clear the terminal");
1768    }
1769
1770    /// Restore the terminal before leaving the application.
1771    /// - disable raw mode, allowing for "normal" terminal behavior,
1772    /// - leave alternate screens (switch back to the main screen).
1773    /// - display the cursor
1774    pub fn restore_terminal(&mut self) -> Result<()> {
1775        disable_raw_mode()?;
1776        execute!(self.term.backend_mut(), LeaveAlternateScreen)?;
1777        self.term.show_cursor()?;
1778        Ok(())
1779    }
1780}