Skip to main content

fm/io/
display.rs

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