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 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
60trait Draw {
63 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
98pub const MIN_WIDTH_FOR_DUAL_PANE: u16 = 120;
100
101enum TabPosition {
102 Left,
103 Right,
104}
105
106struct FilesAttributes {
109 tab_position: TabPosition,
111 is_selected: bool,
113 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 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 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 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 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 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
786impl<'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 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 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 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 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 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 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 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 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, ¤t_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 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 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 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
1457pub 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 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
1492struct 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 fn full_rect(width: u16, height: u16) -> Rect {
1513 Rect::new(0, 0, width, height)
1514 }
1515
1516 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 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 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 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 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 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 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 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
1619pub struct Display {
1622 term: Terminal<CrosstermBackend<Stdout>>,
1625 image_adapter: ImageAdapter,
1627}
1628
1629impl Display {
1630 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 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 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 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 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 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 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}