Skip to main content

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, BorderType, 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(0, 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            Self::mark_span(self.status, file),
528            Span::styled(content, style),
529        ])
530    }
531
532    fn mark_span<'b>(status: &Status, file: &FileInfo) -> Span<'b> {
533        if let Some(index) = status.menu.temp_marks.digit_for(&file.path) {
534            Span::styled(
535                index.to_string(),
536                MENU_STYLES
537                    .get()
538                    .expect("Menu style should be set")
539                    .palette_1,
540            )
541        } else {
542            let first_char = status.menu.marks.char_for(&file.path);
543            Span::styled(
544                String::from(*first_char),
545                MENU_STYLES
546                    .get()
547                    .expect("Menu style should be set")
548                    .palette_2,
549            )
550        }
551    }
552
553    fn reverse_selected(&self, index: usize, style: &mut Style) {
554        if index == self.tab.directory.index {
555            style.add_modifier |= Modifier::REVERSED;
556        }
557    }
558
559    fn color_searched(&self, file: &FileInfo, style: &mut Style) {
560        if self.tab.search.is_match(&file.filename) {
561            style.fg = MENU_STYLES
562                .get()
563                .expect("Menu style should be set")
564                .palette_4
565                .fg;
566        }
567    }
568
569    fn span_flagged_symbol<'b>(&self, file: &FileInfo, style: &mut Style) -> Span<'b> {
570        if self.status.menu.flagged.contains(&file.path) {
571            style.add_modifier |= Modifier::BOLD;
572            Span::styled(
573                "â–ˆ",
574                MENU_STYLES.get().expect("Menu colors should be set").second,
575            )
576        } else {
577            Span::raw("")
578        }
579    }
580}
581
582type Formater = fn(&FileInfo, (usize, usize)) -> String;
583
584struct FileFormater;
585
586impl FileFormater {
587    fn metadata(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
588        file.format_base(owner_sizes.1, owner_sizes.0)
589    }
590
591    fn metadata_no_group(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
592        file.format_no_group(owner_sizes.1)
593    }
594
595    fn metadata_no_permissions(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
596        file.format_no_permissions(owner_sizes.1)
597    }
598
599    fn metadata_no_owner(file: &FileInfo, _owner_sizes: (usize, usize)) -> String {
600        file.format_no_owner()
601    }
602
603    fn simple(_file: &FileInfo, _owner_sizes: (usize, usize)) -> String {
604        " ".to_owned()
605    }
606}
607
608#[derive(Debug)]
609enum FormatKind {
610    Metadata,
611    MetadataNoGroup,
612    MetadataNoPermissions,
613    MetadataNoOwner,
614    Simple,
615}
616
617impl FormatKind {
618    #[rustfmt::skip]
619    fn from_flags(
620        with_metadata: bool,
621        width: u16,
622    ) -> Self {
623        let wide_enough_for_group = width > 70;
624        let wide_enough_for_metadata = width > 50;
625        let wide_enough_for_permissions = width > 40;
626        let wide_enough_for_owner = width > 30;
627
628        match (
629            with_metadata,
630            wide_enough_for_group,
631            wide_enough_for_metadata,
632            wide_enough_for_permissions,
633            wide_enough_for_owner,
634        ) {
635            (true, true,  _,    _,       _)     => Self::Metadata,
636            (true, false, true, _,       _)     => Self::MetadataNoGroup,
637            (true, _,     _,    true,    _)     => Self::MetadataNoPermissions,
638            (true, _,     _,    _,    true)     => Self::MetadataNoOwner,
639            _ => Self::Simple,
640        }
641    }
642}
643
644struct TreeDisplay<'a> {
645    status: &'a Status,
646    tab: &'a Tab,
647}
648
649impl<'a> Draw for TreeDisplay<'a> {
650    fn draw(&self, f: &mut Frame, rect: &Rect) {
651        self.tree(f, rect)
652    }
653}
654
655impl<'a> TreeDisplay<'a> {
656    fn new(files: &'a Files) -> Self {
657        Self {
658            status: files.status,
659            tab: files.tab,
660        }
661    }
662
663    fn tree(&self, f: &mut Frame, rect: &Rect) {
664        Self::tree_content(
665            self.status,
666            &self.tab.tree,
667            &self.tab.users,
668            &self.tab.window,
669            self.status.session.metadata(),
670            f,
671            rect,
672        )
673    }
674
675    fn tree_content(
676        status: &Status,
677        tree: &Tree,
678        users: &Users,
679        window: &ContentWindow,
680        with_metadata: bool,
681        f: &mut Frame,
682        rect: &Rect,
683    ) {
684        let p_rect = rect.offseted(0, 0);
685        let width = p_rect.width.saturating_sub(6);
686        let formater = DirectoryDisplay::pick_formater(with_metadata, width);
687        let with_icon = Self::use_icon(with_metadata);
688        let lines: Vec<_> = tree
689            .lines_enum_skip_take(window)
690            .filter_map(|(index, line_builder)| {
691                Self::tree_line(
692                    status,
693                    index == 0,
694                    line_builder,
695                    &formater,
696                    users,
697                    with_icon,
698                )
699                .ok()
700            })
701            .collect();
702        Paragraph::new(lines).render(p_rect, f.buffer_mut());
703    }
704
705    fn use_icon(with_metadata: bool) -> bool {
706        (!with_metadata && with_icon()) || with_icon_metadata()
707    }
708
709    fn tree_line<'b>(
710        status: &Status,
711        with_offset: bool,
712        line_builder: &'b TLine,
713        formater: &Formater,
714        users: &Users,
715        with_icon: bool,
716    ) -> Result<Line<'b>> {
717        let path = line_builder.path();
718        let fileinfo = FileInfo::new(&line_builder.path, users)?;
719        let mut style = fileinfo.style();
720        Self::reverse_flagged(line_builder, &mut style);
721        Self::color_searched(status, &fileinfo, &mut style);
722        Ok(Line::from(vec![
723            Self::span_flagged_symbol(status, path, &mut style),
724            DirectoryDisplay::mark_span(status, &fileinfo),
725            Self::metadata(&fileinfo, formater, style),
726            Self::prefix(line_builder),
727            Self::whitespaces(status, path, with_offset),
728            Self::filename(line_builder, with_icon, style),
729        ]))
730    }
731
732    fn reverse_flagged(line_builder: &TLine, style: &mut Style) {
733        if line_builder.is_selected {
734            style.add_modifier |= Modifier::REVERSED;
735        }
736    }
737
738    fn color_searched(status: &Status, file: &FileInfo, style: &mut Style) {
739        if status.current_tab().search.is_match(&file.filename) {
740            style.fg = MENU_STYLES
741                .get()
742                .expect("Menu style should be set")
743                .palette_4
744                .fg;
745        }
746    }
747
748    fn span_flagged_symbol<'b>(
749        status: &Status,
750        path: &std::path::Path,
751        style: &mut Style,
752    ) -> Span<'b> {
753        if status.menu.flagged.contains(path) {
754            style.add_modifier |= Modifier::BOLD;
755            Span::styled(
756                "â–ˆ",
757                MENU_STYLES.get().expect("Menu colors should be set").second,
758            )
759        } else {
760            Span::raw(" ")
761        }
762    }
763
764    fn metadata<'b>(fileinfo: &FileInfo, formater: &Formater, style: Style) -> Span<'b> {
765        Span::styled(formater(fileinfo, (6, 6)), style)
766    }
767
768    fn prefix(line_builder: &TLine) -> Span<'_> {
769        Span::raw(line_builder.prefix())
770    }
771
772    fn whitespaces<'b>(status: &Status, path: &std::path::Path, with_offset: bool) -> Span<'b> {
773        Span::raw(" ".repeat(status.menu.flagged.contains(path) as usize + with_offset as usize))
774    }
775
776    fn filename<'b>(line_builder: &TLine, with_icon: bool, style: Style) -> Span<'b> {
777        Span::styled(line_builder.filename(with_icon), style)
778    }
779}
780
781struct PreviewDisplay<'a> {
782    status: &'a Status,
783    tab: &'a Tab,
784}
785
786/// Display a scrollable preview of a file.
787/// Multiple modes are supported :
788/// if the filename extension is recognized, the preview is highlighted,
789/// if the file content is recognized as binary, an hex dump is previewed with 16 bytes lines,
790/// else the content is supposed to be text and shown as such.
791/// It may fail to recognize some usual extensions, notably `.toml`.
792/// It may fail to recognize small files (< 1024 bytes).
793impl<'a> PreviewDisplay<'a> {
794    fn draw(&self, f: &mut Frame, rect: &Rect, image_adapter: &mut ImageAdapter) {
795        self.preview(f, rect, image_adapter)
796    }
797}
798
799impl<'a> PreviewDisplay<'a> {
800    fn new(files: &'a Files) -> Self {
801        Self {
802            status: files.status,
803            tab: files.tab,
804        }
805    }
806
807    fn new_with_args(status: &'a Status, tab: &'a Tab) -> Self {
808        Self { status, tab }
809    }
810
811    fn preview(&self, f: &mut Frame, rect: &Rect, image_adapter: &mut ImageAdapter) {
812        let tab = self.tab;
813        let window = &tab.window;
814        let length = tab.preview.len();
815        match &tab.preview {
816            Preview::Syntaxed(syntaxed) => {
817                let number_col_width = Self::number_width(length);
818                self.syntaxed(f, syntaxed, length, rect, number_col_width, window)
819            }
820            Preview::Binary(bin) => self.binary(f, bin, length, rect, window),
821            Preview::Image(image) => self.image(image, rect, image_adapter),
822            Preview::Tree(tree_preview) => self.tree_preview(f, tree_preview, window, rect),
823            Preview::Text(ansi_text)
824                if matches!(ansi_text.kind, TextKind::CommandStdout | TextKind::Plugin) =>
825            {
826                self.ansi_text(f, ansi_text, length, rect, window)
827            }
828            Preview::Text(text) => self.normal_text(f, text, length, rect, window),
829
830            Preview::Empty => (),
831        };
832    }
833
834    fn line_number_span<'b>(
835        line_number_to_print: &usize,
836        number_col_width: usize,
837        style: Style,
838    ) -> Span<'b> {
839        Span::styled(
840            format!("{line_number_to_print:>number_col_width$}  "),
841            style,
842        )
843    }
844
845    /// Number of digits in decimal representation
846    fn number_width(mut number: usize) -> usize {
847        let mut width = 0;
848        while number != 0 {
849            width += 1;
850            number /= 10;
851        }
852        width
853    }
854
855    /// Draw every line of the text
856    fn normal_text(
857        &self,
858        f: &mut Frame,
859        text: &Text,
860        length: usize,
861        rect: &Rect,
862        window: &ContentWindow,
863    ) {
864        let p_rect = rect.offseted(2, 0);
865        let lines: Vec<_> = text
866            .take_skip(window.top, window.bottom, length)
867            .map(Line::raw)
868            .collect();
869        Paragraph::new(lines).render(p_rect, f.buffer_mut());
870    }
871
872    fn syntaxed(
873        &self,
874        f: &mut Frame,
875        syntaxed: &HLContent,
876        length: usize,
877        rect: &Rect,
878        number_col_width: usize,
879        window: &ContentWindow,
880    ) {
881        let p_rect = rect.offseted(3, 0);
882        let number_col_style = MENU_STYLES.get().expect("").first;
883        let lines: Vec<_> = syntaxed
884            .take_skip_enum(window.top, window.bottom, length)
885            .map(|(index, vec_line)| {
886                let mut line = vec![Self::line_number_span(
887                    &index,
888                    number_col_width,
889                    number_col_style,
890                )];
891                line.append(
892                    &mut vec_line
893                        .iter()
894                        .map(|token| Span::styled(&token.content, token.style))
895                        .collect::<Vec<_>>(),
896                );
897                Line::from(line)
898            })
899            .collect();
900        Paragraph::new(lines).render(p_rect, f.buffer_mut());
901    }
902
903    fn binary(
904        &self,
905        f: &mut Frame,
906        bin: &BinaryContent,
907        length: usize,
908        rect: &Rect,
909        window: &ContentWindow,
910    ) {
911        let p_rect = rect.offseted(3, 0);
912        let line_number_width_hex = bin.number_width_hex();
913        let (style_number, style_ascii) = {
914            let ms = MENU_STYLES.get().expect("Menu colors should be set");
915            (ms.first, ms.second)
916        };
917        let lines: Vec<_> = (*bin)
918            .take_skip_enum(window.top, window.bottom, length)
919            .map(|(index, bin_line)| {
920                Line::from(vec![
921                    Span::styled(
922                        BinLine::format_line_nr_hex(index + 1 + window.top, line_number_width_hex),
923                        style_number,
924                    ),
925                    Span::raw(bin_line.format_hex()),
926                    Span::raw(" "),
927                    Span::styled(bin_line.format_as_ascii(), style_ascii),
928                ])
929            })
930            .collect();
931        Paragraph::new(lines).render(p_rect, f.buffer_mut());
932    }
933
934    /// Draw the image with correct adapter in the current window.
935    /// The position is absolute, which is problematic when the app is embeded into a floating terminal.
936    fn image(&self, image: &DisplayedImage, rect: &Rect, image_adapter: &mut ImageAdapter) {
937        if let Err(e) = image_adapter.draw(image, *rect) {
938            log_info!("Couldn't display {path}: {e:?}", path = image.identifier);
939        }
940    }
941
942    fn tree_preview(&self, f: &mut Frame, tree: &Tree, window: &ContentWindow, rect: &Rect) {
943        TreeDisplay::tree_content(self.status, tree, &self.tab.users, window, false, f, rect)
944    }
945
946    fn ansi_text(
947        &self,
948        f: &mut Frame,
949        ansi_text: &Text,
950        length: usize,
951        rect: &Rect,
952        window: &ContentWindow,
953    ) {
954        let p_rect = rect.offseted(3, 0);
955        let lines: Vec<_> = ansi_text
956            .take_skip(window.top, window.bottom, length)
957            .map(|line| {
958                Line::from(
959                    AnsiString::parse(line)
960                        .iter()
961                        .map(|(chr, style)| Span::styled(chr.to_string(), style))
962                        .collect::<Vec<_>>(),
963                )
964            })
965            .collect();
966        Paragraph::new(lines).render(p_rect, f.buffer_mut());
967    }
968}
969
970struct FilesHeader<'a> {
971    status: &'a Status,
972    tab: &'a Tab,
973    is_selected: bool,
974}
975
976impl<'a> Draw for FilesHeader<'a> {
977    /// Display the top line on terminal.
978    /// Its content depends on the mode.
979    /// In normal mode we display the path and number of files.
980    /// something else.
981    /// The colors are reversed when the tab is selected. It gives a visual indication of where he is.
982    fn draw(&self, f: &mut Frame, rect: &Rect) {
983        let width = rect.width;
984        let header: Box<dyn ClickableLine> = match self.tab.display_mode {
985            DisplayMode::Preview => Box::new(PreviewHeader::new(self.status, self.tab, width)),
986            _ => Box::new(Header::new(self.status, self.tab).expect("Couldn't build header")),
987        };
988        header.draw_left(f, *rect, self.is_selected);
989        header.draw_right(f, *rect, self.is_selected);
990    }
991}
992
993impl<'a> FilesHeader<'a> {
994    fn new(status: &'a Status, tab: &'a Tab, is_selected: bool) -> Self {
995        Self {
996            status,
997            tab,
998            is_selected,
999        }
1000    }
1001}
1002
1003#[derive(Default)]
1004struct FilesSecondLine {
1005    content: Option<String>,
1006    style: Option<Style>,
1007}
1008
1009impl Draw for FilesSecondLine {
1010    fn draw(&self, f: &mut Frame, rect: &Rect) {
1011        let p_rect = rect.offseted(1, 0);
1012        if let (Some(content), Some(style)) = (&self.content, &self.style) {
1013            Span::styled(content, *style).render(p_rect, f.buffer_mut());
1014        };
1015    }
1016}
1017
1018impl FilesSecondLine {
1019    fn new(status: &Status, tab: &Tab) -> Self {
1020        if tab.display_mode.is_preview() || status.session.metadata() {
1021            return Self::default();
1022        };
1023        if let Ok(file) = tab.current_file() {
1024            Self::second_line_detailed(&file)
1025        } else {
1026            Self::default()
1027        }
1028    }
1029
1030    fn second_line_detailed(file: &FileInfo) -> Self {
1031        let owner_size = file.owner.len();
1032        let group_size = file.group.len();
1033        let mut style = file.style();
1034        style.add_modifier ^= Modifier::REVERSED;
1035
1036        Self {
1037            content: Some(file.format_metadata(owner_size, group_size)),
1038            style: Some(style),
1039        }
1040    }
1041}
1042
1043struct LogLine;
1044
1045impl Draw for LogLine {
1046    fn draw(&self, f: &mut Frame, rect: &Rect) {
1047        let p_rect = rect.offseted(4, 0);
1048        let log = &read_last_log_line();
1049        Span::styled(
1050            log,
1051            MENU_STYLES.get().expect("Menu colors should be set").second,
1052        )
1053        .render(p_rect, f.buffer_mut());
1054    }
1055}
1056
1057struct FilesFooter<'a> {
1058    status: &'a Status,
1059    tab: &'a Tab,
1060    is_selected: bool,
1061}
1062
1063impl<'a> Draw for FilesFooter<'a> {
1064    /// Display the top line on terminal.
1065    /// Its content depends on the mode.
1066    /// In normal mode we display the path and number of files.
1067    /// When a confirmation is needed we ask the user to input `'y'` or
1068    /// something else.
1069    /// Returns the result of the number of printed chars.
1070    /// The colors are reversed when the tab is selected. It gives a visual indication of where he is.
1071    fn draw(&self, f: &mut Frame, rect: &Rect) {
1072        match self.tab.display_mode {
1073            DisplayMode::Preview => (),
1074            _ => {
1075                let Ok(footer) = Footer::new(self.status, self.tab) else {
1076                    return;
1077                };
1078                // let p_rect = rect.offseted(0, rect.height.saturating_sub(1));
1079                footer.draw_left(f, *rect, self.is_selected);
1080            }
1081        }
1082    }
1083}
1084
1085impl<'a> FilesFooter<'a> {
1086    fn new(status: &'a Status, tab: &'a Tab, is_selected: bool) -> Self {
1087        Self {
1088            status,
1089            tab,
1090            is_selected,
1091        }
1092    }
1093}
1094
1095struct Menu<'a> {
1096    status: &'a Status,
1097    tab: &'a Tab,
1098}
1099
1100impl<'a> Draw for Menu<'a> {
1101    fn draw(&self, f: &mut Frame, rect: &Rect) {
1102        if !self.tab.need_menu_window() {
1103            return;
1104        }
1105        let mode = self.tab.menu_mode;
1106        self.cursor(f, rect);
1107        MenuFirstLine::new(self.status, rect).draw(f, rect);
1108        self.menu_line(f, rect);
1109        self.content_per_mode(f, rect, mode);
1110        self.binds_per_mode(f, rect, mode);
1111    }
1112}
1113
1114impl<'a> Menu<'a> {
1115    fn new(status: &'a Status, index: usize) -> Self {
1116        Self {
1117            status,
1118            tab: &status.tabs[index],
1119        }
1120    }
1121
1122    /// Render a generic content of elements which are references to str.
1123    /// It creates a new rect, offseted by `x, y` and intersected with rect.
1124    /// Each element of content is wraped by a styled span (with his own style) and then wrapped by a line.
1125    /// The iteration only take enough element to be displayed in the rect.
1126    /// Then we create a paragraph with default parameters and render it.
1127    fn render_content<T>(content: &[T], f: &mut Frame, rect: &Rect, x: u16, y: u16)
1128    where
1129        T: AsRef<str>,
1130    {
1131        let p_rect = rect.offseted(x, y);
1132        let lines: Vec<_> = colored_iter!(content)
1133            .map(|(text, style)| Line::from(vec![Span::styled(text.as_ref(), style)]))
1134            .take(p_rect.height as usize + 2)
1135            .collect();
1136        Paragraph::new(lines).render(p_rect, f.buffer_mut());
1137    }
1138
1139    /// Hide the cursor if the current mode doesn't require one.
1140    /// Otherwise, display a cursor in the top row, at a correct column.
1141    ///
1142    /// # Errors
1143    ///
1144    /// may fail if we can't display on the terminal.
1145    fn cursor(&self, f: &mut Frame, rect: &Rect) {
1146        if self.tab.menu_mode.show_cursor() {
1147            let offset = self.tab.menu_mode.cursor_offset();
1148            let avail = rect.width.saturating_sub(offset + 1) as usize;
1149            let cursor_index = self.status.menu.input.display_index(avail) as u16;
1150            let x = rect.x + offset + cursor_index;
1151            f.set_cursor_position(Position::new(x, rect.y));
1152        }
1153    }
1154
1155    fn menu_line(&self, f: &mut Frame, rect: &Rect) {
1156        let menu_style = MENU_STYLES.get().expect("Menu colors should be set");
1157        let menu = menu_style.second;
1158        match self.tab.menu_mode {
1159            MenuMode::InputSimple(InputSimple::Chmod) => {
1160                let first = menu_style.first;
1161                self.menu_line_chmod(f, rect, first, menu);
1162            }
1163            MenuMode::InputSimple(InputSimple::Remote) => {
1164                let palette_3 = menu_style.palette_3;
1165                self.menu_line_remote(f, rect, palette_3);
1166            }
1167            edit => {
1168                let rect = rect.offseted(2, 1);
1169                Span::styled(edit.second_line(), menu).render(rect, f.buffer_mut());
1170            }
1171        };
1172    }
1173
1174    fn menu_line_chmod(&self, f: &mut Frame, rect: &Rect, first: Style, menu: Style) {
1175        let input = self.status.menu.input.string();
1176        let (text, is_valid) = parse_input_permission(&input);
1177        let style = if is_valid { first } else { menu };
1178        let p_rect = rect.offseted(11, 1);
1179        Line::styled(text.as_ref(), style).render(p_rect, f.buffer_mut());
1180    }
1181
1182    fn menu_line_remote(&self, f: &mut Frame, rect: &Rect, first: Style) {
1183        let input = self.status.menu.input.string();
1184        let current_path = path_to_string(&self.tab.current_directory_path());
1185
1186        if let Some(remote) = Remote::from_input(input, &current_path) {
1187            let command = format!("{command:?}", command = remote.command());
1188            let p_rect = rect.offseted(4, 8);
1189            Line::styled(command, first).render(p_rect, f.buffer_mut());
1190        };
1191    }
1192
1193    fn content_per_mode(&self, f: &mut Frame, rect: &Rect, mode: MenuMode) {
1194        match mode {
1195            MenuMode::Navigate(mode) => self.navigate(mode, f, rect),
1196            MenuMode::NeedConfirmation(mode) => self.confirm(mode, f, rect),
1197            MenuMode::InputCompleted(_) => self.completion(f, rect),
1198            MenuMode::InputSimple(mode) => Self::input_simple(mode.lines(), f, rect),
1199            _ => (),
1200        }
1201    }
1202
1203    fn binds_per_mode(&self, f: &mut Frame, rect: &Rect, mode: MenuMode) {
1204        if mode == MenuMode::Navigate(Navigate::Trash) {
1205            return;
1206        }
1207        let p_rect = rect.offseted(2, rect.height.saturating_sub(2));
1208        Span::styled(
1209            mode.binds_per_mode(),
1210            MENU_STYLES.get().expect("Menu colors should be set").second,
1211        )
1212        .render(p_rect, f.buffer_mut());
1213    }
1214
1215    fn input_simple(lines: &[&str], f: &mut Frame, rect: &Rect) {
1216        let mut p_rect = rect.offseted(4, ContentWindow::WINDOW_MARGIN_TOP_U16);
1217        p_rect.height = p_rect.height.saturating_sub(2);
1218        Self::render_content(lines, f, &p_rect, 0, 0);
1219    }
1220
1221    fn navigate(&self, navigate: Navigate, f: &mut Frame, rect: &Rect) {
1222        if navigate.simple_draw_menu() {
1223            return self.status.menu.draw_navigate(f, rect, navigate);
1224        }
1225        match navigate {
1226            Navigate::Cloud => self.cloud(f, rect),
1227            Navigate::Context => self.context(f, rect),
1228            Navigate::TempMarks(_) => self.temp_marks(f, rect),
1229            Navigate::Flagged => self.flagged(f, rect),
1230            Navigate::History => self.history(f, rect),
1231            Navigate::Picker => self.picker(f, rect),
1232            Navigate::Trash => self.trash(f, rect),
1233            _ => unreachable!("menu.simple_draw_menu should cover this mode"),
1234        }
1235    }
1236
1237    fn history(&self, f: &mut Frame, rect: &Rect) {
1238        let selectable = &self.tab.history;
1239        let mut window = ContentWindow::new(selectable.len(), rect.height as usize);
1240        window.scroll_to(selectable.index);
1241        selectable.draw_menu(f, rect, &window)
1242    }
1243
1244    fn trash(&self, f: &mut Frame, rect: &Rect) {
1245        let trash = &self.status.menu.trash;
1246        if trash.content().is_empty() {
1247            self.trash_is_empty(f, rect)
1248        } else {
1249            self.trash_content(f, rect, trash)
1250        };
1251    }
1252
1253    fn trash_content(&self, f: &mut Frame, rect: &Rect, trash: &Trash) {
1254        trash.draw_menu(f, rect, &self.status.menu.window);
1255
1256        let p_rect = rect.offseted(2, rect.height.saturating_sub(2));
1257        Span::styled(
1258            &trash.help,
1259            MENU_STYLES.get().expect("Menu colors should be set").second,
1260        )
1261        .render(p_rect, f.buffer_mut());
1262    }
1263
1264    fn trash_is_empty(&self, f: &mut Frame, rect: &Rect) {
1265        Self::content_line(
1266            f,
1267            rect,
1268            0,
1269            "Trash is empty",
1270            MENU_STYLES.get().expect("Menu colors should be set").second,
1271        );
1272    }
1273
1274    fn cloud(&self, f: &mut Frame, rect: &Rect) {
1275        let cloud = &self.status.menu.cloud;
1276        let mut desc = cloud.desc();
1277        if let Some((index, metadata)) = &cloud.metadata_repr {
1278            if index == &cloud.index {
1279                desc = format!("{desc} - {metadata}");
1280            }
1281        }
1282        let p_rect = rect.offseted(2, 2);
1283        Span::styled(
1284            desc,
1285            MENU_STYLES
1286                .get()
1287                .expect("Menu colors should be set")
1288                .palette_4,
1289        )
1290        .render(p_rect, f.buffer_mut());
1291        cloud.draw_menu(f, rect, &self.status.menu.window)
1292    }
1293
1294    fn picker(&self, f: &mut Frame, rect: &Rect) {
1295        let selectable = &self.status.menu.picker;
1296        selectable.draw_menu(f, rect, &self.status.menu.window);
1297        if let Some(desc) = &selectable.desc {
1298            let p_rect = rect.offseted(10, 0);
1299            Span::styled(
1300                desc,
1301                MENU_STYLES.get().expect("Menu colors should be set").first,
1302            )
1303            .render(p_rect, f.buffer_mut());
1304        }
1305    }
1306
1307    fn temp_marks(&self, f: &mut Frame, rect: &Rect) {
1308        let selectable = &self.status.menu.temp_marks;
1309        selectable.draw_menu(f, rect, &self.status.menu.window);
1310    }
1311
1312    fn context(&self, f: &mut Frame, rect: &Rect) {
1313        self.context_selectable(f, rect);
1314        self.context_more_infos(f, rect)
1315    }
1316
1317    fn context_selectable(&self, f: &mut Frame, rect: &Rect) {
1318        self.status
1319            .menu
1320            .context
1321            .draw_menu(f, rect, &self.status.menu.window);
1322    }
1323
1324    fn context_more_infos(&self, f: &mut Frame, rect: &Rect) {
1325        let Ok(file_info) = &self.tab.current_file() else {
1326            return;
1327        };
1328        let space_used = self.status.menu.context.content.len() as u16;
1329        let lines = MoreInfos::new(file_info, &self.status.internal_settings.opener).to_lines();
1330        let more_infos: Vec<&String> = lines.iter().filter(|line| !line.is_empty()).collect();
1331        Self::render_content(&more_infos, f, rect, 4, 3 + space_used);
1332    }
1333
1334    fn flagged(&self, f: &mut Frame, rect: &Rect) {
1335        self.flagged_files(f, rect);
1336        self.flagged_selected(f, rect);
1337    }
1338
1339    fn flagged_files(&self, f: &mut Frame, rect: &Rect) {
1340        self.status
1341            .menu
1342            .flagged
1343            .draw_menu(f, rect, &self.status.menu.window);
1344    }
1345
1346    fn flagged_selected(&self, f: &mut Frame, rect: &Rect) {
1347        if let Some(selected) = self.status.menu.flagged.selected() {
1348            let Ok(fileinfo) = FileInfo::new(selected, &self.tab.users) else {
1349                return;
1350            };
1351            let p_rect = rect.offseted(2, 2);
1352            Span::styled(fileinfo.format_metadata(6, 6), fileinfo.style())
1353                .render(p_rect, f.buffer_mut());
1354        };
1355    }
1356
1357    /// Display the possible completion items. The currently selected one is
1358    /// reversed.
1359    fn completion(&self, f: &mut Frame, rect: &Rect) {
1360        self.status
1361            .menu
1362            .completion
1363            .draw_menu(f, rect, &self.status.menu.window)
1364    }
1365
1366    /// Display a list of edited (deleted, copied, moved, trashed) files for confirmation
1367    fn confirm(&self, confirmed_mode: NeedConfirmation, f: &mut Frame, rect: &Rect) {
1368        let dest = path_to_string(
1369            &self
1370                .tab
1371                .directory_of_selected()
1372                .unwrap_or_else(|_| std::path::Path::new("")),
1373        );
1374
1375        Self::content_line(
1376            f,
1377            rect,
1378            0,
1379            &confirmed_mode.confirmation_string(&dest),
1380            MENU_STYLES.get().expect("Menu colors should be set").second,
1381        );
1382        match confirmed_mode {
1383            NeedConfirmation::EmptyTrash => self.confirm_empty_trash(f, rect),
1384            NeedConfirmation::BulkAction => self.confirm_bulk(f, rect),
1385            NeedConfirmation::DeleteCloud => self.confirm_delete_cloud(f, rect),
1386            _ => self.confirm_default(f, rect),
1387        };
1388    }
1389
1390    fn confirm_default(&self, f: &mut Frame, rect: &Rect) {
1391        self.status
1392            .menu
1393            .flagged
1394            .draw_menu(f, rect, &self.status.menu.window);
1395    }
1396
1397    fn confirm_bulk(&self, f: &mut Frame, rect: &Rect) {
1398        let content = self.status.menu.bulk.format_confirmation();
1399
1400        let mut p_rect = rect.offseted(4, 3);
1401        p_rect.height = p_rect.height.saturating_sub(2);
1402        // let content = self.content();
1403        let window = &self.status.menu.window;
1404        use std::cmp::min;
1405        let lines: Vec<_> = colored_skip_take!(content, window)
1406            .map(|(index, item, style)| {
1407                Line::styled(item, self.status.menu.bulk.style(index, &style))
1408            })
1409            .collect();
1410        Paragraph::new(lines).render(p_rect, f.buffer_mut());
1411    }
1412
1413    fn confirm_delete_cloud(&self, f: &mut Frame, rect: &Rect) {
1414        let line = if let Some(selected) = &self.status.menu.cloud.selected() {
1415            &format!(
1416                "{desc}{sel}",
1417                desc = self.status.menu.cloud.desc(),
1418                sel = selected.path()
1419            )
1420        } else {
1421            "No selected file"
1422        };
1423        Self::content_line(
1424            f,
1425            rect,
1426            3,
1427            line,
1428            MENU_STYLES
1429                .get()
1430                .context("MENU_STYLES should be set")
1431                .expect("Couldn't read MENU_STYLES")
1432                .palette_4,
1433        );
1434    }
1435
1436    fn confirm_empty_trash(&self, f: &mut Frame, rect: &Rect) {
1437        if self.status.menu.trash.is_empty() {
1438            self.trash_is_empty(f, rect)
1439        } else {
1440            self.confirm_non_empty_trash(f, rect)
1441        }
1442    }
1443
1444    fn confirm_non_empty_trash(&self, f: &mut Frame, rect: &Rect) {
1445        self.status
1446            .menu
1447            .trash
1448            .draw_menu(f, rect, &self.status.menu.window);
1449    }
1450
1451    fn content_line(f: &mut Frame, rect: &Rect, row: u16, text: &str, style: Style) {
1452        let p_rect = rect.offseted(4, row + ContentWindow::WINDOW_MARGIN_TOP_U16);
1453        Span::styled(text, style).render(p_rect, f.buffer_mut());
1454    }
1455}
1456
1457/// First line of every menu. This is where inputs are shown.
1458/// For most of the menus, it's their name, some spaces and the input typed by the user.
1459pub struct MenuFirstLine {
1460    content: Vec<String>,
1461}
1462
1463impl Draw for MenuFirstLine {
1464    fn draw(&self, f: &mut Frame, rect: &Rect) {
1465        let spans: Vec<_> = std::iter::zip(
1466            self.content.iter(),
1467            MENU_STYLES
1468                .get()
1469                .expect("Menu colors should be set")
1470                .palette()
1471                .iter()
1472                .cycle(),
1473        )
1474        .map(|(text, style)| Span::styled(text, *style))
1475        .collect();
1476        let p_rect = rect.offseted(Self::LEFT_MARGIN, 0);
1477        Line::from(spans).render(p_rect, f.buffer_mut());
1478    }
1479}
1480
1481impl MenuFirstLine {
1482    /// Number of spaces between rect border and first char of the line.
1483    pub const LEFT_MARGIN: u16 = 2;
1484
1485    fn new(status: &Status, rect: &Rect) -> Self {
1486        Self {
1487            content: status.current_tab().menu_mode.line_display(status, rect),
1488        }
1489    }
1490}
1491
1492/// Methods used to create the various rects
1493struct Rects;
1494
1495impl Rects {
1496    const FILES_WITH_LOGLINE: &[Constraint] = &[
1497        Constraint::Length(1),
1498        Constraint::Length(1),
1499        Constraint::Fill(1),
1500        Constraint::Length(1),
1501        Constraint::Length(1),
1502    ];
1503
1504    const FILES_WITHOUT_LOGLINE: &[Constraint] = &[
1505        Constraint::Length(1),
1506        Constraint::Length(1),
1507        Constraint::Fill(1),
1508        Constraint::Length(1),
1509    ];
1510
1511    /// Main rect of the application
1512    fn full_rect(width: u16, height: u16) -> Rect {
1513        Rect::new(0, 0, width, height)
1514    }
1515
1516    /// Main rect but inside its border
1517    fn inside_border_rect(width: u16, height: u16) -> Rect {
1518        Rect::new(1, 1, width.saturating_sub(2), height.saturating_sub(2))
1519    }
1520
1521    /// Horizontal split the inside rect in two
1522    fn left_right_inside_rects(rect: Rect) -> Rc<[Rect]> {
1523        Layout::new(
1524            Direction::Horizontal,
1525            [Constraint::Min(rect.width / 2), Constraint::Fill(1)],
1526        )
1527        .split(rect)
1528    }
1529
1530    /// Bordered rects of the four windows
1531    fn dual_bordered_rect(
1532        parent_wins: Rc<[Rect]>,
1533        have_menu_left: bool,
1534        have_menu_right: bool,
1535    ) -> Vec<Rect> {
1536        let mut bordered_wins =
1537            Self::vertical_split_border(parent_wins[0], have_menu_left).to_vec();
1538        bordered_wins
1539            .append(&mut Self::vertical_split_border(parent_wins[1], have_menu_right).to_vec());
1540        bordered_wins
1541    }
1542
1543    /// Inside rects of the four windows.
1544    fn dual_inside_rect(rect: Rect, have_menu_left: bool, have_menu_right: bool) -> Vec<Rect> {
1545        let left_right = Self::left_right_rects(rect);
1546        let mut areas = Self::vertical_split_inner(left_right[0], have_menu_left).to_vec();
1547        areas.append(&mut Self::vertical_split_inner(left_right[2], have_menu_right).to_vec());
1548        areas
1549    }
1550
1551    /// Main inside rect for left and right.
1552    /// It also returns a padding rect which should be ignored by the caller.
1553    fn left_right_rects(rect: Rect) -> Rc<[Rect]> {
1554        Layout::new(
1555            Direction::Horizontal,
1556            [
1557                Constraint::Min(rect.width / 2 - 1),
1558                Constraint::Max(2),
1559                Constraint::Min(rect.width / 2 - 2),
1560            ],
1561        )
1562        .split(rect)
1563    }
1564
1565    /// Vertical split used to split the inside windows of a pane, left or right.
1566    /// It also recturns a padding rect which should be ignored by the caller.
1567    fn vertical_split_inner(parent_win: Rect, have_menu: bool) -> Rc<[Rect]> {
1568        if have_menu {
1569            Layout::new(
1570                Direction::Vertical,
1571                [
1572                    Constraint::Min(parent_win.height / 2 - 1),
1573                    Constraint::Max(2),
1574                    Constraint::Fill(1),
1575                ],
1576            )
1577            .split(parent_win)
1578        } else {
1579            Rc::new([parent_win, Rect::default(), Rect::default()])
1580        }
1581    }
1582
1583    /// Vertical split used to create the bordered rects of a pane, left or right.
1584    fn vertical_split_border(parent_win: Rect, have_menu: bool) -> Rc<[Rect]> {
1585        let percent = if have_menu { 50 } else { 100 };
1586        Layout::new(
1587            Direction::Vertical,
1588            [Constraint::Percentage(percent), Constraint::Fill(1)],
1589        )
1590        .split(parent_win)
1591    }
1592
1593    /// Split the rect vertically like this:
1594    /// 1   :       header
1595    /// 1   :       copy progress bar or second line
1596    /// fill:       content
1597    /// 1   :       log line
1598    /// 1   :       footer
1599    fn files(rect: &Rect, use_log_line: bool) -> Rc<[Rect]> {
1600        Layout::new(
1601            Direction::Vertical,
1602            if use_log_line {
1603                Self::FILES_WITH_LOGLINE
1604            } else {
1605                Self::FILES_WITHOUT_LOGLINE
1606            },
1607        )
1608        .split(*rect)
1609    }
1610
1611    fn fuzzy(area: &Rect) -> Rc<[Rect]> {
1612        Layout::default()
1613            .direction(Direction::Vertical)
1614            .constraints([Constraint::Length(1), Constraint::Min(0)])
1615            .split(*area)
1616    }
1617}
1618
1619/// Is responsible for displaying content in the terminal.
1620/// It uses an already created terminal.
1621pub struct Display {
1622    /// The Crossterm terminal attached to the display.
1623    /// It will print every symbol shown on screen.
1624    term: Terminal<CrosstermBackend<Stdout>>,
1625    /// The adapter instance used to draw the images
1626    image_adapter: ImageAdapter,
1627}
1628
1629impl Display {
1630    /// Returns a new `Display` instance from a terminal object.
1631    pub fn new(term: Terminal<CrosstermBackend<Stdout>>) -> Self {
1632        log_info!("starting display...");
1633        let image_adapter = ImageAdapter::detect();
1634        Self {
1635            term,
1636            image_adapter,
1637        }
1638    }
1639
1640    /// Display every possible content in the terminal.
1641    ///
1642    /// The top line
1643    ///
1644    /// The files if we're displaying them
1645    ///
1646    /// The cursor if a content is editable
1647    ///
1648    /// The help if `Mode::Help`
1649    ///
1650    /// The jump_list if `Mode::Jump`
1651    ///
1652    /// The completion list if any.
1653    ///
1654    /// The preview in preview mode.
1655    /// Displays one pane or two panes, depending of the width and current
1656    /// status of the application.
1657    pub fn display_all(&mut self, status: &MutexGuard<Status>) {
1658        io::stdout().flush().expect("Couldn't flush the stdout");
1659        if status.should_tabs_images_be_cleared() {
1660            self.clear_images();
1661        }
1662        if status.should_be_cleared() {
1663            self.term.clear().expect("Couldn't clear the terminal");
1664        }
1665        let Ok(Size { width, height }) = self.term.size() else {
1666            return;
1667        };
1668        let full_rect = Rects::full_rect(width, height);
1669        let inside_border_rect = Rects::inside_border_rect(width, height);
1670        let borders = Self::borders(status);
1671        if Self::use_dual_pane(status, width) {
1672            self.draw_dual(full_rect, inside_border_rect, borders, status);
1673        } else {
1674            self.draw_single(full_rect, inside_border_rect, borders, status);
1675        };
1676    }
1677
1678    /// Left File, Left Menu, Right File, Right Menu
1679    fn borders(status: &Status) -> [Style; 4] {
1680        let menu_styles = MENU_STYLES.get().expect("MENU_STYLES should be set");
1681        let mut borders = [menu_styles.inert_border; 4];
1682        let selected_border = menu_styles.selected_border;
1683        borders[status.focus.index()] = selected_border;
1684        borders
1685    }
1686
1687    /// True iff we need to display both panes
1688    fn use_dual_pane(status: &Status, width: u16) -> bool {
1689        status.session.dual() && width >= MIN_WIDTH_FOR_DUAL_PANE
1690    }
1691
1692    fn draw_dual(
1693        &mut self,
1694        full_rect: Rect,
1695        inside_border_rect: Rect,
1696        borders: [Style; 4],
1697        status: &Status,
1698    ) {
1699        let (file_left, file_right) = FilesBuilder::dual(status);
1700        let menu_left = Menu::new(status, 0);
1701        let menu_right = Menu::new(status, 1);
1702        let parent_wins = Rects::left_right_inside_rects(full_rect);
1703        let have_menu_left = status.tabs[0].need_menu_window();
1704        let have_menu_right = status.tabs[1].need_menu_window();
1705        let bordered_wins = Rects::dual_bordered_rect(parent_wins, have_menu_left, have_menu_right);
1706        let inside_wins =
1707            Rects::dual_inside_rect(inside_border_rect, have_menu_left, have_menu_right);
1708        self.render_dual(
1709            borders,
1710            bordered_wins,
1711            inside_wins,
1712            (file_left, file_right),
1713            (menu_left, menu_right),
1714        );
1715    }
1716
1717    fn render_dual(
1718        &mut self,
1719        borders: [Style; 4],
1720        bordered_wins: Vec<Rect>,
1721        inside_wins: Vec<Rect>,
1722        files: (Files, Files),
1723        menus: (Menu, Menu),
1724    ) {
1725        let _ = self.term.draw(|f| {
1726            // 0 File Left | 3 File Right
1727            // 1 padding   | 4 padding
1728            // 2 Menu Left | 5 Menu Right
1729            Self::draw_dual_borders(borders, f, &bordered_wins);
1730            files.0.draw(f, &inside_wins[0], &mut self.image_adapter);
1731            menus.0.draw(f, &inside_wins[2]);
1732            files.1.draw(f, &inside_wins[3], &mut self.image_adapter);
1733            menus.1.draw(f, &inside_wins[5]);
1734        });
1735    }
1736
1737    fn draw_single(
1738        &mut self,
1739        rect: Rect,
1740        inside_border_rect: Rect,
1741        borders: [Style; 4],
1742        status: &Status,
1743    ) {
1744        let file_left = FilesBuilder::single(status);
1745        let menu_left = Menu::new(status, 0);
1746        let need_menu = status.tabs[0].need_menu_window();
1747        let bordered_wins = Rects::vertical_split_border(rect, need_menu);
1748        let inside_wins = Rects::vertical_split_inner(inside_border_rect, need_menu);
1749        self.render_single(borders, bordered_wins, inside_wins, file_left, menu_left)
1750    }
1751
1752    fn render_single(
1753        &mut self,
1754        borders: [Style; 4],
1755        bordered_wins: Rc<[Rect]>,
1756        inside_wins: Rc<[Rect]>,
1757        file_left: Files,
1758        menu_left: Menu,
1759    ) {
1760        let _ = self.term.draw(|f| {
1761            Self::draw_single_borders(borders, f, &bordered_wins);
1762            file_left.draw(f, &inside_wins[0], &mut self.image_adapter);
1763            menu_left.draw(f, &inside_wins[2]);
1764        });
1765    }
1766
1767    fn draw_n_borders(n: usize, borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1768        for i in 0..n {
1769            let bordered_block = Block::default()
1770                .borders(Borders::ALL)
1771                .border_type(BorderType::Rounded)
1772                .border_style(borders[i]);
1773            f.render_widget(bordered_block, wins[i]);
1774        }
1775    }
1776
1777    fn draw_dual_borders(borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1778        Self::draw_n_borders(4, borders, f, wins)
1779    }
1780
1781    fn draw_single_borders(borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1782        Self::draw_n_borders(2, borders, f, wins)
1783    }
1784
1785    /// Clear all images.
1786    pub fn clear_images(&mut self) {
1787        log_info!("display.clear_images()");
1788        self.image_adapter
1789            .clear_all()
1790            .expect("Couldn't clear all the images");
1791        self.term.clear().expect("Couldn't clear the terminal");
1792    }
1793
1794    /// Restore the terminal before leaving the application.
1795    /// - disable raw mode, allowing for "normal" terminal behavior,
1796    /// - leave alternate screens (switch back to the main screen).
1797    /// - display the cursor
1798    pub fn restore_terminal(&mut self) -> Result<()> {
1799        disable_raw_mode()?;
1800        execute!(self.term.backend_mut(), LeaveAlternateScreen)?;
1801        self.term.show_cursor()?;
1802        Ok(())
1803    }
1804}